diff --git a/hugofs/decorators.go b/hugofs/decorators.go index 405c81ce4..a0f81160e 100644 --- a/hugofs/decorators.go +++ b/hugofs/decorators.go @@ -106,6 +106,22 @@ func (fs *baseFileDecoratorFs) Stat(name string) (os.FileInfo, error) { return fim.(os.FileInfo), nil } +func (fs *baseFileDecoratorFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + if lstater, ok := fs.Fs.(afero.Lstater); ok { + fi, ok, err := lstater.LstatIfPossible(name) + if err != nil { + return nil, false, err + } + fim, err := fs.decorate(fi, name) + if err != nil { + return nil, false, err + } + return fim.(os.FileInfo), ok, nil + } + fi, err := fs.Stat(name) + return fi, false, err +} + func (fs *baseFileDecoratorFs) Open(name string) (afero.File, error) { return fs.open(name) } diff --git a/hugofs/fs.go b/hugofs/fs.go index ecc62470e..734521c17 100644 --- a/hugofs/fs.go +++ b/hugofs/fs.go @@ -267,3 +267,13 @@ func ReadDirWithContext(ctx context.Context, f DirOnlyOps, count int) ([]iofs.Di } return v, ctx, nil } + +// LstatIfPossible tries to use LstatIfPossible if the filesystem supports it, otherwise it falls back to Stat. +func LstatIfPossible(fs afero.Fs, name string) (os.FileInfo, error) { + if lstater, ok := fs.(afero.Lstater); ok { + fi, _, err := lstater.LstatIfPossible(name) + return fi, err + } + fi, err := fs.Stat(name) + return fi, err +} diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go index 99c0b4cdc..f9c9c133e 100644 --- a/hugofs/rootmapping_fs.go +++ b/hugofs/rootmapping_fs.go @@ -724,11 +724,16 @@ func (fs *RootMappingFs) statRoot(root *RootMapping, filename string) (FileMetaI } filename = root.filename(filename) - fi, err := fs.Fs.Stat(filename) + fi, err := LstatIfPossible(fs.Fs, filename) if err != nil { return nil, err } + // Don't allow symlinks to escape the mount. + if fi.Mode()&os.ModeSymlink != 0 { + return nil, os.ErrNotExist + } + var opener func() (afero.File, error) if !fi.IsDir() { // Open the file directly. diff --git a/main_test.go b/main_test.go index 36a28debc..12e708dc0 100644 --- a/main_test.go +++ b/main_test.go @@ -237,6 +237,21 @@ var commonTestScriptsParam = testscript.Params{ ts.Fatalf("failed to write file: %v", err) } }, + // ln creates a symlink, but throws an error on Windows. + "ln": func(ts *testscript.TestScript, neg bool, args []string) { + if runtime.GOOS == "windows" { + ts.Fatalf("ln is not supported on Windows") + } + if len(args) != 2 { + ts.Fatalf("usage: ln TARGET LINKNAME") + } + target := ts.MkAbs(args[0]) + linkname := ts.MkAbs(args[1]) + err := os.Symlink(target, linkname) + if err != nil { + ts.Fatalf("failed to create symlink: %v", err) + } + }, // httpget checks that a HTTP resource's body matches (if it compiles as a regexp) or contains all of the strings given as arguments. "httpget": func(ts *testscript.TestScript, neg bool, args []string) { @@ -314,6 +329,10 @@ var commonTestScriptsParam = testscript.Params{ if !ok { ts.Fatalf("stat %s: %v", filename, err) } + if ok && neg { + // OK. + continue + } if fi.Size() == 0 { ts.Fatalf("%s is empty", filename) } diff --git a/testscripts/commands/no_symlinks.txt b/testscripts/commands/no_symlinks.txt new file mode 100644 index 000000000..1a706f996 --- /dev/null +++ b/testscripts/commands/no_symlinks.txt @@ -0,0 +1,39 @@ +[windows] skip + +ln ./rootfile.txt ./themes/mytheme/assets/modassetsymlink.txt +ln ./rootfile.txt ./themes/mytheme/static/modstaticsymlink.txt +ln ./README.md ./content/pagesymlink.md + +hugo + +grep 'OK' public/index.html +! grep 'FAIL' public/index.html + +tree public + +stdout modassetok +! stdout modassetsymlink +stdout 'modstatictok' +! stdout 'modstaticsymlink' +stdout pageok +! stdout pagesymlink + +-- hugo.toml -- +disableKinds = ["taxonomy", "term", "rss"] +[[module.imports]] +path = 'mytheme' +-- README.md -- +Read me. +-- layouts/all.html -- +{{ with resources.Get "modassetok.txt"}}OK {{ .Publish }}{{ else }}FAIL{{ end }} +{{ with resources.Get "modassetsymlink.txt"}}FAIL {{ .Publish }}{{ else }}OK{{ end }} +Page: {{ .RelPermalink }}|{{ .Content }}| +-- content/pageok.md -- +-- themes/mytheme/assets/modassetok.txt -- +Content. +-- themes/mytheme/static/modstatictok.txt -- +Content. +-- rootfile.txt -- +Roo Content. + +