Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions internal/cmd/serverless/pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ import (
// memory and base64 expands it by ~4/3.
const maxPackEntryBytes int64 = 10 << 20 // 10 MiB

// maxPackTotalBytes bounds the archive as a whole. The per-file cap alone does
// not: a virtualenv is thousands of small files and would sail past it.
// maxPackTotalBytes bounds uncompressed file bytes held in memory while packing.
// The per-file cap alone does not: a virtualenv is thousands of small files
// and would sail past it.
const maxPackTotalBytes int64 = 25 << 20 // 25 MiB

// maxArchiveBytes is SourceUploadCreate.declaredByteLength (maximum 10485760),
// which is the zip after compression, not the uncompressed tree.
const maxArchiveBytes int64 = 10 << 20 // 10 MiB

// runwareIgnoreFile is the project's own exclude list, and the only one read.
//
// .gitignore is deliberately NOT consulted. What a project keeps out of version
Expand Down Expand Up @@ -490,6 +495,12 @@ func writeArchive(root string, files []packedFile) ([]byte, error) {
if err := zw.Close(); err != nil {
return nil, fmt.Errorf("close zip: %w", err)
}
if int64(buf.Len()) > maxArchiveBytes {
return nil, fmt.Errorf(
"the packed archive is %s; the maximum is %s.\n%s\nExclude what the app does not need with a %s file",
humanBytes(int64(buf.Len())), humanBytes(maxArchiveBytes), largestFilesSummary(files), runwareIgnoreFile,
)
}
return buf.Bytes(), nil
}

Expand Down
28 changes: 28 additions & 0 deletions internal/cmd/serverless/pack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package serverless
import (
"archive/zip"
"bytes"
"crypto/rand"
"io"
"os"
"path/filepath"
Expand Down Expand Up @@ -531,6 +532,33 @@ func TestPackDirectory_TotalTooLarge(t *testing.T) {
}
}

func TestWriteArchive_RejectsZipOverAPILimit(t *testing.T) {
dir := t.TempDir()
// Uncompressed total stays under maxPackTotalBytes; Deflate cannot shrink
// random bytes enough to fit declaredByteLength.
const chunk = 6 << 20
for _, name := range []string{"a.bin", "b.bin"} {
buf := make([]byte, chunk)
if _, err := rand.Read(buf); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, name), buf, 0o600); err != nil {
t.Fatal(err)
}
}

_, err := writeArchive(dir, []packedFile{
{rel: "a.bin", size: chunk},
{rel: "b.bin", size: chunk},
})
if err == nil {
t.Fatal("expected an error for a zip over declaredByteLength")
}
if !strings.Contains(err.Error(), "packed archive") || !strings.Contains(err.Error(), humanBytes(maxArchiveBytes)) {
t.Errorf("error %q does not name the zip cap", err)
}
}

// TestPackDirectory_ModelFileAlwaysPackedFromExcludedDirectory is the reviewer's
// case: the exemption for the model file has to survive an ignore rule that
// matches one of its ANCESTORS. The walk meets the directory first, and the
Expand Down
Loading