package updater import ( "fmt" "io/fs" "os" "path/filepath" ) func replaceRegularFile(directory, target, pattern string, data []byte, syncer DirectorySyncer) error { if info, err := os.Lstat(target); err == nil { if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { return fmt.Errorf("%w: file target is not a regular file", ErrUnsafeLayout) } } else if !os.IsNotExist(err) { return err } temporary, err := os.CreateTemp(directory, pattern) if err != nil { return err } temporaryPath := temporary.Name() defer os.Remove(temporaryPath) if err := temporary.Chmod(0o600); err != nil { _ = temporary.Close() return err } if _, err := temporary.Write(data); err != nil { _ = temporary.Close() return err } if err := temporary.Sync(); err != nil { _ = temporary.Close() return err } if err := temporary.Close(); err != nil { return err } if err := os.Rename(temporaryPath, target); err != nil { return err } return syncer.SyncDirectory(directory) } func readRegularFile(path string) ([]byte, error) { if err := validateRegularFile(path); err != nil { return nil, err } return os.ReadFile(path) } func removeRegularFile(path, directory string, syncer DirectorySyncer) error { info, err := os.Lstat(path) if os.IsNotExist(err) { return nil } if err != nil { return err } if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { return fmt.Errorf("%w: refuse to remove non-regular file", ErrUnsafeLayout) } if err := os.Remove(path); err != nil { return err } return syncer.SyncDirectory(directory) } func renameDirectory(source, target string, layout updateLayout, syncer DirectorySyncer, description string) error { if source != layout.target && source != layout.staging && source != layout.backup { return fmt.Errorf("%w: source outside managed root", ErrUnsafeLayout) } if target != layout.target && target != layout.staging && target != layout.backup { return fmt.Errorf("%w: target outside managed root", ErrUnsafeLayout) } if err := os.Rename(source, target); err != nil { return fmt.Errorf("%s: %w", description, err) } if err := syncer.SyncDirectory(filepath.Dir(source)); err != nil { return err } if filepath.Dir(target) != filepath.Dir(source) { if err := syncer.SyncDirectory(filepath.Dir(target)); err != nil { return err } } return nil } func removeManagedTree(path string, layout updateLayout, syncer DirectorySyncer) error { if filepath.Dir(path) != filepath.Join(layout.root, backupsDirectoryName) && filepath.Dir(path) != filepath.Join(layout.root, stagingDirectoryName) { return fmt.Errorf("%w: refuse removal outside managed staging or backups", ErrUnsafeLayout) } if err := verifyTreeNoLinks(path); err != nil { return err } if err := os.RemoveAll(path); err != nil { return err } return syncer.SyncDirectory(filepath.Dir(path)) } func verifyTreeNoLinks(path string) error { info, err := os.Lstat(path) if os.IsNotExist(err) { return nil } if err != nil { return err } if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { return fmt.Errorf("%w: managed tree is not a real directory", ErrUnsafeLayout) } return filepath.WalkDir(path, func(current string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if entry.Type()&os.ModeSymlink != 0 { return fmt.Errorf("%w: managed tree contains a symbolic link", ErrUnsafeLayout) } return nil }) }