feat: add bounded binary file writes
This commit is contained in:
@@ -68,10 +68,11 @@ type CaptureLink struct {
|
||||
}
|
||||
|
||||
type CaptureFile struct {
|
||||
Name string `json:"name"`
|
||||
Mime string `json:"mime"`
|
||||
Size int64 `json:"size"`
|
||||
Text string `json:"text"`
|
||||
Name string `json:"name"`
|
||||
Mime string `json:"mime"`
|
||||
Size int64 `json:"size"`
|
||||
Text string `json:"text"`
|
||||
DataBase64 string `json:"dataBase64"`
|
||||
}
|
||||
|
||||
type CaptureBrowser struct {
|
||||
@@ -232,8 +233,8 @@ func (p CapturePayload) Validate() error {
|
||||
if p.Kind == "file" && (p.File == nil || strings.TrimSpace(p.File.Name) == "") {
|
||||
return fmt.Errorf("file.name is required")
|
||||
}
|
||||
if p.Kind == "file" && (p.File == nil || p.File.Text == "") {
|
||||
return fmt.Errorf("file.text is required")
|
||||
if p.Kind == "file" && (p.File == nil || (p.File.Text == "" && strings.TrimSpace(p.File.DataBase64) == "")) {
|
||||
return fmt.Errorf("file.text or file.dataBase64 is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -269,6 +270,7 @@ func (p CapturePayload) EventPayload() map[string]interface{} {
|
||||
result["fileMime"] = strings.TrimSpace(p.File.Mime)
|
||||
result["fileSize"] = p.File.Size
|
||||
result["fileText"] = p.File.Text
|
||||
result["fileDataBase64"] = strings.TrimSpace(p.File.DataBase64)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -108,7 +108,8 @@ func TestReceiverAcceptsFileCaptureAndPublishesEvent(t *testing.T) {
|
||||
"name": "notes.txt",
|
||||
"mime": "text/plain",
|
||||
"size": 11,
|
||||
"text": "hello file"
|
||||
"text": "hello file",
|
||||
"dataBase64": "aGVsbG8gZmlsZQ=="
|
||||
},
|
||||
"browser": {
|
||||
"name": "Firefox"
|
||||
@@ -146,6 +147,9 @@ func TestReceiverAcceptsFileCaptureAndPublishesEvent(t *testing.T) {
|
||||
if payload["fileText"] != "hello file" {
|
||||
t.Fatalf("payload fileText = %v, want hello file", payload["fileText"])
|
||||
}
|
||||
if payload["fileDataBase64"] != "aGVsbG8gZmlsZQ==" {
|
||||
t.Fatalf("payload fileDataBase64 = %v, want aGVsbG8gZmlsZQ==", payload["fileDataBase64"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestReceiverAnnotatesCaptureWithCurrentWorkspace(t *testing.T) {
|
||||
|
||||
@@ -186,6 +186,21 @@ func (s *Service) ReadVaultFileBytes(relativePath string) (FileBytes, error) {
|
||||
}
|
||||
|
||||
func (s *Service) WriteVaultTextFile(relativePath string, content string, options WriteOptions) error {
|
||||
return s.writeVaultFileData(relativePath, []byte(content), options)
|
||||
}
|
||||
|
||||
func (s *Service) WriteVaultFileBytes(relativePath string, dataBase64 string, options WriteOptions) error {
|
||||
data, err := base64.StdEncoding.DecodeString(dataBase64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid-base64: %w", err)
|
||||
}
|
||||
if int64(len(data)) > MaxBinaryReadBytes {
|
||||
return fmt.Errorf("file-too-large: %s", relativePath)
|
||||
}
|
||||
return s.writeVaultFileData(relativePath, data, options)
|
||||
}
|
||||
|
||||
func (s *Service) writeVaultFileData(relativePath string, data []byte, options WriteOptions) error {
|
||||
root, rel, full, err := s.resolveFile(relativePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -234,7 +249,7 @@ func (s *Service) WriteVaultTextFile(relativePath string, content string, option
|
||||
_ = os.Remove(tmpName)
|
||||
}
|
||||
}()
|
||||
if _, err := tmp.WriteString(content); err != nil {
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
_ = tmp.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -221,6 +222,55 @@ func TestWriteVaultTextFileAtomicAndConflictBehavior(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteVaultFileBytesAtomicAndConflictBehavior(t *testing.T) {
|
||||
s, root := newTestService(t)
|
||||
|
||||
if err := s.WriteVaultFileBytes("Images/logo.png", "iVBORw==", WriteOptions{CreateIfMissing: true}); err == nil {
|
||||
t.Fatal("write bytes should fail when parent folder is missing")
|
||||
}
|
||||
if err := s.CreateVaultFolder("Images"); err != nil {
|
||||
t.Fatalf("CreateVaultFolder: %v", err)
|
||||
}
|
||||
if err := s.WriteVaultFileBytes("Images/logo.png", "iVBORw==", WriteOptions{CreateIfMissing: true}); err != nil {
|
||||
t.Fatalf("write bytes create: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(root, "Images", "logo.png"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != string([]byte{0x89, 0x50, 0x4e, 0x47}) {
|
||||
t.Fatalf("file bytes = %v", data)
|
||||
}
|
||||
if err := s.WriteVaultFileBytes("Images/logo.png", "AQID", WriteOptions{CreateIfMissing: true}); err == nil || !strings.Contains(err.Error(), "conflict") {
|
||||
t.Fatalf("write bytes conflict error = %v, want conflict", err)
|
||||
}
|
||||
if err := s.WriteVaultFileBytes("Images/logo.png", "AQID", WriteOptions{Overwrite: true}); err != nil {
|
||||
t.Fatalf("write bytes overwrite: %v", err)
|
||||
}
|
||||
data, err = os.ReadFile(filepath.Join(root, "Images", "logo.png"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != string([]byte{0x01, 0x02, 0x03}) {
|
||||
t.Fatalf("overwritten bytes = %v", data)
|
||||
}
|
||||
|
||||
matches, err := filepath.Glob(filepath.Join(root, "Images", ".verstak-write-*"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(matches) != 0 {
|
||||
t.Fatalf("atomic byte write left temp files: %v", matches)
|
||||
}
|
||||
if err := s.WriteVaultFileBytes("Images/bad.bin", "not-base64!", WriteOptions{CreateIfMissing: true}); err == nil || !strings.Contains(err.Error(), "invalid-base64") {
|
||||
t.Fatalf("invalid base64 error = %v, want invalid-base64", err)
|
||||
}
|
||||
tooLarge := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("a", int(MaxBinaryReadBytes)+1)))
|
||||
if err := s.WriteVaultFileBytes("Images/huge.bin", tooLarge, WriteOptions{CreateIfMissing: true}); err == nil || !strings.Contains(err.Error(), "file-too-large") {
|
||||
t.Fatalf("oversized bytes error = %v, want file-too-large", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateVaultFolderConflict(t *testing.T) {
|
||||
s, _ := newTestService(t)
|
||||
if err := s.CreateVaultFolder("Folder"); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user