[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}