package store import ( "strings" "testing" ) func testBlobs(t *testing.T) *FsBlobStore { t.Helper() b, err := NewFsBlobStore(t.TempDir() + "/files") if err != nil { t.Fatal(err) } return b } func TestBlobPutGetStatDeleteRoundtrip(t *testing.T) { b := testBlobs(t) data := []byte("hello attachment world") sha, size, err := b.Put("abc123/pending", strings.NewReader(string(data))) if err != nil { t.Fatal(err) } if size != int64(len(data)) { t.Fatalf("size = %d want %d", size, len(data)) } if len(sha) != 64 { t.Fatalf("sha256 = %q", sha) } // canonical key is / got, err := b.Get("abc123/" + sha) if err != nil { t.Fatal(err) } buf := make([]byte, len(data)+10) n, _ := got.Read(buf) got.Close() if string(buf[:n]) != string(data) { t.Fatalf("roundtrip mismatch: %q", buf[:n]) } sz, err := b.Stat("abc123/" + sha) if err != nil || sz != int64(len(data)) { t.Fatalf("stat = %d, %v", sz, err) } if err := b.Delete("abc123/" + sha); err != nil { t.Fatalf("delete: %v", err) } if _, err := b.Get("abc123/" + sha); err != ErrBlobNotFound { t.Fatalf("get after delete: %v", err) } } func TestBlobStatMissing(t *testing.T) { b := testBlobs(t) if _, err := b.Stat("nope/deadbeef"); err != ErrBlobNotFound { t.Fatalf("want ErrBlobNotFound, got %v", err) } } func TestBlobTraversalPrevention(t *testing.T) { b := testBlobs(t) evil := []string{ "../../etc/passwd", "../escape", "..\\windows", "/abs/path", "a/b/c", // too many segments "onlyone", // no slash "./relative", // dot segment "../..", // bare traversal "ok/../traverse", // traversal inside } for _, key := range evil { if _, _, err := b.Put(key, strings.NewReader("x")); err == nil { t.Errorf("Put accepted evil key %q", key) } if _, err := b.Get(key); err == nil { t.Errorf("Get accepted evil key %q", key) } if err := b.Delete(key); err == nil { t.Errorf("Delete accepted evil key %q", key) } } } func TestSanitizeFilename(t *testing.T) { cases := [][2]string{ {"../../etc/passwd", "passwd"}, {"C:\\Users\\evil\\file.txt", "file.txt"}, {"normal.txt", "normal.txt"}, {"ac", "ac"}, {"", "file"}, {"..", "file"}, {".hidden", ".hidden"}, {"with\x00null.txt", "withnull.txt"}, {"new\nline.txt", "newline.txt"}, } for _, c := range cases { got := SanitizeFilename(c[0]) if got != c[1] { t.Errorf("SanitizeFilename(%q) = %q want %q", c[0], got, c[1]) } } long := strings.Repeat("x", 300) if got := SanitizeFilename(long); len(got) != MaxFilenameLen { t.Errorf("long name len = %d want %d", len(got), MaxFilenameLen) } // issue #248: a 250-char multipart filename must truncate to the cap repro := strings.Repeat("b", 246) + ".txt" if got := SanitizeFilename(repro); len(got) != MaxFilenameLen { t.Errorf("repro name len = %d want %d", len(got), MaxFilenameLen) } }