feat: add bounded file byte reads
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user