fix(scanner): stop splitting multi-byte characters when truncating tags

sanitize() capped tag values with a byte slice, so a value whose limit falls in
the middle of a multi-byte character was stored as invalid UTF-8. defaultMaxTagLength
is 1024, which is not a multiple of 3, so any sufficiently long CJK title hit this.
Only trailing invalid bytes are trimmed, leaving bad bytes elsewhere in the value
untouched.
This commit is contained in:
Deluan 2026-08-19 23:41:35 -04:00
parent 3f396c5666
commit 34e4b94251
2 changed files with 31 additions and 0 deletions

View File

@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/google/uuid"
"github.com/navidrome/navidrome/consts"
@ -366,6 +367,14 @@ func sanitize(filePath string, tagName model.TagName, tag model.TagConf, value s
if len(value) > maxLength {
log.Trace("Truncated tag value", "tag", tagName, "value", value, "length", len(value), "maxLength", maxLength)
value = value[:maxLength]
// Drop the trailing partial rune the cut may have left behind. Only trailing
// invalid bytes are removed, so pre-existing bad bytes elsewhere are preserved.
for len(value) > 0 {
if r, size := utf8.DecodeLastRuneInString(value); r != utf8.RuneError || size > 1 {
break
}
value = value[:len(value)-1]
}
}
switch tag.Type {

View File

@ -4,6 +4,7 @@ import (
"os"
"strings"
"time"
"unicode/utf8"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/metadata"
@ -122,6 +123,27 @@ var _ = Describe("Metadata", func() {
Expect(pair[0].Value()).To(HaveLen(1048570))
})
It("should not split a multi-byte character when truncating", func() {
// 1024 is not a multiple of 3, so a byte-wise cut lands mid-rune.
props.Tags = model.RawTags{
"Title": {strings.Repeat("日", 2048)},
}
md = metadata.New(filePath, props)
title := md.String(model.TagTitle)
Expect(utf8.ValidString(title)).To(BeTrue(), "truncation produced invalid UTF-8")
Expect(len(title)).To(BeNumerically("<=", 1024))
})
It("should keep invalid bytes that are not at the truncation point", func() {
props.Tags = model.RawTags{
"Title": {"a\xffb" + strings.Repeat("c", 2048)},
}
md = metadata.New(filePath, props)
Expect(md.String(model.TagTitle)).To(HaveLen(1024))
})
It("should split multiple values", func() {
props.Tags = model.RawTags{
"Genre": {"Rock/Pop;;Punk"},