golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/task/darwin.go (about) 1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package task 6 7 import ( 8 "bufio" 9 "bytes" 10 "compress/gzip" 11 "compress/zlib" 12 "encoding/binary" 13 "encoding/xml" 14 "errors" 15 "fmt" 16 "io" 17 "io/fs" 18 "strconv" 19 "strings" 20 ) 21 22 // ReadBinariesFromPKG reads pkg, the Go installer .pkg file, and returns 23 // binaries in bin and pkg/tool directories within GOROOT which we expect 24 // to have been signed by the macOS signing process. 25 // 26 // The map key is a relative path starting with "go/", like "go/bin/gofmt" 27 // or "go/pkg/tool/darwin_arm64/test2json". The map value holds its bytes. 28 func ReadBinariesFromPKG(pkg io.Reader) (map[string][]byte, error) { 29 // Reading the whole file into memory isn't ideal, but it makes 30 // the implementation of pkgPayload easier, and we only have at 31 // most a few .pkg installers to process. 32 data, err := io.ReadAll(pkg) 33 if err != nil { 34 return nil, err 35 } 36 payload, err := pkgPayload(data) 37 if errors.Is(err, errNoXARHeader) && bytes.HasPrefix(data, []byte("I'm a PKG! -signed <macOS>\n")) { 38 // This invalid XAR file is a fake installer produced by release tests. 39 // Since its prefix indicates it was signed, return a fake signed go command binary. 40 return map[string][]byte{"go/bin/go": []byte("fake go command -signed <macOS>")}, nil 41 } else if err != nil { 42 return nil, err 43 } 44 ix, err := indexCpioGz(payload) 45 if err != nil { 46 return nil, err 47 } 48 var binaries = make(map[string][]byte) // Relative path starting with "go/" → binary data. 49 for nameWithinPayload, f := range ix { 50 name, ok := strings.CutPrefix(nameWithinPayload, "./usr/local/") // Trim ./usr/local/go/ down to just go/. 51 if !ok { 52 continue 53 } 54 if !strings.HasPrefix(name, "go/bin/") && !strings.HasPrefix(name, "go/pkg/tool/") { 55 continue 56 } 57 if !f.Mode.IsRegular() || f.Mode.Perm()&0100 == 0 { 58 continue 59 } 60 binaries[name] = f.Data 61 } 62 return binaries, nil 63 } 64 65 // A minimal xar parser, enough to read macOS .pkg files. 66 // Command golang.org/x/build/cmd/gorebuild also has one 67 // for its internal needs. 68 // 69 // See https://en.wikipedia.org/wiki/Xar_(archiver) 70 // and https://github.com/mackyle/xar/wiki/xarformat. 71 72 // xarHeader is the main XML data structure for the xar header. 73 type xarHeader struct { 74 XMLName xml.Name `xml:"xar"` 75 TOC xarTOC `xml:"toc"` 76 } 77 78 // xarTOC is the table of contents. 79 type xarTOC struct { 80 Files []*xarFile `xml:"file"` 81 } 82 83 // xarFile is a single file in the table of contents. 84 // Directories have Type "directory" and contain other files. 85 type xarFile struct { 86 Data xarFileData `xml:"data"` 87 Name string `xml:"name"` 88 Type string `xml:"type"` // "file", "directory" 89 Files []*xarFile `xml:"file"` 90 } 91 92 // xarFileData is the metadata describing a single file. 93 type xarFileData struct { 94 Length int64 `xml:"length"` 95 Offset int64 `xml:"offset"` 96 Size int64 `xml:"size"` 97 Encoding xarEncoding `xml:"encoding"` 98 } 99 100 // xarEncoding has an attribute giving the encoding for a file's content. 101 type xarEncoding struct { 102 Style string `xml:"style,attr"` 103 } 104 105 var errNoXARHeader = fmt.Errorf("not an XAR file format (missing a 28+ byte header with 'xar!' magic number)") 106 107 // pkgPayload parses data as a macOS pkg file for the Go installer 108 // and returns the content of the file org.golang.go.pkg/Payload. 109 func pkgPayload(data []byte) ([]byte, error) { 110 if len(data) < 28 || string(data[0:4]) != "xar!" { 111 return nil, errNoXARHeader 112 } 113 be := binary.BigEndian 114 hdrSize := be.Uint16(data[4:]) 115 vers := be.Uint16(data[6:]) 116 tocCSize := be.Uint64(data[8:]) 117 tocUSize := be.Uint64(data[16:]) 118 119 if vers != 1 { 120 return nil, fmt.Errorf("bad xar version %d", vers) 121 } 122 if int(hdrSize) >= len(data) || uint64(len(data))-uint64(hdrSize) < tocCSize { 123 return nil, fmt.Errorf("xar header bounds not in file") 124 } 125 126 data = data[hdrSize:] 127 chdr, data := data[:tocCSize], data[tocCSize:] 128 129 // Header is zlib-compressed XML. 130 zr, err := zlib.NewReader(bytes.NewReader(chdr)) 131 if err != nil { 132 return nil, fmt.Errorf("reading xar header: %v", err) 133 } 134 defer zr.Close() 135 hdrXML := make([]byte, tocUSize+1) 136 n, err := io.ReadFull(zr, hdrXML) 137 if uint64(n) != tocUSize { 138 return nil, fmt.Errorf("invalid xar header size %d", n) 139 } 140 if err != io.ErrUnexpectedEOF { 141 return nil, fmt.Errorf("reading xar header: %v", err) 142 } 143 hdrXML = hdrXML[:tocUSize] 144 var hdr xarHeader 145 if err := xml.Unmarshal(hdrXML, &hdr); err != nil { 146 return nil, fmt.Errorf("unmarshaling xar header: %v", err) 147 } 148 149 // Walk TOC file tree to find org.golang.go.pkg/Payload. 150 for _, f := range hdr.TOC.Files { 151 if f.Name == "org.golang.go.pkg" && f.Type == "directory" { 152 for _, f := range f.Files { 153 if f.Name == "Payload" { 154 if f.Type != "file" { 155 return nil, fmt.Errorf("bad xar payload type %s", f.Type) 156 } 157 if f.Data.Encoding.Style != "application/octet-stream" { 158 return nil, fmt.Errorf("bad xar encoding %s", f.Data.Encoding.Style) 159 } 160 if f.Data.Offset >= int64(len(data)) || f.Data.Size > int64(len(data))-f.Data.Offset { 161 return nil, fmt.Errorf("xar payload bounds not in file") 162 } 163 return data[f.Data.Offset:][:f.Data.Size], nil 164 } 165 } 166 } 167 } 168 return nil, fmt.Errorf("payload not found") 169 } 170 171 // A cpioFile represents a single file in a CPIO archive. 172 type cpioFile struct { 173 Name string 174 Mode fs.FileMode 175 Data []byte 176 } 177 178 // indexCpioGz parses data as a gzip-compressed cpio file and returns an index of its content. 179 func indexCpioGz(data []byte) (map[string]*cpioFile, error) { 180 zr, err := gzip.NewReader(bytes.NewReader(data)) 181 if err != nil { 182 return nil, err 183 } 184 br := bufio.NewReader(zr) 185 186 const hdrSize = 76 187 188 ix := make(map[string]*cpioFile) 189 hdr := make([]byte, hdrSize) 190 for { 191 _, err := io.ReadFull(br, hdr) 192 if err != nil { 193 if err == io.EOF { 194 break 195 } 196 return nil, fmt.Errorf("reading archive: %v", err) 197 } 198 199 // https://www.mkssoftware.com/docs/man4/cpio.4.asp 200 // 201 // hdr[0:6] "070707" 202 // hdr[6:12] device number (all numbers '0'-padded octal) 203 // hdr[12:18] inode number 204 // hdr[18:24] mode 205 // hdr[24:30] uid 206 // hdr[30:36] gid 207 // hdr[36:42] nlink 208 // hdr[42:48] rdev 209 // hdr[48:59] mtime 210 // hdr[59:65] name length 211 // hdr[65:76] file size 212 213 if !allOctal(hdr[:]) || string(hdr[:6]) != "070707" { 214 return nil, fmt.Errorf("reading archive: malformed entry") 215 } 216 mode, _ := strconv.ParseInt(string(hdr[18:24]), 8, 64) 217 nameLen, _ := strconv.ParseInt(string(hdr[59:65]), 8, 64) 218 size, _ := strconv.ParseInt(string(hdr[65:76]), 8, 64) 219 nameBuf := make([]byte, nameLen) 220 if _, err := io.ReadFull(br, nameBuf); err != nil { 221 return nil, fmt.Errorf("reading archive: %v", err) 222 } 223 if nameLen == 0 || nameBuf[nameLen-1] != 0 { 224 return nil, fmt.Errorf("reading archive: malformed entry") 225 } 226 name := string(nameBuf[:nameLen-1]) 227 228 // The MKS cpio page says "TRAILER!!" 229 // but the Apple pkg files use "TRAILER!!!". 230 if name == "TRAILER!!!" { 231 break 232 } 233 234 fmode := fs.FileMode(mode & 0777) 235 if mode&040000 != 0 { 236 fmode |= fs.ModeDir 237 } 238 239 data, err := io.ReadAll(io.LimitReader(br, size)) 240 if err != nil { 241 return nil, fmt.Errorf("reading archive: %v", err) 242 } 243 if size != int64(len(data)) { 244 return nil, fmt.Errorf("reading archive: short file") 245 } 246 247 if fmode&fs.ModeDir != 0 { 248 continue 249 } 250 251 ix[name] = &cpioFile{name, fmode, data} 252 } 253 return ix, nil 254 } 255 256 // allOctal reports whether x is entirely ASCII octal digits. 257 func allOctal(x []byte) bool { 258 for _, b := range x { 259 if b < '0' || '7' < b { 260 return false 261 } 262 } 263 return true 264 }