[mirror] Scalable static site server for Git forges (like GitHub Pages)
1package git_pages
2
3import (
4 "archive/tar"
5 "errors"
6 "fmt"
7 "io"
8 "maps"
9 "slices"
10 "strings"
11)
12
13var ErrMalformedPatch = errors.New("malformed patch")
14
15type CreateParentsMode int
16
17const (
18 RequireParents CreateParentsMode = iota
19 CreateParents
20)
21
22// Mutates `manifest` according to a tar stream and the following rules:
23// - A character device with major 0 and minor 0 is a "whiteout marker". When placed
24// at a given path, this path and its entire subtree (if any) are removed from the manifest.
25// - When a directory is placed at a given path, this path and its entire subtree (if any) are
26// removed from the manifest and replaced with the contents of the directory.
27func ApplyTarPatch(manifest *Manifest, reader io.Reader, parents CreateParentsMode) error {
28 type Node struct {
29 entry *Entry
30 children map[string]*Node
31 }
32
33 // Extract the manifest contents (which is using a flat hash map) into a directory tree
34 // so that recursive delete operations have O(1) complexity. s
35 var root *Node
36 sortedNames := slices.Sorted(maps.Keys(manifest.GetContents()))
37 for _, name := range sortedNames {
38 entry := manifest.Contents[name]
39 node := &Node{entry: entry}
40 if entry.GetType() == Type_Directory {
41 node.children = map[string]*Node{}
42 }
43 if name == "" {
44 root = node
45 } else {
46 segments := strings.Split(name, "/")
47 fileName := segments[len(segments)-1]
48 iter := root
49 for _, segment := range segments[:len(segments)-1] {
50 if iter.children == nil {
51 panic("malformed manifest")
52 } else if _, exists := iter.children[segment]; !exists {
53 panic("malformed manifest")
54 } else {
55 iter = iter.children[segment]
56 }
57 }
58 iter.children[fileName] = node
59 }
60 }
61 manifest.Contents = map[string]*Entry{}
62
63 // Process the archive as a patch operation.
64 archive := tar.NewReader(reader)
65 for {
66 header, err := archive.Next()
67 if err == io.EOF {
68 break
69 } else if err != nil {
70 return err
71 }
72
73 segments := strings.Split(strings.TrimRight(header.Name, "/"), "/")
74 fileName := segments[len(segments)-1]
75 node := root
76 for index, segment := range segments[:len(segments)-1] {
77 if node.children == nil {
78 dirName := strings.Join(segments[:index], "/")
79 return fmt.Errorf("%w: %s: not a directory", ErrMalformedPatch, dirName)
80 }
81 if _, exists := node.children[segment]; !exists {
82 switch parents {
83 case RequireParents:
84 nodeName := strings.Join(segments[:index+1], "/")
85 return fmt.Errorf("%w: %s: path not found", ErrMalformedPatch, nodeName)
86 case CreateParents:
87 node.children[segment] = &Node{
88 entry: NewManifestEntry(Type_Directory, nil),
89 children: map[string]*Node{},
90 }
91 }
92 }
93 node = node.children[segment]
94 }
95 if node.children == nil {
96 dirName := strings.Join(segments[:len(segments)-1], "/")
97 return fmt.Errorf("%w: %s: not a directory", ErrMalformedPatch, dirName)
98 }
99
100 switch header.Typeflag {
101 case tar.TypeReg:
102 fileData, err := io.ReadAll(archive)
103 if err != nil {
104 return fmt.Errorf("tar: %s: %w", header.Name, err)
105 }
106 node.children[fileName] = &Node{
107 entry: NewManifestEntry(Type_InlineFile, fileData),
108 }
109 case tar.TypeSymlink:
110 node.children[fileName] = &Node{
111 entry: NewManifestEntry(Type_Symlink, []byte(header.Linkname)),
112 }
113 case tar.TypeDir:
114 node.children[fileName] = &Node{
115 entry: NewManifestEntry(Type_Directory, nil),
116 children: map[string]*Node{},
117 }
118 case tar.TypeChar:
119 if header.Devmajor == 0 && header.Devminor == 0 {
120 delete(node.children, fileName)
121 } else {
122 AddProblem(manifest, header.Name,
123 "tar: unsupported chardev %d,%d", header.Devmajor, header.Devminor)
124 }
125 default:
126 AddProblem(manifest, header.Name,
127 "tar: unsupported type '%c'", header.Typeflag)
128 continue
129 }
130 }
131
132 // Repopulate manifest contents with the updated directory tree.
133 var traverse func([]string, *Node)
134 traverse = func(segments []string, node *Node) {
135 manifest.Contents[strings.Join(segments, "/")] = node.entry
136 for fileName, childNode := range node.children {
137 traverse(append(segments, fileName), childNode)
138 }
139 }
140 traverse([]string{}, root)
141 return nil
142}