Artifact f7c7a34540730c428dd65b51c52808c7301902bd63d6f54b4ddc2f95c9196edb:
- File
internal/checkout/util_test.go
— part of check-in
[f5744359d3]
at
2026-04-19 16:30:54
on branch trunk
— Initial open source release
libfossil is a pure-Go implementation of the Fossil SCM, reading and writing the same .fossil SQLite repository format as upstream Fossil.
This is the first public release of code that previously lived in a private repository. The module path is github.com/danmestas/libfossil and the binary is named libfossil to avoid $PATH collisions with upstream fossil.
Included: - Repo lifecycle (create, clone), checkout/checkin, timeline, merge, diff, annotate, shun, tag, branch, bisect, stash, undo/redo - Sync-over-HTTP protocol (client and handler) - Two SQLite drivers: modernc (pure Go, default) and ncruces (wasm-capable) - Observer interfaces (SyncObserver, CheckoutObserver) with an optional OpenTelemetry implementation in observer/otel (separate submodule) - Deterministic simulation testing harness with BUGGIFY fault injection - Kong-based embeddable CLI under cli/ and a standalone binary under cmd/libfossil/ (user: [email protected], size: 8535) [annotate] [blame] [check-ins using]
package checkout import ( "path/filepath" "strings" "testing" "github.com/danmestas/libfossil/simio" ) func TestFileContent(t *testing.T) { r, cleanup := newTestRepoWithCheckin(t) defer cleanup() dir := t.TempDir() co, err := Create(r, dir, CreateOpts{}) if err != nil { t.Fatal(err) } defer co.Close() rid, _, _ := co.Version() // Use MemStorage to capture extracted files mem := simio.NewMemStorage() co.env = &simio.Env{Storage: mem, Clock: simio.RealClock{}, Rand: simio.CryptoRand{}} co.dir = "/checkout" // Extract files if err := co.Extract(rid, ExtractOpts{}); err != nil { t.Fatal(err) } // Read via FileContent content, err := co.FileContent("hello.txt") if err != nil { t.Fatal("FileContent failed:", err) } if string(content) != "hello world\n" { t.Fatalf("FileContent = %q, want %q", content, "hello world\n") } // Read nested file content2, err := co.FileContent("src/main.go") if err != nil { t.Fatal("FileContent src/main.go failed:", err) } if string(content2) != "package main\n" { t.Fatalf("FileContent src/main.go = %q", content2) } // Non-existent file should error _, err = co.FileContent("nonexistent.txt") if err == nil { t.Fatal("FileContent should fail for non-existent file") } } func TestWriteManifest(t *testing.T) { r, cleanup := newTestRepoWithCheckin(t) defer cleanup() dir := t.TempDir() co, err := Create(r, dir, CreateOpts{}) if err != nil { t.Fatal(err) } defer co.Close() // Use MemStorage mem := simio.NewMemStorage() co.env = &simio.Env{Storage: mem, Clock: simio.RealClock{}, Rand: simio.CryptoRand{}} co.dir = "/checkout" _, uuid, _ := co.Version() // Write both manifest files if err := co.WriteManifest(ManifestMain | ManifestUUID); err != nil { t.Fatal("WriteManifest failed:", err) } // Verify manifest file exists and contains content manifestData, err := mem.ReadFile("/checkout/manifest") if err != nil { t.Fatal("manifest file not found:", err) } if len(manifestData) == 0 { t.Fatal("manifest file is empty") } // Check that it looks like a manifest (should start with "C ") if !strings.HasPrefix(string(manifestData), "C ") { t.Fatalf("manifest content doesn't look like a manifest: %q", string(manifestData[:min(50, len(manifestData))])) } // Verify manifest.uuid file uuidData, err := mem.ReadFile("/checkout/manifest.uuid") if err != nil { t.Fatal("manifest.uuid file not found:", err) } if string(uuidData) != uuid+"\n" { t.Fatalf("manifest.uuid = %q, want %q", uuidData, uuid+"\n") } } func TestWriteManifestFlags(t *testing.T) { r, cleanup := newTestRepoWithCheckin(t) defer cleanup() dir := t.TempDir() co, err := Create(r, dir, CreateOpts{}) if err != nil { t.Fatal(err) } defer co.Close() mem := simio.NewMemStorage() co.env = &simio.Env{Storage: mem, Clock: simio.RealClock{}, Rand: simio.CryptoRand{}} co.dir = "/checkout" // Write only UUID if err := co.WriteManifest(ManifestUUID); err != nil { t.Fatal("WriteManifest ManifestUUID failed:", err) } // manifest.uuid should exist if _, err := mem.ReadFile("/checkout/manifest.uuid"); err != nil { t.Fatal("manifest.uuid not found:", err) } // manifest should NOT exist if _, err := mem.ReadFile("/checkout/manifest"); err == nil { t.Fatal("manifest should not exist when only ManifestUUID is set") } // Clear storage mem = simio.NewMemStorage() co.env.Storage = mem // Write only main manifest if err := co.WriteManifest(ManifestMain); err != nil { t.Fatal("WriteManifest ManifestMain failed:", err) } // manifest should exist if _, err := mem.ReadFile("/checkout/manifest"); err != nil { t.Fatal("manifest not found:", err) } // manifest.uuid should NOT exist if _, err := mem.ReadFile("/checkout/manifest.uuid"); err == nil { t.Fatal("manifest.uuid should not exist when only ManifestMain is set") } } func TestCheckFilename(t *testing.T) { r, cleanup := newTestRepoWithCheckin(t) defer cleanup() dir := t.TempDir() co, err := Create(r, dir, CreateOpts{}) if err != nil { t.Fatal(err) } defer co.Close() tests := []struct { name string input string want string wantErr bool }{ {"simple relative", "hello.txt", "hello.txt", false}, {"nested relative", "src/main.go", "src/main.go", false}, {"with dots", "./src/../hello.txt", "hello.txt", false}, {"escape parent", "../etc/passwd", "", true}, {"escape complex", "../../etc/passwd", "", true}, {"just dotdot", "..", "", true}, {"dotdot prefix", "../sibling", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := co.CheckFilename(tt.input) if (err != nil) != tt.wantErr { t.Errorf("CheckFilename(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) return } if !tt.wantErr && got != tt.want { t.Errorf("CheckFilename(%q) = %q, want %q", tt.input, got, tt.want) } }) } } func TestCheckFilenameAbsolute(t *testing.T) { r, cleanup := newTestRepoWithCheckin(t) defer cleanup() dir := t.TempDir() co, err := Create(r, dir, CreateOpts{}) if err != nil { t.Fatal(err) } defer co.Close() // Absolute path within checkout absInside := filepath.Join(dir, "hello.txt") got, err := co.CheckFilename(absInside) if err != nil { t.Errorf("CheckFilename(%q) unexpected error: %v", absInside, err) } if got != "hello.txt" { t.Errorf("CheckFilename(%q) = %q, want %q", absInside, got, "hello.txt") } // Absolute path outside checkout absOutside := "/etc/passwd" _, err = co.CheckFilename(absOutside) if err == nil { t.Errorf("CheckFilename(%q) should fail for path outside checkout", absOutside) } } func TestIsRootedIn(t *testing.T) { r, cleanup := newTestRepoWithCheckin(t) defer cleanup() dir := t.TempDir() co, err := Create(r, dir, CreateOpts{}) if err != nil { t.Fatal(err) } defer co.Close() tests := []struct { name string absPath string want bool }{ {"inside exact", dir, true}, {"inside nested", filepath.Join(dir, "subdir", "file.txt"), true}, {"inside single file", filepath.Join(dir, "file.txt"), true}, {"outside parent", filepath.Dir(dir), false}, {"outside sibling", filepath.Join(filepath.Dir(dir), "sibling"), false}, {"outside root", "/etc/passwd", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := co.IsRootedIn(tt.absPath) if got != tt.want { t.Errorf("IsRootedIn(%q) = %v, want %v", tt.absPath, got, tt.want) } }) } } func TestSafePathRejectsTraversal(t *testing.T) { r, cleanup := newTestRepoWithCheckin(t) defer cleanup() dir := t.TempDir() co, err := Create(r, dir, CreateOpts{}) if err != nil { t.Fatal(err) } defer co.Close() bad := []string{"../etc/passwd", "../../root/.ssh/id_rsa", "/etc/shadow", "foo/../../bar"} for _, p := range bad { _, err := co.safePath(p) if err == nil { t.Fatalf("safePath(%q) should have failed", p) } } good := []string{"hello.txt", "src/main.go", "a/b/c.txt"} for _, p := range good { path, err := co.safePath(p) if err != nil { t.Fatalf("safePath(%q) failed: %v", p, err) } if path == "" { t.Fatalf("safePath(%q) returned empty", p) } if !strings.HasPrefix(path, dir) { t.Fatalf("safePath(%q) = %q, not within checkout dir %q", p, path, dir) } } } func TestFileContentRejectsTraversal(t *testing.T) { r, cleanup := newTestRepoWithCheckin(t) defer cleanup() dir := t.TempDir() co, err := Create(r, dir, CreateOpts{}) if err != nil { t.Fatal(err) } defer co.Close() _, err = co.FileContent("../etc/passwd") if err == nil { t.Fatal("FileContent(\"../etc/passwd\") should have failed") } if !strings.Contains(err.Error(), "escapes checkout") { t.Fatalf("FileContent error should mention escapes checkout, got: %v", err) } } func TestRenameRejectsTraversal(t *testing.T) { r, cleanup := newTestRepoWithCheckin(t) defer cleanup() dir := t.TempDir() co, err := Create(r, dir, CreateOpts{}) if err != nil { t.Fatal(err) } defer co.Close() rid, _, _ := co.Version() if err := co.Extract(rid, ExtractOpts{}); err != nil { t.Fatal(err) } // Rename To with traversal should fail err = co.Rename(RenameOpts{ From: "hello.txt", To: "../outside", DoFsMove: true, }) if err == nil { t.Fatal("Rename to ../outside should have failed") } if !strings.Contains(err.Error(), "escapes checkout") { t.Fatalf("Rename error should mention escapes checkout, got: %v", err) } } func min(a, b int) int { if a < b { return a } return b }