156 lines
4.3 KiB
Go
156 lines
4.3 KiB
Go
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)
|
|
}
|
|
}
|