diff --git a/backend/internal/bookfile/reader.go b/backend/internal/bookfile/reader.go new file mode 100644 index 0000000..6bc9dcf --- /dev/null +++ b/backend/internal/bookfile/reader.go @@ -0,0 +1,37 @@ +package bookfile + +import ( + "os" + "path/filepath" + "strings" +) + +// OpenReaderAt opens a book file and returns a ReaderAt + size. +// Consolidates the 3 places that open a book file + stat + get ReaderAt. +func OpenReaderAt(root, rel string) (*os.File, int64, error) { + abs := filepath.Join(root, filepath.FromSlash(rel)) + f, err := os.Open(abs) + if err != nil { + return nil, 0, err + } + st, err := f.Stat() + if err != nil { + f.Close() + return nil, 0, err + } + return f, st.Size(), nil +} + +// Contains reports whether child is inside parent using EvalSymlinks semantics. +// Falls back to Clean if EvalSymlinks fails (e.g., path doesn't exist yet). +func Contains(parent, child string) bool { + p, err := filepath.EvalSymlinks(parent) + if err != nil { + p = filepath.Clean(parent) + } + c, err := filepath.EvalSymlinks(child) + if err != nil { + c = filepath.Clean(child) + } + return c == p || strings.HasPrefix(c, p+string(os.PathSeparator)) +} diff --git a/backend/internal/bookfile/reader_test.go b/backend/internal/bookfile/reader_test.go new file mode 100644 index 0000000..e962eab --- /dev/null +++ b/backend/internal/bookfile/reader_test.go @@ -0,0 +1,51 @@ +package bookfile + +import ( + "os" + "path/filepath" + "testing" +) + +func TestContains(t *testing.T) { + dir := t.TempDir() + sub := filepath.Join(dir, "sub") + if err := os.MkdirAll(sub, 0o755); err != nil { + t.Fatal(err) + } + if !Contains(dir, sub) { + t.Fatal("sub should be inside dir") + } + if Contains(sub, dir) { + t.Fatal("dir should not be inside sub") + } + if Contains(dir, "/completely/different") { + t.Fatal("unrelated path should not be inside dir") + } + // Same path. + if !Contains(dir, dir) { + t.Fatal("dir should contain itself") + } +} + +func TestOpenReaderAt(t *testing.T) { + dir := t.TempDir() + // Create a test file. + path := filepath.Join(dir, "test.txt") + if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil { + t.Fatal(err) + } + f, size, err := OpenReaderAt(dir, "test.txt") + if err != nil { + t.Fatal(err) + } + defer f.Close() + if size != 5 { + t.Fatalf("size = %d, want 5", size) + } + + // Non-existent file. + _, _, err = OpenReaderAt(dir, "nope.txt") + if err == nil { + t.Fatal("expected error for non-existent file") + } +}