1// Copyright ©2015 Steve Francia <spf@spf13.com>
2// Portions Copyright ©2015 The Hugo Authors
3// Portions Copyright 2016-present Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//     http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package afero
18
19import (
20	"bytes"
21	"fmt"
22	"io"
23	"log"
24	"os"
25	"path/filepath"
26	"strings"
27	"unicode"
28
29	"golang.org/x/text/transform"
30	"golang.org/x/text/unicode/norm"
31)
32
33// Filepath separator defined by os.Separator.
34const FilePathSeparator = string(filepath.Separator)
35
36// Takes a reader and a path and writes the content
37func (a Afero) WriteReader(path string, r io.Reader) (err error) {
38	return WriteReader(a.Fs, path, r)
39}
40
41func WriteReader(fs Fs, path string, r io.Reader) (err error) {
42	dir, _ := filepath.Split(path)
43	ospath := filepath.FromSlash(dir)
44
45	if ospath != "" {
46		err = fs.MkdirAll(ospath, 0777) // rwx, rw, r
47		if err != nil {
48			if err != os.ErrExist {
49				log.Panicln(err)
50			}
51		}
52	}
53
54	file, err := fs.Create(path)
55	if err != nil {
56		return
57	}
58	defer file.Close()
59
60	_, err = io.Copy(file, r)
61	return
62}
63
64// Same as WriteReader but checks to see if file/directory already exists.
65func (a Afero) SafeWriteReader(path string, r io.Reader) (err error) {
66	return SafeWriteReader(a.Fs, path, r)
67}
68
69func SafeWriteReader(fs Fs, path string, r io.Reader) (err error) {
70	dir, _ := filepath.Split(path)
71	ospath := filepath.FromSlash(dir)
72
73	if ospath != "" {
74		err = fs.MkdirAll(ospath, 0777) // rwx, rw, r
75		if err != nil {
76			return
77		}
78	}
79
80	exists, err := Exists(fs, path)
81	if err != nil {
82		return
83	}
84	if exists {
85		return fmt.Errorf("%v already exists", path)
86	}
87
88	file, err := fs.Create(path)
89	if err != nil {
90		return
91	}
92	defer file.Close()
93
94	_, err = io.Copy(file, r)
95	return
96}
97
98func (a Afero) GetTempDir(subPath string) string {
99	return GetTempDir(a.Fs, subPath)
100}
101
102// GetTempDir returns the default temp directory with trailing slash
103// if subPath is not empty then it will be created recursively with mode 777 rwx rwx rwx
104func GetTempDir(fs Fs, subPath string) string {
105	addSlash := func(p string) string {
106		if FilePathSeparator != p[len(p)-1:] {
107			p = p + FilePathSeparator
108		}
109		return p
110	}
111	dir := addSlash(os.TempDir())
112
113	if subPath != "" {
114		// preserve windows backslash :-(
115		if FilePathSeparator == "\\" {
116			subPath = strings.Replace(subPath, "\\", "____", -1)
117		}
118		dir = dir + UnicodeSanitize((subPath))
119		if FilePathSeparator == "\\" {
120			dir = strings.Replace(dir, "____", "\\", -1)
121		}
122
123		if exists, _ := Exists(fs, dir); exists {
124			return addSlash(dir)
125		}
126
127		err := fs.MkdirAll(dir, 0777)
128		if err != nil {
129			panic(err)
130		}
131		dir = addSlash(dir)
132	}
133	return dir
134}
135
136// Rewrite string to remove non-standard path characters
137func UnicodeSanitize(s string) string {
138	source := []rune(s)
139	target := make([]rune, 0, len(source))
140
141	for _, r := range source {
142		if unicode.IsLetter(r) ||
143			unicode.IsDigit(r) ||
144			unicode.IsMark(r) ||
145			r == '.' ||
146			r == '/' ||
147			r == '\\' ||
148			r == '_' ||
149			r == '-' ||
150			r == '%' ||
151			r == ' ' ||
152			r == '#' {
153			target = append(target, r)
154		}
155	}
156
157	return string(target)
158}
159
160// Transform characters with accents into plan forms
161func NeuterAccents(s string) string {
162	t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
163	result, _, _ := transform.String(t, string(s))
164
165	return result
166}
167
168func isMn(r rune) bool {
169	return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks
170}
171
172func (a Afero) FileContainsBytes(filename string, subslice []byte) (bool, error) {
173	return FileContainsBytes(a.Fs, filename, subslice)
174}
175
176// Check if a file contains a specified byte slice.
177func FileContainsBytes(fs Fs, filename string, subslice []byte) (bool, error) {
178	f, err := fs.Open(filename)
179	if err != nil {
180		return false, err
181	}
182	defer f.Close()
183
184	return readerContainsAny(f, subslice), nil
185}
186
187func (a Afero) FileContainsAnyBytes(filename string, subslices [][]byte) (bool, error) {
188	return FileContainsAnyBytes(a.Fs, filename, subslices)
189}
190
191// Check if a file contains any of the specified byte slices.
192func FileContainsAnyBytes(fs Fs, filename string, subslices [][]byte) (bool, error) {
193	f, err := fs.Open(filename)
194	if err != nil {
195		return false, err
196	}
197	defer f.Close()
198
199	return readerContainsAny(f, subslices...), nil
200}
201
202// readerContains reports whether any of the subslices is within r.
203func readerContainsAny(r io.Reader, subslices ...[]byte) bool {
204
205	if r == nil || len(subslices) == 0 {
206		return false
207	}
208
209	largestSlice := 0
210
211	for _, sl := range subslices {
212		if len(sl) > largestSlice {
213			largestSlice = len(sl)
214		}
215	}
216
217	if largestSlice == 0 {
218		return false
219	}
220
221	bufflen := largestSlice * 4
222	halflen := bufflen / 2
223	buff := make([]byte, bufflen)
224	var err error
225	var n, i int
226
227	for {
228		i++
229		if i == 1 {
230			n, err = io.ReadAtLeast(r, buff[:halflen], halflen)
231		} else {
232			if i != 2 {
233				// shift left to catch overlapping matches
234				copy(buff[:], buff[halflen:])
235			}
236			n, err = io.ReadAtLeast(r, buff[halflen:], halflen)
237		}
238
239		if n > 0 {
240			for _, sl := range subslices {
241				if bytes.Contains(buff, sl) {
242					return true
243				}
244			}
245		}
246
247		if err != nil {
248			break
249		}
250	}
251	return false
252}
253
254func (a Afero) DirExists(path string) (bool, error) {
255	return DirExists(a.Fs, path)
256}
257
258// DirExists checks if a path exists and is a directory.
259func DirExists(fs Fs, path string) (bool, error) {
260	fi, err := fs.Stat(path)
261	if err == nil && fi.IsDir() {
262		return true, nil
263	}
264	if os.IsNotExist(err) {
265		return false, nil
266	}
267	return false, err
268}
269
270func (a Afero) IsDir(path string) (bool, error) {
271	return IsDir(a.Fs, path)
272}
273
274// IsDir checks if a given path is a directory.
275func IsDir(fs Fs, path string) (bool, error) {
276	fi, err := fs.Stat(path)
277	if err != nil {
278		return false, err
279	}
280	return fi.IsDir(), nil
281}
282
283func (a Afero) IsEmpty(path string) (bool, error) {
284	return IsEmpty(a.Fs, path)
285}
286
287// IsEmpty checks if a given file or directory is empty.
288func IsEmpty(fs Fs, path string) (bool, error) {
289	if b, _ := Exists(fs, path); !b {
290		return false, fmt.Errorf("%q path does not exist", path)
291	}
292	fi, err := fs.Stat(path)
293	if err != nil {
294		return false, err
295	}
296	if fi.IsDir() {
297		f, err := fs.Open(path)
298		if err != nil {
299			return false, err
300		}
301		defer f.Close()
302		list, err := f.Readdir(-1)
303		return len(list) == 0, nil
304	}
305	return fi.Size() == 0, nil
306}
307
308func (a Afero) Exists(path string) (bool, error) {
309	return Exists(a.Fs, path)
310}
311
312// Check if a file or directory exists.
313func Exists(fs Fs, path string) (bool, error) {
314	_, err := fs.Stat(path)
315	if err == nil {
316		return true, nil
317	}
318	if os.IsNotExist(err) {
319		return false, nil
320	}
321	return false, err
322}
323
324func FullBaseFsPath(basePathFs *BasePathFs, relativePath string) string {
325	combinedPath := filepath.Join(basePathFs.path, relativePath)
326	if parent, ok := basePathFs.source.(*BasePathFs); ok {
327		return FullBaseFsPath(parent, combinedPath)
328	}
329
330	return combinedPath
331}
332