aboutsummaryrefslogtreecommitdiff
path: root/network/receivepack/service/quarantine.go
diff options
context:
space:
mode:
Diffstat (limited to 'network/receivepack/service/quarantine.go')
-rw-r--r--network/receivepack/service/quarantine.go274
1 files changed, 274 insertions, 0 deletions
diff --git a/network/receivepack/service/quarantine.go b/network/receivepack/service/quarantine.go
new file mode 100644
index 00000000..0bd98aeb
--- /dev/null
+++ b/network/receivepack/service/quarantine.go
@@ -0,0 +1,274 @@
+package service
+
+import (
+ "bytes"
+ "crypto/rand"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path"
+ "slices"
+)
+
+// createQuarantineRoot creates one per-push quarantine directory beneath the
+// permanent objects root.
+//
+// It returns both the quarantine directory name relative to ObjectsRoot and an
+// opened root for that directory. Callers use the name for later promotion or
+// removal relative to ObjectsRoot, and use the opened root for capability-based
+// access within the quarantine itself.
+func (service *Service) createQuarantineRoot() (string, *os.Root, error) {
+ name := "tmp_objdir-incoming-" + rand.Text()
+
+ err := service.opts.ObjectsRoot.Mkdir(name, 0o700)
+ if err != nil {
+ return "", nil, err
+ }
+
+ root, err := service.opts.ObjectsRoot.OpenRoot(name)
+ if err != nil {
+ _ = service.opts.ObjectsRoot.RemoveAll(name)
+
+ return "", nil, err
+ }
+
+ return name, root, nil
+}
+
+func (service *Service) openQuarantinePackRoot(quarantineRoot *os.Root) (*os.Root, error) {
+ err := quarantineRoot.Mkdir("pack", 0o755)
+ if err != nil && !os.IsExist(err) {
+ return nil, err
+ }
+
+ return quarantineRoot.OpenRoot("pack")
+}
+
+func (service *Service) promoteQuarantine(quarantineName string, quarantineRoot *os.Root) error {
+ if quarantineName == "" || quarantineRoot == nil {
+ return nil
+ }
+
+ return service.promoteQuarantineDir(quarantineName, quarantineRoot, ".")
+}
+
+func (service *Service) promoteQuarantineDir(quarantineName string, quarantineRoot *os.Root, rel string) error {
+ entries, err := fs.ReadDir(quarantineRoot.FS(), rel)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+
+ slices.SortFunc(entries, func(left, right fs.DirEntry) int {
+ return packCopyPriority(left.Name()) - packCopyPriority(right.Name())
+ })
+
+ for _, entry := range entries {
+ childRel := entry.Name()
+ if rel != "." {
+ childRel = path.Join(rel, entry.Name())
+ }
+
+ if entry.IsDir() {
+ err = service.opts.ObjectsRoot.Mkdir(childRel, 0o755)
+ if err != nil && !os.IsExist(err) {
+ return err
+ }
+
+ err = service.applyPromotedDirectoryPermissions(childRel)
+ if err != nil {
+ return err
+ }
+
+ err = service.promoteQuarantineDir(quarantineName, quarantineRoot, childRel)
+ if err != nil {
+ return err
+ }
+
+ continue
+ }
+
+ err = finalizeQuarantineFile(
+ service.opts.ObjectsRoot,
+ path.Join(quarantineName, childRel),
+ childRel,
+ isLooseObjectShardPath(rel),
+ service.opts.PromotedObjectPermissions,
+ )
+ if err == nil {
+ continue
+ }
+
+ return err
+ }
+
+ return nil
+}
+
+func packCopyPriority(name string) int {
+ if !pathHasPackPrefix(name) {
+ return 0
+ }
+
+ switch {
+ case path.Ext(name) == ".keep":
+ return 1
+ case path.Ext(name) == ".pack":
+ return 2
+ case path.Ext(name) == ".rev":
+ return 3
+ case path.Ext(name) == ".idx":
+ return 4
+ default:
+ return 5
+ }
+}
+
+func pathHasPackPrefix(name string) bool {
+ return len(name) >= 4 && name[:4] == "pack"
+}
+
+func isLooseObjectShardPath(rel string) bool {
+ return len(rel) == 2 && isHex(rel[0]) && isHex(rel[1])
+}
+
+func isHex(ch byte) bool {
+ return ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')
+}
+
+func (service *Service) applyPromotedDirectoryPermissions(name string) error {
+ if service.opts.PromotedObjectPermissions == nil {
+ return nil
+ }
+
+ return service.opts.ObjectsRoot.Chmod(name, service.opts.PromotedObjectPermissions.DirMode)
+}
+
+func applyPromotedFilePermissions(
+ root *os.Root,
+ name string,
+ perms *PromotedObjectPermissions,
+) error {
+ if perms == nil {
+ return nil
+ }
+
+ return root.Chmod(name, perms.FileMode)
+}
+
+func finalizeQuarantineFile(
+ root *os.Root,
+ src, dst string,
+ skipCollisionCheck bool,
+ perms *PromotedObjectPermissions,
+) error {
+ const maxVanishedRetries = 5
+
+ for retries := 0; ; retries++ {
+ err := root.Link(src, dst)
+ switch {
+ case err == nil:
+ _ = root.Remove(src)
+
+ return applyPromotedFilePermissions(root, dst, perms)
+ case !errors.Is(err, fs.ErrExist):
+ _, statErr := root.Stat(dst)
+ switch {
+ case statErr == nil:
+ err = fs.ErrExist
+ case errors.Is(statErr, fs.ErrNotExist):
+ renameErr := root.Rename(src, dst)
+ if renameErr == nil {
+ return applyPromotedFilePermissions(root, dst, perms)
+ }
+
+ err = renameErr
+ default:
+ _ = root.Remove(src)
+
+ return statErr
+ }
+ }
+
+ if !errors.Is(err, fs.ErrExist) {
+ _ = root.Remove(src)
+
+ return fmt.Errorf("promote quarantine %q -> %q: %w", src, dst, err)
+ }
+
+ if skipCollisionCheck {
+ _ = root.Remove(src)
+
+ return applyPromotedFilePermissions(root, dst, perms)
+ }
+
+ equal, vanished, cmpErr := compareRootFiles(root, src, dst)
+ if vanished {
+ if retries >= maxVanishedRetries {
+ return fmt.Errorf("promote quarantine %q -> %q: destination repeatedly vanished", src, dst)
+ }
+
+ continue
+ }
+
+ if cmpErr != nil {
+ return cmpErr
+ }
+
+ if !equal {
+ return fmt.Errorf("promote quarantine %q -> %q: files differ in contents", src, dst)
+ }
+
+ _ = root.Remove(src)
+
+ return applyPromotedFilePermissions(root, dst, perms)
+ }
+}
+
+func compareRootFiles(root *os.Root, left, right string) (equal bool, vanished bool, err error) {
+ leftFile, err := root.Open(left)
+ if err != nil {
+ return false, false, err
+ }
+
+ defer func() {
+ _ = leftFile.Close()
+ }()
+
+ rightFile, err := root.Open(right)
+ if err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ return false, true, nil
+ }
+
+ return false, false, err
+ }
+
+ defer func() {
+ _ = rightFile.Close()
+ }()
+
+ var leftBuf, rightBuf [4096]byte
+
+ for {
+ leftN, leftErr := leftFile.Read(leftBuf[:])
+ rightN, rightErr := rightFile.Read(rightBuf[:])
+
+ if leftErr != nil && !errors.Is(leftErr, io.EOF) {
+ return false, false, leftErr
+ }
+
+ if rightErr != nil && !errors.Is(rightErr, io.EOF) {
+ return false, false, rightErr
+ }
+
+ if leftN != rightN || !bytes.Equal(leftBuf[:leftN], rightBuf[:rightN]) {
+ return false, false, nil
+ }
+
+ if leftErr != nil || rightErr != nil {
+ return true, false, nil
+ }
+ }
+}