blob: ab946d140cdcd9e47e820131cfdb4af44670b679 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +01004package main
5
6import (
7 "flag"
8 "io"
9 "log"
10 "os"
11 "path"
12 "sort"
13 "strings"
14
15 "github.com/cavaliergopher/cpio"
Lorenz Brun62f1d362023-11-14 16:18:24 +010016 "github.com/klauspost/compress/zstd"
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +010017 "golang.org/x/sys/unix"
18
Tim Windelschmidtc2290c22024-08-15 19:56:00 +020019 "source.monogon.dev/osbase/build/fsspec"
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +010020)
21
22var (
23 outPath = flag.String("out", "", "Output file path")
24)
25
26type placeEnum int
27
28const (
29 // placeNone implies that currently nothing is placed at that path.
30 // Can be overridden by everything.
31 placeNone placeEnum = 0
32 // placeDirImplicit means that there is currently a implied directory
33 // at the given path. It can be overridden by (and only by) an explicit
34 // directory.
35 placeDirImplicit placeEnum = 1
36 // placeDirExplicit means that there is an explicit (i.e. specified by
37 // the FSSpec) directory at the given path. Nothing else can override
38 // this.
39 placeDirExplicit placeEnum = 2
40 // placeNonDir means that there is a file-type resource (i.e a file, symlink
41 // or special_file) at the given path. Nothing else can override this.
42 placeNonDir placeEnum = 3
43)
44
45// place represents the state a given canonical path is in during metadata
46// construction. Its zero value is { State: placeNone, Inode: nil }.
47type place struct {
48 State placeEnum
49 // Inode contains one of the types inside an FSSpec (e.g. *fsspec.File)
50 Inode interface{}
51}
52
Lorenz Brun62f1d362023-11-14 16:18:24 +010053// Usage: -out <out-path.cpio.zst> fsspec-path...
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +010054func main() {
55 flag.Parse()
56 outFile, err := os.Create(*outPath)
57 if err != nil {
58 log.Fatalf("Failed to open CPIO output file: %v", err)
59 }
60 defer outFile.Close()
Lorenz Brun62f1d362023-11-14 16:18:24 +010061 compressedOut, err := zstd.NewWriter(outFile)
62 if err != nil {
63 log.Fatalf("While initializing zstd writer: %v", err)
64 }
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +010065 defer compressedOut.Close()
66 cpioWriter := cpio.NewWriter(compressedOut)
67 defer cpioWriter.Close()
68
69 spec, err := fsspec.ReadMergeSpecs(flag.Args())
70 if err != nil {
71 log.Fatalf("failed to load specs: %v", err)
72 }
73
74 // Map of paths to metadata for validation & implicit directory injection
75 places := make(map[string]place)
76
77 // The idea behind this machinery is that we try to place all files and
78 // directories into a map while creating the required parent directories
79 // on-the-fly as implicit directories. Overriding an implicit directory
80 // with an explicit one is allowed thus the actual order in which this
81 // structure is created does not matter. All non-directories cannot be
82 // overridden anyways so their insertion order does not matter.
83 // This also has the job of validating the FSSpec structure, ensuring that
84 // there are no duplicate paths and that there is nothing placed below a
85 // non-directory.
86 var placeInode func(p string, isDir bool, inode interface{})
87 placeInode = func(p string, isDir bool, inode interface{}) {
88 cleanPath := path.Clean(p)
89 if !isDir {
90 if places[cleanPath].State != placeNone {
91 log.Fatalf("Invalid FSSpec: Duplicate Inode at %q", cleanPath)
92 }
93 places[cleanPath] = place{
94 State: placeNonDir,
95 Inode: inode,
96 }
97 } else {
98 switch places[cleanPath].State {
99 case placeNone:
100 if inode != nil {
101 places[cleanPath] = place{
102 State: placeDirExplicit,
103 Inode: inode,
104 }
105 } else {
106 places[cleanPath] = place{
107 State: placeDirImplicit,
108 Inode: &fsspec.Directory{Path: cleanPath, Mode: 0555},
109 }
110 }
111 case placeDirImplicit:
112 if inode != nil {
113 places[cleanPath] = place{
114 State: placeDirExplicit,
115 Inode: inode,
116 }
117 }
118 case placeDirExplicit:
119 if inode != nil {
120 log.Fatalf("Invalid FSSpec: Conflicting explicit directories at %v", cleanPath)
121 }
122 case placeNonDir:
123 log.Fatalf("Invalid FSSpec: Trying to place inode below non-directory at #{cleanPath}")
124 default:
125 panic("unhandled placeEnum value")
126 }
127 }
128 parentPath, _ := path.Split(p)
129 parentPath = path.Clean(parentPath)
130 if parentPath == "/" || parentPath == p {
131 return
132 }
133 placeInode(parentPath, true, nil)
134 }
135 for _, d := range spec.Directory {
136 placeInode(d.Path, true, d)
137 }
138 for _, f := range spec.File {
139 placeInode(f.Path, false, f)
140 }
141 for _, s := range spec.SymbolicLink {
142 placeInode(s.Path, false, s)
143 }
144 for _, s := range spec.SpecialFile {
145 placeInode(s.Path, false, s)
146 }
147
148 var writeOrder []string
149 for path := range places {
150 writeOrder = append(writeOrder, path)
151 }
152 // Sorting a list of normalized paths representing a tree gives us Depth-
153 // first search (DFS) order which is the correct order for writing archives.
154 // This also makes the output reproducible.
155 sort.Strings(writeOrder)
156
157 for _, path := range writeOrder {
158 place := places[path]
159 switch i := place.Inode.(type) {
160 case *fsspec.File:
161 inF, err := os.Open(i.SourcePath)
162 if err != nil {
163 log.Fatalf("Failed to open source path for file %q: %v", i.Path, err)
164 }
165 inFStat, err := inF.Stat()
166 if err != nil {
167 log.Fatalf("Failed to stat source path for file %q: %v", i.Path, err)
168 }
169 if err := cpioWriter.WriteHeader(&cpio.Header{
170 Mode: cpio.FileMode(i.Mode),
171 Name: strings.TrimPrefix(i.Path, "/"),
172 Size: inFStat.Size(),
173 }); err != nil {
174 log.Fatalf("Failed to write cpio header for file %q: %v", i.Path, err)
175 }
Lorenz Brun62f1d362023-11-14 16:18:24 +0100176 if n, err := io.Copy(cpioWriter, inF); err != nil || n != inFStat.Size() {
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100177 log.Fatalf("Failed to copy file %q into cpio: %v", i.SourcePath, err)
178 }
179 inF.Close()
180 case *fsspec.Directory:
181 if err := cpioWriter.WriteHeader(&cpio.Header{
Lorenz Brund13c1c62022-03-30 19:58:58 +0200182 Mode: cpio.FileMode(i.Mode) | cpio.TypeDir,
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100183 Name: strings.TrimPrefix(i.Path, "/"),
184 }); err != nil {
185 log.Fatalf("Failed to write cpio header for directory %q: %v", i.Path, err)
186 }
187 case *fsspec.SymbolicLink:
188 if err := cpioWriter.WriteHeader(&cpio.Header{
189 // Symlinks are 0777 by definition (from man 7 symlink on Linux)
Lorenz Brun51350b02023-04-18 00:34:53 +0200190 Mode: 0777 | cpio.TypeSymlink,
191 Name: strings.TrimPrefix(i.Path, "/"),
192 Size: int64(len(i.TargetPath)),
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100193 }); err != nil {
194 log.Fatalf("Failed to write cpio header for symlink %q: %v", i.Path, err)
195 }
Lorenz Brun51350b02023-04-18 00:34:53 +0200196 if _, err := cpioWriter.Write([]byte(i.TargetPath)); err != nil {
197 log.Fatalf("Failed to write cpio symlink %q: %v", i.Path, err)
198 }
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100199 case *fsspec.SpecialFile:
200 mode := cpio.FileMode(i.Mode)
201 switch i.Type {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100202 case fsspec.SpecialFile_TYPE_CHARACTER_DEV:
Lorenz Brund13c1c62022-03-30 19:58:58 +0200203 mode |= cpio.TypeChar
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100204 case fsspec.SpecialFile_TYPE_BLOCK_DEV:
Lorenz Brund13c1c62022-03-30 19:58:58 +0200205 mode |= cpio.TypeBlock
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100206 case fsspec.SpecialFile_TYPE_FIFO:
Lorenz Brund13c1c62022-03-30 19:58:58 +0200207 mode |= cpio.TypeFifo
Lorenz Brunb6a9d3c2022-01-27 18:56:20 +0100208 }
209
210 if err := cpioWriter.WriteHeader(&cpio.Header{
211 Mode: mode,
212 Name: strings.TrimPrefix(i.Path, "/"),
213 DeviceID: int(unix.Mkdev(i.Major, i.Minor)),
214 }); err != nil {
215 log.Fatalf("Failed to write CPIO header for special file %q: %v", i.Path, err)
216 }
217 default:
218 panic("inode type not handled")
219 }
220 }
221}