feat: add bounded file byte reads

This commit is contained in:
2026-06-29 03:16:16 +08:00
parent aa4905a097
commit 57677d1b1b
13 changed files with 208 additions and 13 deletions
+37
View File
@@ -1,6 +1,7 @@
package files
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/fs"
@@ -148,6 +149,42 @@ func (s *Service) ReadVaultTextFile(relativePath string) (string, error) {
return string(data), nil
}
func (s *Service) ReadVaultFileBytes(relativePath string) (FileBytes, error) {
root, rel, full, err := s.resolveFile(relativePath)
if err != nil {
return FileBytes{}, err
}
if err := rejectSymlinkPath(root, rel, true); err != nil {
return FileBytes{}, err
}
info, err := os.Lstat(full)
if err != nil {
if os.IsNotExist(err) {
return FileBytes{}, fmt.Errorf("not-found: %s", rel)
}
return FileBytes{}, err
}
if info.Mode()&os.ModeSymlink != 0 {
return FileBytes{}, fmt.Errorf("symlink-not-allowed: %s", rel)
}
if !info.Mode().IsRegular() {
return FileBytes{}, fmt.Errorf("not-regular-file: %s", rel)
}
if info.Size() > MaxBinaryReadBytes {
return FileBytes{}, fmt.Errorf("file-too-large: %s", rel)
}
data, err := os.ReadFile(full)
if err != nil {
return FileBytes{}, err
}
return FileBytes{
RelativePath: rel,
Size: int64(len(data)),
MimeHint: mime.TypeByExtension(filepath.Ext(info.Name())),
DataBase64: base64.StdEncoding.EncodeToString(data),
}, nil
}
func (s *Service) WriteVaultTextFile(relativePath string, content string, options WriteOptions) error {
root, rel, full, err := s.resolveFile(relativePath)
if err != nil {
+44
View File
@@ -87,6 +87,9 @@ func TestPathPolicyRejectsUnsafeOperations(t *testing.T) {
if _, err := s.ReadVaultTextFile(input); err == nil {
t.Fatal("read: expected error")
}
if _, err := s.ReadVaultFileBytes(input); err == nil {
t.Fatal("read bytes: expected error")
}
if err := s.WriteVaultTextFile(input, "x", WriteOptions{CreateIfMissing: true}); err == nil {
t.Fatal("write: expected error")
}
@@ -137,6 +140,47 @@ func TestReadVaultTextFileRules(t *testing.T) {
}
}
func TestReadVaultFileBytesRules(t *testing.T) {
s, root := newTestService(t)
imageBytes := []byte{0x89, 0x50, 0x4e, 0x47}
if err := os.WriteFile(filepath.Join(root, "image.png"), imageBytes, 0o644); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(root, "Folder"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "huge.bin"), []byte(strings.Repeat("a", int(MaxBinaryReadBytes)+1)), 0o644); err != nil {
t.Fatal(err)
}
result, err := s.ReadVaultFileBytes("image.png")
if err != nil {
t.Fatalf("ReadVaultFileBytes image: %v", err)
}
if result.RelativePath != "image.png" {
t.Fatalf("relative path = %q, want image.png", result.RelativePath)
}
if result.Size != int64(len(imageBytes)) {
t.Fatalf("size = %d, want %d", result.Size, len(imageBytes))
}
if result.MimeHint != "image/png" {
t.Fatalf("mime hint = %q, want image/png", result.MimeHint)
}
if result.DataBase64 != "iVBORw==" {
t.Fatalf("dataBase64 = %q, want iVBORw==", result.DataBase64)
}
if _, err := s.ReadVaultFileBytes("Folder"); err == nil || !strings.Contains(err.Error(), "not-regular-file") {
t.Fatalf("read folder error = %v, want not-regular-file", err)
}
if _, err := s.ReadVaultFileBytes("missing.png"); err == nil || !strings.Contains(err.Error(), "not-found") {
t.Fatalf("read missing error = %v, want not-found", err)
}
if _, err := s.ReadVaultFileBytes("huge.bin"); err == nil || !strings.Contains(err.Error(), "file-too-large") {
t.Fatalf("read huge error = %v, want file-too-large", err)
}
}
func TestWriteVaultTextFileAtomicAndConflictBehavior(t *testing.T) {
s, root := newTestService(t)
+8
View File
@@ -1,6 +1,7 @@
package files
const MaxTextFileBytes int64 = 2 * 1024 * 1024
const MaxBinaryReadBytes int64 = 8 * 1024 * 1024
type FileType string
@@ -39,6 +40,13 @@ type FileMetadata struct {
CanWrite bool `json:"canWrite"`
}
type FileBytes struct {
RelativePath string `json:"relativePath"`
Size int64 `json:"size"`
MimeHint string `json:"mimeHint"`
DataBase64 string `json:"dataBase64"`
}
type ExternalOpenTarget struct {
RelativePath string `json:"relativePath"`
AbsolutePath string `json:"absolutePath"`