From 828fb1244c0697c830e2f6abfeb846252148bae2 Mon Sep 17 00:00:00 2001 From: Anirudh Date: Mon, 19 Oct 2026 16:40:11 +0530 Subject: [PATCH] picod: allow empty file content in JSON upload UploadFileRequest.Content used binding:required on a string, which rejects an empty string, so an empty file could not be created via JSON upload (the base64 of an empty file is an empty string). Make Content a *string so an empty value is accepted while a missing field is still rejected. Signed-off-by: Anirudh --- pkg/picod/files.go | 8 +++--- pkg/picod/files_test.go | 62 +++++++++++++++++++++++++++++++++++++++++ pkg/picod/picod_test.go | 4 +-- 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/pkg/picod/files.go b/pkg/picod/files.go index a6014eb73..fc6e9913a 100644 --- a/pkg/picod/files.go +++ b/pkg/picod/files.go @@ -46,9 +46,9 @@ type FileInfo struct { // UploadFileRequest defines JSON upload request body type UploadFileRequest struct { - Path string `json:"path" binding:"required"` - Content string `json:"content" binding:"required"` // Base64 encoded content - Mode string `json:"mode"` + Path string `json:"path" binding:"required"` + Content *string `json:"content" binding:"required"` // Base64 encoded content + Mode string `json:"mode"` } // UploadFileHandler handles file upload requests @@ -175,7 +175,7 @@ func (s *Server) handleJSONBase64Upload(c *gin.Context) { } // Decode Base64 content - decodedContent, err := base64.StdEncoding.DecodeString(req.Content) + decodedContent, err := base64.StdEncoding.DecodeString(*req.Content) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Invalid base64 content: %v", err), diff --git a/pkg/picod/files_test.go b/pkg/picod/files_test.go index dc7b62544..6304b62f9 100644 --- a/pkg/picod/files_test.go +++ b/pkg/picod/files_test.go @@ -17,10 +17,14 @@ limitations under the License. package picod import ( + "bytes" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) @@ -321,3 +325,61 @@ func TestMkdirSafe(t *testing.T) { assert.True(t, os.IsNotExist(statErr), "directory should not exist outside workspace") }) } + +func TestHandleJSONBase64Upload_Content(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "picod-upload-*") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + server := &Server{} + assert.NoError(t, server.setWorkspace(tmpDir)) + + tests := []struct { + name string + body string + file string + expectedCode int + expectedData string + }{ + { + name: "empty content creates an empty file", + body: `{"path": "empty.txt", "content": ""}`, + file: "empty.txt", + expectedCode: http.StatusOK, + expectedData: "", + }, + { + name: "missing content is rejected", + body: `{"path": "missing.txt"}`, + file: "missing.txt", + expectedCode: http.StatusBadRequest, + }, + { + name: "non-empty content is written", + body: `{"path": "hello.txt", "content": "aGVsbG8="}`, + file: "hello.txt", + expectedCode: http.StatusOK, + expectedData: "hello", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "/api/files", bytes.NewBufferString(tt.body)) + c.Request.Header.Set("Content-Type", "application/json") + + server.UploadFileHandler(c) + + assert.Equal(t, tt.expectedCode, w.Code, w.Body.String()) + data, readErr := os.ReadFile(filepath.Join(tmpDir, tt.file)) + if tt.expectedCode != http.StatusOK { + assert.True(t, os.IsNotExist(readErr), "file should not be created") + return + } + assert.NoError(t, readErr) + assert.Equal(t, tt.expectedData, string(data)) + }) + } +} diff --git a/pkg/picod/picod_test.go b/pkg/picod/picod_test.go index ea116d1b0..af5f6beff 100644 --- a/pkg/picod/picod_test.go +++ b/pkg/picod/picod_test.go @@ -212,7 +212,7 @@ func TestPicoD_EndToEnd(t *testing.T) { contentB64 := base64.StdEncoding.EncodeToString([]byte(content)) uploadReq := UploadFileRequest{ Path: "test.txt", - Content: contentB64, + Content: &contentB64, Mode: "0644", } body, _ := json.Marshal(uploadReq) @@ -290,7 +290,7 @@ func TestPicoD_EndToEnd(t *testing.T) { // 6. Jail Escape Attempt (Should Fail) escapeReq := UploadFileRequest{ Path: "../outside.txt", - Content: contentB64, + Content: &contentB64, } escapeBody, _ := json.Marshal(escapeReq) req, _ = http.NewRequest("POST", ts.URL+"/api/files", bytes.NewBuffer(escapeBody))