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  }