package fs import ( "os" "path/filepath" "runtime" "testing" ) func mkdir(t *testing.T, parts ...string) { t.Helper() if err := os.MkdirAll(filepath.Join(parts...), 0o755); err != nil { t.Fatal(err) } } func TestResolveCanonical_RootAndEmpty(t *testing.T) { root := t.TempDir() for _, in := range []string{"/", "", "//"} { abs, url, ok := ResolveCanonical(root, in) if !ok { t.Fatalf("%q: ok=false", in) } if abs != root || url != "/" { t.Fatalf("%q: abs=%q url=%q", in, abs, url) } } } func TestResolveCanonical_ExactCase(t *testing.T) { root := t.TempDir() mkdir(t, root, "archive", "incoming") abs, url, ok := ResolveCanonical(root, "/archive/incoming") if !ok || url != "/archive/incoming" { t.Fatalf("ok=%v url=%q", ok, url) } if abs != filepath.Join(root, "archive", "incoming") { t.Fatalf("abs=%q", abs) } } func TestResolveCanonical_MixedCaseURLLowercaseOnDisk(t *testing.T) { root := t.TempDir() mkdir(t, root, "archive", "incoming") abs, url, ok := ResolveCanonical(root, "/Archive/Incoming") if !ok || url != "/archive/incoming" { t.Fatalf("ok=%v url=%q", ok, url) } if abs != filepath.Join(root, "archive", "incoming") { t.Fatalf("abs=%q", abs) } } func TestResolveCanonical_OnlyMixedCaseExists(t *testing.T) { root := t.TempDir() mkdir(t, root, "Archive", "Incoming") abs, url, ok := ResolveCanonical(root, "/archive/incoming") if !ok || url != "/Archive/Incoming" { t.Fatalf("ok=%v url=%q", ok, url) } if abs != filepath.Join(root, "Archive", "Incoming") { t.Fatalf("abs=%q", abs) } } func TestResolveCanonical_BothCasesExistLowercaseWins(t *testing.T) { if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { t.Skip("filesystem may be case-insensitive; tiebreak only meaningful on case-sensitive FS") } root := t.TempDir() mkdir(t, root, "Archive") mkdir(t, root, "archive") if err := os.WriteFile(filepath.Join(root, "Archive", "marker"), []byte("upper"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "archive", "marker"), []byte("lower"), 0o644); err != nil { t.Fatal(err) } for _, in := range []string{"/Archive/marker", "/archive/marker", "/aRcHiVe/marker"} { abs, url, ok := ResolveCanonical(root, in) if !ok { t.Fatalf("%q: ok=false", in) } if url != "/archive/marker" { t.Fatalf("%q: url=%q (want /archive/marker)", in, url) } body, err := os.ReadFile(abs) if err != nil { t.Fatalf("%q: read %s: %v", in, abs, err) } if string(body) != "lower" { t.Fatalf("%q: body=%q (want \"lower\" — lowercase variant must win)", in, body) } } } func TestResolveCanonical_NonexistentSegmentPreservesRemainder(t *testing.T) { root := t.TempDir() mkdir(t, root, "archive") abs, url, ok := ResolveCanonical(root, "/Archive/.archive/TR-001.html") if !ok { t.Fatal("ok=false") } // Walk canonicalizes "Archive" to "archive"; the virtual ".archive" // segment doesn't exist on disk, so the remainder passes through // unchanged so the dispatcher's virtual-prefix routing still fires. if url != "/archive/.archive/TR-001.html" { t.Fatalf("url=%q", url) } if abs != filepath.Join(root, "archive", ".archive", "TR-001.html") { t.Fatalf("abs=%q", abs) } } func TestResolveCanonical_FileSegmentTerminatesWalk(t *testing.T) { root := t.TempDir() mkdir(t, root, "archive") if err := os.WriteFile(filepath.Join(root, "archive", "Doc.PDF"), []byte("x"), 0o644); err != nil { t.Fatal(err) } abs, url, ok := ResolveCanonical(root, "/Archive/doc.pdf") if !ok { t.Fatal("ok=false") } // On Linux Doc.PDF exists but doc.pdf does not — exact-case tier // finds Doc.PDF and uses it. if url != "/archive/Doc.PDF" { t.Fatalf("url=%q", url) } _ = abs } func TestResolveCanonical_RejectsEscape(t *testing.T) { root := t.TempDir() mkdir(t, root, "archive") // filepath.Clean reduces "/archive/../.." to "/.."; Resolve sees // segments that don't exist on disk and walks them verbatim. The // final containment check must reject the result. _, _, ok := ResolveCanonical(root, "/archive/../../etc") if ok { t.Fatal("expected ok=false for escape path") } } func TestResolveCanonical_TrailingSlashesNormalized(t *testing.T) { root := t.TempDir() mkdir(t, root, "archive", "incoming") _, url, ok := ResolveCanonical(root, "/Archive/Incoming/") if !ok { t.Fatal("ok=false") } if url != "/archive/incoming" { t.Fatalf("url=%q", url) } }