Something went wrong. Try again.
[mirror] Scalable static site server for Git forges (like GitHub Pages)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181package git_pages
import ( "archive/tar" "errors" "fmt" "io" "maps" "slices" "strings")
var ErrMalformedPatch = errors.New("malformed patch")
type CreateParentsMode int
const ( RequireParents CreateParentsMode = iota CreateParents)
// Mutates `manifest` according to a tar stream and the following rules:// - A character device with major 0 and minor 0 is a "whiteout marker". When placed// at a given path, this path and its entire subtree (if any) are removed from the manifest.// - When a directory is placed at a given path, this path and its entire subtree (if any) are// removed from the manifest and replaced with the contents of the directory.func ApplyTarPatch(manifest *Manifest, reader io.Reader, parents CreateParentsMode) error { type Node struct { entry *Entry children map[string]*Node }
// Index the manifest for incremental update operations. index := IndexManifestByGitHash(manifest) missing := []string{}
// Extract the manifest contents (which is using a flat hash map) into a directory tree // so that recursive delete operations have O(1) complexity. var root *Node sortedNames := slices.Sorted(maps.Keys(manifest.GetContents())) for _, name := range sortedNames { entry := manifest.Contents[name] node := &Node{entry: entry} if entry.GetType() == Type_Directory { node.children = map[string]*Node{} } if name == "" { root = node } else { segments := strings.Split(name, "/") fileName := segments[len(segments)-1] iter := root for _, segment := range segments[:len(segments)-1] { if iter.children == nil { break // error handled below } else if _, exists := iter.children[segment]; !exists { panic("malformed manifest (node does not exist)") } else { iter = iter.children[segment] } } if iter.children == nil { panic("malformed manifest (not a directory)") } iter.children[fileName] = node } } manifest.Contents = map[string]*Entry{}
// Process the archive as a patch operation. archive := tar.NewReader(reader) for { header, err := archive.Next() if err == io.EOF { break } else if err != nil { return err }
memberName := normalizeArchiveMemberName(header.Name) // This is a special case: while member name "a" refers to a child node of the root // node, the member name "" refers to the root node itself, which violates assumptions // of the generic code afterwards. if memberName == "" { switch header.Typeflag { case tar.TypeDir: root = &Node{ entry: NewManifestEntry(Type_Directory, nil), children: map[string]*Node{}, } continue default: AddProblem(manifest, header.Name, "tar: unsupported type '%c' for root node", header.Typeflag) continue } }
segments := strings.Split(memberName, "/") fileName := segments[len(segments)-1] node := root for index, segment := range segments[:len(segments)-1] { if node.children == nil { dirName := strings.Join(segments[:index], "/") return fmt.Errorf("%w: %s: not a directory", ErrMalformedPatch, dirName) } if _, exists := node.children[segment]; !exists { switch parents { case RequireParents: nodeName := strings.Join(segments[:index+1], "/") return fmt.Errorf("%w: %s: path not found", ErrMalformedPatch, nodeName) case CreateParents: node.children[segment] = &Node{ entry: NewManifestEntry(Type_Directory, nil), children: map[string]*Node{}, } } } node = node.children[segment] } if node.children == nil { dirName := strings.Join(segments[:len(segments)-1], "/") return fmt.Errorf("%w: %s: not a directory", ErrMalformedPatch, dirName) }
switch header.Typeflag { case tar.TypeReg: fileData, err := io.ReadAll(archive) if err != nil { return fmt.Errorf("tar: %s: %w", header.Name, err) } node.children[fileName] = &Node{ entry: NewManifestEntry(Type_InlineFile, fileData), } case tar.TypeSymlink: if hash, found := strings.CutPrefix(header.Linkname, BlobReferencePrefix); found { if entry, found := index[hash]; found { node.children[fileName] = &Node{entry: entry} } else { missing = append(missing, hash) } } else { node.children[fileName] = &Node{ entry: NewManifestEntry(Type_Symlink, []byte(header.Linkname)), } } case tar.TypeDir: node.children[fileName] = &Node{ entry: NewManifestEntry(Type_Directory, nil), children: map[string]*Node{}, } case tar.TypeChar: if header.Devmajor == 0 && header.Devminor == 0 { delete(node.children, fileName) } else { AddProblem(manifest, header.Name, "tar: unsupported chardev %d,%d", header.Devmajor, header.Devminor) } default: AddProblem(manifest, header.Name, "tar: unsupported type '%c'", header.Typeflag) continue } }
if len(missing) > 0 { return UnresolvedRefError{missing} }
// Repopulate manifest contents with the updated directory tree. var traverse func([]string, *Node) traverse = func(segments []string, node *Node) { manifest.Contents[strings.Join(segments, "/")] = node.entry for fileName, childNode := range node.children { traverse(append(segments, fileName), childNode) } } traverse([]string{}, root) return nil}