1- // Package export writes filesystem data to portable archive formats.
1+ // Package export reads and writes filesystem data in portable archive formats.
22package export
33
44import (
55 "archive/zip"
6+ "bytes"
7+ "fmt"
68 "io"
79 "os"
810 "path"
@@ -28,7 +30,104 @@ func Zip(fs billy.Filesystem, w io.Writer) error {
2830 }
2931 }
3032
31- return zw .Close ()
33+ return nil
34+ }
35+
36+ // ValidateZip checks that data contains a safe, supported ZIP archive.
37+ func ValidateZip (data []byte ) error {
38+ _ , err := validatedZip (data )
39+ return err
40+ }
41+
42+ // Unzip restores a validated ZIP archive into fs.
43+ func Unzip (fs billy.Filesystem , data []byte ) error {
44+ zr , err := validatedZip (data )
45+ if err != nil {
46+ return err
47+ }
48+
49+ for _ , file := range zr .File {
50+ name , _ := validZipPath (file .Name )
51+ if file .FileInfo ().IsDir () {
52+ if err := fs .MkdirAll (name , directoryMode (file .Mode ())); err != nil {
53+ return fmt .Errorf ("create directory %q: %w" , name , err )
54+ }
55+ continue
56+ }
57+ if err := extractZipFile (fs , file , name ); err != nil {
58+ return err
59+ }
60+ }
61+ return nil
62+ }
63+
64+ func validatedZip (data []byte ) (* zip.Reader , error ) {
65+ zr , err := zip .NewReader (bytes .NewReader (data ), int64 (len (data )))
66+ if err != nil {
67+ return nil , fmt .Errorf ("open ZIP: %w" , err )
68+ }
69+
70+ seen := make (map [string ]struct {}, len (zr .File ))
71+ for _ , file := range zr .File {
72+ name , err := validZipPath (file .Name )
73+ if err != nil {
74+ return nil , err
75+ }
76+ if _ , duplicate := seen [name ]; duplicate {
77+ return nil , fmt .Errorf ("duplicate ZIP path %q" , name )
78+ }
79+ seen [name ] = struct {}{}
80+ if ! file .FileInfo ().IsDir () && ! file .Mode ().IsRegular () {
81+ return nil , fmt .Errorf ("unsupported ZIP entry %q" , file .Name )
82+ }
83+ }
84+ return zr , nil
85+ }
86+
87+ func validZipPath (name string ) (string , error ) {
88+ trimmed := strings .TrimSuffix (name , "/" )
89+ cleaned := path .Clean (trimmed )
90+ if trimmed == "" || cleaned != trimmed || path .IsAbs (cleaned ) || cleaned == ".." ||
91+ strings .HasPrefix (cleaned , "../" ) || strings .ContainsAny (cleaned , `\:` ) {
92+ return "" , fmt .Errorf ("invalid ZIP path %q" , name )
93+ }
94+ return cleaned , nil
95+ }
96+
97+ func extractZipFile (fs billy.Filesystem , entry * zip.File , name string ) error {
98+ if dir := path .Dir (name ); dir != "." {
99+ if err := fs .MkdirAll (dir , 0o755 ); err != nil {
100+ return fmt .Errorf ("create directory %q: %w" , dir , err )
101+ }
102+ }
103+
104+ source , err := entry .Open ()
105+ if err != nil {
106+ return fmt .Errorf ("open ZIP entry %q: %w" , name , err )
107+ }
108+ defer source .Close ()
109+
110+ mode := entry .Mode ().Perm ()
111+ if mode == 0 {
112+ mode = 0o644
113+ }
114+ destination , err := fs .OpenFile (name , os .O_CREATE | os .O_WRONLY | os .O_TRUNC , mode )
115+ if err != nil {
116+ return fmt .Errorf ("create restored file %q: %w" , name , err )
117+ }
118+ defer destination .Close ()
119+
120+ if _ , err := io .Copy (destination , source ); err != nil {
121+ return fmt .Errorf ("restore file %q: %w" , name , err )
122+ }
123+ return nil
124+ }
125+
126+ func directoryMode (mode os.FileMode ) os.FileMode {
127+ if mode = mode .Perm (); mode == 0 {
128+ return 0o755
129+ }
130+ return mode
32131}
33132
34133type fileEntry struct {
0 commit comments