@@ -29,6 +29,7 @@ import (
2929 "strings"
3030
3131 "github.com/bodgit/sevenzip"
32+ "github.com/klauspost/compress/zstd"
3233 "github.com/ulikunitz/xz"
3334)
3435
@@ -262,6 +263,159 @@ loop:
262263 return nil
263264}
264265
266+ type ZstdTarDecompressor struct {
267+ src string
268+ }
269+
270+ func (z * ZstdTarDecompressor ) Decompress (dest string ) error {
271+ rootFolderInTar := findRootFolderInZstdTar (z .src )
272+ file , err := os .Open (z .src )
273+ if err != nil {
274+ return err
275+ }
276+ defer file .Close ()
277+
278+ zr , err := zstd .NewReader (file )
279+ if err != nil {
280+ return err
281+ }
282+ defer zr .Close ()
283+
284+ tr := tar .NewReader (zr )
285+ var symlinks []symlink
286+
287+ loop:
288+ for {
289+ header , err := tr .Next ()
290+ switch {
291+ case err == io .EOF :
292+ break loop
293+ case err != nil :
294+ return err
295+ case header == nil :
296+ continue
297+ }
298+
299+ target , err := safeZstdTarTarget (dest , header .Name , rootFolderInTar )
300+ if err != nil {
301+ return err
302+ }
303+
304+ switch header .Typeflag {
305+ case tar .TypeDir :
306+ if _ , err := os .Stat (target ); err != nil {
307+ if err := os .MkdirAll (target , 0755 ); err != nil {
308+ return err
309+ }
310+ }
311+ case tar .TypeReg :
312+ if err := os .MkdirAll (filepath .Dir (target ), 0755 ); err != nil {
313+ return err
314+ }
315+ f , err := os .OpenFile (target , os .O_CREATE | os .O_RDWR , os .FileMode (header .Mode ))
316+ if err != nil {
317+ return err
318+ }
319+ if _ , err := io .Copy (f , tr ); err != nil {
320+ _ = f .Close ()
321+ return err
322+ }
323+ if err := f .Close (); err != nil {
324+ return err
325+ }
326+ case tar .TypeSymlink :
327+ symlinks = append (symlinks , symlink {header .Linkname , target })
328+ }
329+ }
330+
331+ for _ , s := range symlinks {
332+ dir := filepath .Dir (s .newname )
333+ if _ , err := os .Stat (dir ); os .IsNotExist (err ) {
334+ if err := os .MkdirAll (dir , 0755 ); err != nil {
335+ return err
336+ }
337+ }
338+ if err = os .Symlink (s .oldname , s .newname ); err != nil {
339+ return err
340+ }
341+ }
342+ return nil
343+ }
344+
345+ func findRootFolderInZstdTar (tarFilePath string ) string {
346+ file , err := os .Open (tarFilePath )
347+ if err != nil {
348+ return ""
349+ }
350+ defer file .Close ()
351+
352+ zr , err := zstd .NewReader (file )
353+ if err != nil {
354+ return ""
355+ }
356+ defer zr .Close ()
357+
358+ tr := tar .NewReader (zr )
359+ var firstElement string
360+
361+ for {
362+ header , err := tr .Next ()
363+ if err == io .EOF {
364+ break
365+ }
366+ if err != nil || header == nil {
367+ return ""
368+ }
369+
370+ normalizedPath := strings .Trim (strings .ReplaceAll (header .Name , "\\ " , "/" ), "/" )
371+ if normalizedPath == "" || strings .HasPrefix (normalizedPath , ".DS_Store" ) || strings .HasPrefix (normalizedPath , "__MACOSX" ) {
372+ continue
373+ }
374+
375+ currentFirstElement := strings .Split (normalizedPath , "/" )[0 ]
376+ if firstElement != "" && firstElement != currentFirstElement {
377+ return ""
378+ }
379+ if firstElement == "" {
380+ firstElement = currentFirstElement
381+ }
382+ }
383+ return firstElement
384+ }
385+
386+ func safeZstdTarTarget (dest string , name string , rootFolderInTar string ) (string , error ) {
387+ normalizedPath := strings .ReplaceAll (name , "\\ " , "/" )
388+ if strings .HasPrefix (normalizedPath , "/" ) {
389+ return "" , fmt .Errorf ("archive entry %q is outside destination" , name )
390+ }
391+ normalizedPath = strings .Trim (normalizedPath , "/" )
392+ if normalizedPath == "" {
393+ return "" , fmt .Errorf ("archive entry %q is empty" , name )
394+ }
395+
396+ parts := strings .Split (normalizedPath , "/" )
397+ if len (parts ) > 1 && rootFolderInTar != "" && parts [0 ] == rootFolderInTar {
398+ parts = parts [1 :]
399+ }
400+ fname := filepath .Clean (strings .Join (parts , "/" ))
401+ if fname == "." {
402+ return "" , fmt .Errorf ("archive entry %q is empty" , name )
403+ }
404+ if ! filepath .IsLocal (fname ) {
405+ return "" , fmt .Errorf ("archive entry %q is outside destination" , name )
406+ }
407+
408+ target := filepath .Join (dest , fname )
409+ rel , err := filepath .Rel (dest , target )
410+ if err != nil {
411+ return "" , err
412+ }
413+ if rel == ".." || strings .HasPrefix (rel , ".." + string (os .PathSeparator )) {
414+ return "" , fmt .Errorf ("archive entry %q is outside destination" , name )
415+ }
416+ return target , nil
417+ }
418+
265419type ZipDecompressor struct {
266420 src string
267421}
@@ -525,6 +679,11 @@ func NewDecompressor(src string) Decompressor {
525679 src : src ,
526680 }
527681 }
682+ if strings .HasSuffix (filename , ".tar.zst" ) || strings .HasSuffix (filename , ".tzst" ) {
683+ return & ZstdTarDecompressor {
684+ src : src ,
685+ }
686+ }
528687 if strings .HasSuffix (filename , ".zip" ) {
529688 return & ZipDecompressor {
530689 src : src ,
0 commit comments