Drop symlinks in os.ReadDir, os.ReadFile, os.Stat and os.FileExists

Fixes #15019
This commit is contained in:
Bjørn Erik Pedersen
2026-06-09 11:20:01 +02:00
parent 11f09f59fc
commit cf9c8f93ca
7 changed files with 115 additions and 8 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ require (
github.com/bep/lazycache v0.8.1
github.com/bep/logg v0.4.0
github.com/bep/mclib v1.20401.20400
github.com/bep/overlayfs v0.10.0
github.com/bep/overlayfs v0.11.0
github.com/bep/simplecobra v0.7.0
github.com/bep/textandbinarywriter v0.1.0
github.com/bep/tmc v0.6.0
+2
View File
@@ -174,6 +174,8 @@ github.com/bep/mclib v1.20401.20400 h1:silTOMNlNI7yHBb+HxEE0THIVFVWo/0I4SCH69Fxt
github.com/bep/mclib v1.20401.20400/go.mod h1:v5Hh3EIinPn7epigP28uf9JCkZlYzBS2vEOPe2wrHzM=
github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE=
github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro=
github.com/bep/overlayfs v0.11.0 h1:aymHDGC0CHpvn0XvTfgpK6skCp16oMi+tdUF32l6pPs=
github.com/bep/overlayfs v0.11.0/go.mod h1:L+ggdoKm+Y7Xb4a1osd+/LOPG4qsY62snqRqJH5Mspc=
github.com/bep/simplecobra v0.7.0 h1:kG8ZPwEc1o96hlIVGXcrrvwC8RornBqvMD3+pS0Z7y0=
github.com/bep/simplecobra v0.7.0/go.mod h1:PDXvBWH1ZMX05DRQ25ub/C6kKUuq+jROPgjbVz8wO1g=
github.com/bep/textandbinarywriter v0.1.0 h1:KXmXsRN2Uhwhm1G3e/snM8+5SPQBJrCEpIosdIBR3po=
+18
View File
@@ -46,6 +46,7 @@ func NewComponentFs(opts ComponentFsOptions) *componentFs {
var (
_ FilesystemUnwrapper = (*componentFs)(nil)
_ ReadDirWithContextDir = (*componentFsDir)(nil)
_ afero.Lstater = (*componentFs)(nil)
)
// componentFs is a filesystem that holds one of the Hugo components, e.g. content, layouts etc.
@@ -307,6 +308,23 @@ func (fs *componentFs) Stat(name string) (os.FileInfo, error) {
return fim, nil
}
func (fs *componentFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
if lstater, ok := fs.Fs.(afero.Lstater); ok {
fi, b, err := lstater.LstatIfPossible(name)
if err != nil {
return nil, false, err
}
fim, ok := fs.applyMeta(fi, name)
if !ok {
return nil, false, os.ErrNotExist
}
return fim, b, nil
}
fi, err := fs.Stat(name)
return fi, false, err
}
func (fs *componentFs) applyMeta(fi FileNameIsDir, name string) (FileMetaInfo, bool) {
if runtime.GOOS == "darwin" {
name = norm.NFC.String(name)
+10
View File
@@ -244,6 +244,8 @@ func WrapFilesystem(container, content afero.Fs) afero.Fs {
return filesystemsWrapper{Fs: container, content: content}
}
var _ afero.Lstater = (*filesystemsWrapper)(nil)
type filesystemsWrapper struct {
afero.Fs
content afero.Fs
@@ -253,6 +255,14 @@ func (w filesystemsWrapper) UnwrapFilesystem() afero.Fs {
return w.content
}
func (w filesystemsWrapper) LstatIfPossible(name string) (os.FileInfo, bool, error) {
if lstater, ok := w.Fs.(afero.Lstater); ok {
return lstater.LstatIfPossible(name)
}
fi, err := w.Fs.Stat(name)
return fi, false, err
}
type ReadDirWithContextDir interface {
ReadDirWithContext(context context.Context, count int) ([]iofs.DirEntry, context.Context, error)
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugofs
import (
"os"
"github.com/spf13/afero"
)
// NewDropSymlinksFs returns an afero.Fs wrapper that treats symlinks as non-existing files.
func NewDropSymlinksFs(base afero.Fs) *DropSymlinksFs {
return &DropSymlinksFs{base}
}
// DropSymlinksFs is an afero.Fs wrapper that treats symlinks as non-existing files.
type DropSymlinksFs struct {
afero.Fs
}
func (fs *DropSymlinksFs) Open(name string) (afero.File, error) {
if _, err := fs.Stat(name); err != nil {
return nil, err
}
f, err := fs.Fs.Open(name)
if err != nil {
return nil, err
}
return f, nil
}
func (fs *DropSymlinksFs) Stat(name string) (os.FileInfo, error) {
fi, err := LstatIfPossible(fs.Fs, name)
if err != nil {
return nil, err
}
if fi.Mode()&os.ModeSymlink != 0 {
return nil, os.ErrNotExist
}
return fi, nil
}
+19 -1
View File
@@ -3,6 +3,7 @@
ln ./rootfile.txt ./themes/mytheme/assets/modassetsymlink.txt
ln ./rootfile.txt ./themes/mytheme/static/modstaticsymlink.txt
ln ./README.md ./content/pagesymlink.md
ln ./rootdir ./assets/myassets/symlinkdir
hugo
@@ -27,13 +28,30 @@ 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 }}
{{ with resources.GetMatch "modassetsymlink.txt"}}FAIL {{ .Publish }}{{ else }}OK{{ end }}
{{ with resources.GetMatch "myassets/symlinkdir/**"}}FAIL {{ .Publish }}{{ else }}OK{{ end }}
Page: {{ .RelPermalink }}|{{ .Content }}|
{{/* os template package. */}}
{{ $symFilePath := "content/pagesymlink.md" }}
{{ with os.ReadDir "assets/myassets/symlinkdir" }}FAIL {{ len . }}{{ else }}OK{{ end }}
{{ with os.Stat $symFilePath }}FAIL{{ else }}OK{{ end }}
{{ with os.ReadFile $symFilePath }}FAIL{{ else }}OK{{ end }}
{{ with os.FileExists $symFilePath }}FAIL{{ else }}OK{{ end }}
-- content/pageok.md --
-- themes/mytheme/assets/modassetok.txt --
Content.
-- themes/mytheme/static/modstatictok.txt --
Content.
-- rootfile.txt --
Roo Content.
Root Content.
-- assets/myassets/myfile.txt --
My file.
-- rootdir/rootdirfile1.txt --
Rootdirfile1 content.
-- rootdir/rootdirfile2.txt --
Rootdirfile2 content.
+13 -6
View File
@@ -24,24 +24,25 @@ import (
"github.com/bep/overlayfs"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
"github.com/spf13/cast"
)
// New returns a new instance of the os-namespaced template functions.
func New(d *deps.Deps) *Namespace {
var readFileFs, workFs afero.Fs
var readFileFs, workFs *hugofs.DropSymlinksFs
// The docshelper script does not have or need all the dependencies set up.
if d.PathSpec != nil {
readFileFs = overlayfs.New(overlayfs.Options{
readFileFs = hugofs.NewDropSymlinksFs(overlayfs.New(overlayfs.Options{
Fss: []afero.Fs{
d.PathSpec.BaseFs.Work,
d.PathSpec.BaseFs.Content.Fs,
},
})
}))
// See #9599
workFs = d.PathSpec.BaseFs.WorkDir
workFs = hugofs.NewDropSymlinksFs(d.PathSpec.BaseFs.WorkDir)
}
return &Namespace{
@@ -53,8 +54,8 @@ func New(d *deps.Deps) *Namespace {
// Namespace provides template functions for the "os" namespace.
type Namespace struct {
readFileFs afero.Fs
workFs afero.Fs
readFileFs *hugofs.DropSymlinksFs
workFs *hugofs.DropSymlinksFs
deps *deps.Deps
}
@@ -118,6 +119,9 @@ func (ns *Namespace) ReadDir(i any) ([]_os.FileInfo, error) {
list, err := afero.ReadDir(ns.workFs, path)
if err != nil {
if herrors.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read directory %q: %s", path, err)
}
@@ -156,6 +160,9 @@ func (ns *Namespace) Stat(i any) (_os.FileInfo, error) {
r, err := ns.readFileFs.Stat(path)
if err != nil {
if herrors.IsNotExist(err) {
return nil, nil
}
return nil, err
}