1// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package modfetch
6
7import (
8	"archive/zip"
9	"fmt"
10	"io"
11	"io/ioutil"
12	"os"
13	"path"
14	"path/filepath"
15	"strings"
16
17	"cmd/go/internal/modfetch/codehost"
18	"cmd/go/internal/module"
19	"cmd/go/internal/str"
20)
21
22func Unzip(dir, zipfile, prefix string, maxSize int64) error {
23	// TODO(bcmills): The maxSize parameter is invariantly 0. Remove it.
24	if maxSize == 0 {
25		maxSize = codehost.MaxZipFile
26	}
27
28	// Directory can exist, but must be empty.
29	files, _ := ioutil.ReadDir(dir)
30	if len(files) > 0 {
31		return fmt.Errorf("target directory %v exists and is not empty", dir)
32	}
33	if err := os.MkdirAll(dir, 0777); err != nil {
34		return err
35	}
36
37	f, err := os.Open(zipfile)
38	if err != nil {
39		return err
40	}
41	defer f.Close()
42	info, err := f.Stat()
43	if err != nil {
44		return err
45	}
46
47	z, err := zip.NewReader(f, info.Size())
48	if err != nil {
49		return fmt.Errorf("unzip %v: %s", zipfile, err)
50	}
51
52	foldPath := make(map[string]string)
53	var checkFold func(string) error
54	checkFold = func(name string) error {
55		fold := str.ToFold(name)
56		if foldPath[fold] == name {
57			return nil
58		}
59		dir := path.Dir(name)
60		if dir != "." {
61			if err := checkFold(dir); err != nil {
62				return err
63			}
64		}
65		if foldPath[fold] == "" {
66			foldPath[fold] = name
67			return nil
68		}
69		other := foldPath[fold]
70		return fmt.Errorf("unzip %v: case-insensitive file name collision: %q and %q", zipfile, other, name)
71	}
72
73	// Check total size, valid file names.
74	var size int64
75	for _, zf := range z.File {
76		if !str.HasPathPrefix(zf.Name, prefix) {
77			return fmt.Errorf("unzip %v: unexpected file name %s", zipfile, zf.Name)
78		}
79		if zf.Name == prefix || strings.HasSuffix(zf.Name, "/") {
80			continue
81		}
82		name := zf.Name[len(prefix)+1:]
83		if err := module.CheckFilePath(name); err != nil {
84			return fmt.Errorf("unzip %v: %v", zipfile, err)
85		}
86		if err := checkFold(name); err != nil {
87			return err
88		}
89		if path.Clean(zf.Name) != zf.Name || strings.HasPrefix(zf.Name[len(prefix)+1:], "/") {
90			return fmt.Errorf("unzip %v: invalid file name %s", zipfile, zf.Name)
91		}
92		s := int64(zf.UncompressedSize64)
93		if s < 0 || maxSize-size < s {
94			return fmt.Errorf("unzip %v: content too large", zipfile)
95		}
96		size += s
97	}
98
99	// Unzip, enforcing sizes checked earlier.
100	for _, zf := range z.File {
101		if zf.Name == prefix || strings.HasSuffix(zf.Name, "/") {
102			continue
103		}
104		name := zf.Name[len(prefix):]
105		dst := filepath.Join(dir, name)
106		if err := os.MkdirAll(filepath.Dir(dst), 0777); err != nil {
107			return err
108		}
109		w, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0444)
110		if err != nil {
111			return fmt.Errorf("unzip %v: %v", zipfile, err)
112		}
113		r, err := zf.Open()
114		if err != nil {
115			w.Close()
116			return fmt.Errorf("unzip %v: %v", zipfile, err)
117		}
118		lr := &io.LimitedReader{R: r, N: int64(zf.UncompressedSize64) + 1}
119		_, err = io.Copy(w, lr)
120		r.Close()
121		if err != nil {
122			w.Close()
123			return fmt.Errorf("unzip %v: %v", zipfile, err)
124		}
125		if err := w.Close(); err != nil {
126			return fmt.Errorf("unzip %v: %v", zipfile, err)
127		}
128		if lr.N <= 0 {
129			return fmt.Errorf("unzip %v: content too large", zipfile)
130		}
131	}
132
133	return nil
134}
135
136// makeDirsReadOnly makes a best-effort attempt to remove write permissions for dir
137// and its transitive contents.
138func makeDirsReadOnly(dir string) {
139	type pathMode struct {
140		path string
141		mode os.FileMode
142	}
143	var dirs []pathMode // in lexical order
144	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
145		if err == nil && info.Mode()&0222 != 0 {
146			if info.IsDir() {
147				dirs = append(dirs, pathMode{path, info.Mode()})
148			}
149		}
150		return nil
151	})
152
153	// Run over list backward to chmod children before parents.
154	for i := len(dirs) - 1; i >= 0; i-- {
155		os.Chmod(dirs[i].path, dirs[i].mode&^0222)
156	}
157}
158
159// RemoveAll removes a directory written by Download or Unzip, first applying
160// any permission changes needed to do so.
161func RemoveAll(dir string) error {
162	// Module cache has 0555 directories; make them writable in order to remove content.
163	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
164		if err != nil {
165			return nil // ignore errors walking in file system
166		}
167		if info.IsDir() {
168			os.Chmod(path, 0777)
169		}
170		return nil
171	})
172	return os.RemoveAll(dir)
173}
174