Files
overlayfs/mergefs.go
Matheus Sampaio Queiroga bdaebacbf1
Some checks failed
Golang test / go-test (ubuntu-latest) (push) Successful in 16s
Golang test / go-test (windows-latest) (push) Failing after 6m56s
Refactor overlayfs: improve path validation and clean up code structure; add comprehensive tests for read/write operations.
Signed-off-by: Matheus Sampaio Queiroga <srherobrine20@gmail.com>
2025-07-13 01:11:54 -03:00

253 lines
6.4 KiB
Go

package overlayfs
import (
"errors"
"io"
"io/fs"
"iter"
"os"
"path/filepath"
"slices"
"strings"
)
var ErrNotImplemented = errors.New("not implemented")
const (
OpaqueWhiteout string = ".wh." // File to ignore
fileCreateFlags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY // Flags to create file, if exists replace data
)
type flages int
func (f flages) Flags() (flags []int) {
if int(f)&os.O_RDONLY != 0 {
flags = append(flags, os.O_RDONLY)
}
if int(f)&os.O_WRONLY != 0 {
flags = append(flags, os.O_WRONLY)
}
if int(f)&os.O_RDWR != 0 {
flags = append(flags, os.O_RDWR)
}
if int(f)&os.O_APPEND != 0 {
flags = append(flags, os.O_APPEND)
}
if int(f)&os.O_CREATE != 0 {
flags = append(flags, os.O_CREATE)
}
if int(f)&os.O_EXCL != 0 {
flags = append(flags, os.O_EXCL)
}
if int(f)&os.O_SYNC != 0 {
flags = append(flags, os.O_SYNC)
}
if int(f)&os.O_TRUNC != 0 {
flags = append(flags, os.O_TRUNC)
}
return
}
// Check flag have write permissions
func (f flages) IsRead() bool {
flags := f.Flags()
return slices.Contains(flags, os.O_RDWR) ||
slices.Contains(flags, os.O_RDONLY)
}
// Check flag have write permissions
func (f flages) IsWrite() bool {
flags := f.Flags()
return slices.Contains(flags, os.O_RDWR) ||
slices.Contains(flags, os.O_WRONLY) ||
slices.Contains(flags, os.O_CREATE) ||
slices.Contains(flags, os.O_TRUNC)
}
func (f flages) CreateIfNotExist() bool {
flags := f.Flags()
return slices.Contains(flags, os.O_CREATE)
}
func rewriteName(name string, err error) error {
if err == nil {
return err
} else if e, ok := err.(*fs.PathError); ok {
e.Path = name
} else if e, ok := err.(*os.LinkError); ok {
e.Old = name
}
return err
}
// Check if rw overlayfs
func (over Overlayfs) isRW() bool {
rooted := strings.TrimSpace(over.Upper)
return rooted != "" && (filepath.IsAbs(rooted) || filepath.IsLocal(rooted))
}
type fileLayer struct {
Layer string // layer Path
Stat fs.FileInfo // File info
FromUpper bool // is file present on Upper dir
}
func (over Overlayfs) allLayersSeq() iter.Seq[string] {
return func(yield func(string) bool) {
if over.Upper != "" {
if !yield(over.Upper) {
return
}
}
for _, layer := range over.Lower {
if layer = strings.TrimSpace(layer); layer == "" {
continue
} else if !yield(layer) {
return
}
}
}
}
func (over Overlayfs) isPathInsideOverlay(path string) bool {
for layer := range over.allLayersSeq() {
if !strings.HasPrefix(filepath.Join(layer, path), layer) {
return false
}
}
return true
}
func (over Overlayfs) retrieveFileFromLayers(name string) ([]fileLayer, error) {
content, layers := []fileLayer{}, over.allLayersSeq()
for layer := range layers {
stat, err := os.Stat(filepath.Join(layer, name))
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, rewriteName(name, err)
}
content = append(content, fileLayer{
FromUpper: layer == over.Upper,
Layer: layer,
Stat: stat,
})
}
return content, nil
}
func toMarkedToDeleteFilename(name string) string {
dir, name := filepath.Split(name)
return filepath.Join(dir, OpaqueWhiteout+name)
}
// Remove opaque file if exists
func (over Overlayfs) removeIfDeleted(name string) error {
if over.isRW() {
dir, name := filepath.Split(name)
name = filepath.Join(over.Upper, dir, OpaqueWhiteout+name)
if _, err := os.Stat(name); err == nil {
return os.Remove(name)
}
}
return nil
}
func (over Overlayfs) makeFileDeleted(name string) error {
if over.isRW() {
fullPath := filepath.Join(over.Upper, toMarkedToDeleteFilename(name))
if _, err := os.Stat(filepath.Dir(fullPath)); err != nil {
if err = os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
return err
}
}
return os.WriteFile(fullPath, nil, 0666)
}
return nil
}
// Check if file marked deleted
func (over Overlayfs) isFileMarkedDeleted(name string) (bool, error) {
if over.isRW() && !(name == "." || name == "") {
for len(name) > 2 {
var filename string
name, filename = filepath.Split(name)
if filename == "" {
if name[len(name)-1] == filepath.Separator {
filename = filepath.Base(name)
name = ""
}
}
_, err := os.Stat(filepath.Join(name, OpaqueWhiteout+filename))
if err == nil {
return true, nil
} else if os.IsNotExist(err) {
continue
}
}
}
return false, nil
}
// Clone from lower layer to upper to modifications
func (over Overlayfs) copyFileToUpperIfAbsent(name string) error {
files, err := over.retrieveFileFromLayers(name)
if err != nil {
return rewriteName(name, err)
}
// Locate layers
fromUpperIndex, fromFistLowIndex := slices.IndexFunc(files, func(entry fileLayer) bool { return entry.FromUpper }), slices.IndexFunc(files, func(entry fileLayer) bool { return !entry.FromUpper })
if (fromUpperIndex == -1 && fromFistLowIndex == -1) || fromUpperIndex != -1 {
return nil // Ignore copy
} else if fromFistLowIndex == -1 {
return &fs.PathError{Op: "copy", Path: name, Err: fs.ErrNotExist}
}
// File stats
fileInfo := files[fromFistLowIndex]
lowPath, uperPath := filepath.Join(fileInfo.Layer, name), filepath.Join(over.Upper, name)
// Remove opaque file
if err := over.removeIfDeleted(name); err != nil {
return err
}
return rewriteName(name, over.copyFromTo(fileInfo, name, lowPath, uperPath))
}
func (over Overlayfs) copyFromTo(fileInfo fileLayer, name, lowPath, uperPath string) error {
switch fileInfo.Stat.Mode().Type() {
case fs.ModeAppend, fs.ModeExclusive, fs.ModeTemporary, fs.ModeDevice, fs.ModeNamedPipe, fs.ModeSocket, fs.ModeSetuid, fs.ModeSetgid, fs.ModeCharDevice, fs.ModeSticky, fs.ModeIrregular:
return &fs.PathError{Op: "copy", Path: name, Err: fs.ErrInvalid}
case fs.ModeDir:
return os.CopyFS(uperPath, os.DirFS(lowPath)) // Copy dir full dir
case fs.ModeSymlink:
target, err := os.Readlink(filepath.Join(lowPath))
if err == nil {
err = os.Symlink(target, uperPath)
}
return rewriteName(name, err)
default:
// Open file
fromReadFile, err := os.Open(lowPath)
if err != nil {
return rewriteName(name, err)
}
defer fromReadFile.Close() // Close file
// Create target file
targetWriteFile, err := os.OpenFile(filepath.Join(over.Upper, name), fileCreateFlags, fileInfo.Stat.Mode().Perm())
if err != nil {
return rewriteName(name, err)
}
defer targetWriteFile.Close()
// Copy file
_, err = io.Copy(targetWriteFile, fromReadFile)
return rewriteName(name, err)
}
}