Files
tver/internal/media/path_test.go
T

155 lines
5.4 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package media
import (
"path/filepath"
"strings"
"testing"
"unicode"
"unicode/utf8"
)
func TestRelativeVideoPath(t *testing.T) {
got := RelativeVideoPath("番組名", "第1話", "ep123")
want := filepath.Join("番組名", "番組名 第1話 [ep123].mp4")
if got != want {
t.Fatalf("path=%q want=%q", got, want)
}
other := RelativeVideoPath("番組名", "第1話", "ep456")
if got == other || !strings.Contains(other, "[ep456]") {
t.Fatalf("ID による衝突回避: %q %q", got, other)
}
}
func TestRelativeVideoPathSanitizesComponents(t *testing.T) {
got := RelativeVideoPath(" 番組:/\\*?<>|\"\x00\n\t\x7f\u0085. ", "\x00\n", "../")
if filepath.IsAbs(got) || strings.ContainsAny(got, `:*?<>|"`) || strings.Contains(got, "..") {
t.Fatalf("禁止文字が残った: %q", got)
}
for _, r := range got {
if unicode.IsControl(r) {
t.Errorf("制御文字が残った: %U in %q", r, got)
}
}
parts := strings.Split(filepath.ToSlash(got), "/")
if len(parts) != 2 || parts[0] == "" || !strings.HasSuffix(got, ".mp4") {
t.Fatalf("相対パスの形: %q", got)
}
}
func TestRelativeVideoPathHandlesEmptyAndLongNames(t *testing.T) {
for _, name := range []string{"", " ", ".", "..", "..."} {
got := RelativeVideoPath(name, name, "")
if !strings.Contains(got, "名称未設定") {
t.Errorf("空の代替名 %q: %q", name, got)
}
}
long := strings.Repeat("界", 200)
got := RelativeVideoPath(long, long, "id")
parts := strings.Split(filepath.ToSlash(got), "/")
if len(parts) != 2 || len([]byte(parts[0])) != 60 || !utf8.ValidString(parts[0]) {
t.Fatalf("番組名の60バイト境界: %q (%d bytes)", parts[0], len([]byte(parts[0])))
}
for _, part := range strings.Split(filepath.ToSlash(got), "/") {
if len([]byte(part)) > 240 {
t.Fatalf("長すぎる要素: %d bytes", len([]byte(part)))
}
if !utf8.ValidString(part) {
t.Fatalf("不正な UTF-8: %q", part)
}
}
if got := sanitizeComponent(strings.Repeat("a", 61)); len([]byte(got)) != 60 {
t.Fatalf("ASCII の60バイト境界: %d %q", len([]byte(got)), got)
}
}
func TestSanitizeComponentTrimsWhitespaceBeforeLengthLimit(t *testing.T) {
for _, tt := range []struct {
name, input, want string
}{
{"前後の空白", " \t 番組名\n ", "_ 番組名_"},
{"空白のみ", "   ", "名称未設定"},
{"制御文字のみ", "\t\n", "_"},
{"多数の先頭空白", strings.Repeat(" ", maxComponentRunes+10) + "番組名", "番組名"},
{"先頭空白と長い名前", strings.Repeat(" ", 70) + strings.Repeat("a", 70), strings.Repeat("a", 60)},
} {
t.Run(tt.name, func(t *testing.T) {
if got := sanitizeComponent(tt.input); got != tt.want {
t.Errorf("sanitizeComponent()=%q want=%q", got, tt.want)
}
})
}
}
func TestSanitizeComponentTrimsAgainAfterLengthLimit(t *testing.T) {
for _, tt := range []struct {
name, input, want string
}{
{"60バイト目が空白", strings.Repeat("a", 59) + " " + "b", strings.Repeat("a", 59)},
{"60バイト目がドット", strings.Repeat("a", 59) + "." + "b", strings.Repeat("a", 59)},
{"切り詰め部分が空白のみ", "a" + strings.Repeat(" ", 59) + "b", "a"},
{"切り詰め部分がドットのみ", "a" + strings.Repeat(".", 59) + "b", "a"},
{"複数バイト文字の直後がドット", strings.Repeat("界", 19) + ".続", strings.Repeat("界", 19)},
} {
t.Run(tt.name, func(t *testing.T) {
got := sanitizeComponent(tt.input)
if got != tt.want {
t.Errorf("sanitizeComponent()=%q want=%q", got, tt.want)
}
if strings.Trim(got, " .") != got || got == "" {
t.Errorf("切り詰め後の名前が不正: %q", got)
}
})
}
}
func TestSanitizeComponentSafetyProperties(t *testing.T) {
var controls strings.Builder
for r := rune(0); r <= 0x9f; r++ {
if unicode.IsControl(r) {
controls.WriteRune(r)
}
}
inputs := []string{
"", " ", ".", "..", "...", `<>:"/\|?*`, controls.String(),
" \t 番組名\n. ", "a/b\\c", strings.Repeat("a", 500),
strings.Repeat("界", 500), strings.Repeat("😀", 500),
strings.Repeat(".", 100) + "name" + strings.Repeat(" ", 100),
string([]byte{0xff, 0xfe, 'a', 0xc0, 0xaf}),
}
for i, input := range inputs {
got := sanitizeComponent(input)
if got == "" || got == "." || got == ".." {
t.Errorf("case %d: 空またはdot segment: %q", i, got)
}
if strings.Trim(got, " .") != got {
t.Errorf("case %d: 前後に空白またはdot: %q", i, got)
}
if len([]byte(got)) > maxComponentBytes || utf8.RuneCountInString(got) > maxComponentRunes {
t.Errorf("case %d: 上限超過: bytes=%d runes=%d %q", i, len([]byte(got)), utf8.RuneCountInString(got), got)
}
if !utf8.ValidString(got) {
t.Errorf("case %d: 不正なUTF-8: %q", i, got)
}
if strings.ContainsAny(got, `<>:"/\|?*`) {
t.Errorf("case %d: 禁止文字が残った: %q", i, got)
}
for _, r := range got {
if unicode.IsControl(r) {
t.Errorf("case %d: 制御文字が残った: %U in %q", i, r, got)
}
}
if again := sanitizeComponent(got); again != got {
t.Errorf("case %d: 再適用で変化: %q -> %q", i, got, again)
}
path := RelativeVideoPath(input, input, input)
parts := strings.Split(filepath.ToSlash(path), "/")
if filepath.IsAbs(path) || len(parts) != 2 || parts[0] == "" || parts[0] == "." || parts[0] == ".." || !strings.HasSuffix(parts[1], ".mp4") {
t.Errorf("case %d: 安全でない相対パス: %q parts=%q", i, path, parts)
}
if !utf8.ValidString(path) {
t.Errorf("case %d: パスが不正なUTF-8: %q", i, path)
}
}
}