fix(converter): preserve OriginalName through conversion and fix race

- Fix critical bug: convertPageFile and splitAndConvert now copy
  OriginalName to the output PageFile so --keep-filenames works
  end-to-end on actually-converted pages.
- Fix concurrency race: ExtractChapter deduplicates OriginalName when
  source archive has same base filename in different subdirectories.
- Fix collision fallback: resolvePageName loops until an unused
  indexed name is found instead of single-shot assignment.
- Fix gofmt indentation in optimize_command.go.
This commit is contained in:
Antoine Aflalo
2026-07-21 19:45:25 -04:00
parent f3ba585c13
commit 21f92bc6f5
5 changed files with 147 additions and 19 deletions
+79
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"testing"
"time"
@@ -202,6 +203,84 @@ func TestExtractChapter_PagesHaveSequentialIndices(t *testing.T) {
})
}
// writeCollisionCBZ builds a CBZ containing the same image base name in two
// different subdirectories plus a normal unique page. The test fixture is
// specifically crafted to reproduce the race fixed in ExtractChapter: when
// keep-filenames naively copied filepath.Base(path) into OriginalName, both
// a/page.png and b/page.png would have ended up with the same stem and the
// WebP converter would have raced on a single shared intermediate path.
func writeCollisionCBZ(t *testing.T, dir string) string {
t.Helper()
cbzPath := filepath.Join(dir, "collisions.cbz")
f, err := os.Create(cbzPath)
require.NoError(t, err)
w := zip.NewWriter(f)
for _, entry := range []string{"a/page.png", "b/page.png", "cover.png"} {
fw, err := w.Create(entry)
require.NoError(t, err)
_, err = fw.Write([]byte("dummy image bytes for " + entry))
require.NoError(t, err)
}
require.NoError(t, w.Close())
require.NoError(t, f.Close())
return cbzPath
}
func TestExtractChapter_KeepFilenames_DeduplicatesCollidingNames(t *testing.T) {
tmpDir := t.TempDir()
cbzPath := writeCollisionCBZ(t, tmpDir)
t.Run("keep-filenames resolves colliding base names", func(t *testing.T) {
chapter, err := ExtractChapter(context.Background(), cbzPath, true)
require.NoError(t, err)
defer func() { _ = chapter.Cleanup() }()
require.Len(t, chapter.Pages, 3, "expected 3 pages: two colliding + one unique")
// All OriginalNames must be non-empty bare base names and unique.
seen := make(map[string]struct{}, len(chapter.Pages))
for _, page := range chapter.Pages {
assert.NotEmpty(t, page.OriginalName, "page %d must have OriginalName set", page.Index)
assert.Equal(t, filepath.Base(page.OriginalName), page.OriginalName,
"page %d OriginalName should be a bare base filename", page.Index)
if _, dup := seen[page.OriginalName]; dup {
t.Errorf("duplicate OriginalName across pages: %q", page.OriginalName)
}
seen[page.OriginalName] = struct{}{}
}
// The unique page keeps its original name; the two colliding pages
// must come out as one bare "page.png" and one indexed variant.
assert.Contains(t, seen, "page.png", "one colliding page should keep the bare page.png name")
assert.Contains(t, seen, "cover.png", "the unique page should keep its original name")
indexedPattern := regexp.MustCompile(`^page_\d{4}\.png$`)
indexedCount := 0
for name := range seen {
if indexedPattern.MatchString(name) {
indexedCount++
}
}
assert.Equal(t, 1, indexedCount,
"exactly one colliding page should be resolved to the page_NNNN.png pattern, got %v", seen)
})
t.Run("keep-filenames off leaves OriginalName empty (regression guard)", func(t *testing.T) {
// Same collision-prone archive, but with keep-filenames off: the
// dedup logic must not run, so OriginalName stays empty for every
// page. This is the historical default and must not regress.
chapter, err := ExtractChapter(context.Background(), cbzPath, false)
require.NoError(t, err)
defer func() { _ = chapter.Cleanup() }()
require.Len(t, chapter.Pages, 3)
for _, page := range chapter.Pages {
assert.Empty(t, page.OriginalName, "page %d must have empty OriginalName when keep-filenames is off", page.Index)
}
})
}
func TestExtractChapter_Cleanup(t *testing.T) {
chapter, err := ExtractChapter(context.Background(), "../../testdata/Chapter 128.cbz", false)
require.NoError(t, err)