@@ -6,43 +6,153 @@ import (
66 "bytes"
77 "errors"
88 "io"
9- "log "
9+ "math "
1010 "strings"
11+
12+ "golang.org/x/xerrors"
13+ )
14+
15+ // Ref:
16+ // https://github.com/golang/go/blob/go1.24.0/src/archive/tar/format.go
17+ // https://github.com/golang/go/blob/go1.24.0/src/archive/tar/writer.go
18+ const (
19+ tarBlockSize = 512
20+ tarEndBlockBytes = 2 * tarBlockSize
1121)
1222
23+ // ErrArchiveTooLarge reports that archive expansion would exceed the
24+ // configured limit.
25+ var ErrArchiveTooLarge = xerrors .New ("archive exceeds maximum size" )
26+
27+ // ErrInvalidZipContent reports that a ZIP entry is malformed or its
28+ // contents fail validation during conversion.
29+ var ErrInvalidZipContent = xerrors .New ("invalid zip content" )
30+
1331// CreateTarFromZip converts the given zipReader to a tar archive.
32+ // maxSize limits the total tar output, including tar metadata.
1433func CreateTarFromZip (zipReader * zip.Reader , maxSize int64 ) ([]byte , error ) {
34+ err := validateZipArchiveSize (zipReader , maxSize )
35+ if err != nil {
36+ return nil , err
37+ }
38+
1539 var tarBuffer bytes.Buffer
16- err : = writeTarArchive (& tarBuffer , zipReader , maxSize )
40+ err = writeTarArchive (& tarBuffer , zipReader , maxSize )
1741 if err != nil {
1842 return nil , err
1943 }
2044 return tarBuffer .Bytes (), nil
2145}
2246
23- func writeTarArchive (w io.Writer , zipReader * zip.Reader , maxSize int64 ) error {
24- tarWriter := tar .NewWriter (w )
25- defer tarWriter .Close ()
47+ // validateZipArchiveSize performs a metadata-based preflight size
48+ // check before conversion. The actual tar output limit will still be
49+ // enforced while streaming.
50+ func validateZipArchiveSize (zipReader * zip.Reader , maxSize int64 ) error {
51+ if maxSize < 0 {
52+ return ErrArchiveTooLarge
53+ }
54+
55+ maxBytes := uint64 (maxSize )
56+ totalBytes := uint64 (tarEndBlockBytes )
57+ if totalBytes > maxBytes {
58+ return ErrArchiveTooLarge
59+ }
2660
2761 for _ , file := range zipReader .File {
28- err := processFileInZipArchive (file , tarWriter , maxSize )
62+ entrySize , err := projectedTarEntrySize (file )
2963 if err != nil {
3064 return err
3165 }
66+ if entrySize > maxBytes - totalBytes {
67+ return ErrArchiveTooLarge
68+ }
69+ totalBytes += entrySize
3270 }
71+
3372 return nil
3473}
3574
36- func processFileInZipArchive (file * zip.File , tarWriter * tar.Writer , maxSize int64 ) error {
75+ func projectedTarEntrySize (file * zip.File ) (uint64 , error ) {
76+ // Each tar entry contributes one header block plus its data
77+ // rounded up to the next tar block boundary.
78+ size := file .UncompressedSize64
79+ if remainder := size % tarBlockSize ; remainder != 0 {
80+ padding := tarBlockSize - remainder
81+ if size > math .MaxUint64 - padding {
82+ return 0 , ErrArchiveTooLarge
83+ }
84+ size += padding
85+ }
86+
87+ if size > math .MaxUint64 - tarBlockSize {
88+ return 0 , ErrArchiveTooLarge
89+ }
90+
91+ return tarBlockSize + size , nil
92+ }
93+
94+ type limitedWriter struct {
95+ w io.Writer
96+ remaining int64
97+ }
98+
99+ func (w * limitedWriter ) Write (p []byte ) (int , error ) {
100+ if len (p ) == 0 {
101+ return 0 , nil
102+ }
103+ if w .remaining <= 0 {
104+ return 0 , ErrArchiveTooLarge
105+ }
106+
107+ origLen := len (p )
108+ if int64 (origLen ) > w .remaining {
109+ p = p [:int (w .remaining )]
110+ }
111+
112+ n , err := w .w .Write (p )
113+ // io.Writer may report both written bytes and an error, so
114+ // account for any accepted bytes before returning the error.
115+ w .remaining -= int64 (n )
116+ if err != nil {
117+ return n , err
118+ }
119+ if n < origLen {
120+ return n , ErrArchiveTooLarge
121+ }
122+ return n , nil
123+ }
124+
125+ func writeTarArchive (w io.Writer , zipReader * zip.Reader , maxSize int64 ) error {
126+ tarWriter := tar .NewWriter (& limitedWriter {
127+ w : w ,
128+ remaining : maxSize ,
129+ })
130+
131+ for _ , file := range zipReader .File {
132+ err := processFileInZipArchive (file , tarWriter )
133+ if err != nil {
134+ return err
135+ }
136+ }
137+
138+ return tarWriter .Close ()
139+ }
140+
141+ func processFileInZipArchive (file * zip.File , tarWriter * tar.Writer ) error {
37142 fileReader , err := file .Open ()
38143 if err != nil {
39144 return err
40145 }
41146 defer fileReader .Close ()
42147
148+ size := file .FileInfo ().Size ()
149+ if size < 0 {
150+ return ErrArchiveTooLarge
151+ }
152+
43153 err = tarWriter .WriteHeader (& tar.Header {
44154 Name : file .Name ,
45- Size : file . FileInfo (). Size () ,
155+ Size : size ,
46156 Mode : int64 (file .Mode ()),
47157 ModTime : file .Modified ,
48158 // Note: Zip archives do not store ownership information.
@@ -53,12 +163,17 @@ func processFileInZipArchive(file *zip.File, tarWriter *tar.Writer, maxSize int6
53163 return err
54164 }
55165
56- n , err := io .CopyN (tarWriter , fileReader , maxSize )
57- log .Println (file .Name , n , err )
58- if errors .Is (err , io .EOF ) {
59- err = nil
166+ _ , err = io .CopyN (tarWriter , fileReader , size )
167+ switch {
168+ case errors .Is (err , io .EOF ), errors .Is (err , io .ErrUnexpectedEOF ):
169+ return ErrInvalidZipContent
170+ case errors .Is (err , zip .ErrChecksum ), errors .Is (err , zip .ErrFormat ):
171+ return ErrInvalidZipContent
172+ case err != nil :
173+ return err
174+ default :
175+ return nil
60176 }
61- return err
62177}
63178
64179// CreateZipFromTar converts the given tarReader to a zip archive.
0 commit comments