arblant
firefox-vpn-client / core / internal / update / unzip.go
unzip.go99 lines · 2.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
package update

import (
	"archive/zip"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
)

// unzip unpacks the zip at file into dir. The release's zip holds one
// folder ("Firefox VPN/...") whose contents go into dir; names that would
// land outside dir are refused, on every system as on Windows (no colon:
// a drive, or an NTFS stream), and so are links.
func unzip(file, dir string) error {
	r, err := zip.OpenReader(file)
	if err != nil {
		return err
	}
	defer r.Close()
	top := topFolder(r.File)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	for _, f := range r.File {
		name := strings.TrimPrefix(zipName(f), top)
		if name == "" {
			continue
		}
		if strings.Contains(name, ":") || !filepath.IsLocal(filepath.FromSlash(strings.TrimSuffix(name, "/"))) {
			return fmt.Errorf("%q is outside the app's folder", f.Name)
		}
		if f.Mode()&os.ModeSymlink != 0 {
			return fmt.Errorf("%q is a link", f.Name)
		}
		dest := filepath.Join(dir, filepath.FromSlash(name))
		if f.FileInfo().IsDir() || strings.HasSuffix(name, "/") {
			if err := os.MkdirAll(dest, 0o755); err != nil {
				return err
			}
			continue
		}
		if err := unzipFile(f, dest); err != nil {
			return fmt.Errorf("%s: %w", name, err)
		}
	}
	return nil
}

// zipName is an entry's name with forward slashes.
func zipName(f *zip.File) string {
	return strings.TrimPrefix(strings.ReplaceAll(f.Name, `\`, "/"), "./")
}

// topFolder is "<folder>/" when every entry is inside that one folder, else
// "".
func topFolder(files []*zip.File) string {
	top := ""
	for _, f := range files {
		first, _, inside := strings.Cut(zipName(f), "/")
		if !inside && !f.FileInfo().IsDir() {
			return ""
		}
		if top == "" {
			top = first
		} else if first != top {
			return ""
		}
	}
	if top == "" {
		return ""
	}
	return top + "/"
}

func unzipFile(f *zip.File, dest string) error {
	if f.UncompressedSize64 > 4*maxPackageSize {
		return errors.New("too large")
	}
	if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
		return err
	}
	in, err := f.Open()
	if err != nil {
		return err
	}
	defer in.Close()
	out, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
	if err != nil {
		return err
	}
	if _, err := io.Copy(out, in); err != nil {
		out.Close()
		return err
	}
	return out.Close()
}