1// Copyright 2018 The Hugo Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package hugofs
15
16import (
17	"errors"
18	"os"
19	"path/filepath"
20
21	"github.com/gohugoio/hugo/common/loggers"
22
23	"github.com/spf13/afero"
24)
25
26var ErrPermissionSymlink = errors.New("symlinks not allowed in this filesystem")
27
28// NewNoSymlinkFs creates a new filesystem that prevents symlinks.
29func NewNoSymlinkFs(fs afero.Fs, logger loggers.Logger, allowFiles bool) afero.Fs {
30	return &noSymlinkFs{Fs: fs, logger: logger, allowFiles: allowFiles}
31}
32
33// noSymlinkFs is a filesystem that prevents symlinking.
34type noSymlinkFs struct {
35	allowFiles bool // block dirs only
36	logger     loggers.Logger
37	afero.Fs
38}
39
40type noSymlinkFile struct {
41	fs *noSymlinkFs
42	afero.File
43}
44
45func (f *noSymlinkFile) Readdir(count int) ([]os.FileInfo, error) {
46	fis, err := f.File.Readdir(count)
47
48	filtered := fis[:0]
49	for _, x := range fis {
50		filename := filepath.Join(f.Name(), x.Name())
51		if _, err := f.fs.checkSymlinkStatus(filename, x); err != nil {
52			// Log a warning and drop the file from the list
53			logUnsupportedSymlink(filename, f.fs.logger)
54		} else {
55			filtered = append(filtered, x)
56		}
57	}
58
59	return filtered, err
60}
61
62func (f *noSymlinkFile) Readdirnames(count int) ([]string, error) {
63	dirs, err := f.Readdir(count)
64	if err != nil {
65		return nil, err
66	}
67	return fileInfosToNames(dirs), nil
68}
69
70func (fs *noSymlinkFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
71	return fs.stat(name)
72}
73
74func (fs *noSymlinkFs) Stat(name string) (os.FileInfo, error) {
75	fi, _, err := fs.stat(name)
76	return fi, err
77}
78
79func (fs *noSymlinkFs) stat(name string) (os.FileInfo, bool, error) {
80	var (
81		fi       os.FileInfo
82		wasLstat bool
83		err      error
84	)
85
86	if lstater, ok := fs.Fs.(afero.Lstater); ok {
87		fi, wasLstat, err = lstater.LstatIfPossible(name)
88	} else {
89		fi, err = fs.Fs.Stat(name)
90	}
91
92	if err != nil {
93		return nil, false, err
94	}
95
96	fi, err = fs.checkSymlinkStatus(name, fi)
97
98	return fi, wasLstat, err
99}
100
101func (fs *noSymlinkFs) checkSymlinkStatus(name string, fi os.FileInfo) (os.FileInfo, error) {
102	var metaIsSymlink bool
103
104	if fim, ok := fi.(FileMetaInfo); ok {
105		meta := fim.Meta()
106		metaIsSymlink = meta.IsSymlink
107	}
108
109	if metaIsSymlink {
110		if fs.allowFiles && !fi.IsDir() {
111			return fi, nil
112		}
113		return nil, ErrPermissionSymlink
114	}
115
116	// Also support non-decorated filesystems, e.g. the Os fs.
117	if isSymlink(fi) {
118		// Need to determine if this is a directory or not.
119		_, sfi, err := evalSymlinks(fs.Fs, name)
120		if err != nil {
121			return nil, err
122		}
123		if fs.allowFiles && !sfi.IsDir() {
124			// Return the original FileInfo to get the expected Name.
125			return fi, nil
126		}
127		return nil, ErrPermissionSymlink
128	}
129
130	return fi, nil
131}
132
133func (fs *noSymlinkFs) Open(name string) (afero.File, error) {
134	if _, _, err := fs.stat(name); err != nil {
135		return nil, err
136	}
137	return fs.wrapFile(fs.Fs.Open(name))
138}
139
140func (fs *noSymlinkFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
141	if _, _, err := fs.stat(name); err != nil {
142		return nil, err
143	}
144	return fs.wrapFile(fs.Fs.OpenFile(name, flag, perm))
145}
146
147func (fs *noSymlinkFs) wrapFile(f afero.File, err error) (afero.File, error) {
148	if err != nil {
149		return nil, err
150	}
151
152	return &noSymlinkFile{File: f, fs: fs}, nil
153}
154