mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-06-10 13:30:49 +00:00
970806ab4a
To reproduce, use PUT to upload this archive (`unzstd | base64 -d`): KLUv/QRY7QIAxAJhL2IAMDAwMDY0NDAwMDAwMDEAADAwNzU2MAAgMAB1c3RhcgAwAGEAMzM3 YREA/UEF/EC9Y0AdDJBP8GDCTaDGBxATkAAd3gJoMPAbJANAciACGDTAsXKZngAR/m3nXA== then issue any PATCH request to that site. After this commit, the server returns "malformed manifest (not a directory)" instead of "assignment to entry in nil map". While ideally incoming manifests should be checked for consistency regardless of how they're uploaded, in practice this is only a self-DoS so it's probably not worth fixing. V12-Ref: F-77244
181 lines
5.2 KiB
Go
181 lines
5.2 KiB
Go
package 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
|
|
}
|