src/archive/tar/writer.go GO 707 lines View on github.com → Search inside
1// Copyright 2009 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45package tar67import (8	"errors"9	"fmt"10	"io"11	"io/fs"12	"maps"13	"path"14	"slices"15	"strings"16	"time"17)1819// Writer provides sequential writing of a tar archive.20// [Writer.WriteHeader] begins a new file with the provided [Header],21// and then Writer can be treated as an io.Writer to supply that file's data.22type Writer struct {23	w    io.Writer24	pad  int64      // Amount of padding to write after current file entry25	curr fileWriter // Writer for current file entry26	hdr  Header     // Shallow copy of Header that is safe for mutations27	blk  block      // Buffer to use as temporary local storage2829	// err is a persistent error.30	// It is only the responsibility of every exported method of Writer to31	// ensure that this error is sticky.32	err error33}3435// NewWriter creates a new Writer writing to w.36func NewWriter(w io.Writer) *Writer {37	return &Writer{w: w, curr: &regFileWriter{w, 0}}38}3940type fileWriter interface {41	io.Writer42	fileState4344	ReadFrom(io.Reader) (int64, error)45}4647// Flush finishes writing the current file's block padding.48// The current file must be fully written before Flush can be called.49//50// This is unnecessary as the next call to [Writer.WriteHeader] or [Writer.Close]51// will implicitly flush out the file's padding.52func (tw *Writer) Flush() error {53	if tw.err != nil {54		return tw.err55	}56	if nb := tw.curr.logicalRemaining(); nb > 0 {57		return fmt.Errorf("archive/tar: missed writing %d bytes", nb)58	}59	if _, tw.err = tw.w.Write(zeroBlock[:tw.pad]); tw.err != nil {60		return tw.err61	}62	tw.pad = 063	return nil64}6566// WriteHeader writes hdr and prepares to accept the file's contents.67// The Header.Size determines how many bytes can be written for the next file.68// If the current file is not fully written, then this returns an error.69// This implicitly flushes any padding necessary before writing the header.70func (tw *Writer) WriteHeader(hdr *Header) error {71	if err := tw.Flush(); err != nil {72		return err73	}74	tw.hdr = *hdr // Shallow copy of Header7576	// Avoid usage of the legacy TypeRegA flag, and automatically promote77	// it to use TypeReg or TypeDir.78	if tw.hdr.Typeflag == TypeRegA {79		if strings.HasSuffix(tw.hdr.Name, "/") {80			tw.hdr.Typeflag = TypeDir81		} else {82			tw.hdr.Typeflag = TypeReg83		}84	}8586	// Round ModTime and ignore AccessTime and ChangeTime unless87	// the format is explicitly chosen.88	// This ensures nominal usage of WriteHeader (without specifying the format)89	// does not always result in the PAX format being chosen, which90	// causes a 1KiB increase to every header.91	if tw.hdr.Format == FormatUnknown {92		tw.hdr.ModTime = tw.hdr.ModTime.Round(time.Second)93		tw.hdr.AccessTime = time.Time{}94		tw.hdr.ChangeTime = time.Time{}95	}9697	allowedFormats, paxHdrs, err := tw.hdr.allowedFormats()98	switch {99	case allowedFormats.has(FormatUSTAR):100		tw.err = tw.writeUSTARHeader(&tw.hdr)101		return tw.err102	case allowedFormats.has(FormatPAX):103		tw.err = tw.writePAXHeader(&tw.hdr, paxHdrs)104		return tw.err105	case allowedFormats.has(FormatGNU):106		tw.err = tw.writeGNUHeader(&tw.hdr)107		return tw.err108	default:109		return err // Non-fatal error110	}111}112113func (tw *Writer) writeUSTARHeader(hdr *Header) error {114	// Check if we can use USTAR prefix/suffix splitting.115	var namePrefix string116	if prefix, suffix, ok := splitUSTARPath(hdr.Name); ok {117		namePrefix, hdr.Name = prefix, suffix118	}119120	// Pack the main header.121	var f formatter122	blk := tw.templateV7Plus(hdr, f.formatString, f.formatOctal)123	f.formatString(blk.toUSTAR().prefix(), namePrefix)124	blk.setFormat(FormatUSTAR)125	if f.err != nil {126		return f.err // Should never happen since header is validated127	}128	return tw.writeRawHeader(blk, hdr.Size, hdr.Typeflag)129}130131func (tw *Writer) writePAXHeader(hdr *Header, paxHdrs map[string]string) error {132	realName, realSize := hdr.Name, hdr.Size133134	// TODO(dsnet): Re-enable this when adding sparse support.135	// See https://golang.org/issue/22735136	/*137		// Handle sparse files.138		var spd sparseDatas139		var spb []byte140		if len(hdr.SparseHoles) > 0 {141			sph := append([]sparseEntry{}, hdr.SparseHoles...) // Copy sparse map142			sph = alignSparseEntries(sph, hdr.Size)143			spd = invertSparseEntries(sph, hdr.Size)144145			// Format the sparse map.146			hdr.Size = 0 // Replace with encoded size147			spb = append(strconv.AppendInt(spb, int64(len(spd)), 10), '\n')148			for _, s := range spd {149				hdr.Size += s.Length150				spb = append(strconv.AppendInt(spb, s.Offset, 10), '\n')151				spb = append(strconv.AppendInt(spb, s.Length, 10), '\n')152			}153			pad := blockPadding(int64(len(spb)))154			spb = append(spb, zeroBlock[:pad]...)155			hdr.Size += int64(len(spb)) // Accounts for encoded sparse map156157			// Add and modify appropriate PAX records.158			dir, file := path.Split(realName)159			hdr.Name = path.Join(dir, "GNUSparseFile.0", file)160			paxHdrs[paxGNUSparseMajor] = "1"161			paxHdrs[paxGNUSparseMinor] = "0"162			paxHdrs[paxGNUSparseName] = realName163			paxHdrs[paxGNUSparseRealSize] = strconv.FormatInt(realSize, 10)164			paxHdrs[paxSize] = strconv.FormatInt(hdr.Size, 10)165			delete(paxHdrs, paxPath) // Recorded by paxGNUSparseName166		}167	*/168	_ = realSize169170	// Write PAX records to the output.171	isGlobal := hdr.Typeflag == TypeXGlobalHeader172	if len(paxHdrs) > 0 || isGlobal {173		// Write each record to a buffer.174		var buf strings.Builder175		// Sort keys for deterministic ordering.176		for _, k := range slices.Sorted(maps.Keys(paxHdrs)) {177			rec, err := formatPAXRecord(k, paxHdrs[k])178			if err != nil {179				return err180			}181			buf.WriteString(rec)182		}183184		// Write the extended header file.185		var name string186		var flag byte187		if isGlobal {188			name = realName189			if name == "" {190				name = "GlobalHead.0.0"191			}192			flag = TypeXGlobalHeader193		} else {194			dir, file := path.Split(realName)195			name = path.Join(dir, "PaxHeaders.0", file)196			flag = TypeXHeader197		}198		data := buf.String()199		if len(data) > maxSpecialFileSize {200			return ErrFieldTooLong201		}202		if err := tw.writeRawFile(name, data, flag, FormatPAX); err != nil || isGlobal {203			return err // Global headers return here204		}205	}206207	// Pack the main header.208	var f formatter // Ignore errors since they are expected209	fmtStr := func(b []byte, s string) { f.formatString(b, toASCII(s)) }210	blk := tw.templateV7Plus(hdr, fmtStr, f.formatOctal)211	blk.setFormat(FormatPAX)212	if err := tw.writeRawHeader(blk, hdr.Size, hdr.Typeflag); err != nil {213		return err214	}215216	// TODO(dsnet): Re-enable this when adding sparse support.217	// See https://golang.org/issue/22735218	/*219		// Write the sparse map and setup the sparse writer if necessary.220		if len(spd) > 0 {221			// Use tw.curr since the sparse map is accounted for in hdr.Size.222			if _, err := tw.curr.Write(spb); err != nil {223				return err224			}225			tw.curr = &sparseFileWriter{tw.curr, spd, 0}226		}227	*/228	return nil229}230231func (tw *Writer) writeGNUHeader(hdr *Header) error {232	// Use long-link files if Name or Linkname exceeds the field size.233	const longName = "././@LongLink"234	if len(hdr.Name) > nameSize {235		data := hdr.Name + "\x00"236		if err := tw.writeRawFile(longName, data, TypeGNULongName, FormatGNU); err != nil {237			return err238		}239	}240	if len(hdr.Linkname) > nameSize {241		data := hdr.Linkname + "\x00"242		if err := tw.writeRawFile(longName, data, TypeGNULongLink, FormatGNU); err != nil {243			return err244		}245	}246247	// Pack the main header.248	var f formatter // Ignore errors since they are expected249	var spd sparseDatas250	var spb []byte251	blk := tw.templateV7Plus(hdr, f.formatString, f.formatNumeric)252	if !hdr.AccessTime.IsZero() {253		f.formatNumeric(blk.toGNU().accessTime(), hdr.AccessTime.Unix())254	}255	if !hdr.ChangeTime.IsZero() {256		f.formatNumeric(blk.toGNU().changeTime(), hdr.ChangeTime.Unix())257	}258	// TODO(dsnet): Re-enable this when adding sparse support.259	// See https://golang.org/issue/22735260	/*261		if hdr.Typeflag == TypeGNUSparse {262			sph := append([]sparseEntry{}, hdr.SparseHoles...) // Copy sparse map263			sph = alignSparseEntries(sph, hdr.Size)264			spd = invertSparseEntries(sph, hdr.Size)265266			// Format the sparse map.267			formatSPD := func(sp sparseDatas, sa sparseArray) sparseDatas {268				for i := 0; len(sp) > 0 && i < sa.MaxEntries(); i++ {269					f.formatNumeric(sa.Entry(i).Offset(), sp[0].Offset)270					f.formatNumeric(sa.Entry(i).Length(), sp[0].Length)271					sp = sp[1:]272				}273				if len(sp) > 0 {274					sa.IsExtended()[0] = 1275				}276				return sp277			}278			sp2 := formatSPD(spd, blk.GNU().Sparse())279			for len(sp2) > 0 {280				var spHdr block281				sp2 = formatSPD(sp2, spHdr.Sparse())282				spb = append(spb, spHdr[:]...)283			}284285			// Update size fields in the header block.286			realSize := hdr.Size287			hdr.Size = 0 // Encoded size; does not account for encoded sparse map288			for _, s := range spd {289				hdr.Size += s.Length290			}291			copy(blk.V7().Size(), zeroBlock[:]) // Reset field292			f.formatNumeric(blk.V7().Size(), hdr.Size)293			f.formatNumeric(blk.GNU().RealSize(), realSize)294		}295	*/296	blk.setFormat(FormatGNU)297	if err := tw.writeRawHeader(blk, hdr.Size, hdr.Typeflag); err != nil {298		return err299	}300301	// Write the extended sparse map and setup the sparse writer if necessary.302	if len(spd) > 0 {303		// Use tw.w since the sparse map is not accounted for in hdr.Size.304		if _, err := tw.w.Write(spb); err != nil {305			return err306		}307		tw.curr = &sparseFileWriter{tw.curr, spd, 0}308	}309	return nil310}311312type (313	stringFormatter func([]byte, string)314	numberFormatter func([]byte, int64)315)316317// templateV7Plus fills out the V7 fields of a block using values from hdr.318// It also fills out fields (uname, gname, devmajor, devminor) that are319// shared in the USTAR, PAX, and GNU formats using the provided formatters.320//321// The block returned is only valid until the next call to322// templateV7Plus or writeRawFile.323func (tw *Writer) templateV7Plus(hdr *Header, fmtStr stringFormatter, fmtNum numberFormatter) *block {324	tw.blk.reset()325326	modTime := hdr.ModTime327	if modTime.IsZero() {328		modTime = time.Unix(0, 0)329	}330331	v7 := tw.blk.toV7()332	v7.typeFlag()[0] = hdr.Typeflag333	fmtStr(v7.name(), hdr.Name)334	fmtStr(v7.linkName(), hdr.Linkname)335	fmtNum(v7.mode(), hdr.Mode)336	fmtNum(v7.uid(), int64(hdr.Uid))337	fmtNum(v7.gid(), int64(hdr.Gid))338	fmtNum(v7.size(), hdr.Size)339	fmtNum(v7.modTime(), modTime.Unix())340341	ustar := tw.blk.toUSTAR()342	fmtStr(ustar.userName(), hdr.Uname)343	fmtStr(ustar.groupName(), hdr.Gname)344	fmtNum(ustar.devMajor(), hdr.Devmajor)345	fmtNum(ustar.devMinor(), hdr.Devminor)346347	return &tw.blk348}349350// writeRawFile writes a minimal file with the given name and flag type.351// It uses format to encode the header format and will write data as the body.352// It uses default values for all of the other fields (as BSD and GNU tar does).353func (tw *Writer) writeRawFile(name, data string, flag byte, format Format) error {354	tw.blk.reset()355356	// Best effort for the filename.357	name = toASCII(name)358	if len(name) > nameSize {359		name = name[:nameSize]360	}361	name = strings.TrimRight(name, "/")362363	var f formatter364	v7 := tw.blk.toV7()365	v7.typeFlag()[0] = flag366	f.formatString(v7.name(), name)367	f.formatOctal(v7.mode(), 0)368	f.formatOctal(v7.uid(), 0)369	f.formatOctal(v7.gid(), 0)370	f.formatOctal(v7.size(), int64(len(data))) // Must be < 8GiB371	f.formatOctal(v7.modTime(), 0)372	tw.blk.setFormat(format)373	if f.err != nil {374		return f.err // Only occurs if size condition is violated375	}376377	// Write the header and data.378	if err := tw.writeRawHeader(&tw.blk, int64(len(data)), flag); err != nil {379		return err380	}381	_, err := io.WriteString(tw, data)382	return err383}384385// writeRawHeader writes the value of blk, regardless of its value.386// It sets up the Writer such that it can accept a file of the given size.387// If the flag is a special header-only flag, then the size is treated as zero.388func (tw *Writer) writeRawHeader(blk *block, size int64, flag byte) error {389	if err := tw.Flush(); err != nil {390		return err391	}392	if _, err := tw.w.Write(blk[:]); err != nil {393		return err394	}395	if isHeaderOnlyType(flag) {396		size = 0397	}398	tw.curr = &regFileWriter{tw.w, size}399	tw.pad = blockPadding(size)400	return nil401}402403// AddFS adds the files from fs.FS to the archive.404// It walks the directory tree starting at the root of the filesystem405// adding each file to the tar archive while maintaining the directory structure.406func (tw *Writer) AddFS(fsys fs.FS) error {407	return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {408		if err != nil {409			return err410		}411		if name == "." {412			return nil413		}414		info, err := d.Info()415		if err != nil {416			return err417		}418		linkTarget := ""419		if typ := d.Type(); typ == fs.ModeSymlink {420			var err error421			linkTarget, err = fs.ReadLink(fsys, name)422			if err != nil {423				return err424			}425		} else if !typ.IsRegular() && typ != fs.ModeDir {426			return errors.New("tar: cannot add non-regular file")427		}428		h, err := FileInfoHeader(info, linkTarget)429		if err != nil {430			return err431		}432		h.Name = name433		if d.IsDir() {434			h.Name += "/"435		}436		if err := tw.WriteHeader(h); err != nil {437			return err438		}439		if !d.Type().IsRegular() {440			return nil441		}442		f, err := fsys.Open(name)443		if err != nil {444			return err445		}446		defer f.Close()447		_, err = io.Copy(tw, f)448		return err449	})450}451452// splitUSTARPath splits a path according to USTAR prefix and suffix rules.453// If the path is not splittable, then it will return ("", "", false).454func splitUSTARPath(name string) (prefix, suffix string, ok bool) {455	length := len(name)456	if length <= nameSize || !isASCII(name) {457		return "", "", false458	} else if length > prefixSize+1 {459		length = prefixSize + 1460	} else if name[length-1] == '/' {461		length--462	}463464	i := strings.LastIndex(name[:length], "/")465	nlen := len(name) - i - 1 // nlen is length of suffix466	plen := i                 // plen is length of prefix467	if i <= 0 || nlen > nameSize || nlen == 0 || plen > prefixSize {468		return "", "", false469	}470	return name[:i], name[i+1:], true471}472473// Write writes to the current file in the tar archive.474// Write returns the error [ErrWriteTooLong] if more than475// Header.Size bytes are written after [Writer.WriteHeader].476//477// Calling Write on special types like [TypeLink], [TypeSymlink], [TypeChar],478// [TypeBlock], [TypeDir], and [TypeFifo] returns (0, [ErrWriteTooLong]) regardless479// of what the [Header.Size] claims.480func (tw *Writer) Write(b []byte) (int, error) {481	if tw.err != nil {482		return 0, tw.err483	}484	n, err := tw.curr.Write(b)485	if err != nil && err != ErrWriteTooLong {486		tw.err = err487	}488	return n, err489}490491// readFrom populates the content of the current file by reading from r.492// The bytes read must match the number of remaining bytes in the current file.493//494// If the current file is sparse and r is an io.ReadSeeker,495// then readFrom uses Seek to skip past holes defined in Header.SparseHoles,496// assuming that skipped regions are all NULs.497// This always reads the last byte to ensure r is the right size.498//499// TODO(dsnet): Re-export this when adding sparse file support.500// See https://golang.org/issue/22735501func (tw *Writer) readFrom(r io.Reader) (int64, error) {502	if tw.err != nil {503		return 0, tw.err504	}505	n, err := tw.curr.ReadFrom(r)506	if err != nil && err != ErrWriteTooLong {507		tw.err = err508	}509	return n, err510}511512// Close closes the tar archive by flushing the padding, and writing the footer.513// If the current file (from a prior call to [Writer.WriteHeader]) is not fully written,514// then this returns an error.515func (tw *Writer) Close() error {516	if tw.err == ErrWriteAfterClose {517		return nil518	}519	if tw.err != nil {520		return tw.err521	}522523	// Trailer: two zero blocks.524	err := tw.Flush()525	for i := 0; i < 2 && err == nil; i++ {526		_, err = tw.w.Write(zeroBlock[:])527	}528529	// Ensure all future actions are invalid.530	tw.err = ErrWriteAfterClose531	return err // Report IO errors532}533534// regFileWriter is a fileWriter for writing data to a regular file entry.535type regFileWriter struct {536	w  io.Writer // Underlying Writer537	nb int64     // Number of remaining bytes to write538}539540func (fw *regFileWriter) Write(b []byte) (n int, err error) {541	overwrite := int64(len(b)) > fw.nb542	if overwrite {543		b = b[:fw.nb]544	}545	if len(b) > 0 {546		n, err = fw.w.Write(b)547		fw.nb -= int64(n)548	}549	switch {550	case err != nil:551		return n, err552	case overwrite:553		return n, ErrWriteTooLong554	default:555		return n, nil556	}557}558559func (fw *regFileWriter) ReadFrom(r io.Reader) (int64, error) {560	return io.Copy(struct{ io.Writer }{fw}, r)561}562563// logicalRemaining implements fileState.logicalRemaining.564func (fw regFileWriter) logicalRemaining() int64 {565	return fw.nb566}567568// physicalRemaining implements fileState.physicalRemaining.569func (fw regFileWriter) physicalRemaining() int64 {570	return fw.nb571}572573// sparseFileWriter is a fileWriter for writing data to a sparse file entry.574type sparseFileWriter struct {575	fw  fileWriter  // Underlying fileWriter576	sp  sparseDatas // Normalized list of data fragments577	pos int64       // Current position in sparse file578}579580func (sw *sparseFileWriter) Write(b []byte) (n int, err error) {581	overwrite := int64(len(b)) > sw.logicalRemaining()582	if overwrite {583		b = b[:sw.logicalRemaining()]584	}585586	b0 := b587	endPos := sw.pos + int64(len(b))588	for endPos > sw.pos && err == nil {589		var nf int // Bytes written in fragment590		dataStart, dataEnd := sw.sp[0].Offset, sw.sp[0].endOffset()591		if sw.pos < dataStart { // In a hole fragment592			bf := b[:min(int64(len(b)), dataStart-sw.pos)]593			nf, err = zeroWriter{}.Write(bf)594		} else { // In a data fragment595			bf := b[:min(int64(len(b)), dataEnd-sw.pos)]596			nf, err = sw.fw.Write(bf)597		}598		b = b[nf:]599		sw.pos += int64(nf)600		if sw.pos >= dataEnd && len(sw.sp) > 1 {601			sw.sp = sw.sp[1:] // Ensure last fragment always remains602		}603	}604605	n = len(b0) - len(b)606	switch {607	case err == ErrWriteTooLong:608		return n, errMissData // Not possible; implies bug in validation logic609	case err != nil:610		return n, err611	case sw.logicalRemaining() == 0 && sw.physicalRemaining() > 0:612		return n, errUnrefData // Not possible; implies bug in validation logic613	case overwrite:614		return n, ErrWriteTooLong615	default:616		return n, nil617	}618}619620func (sw *sparseFileWriter) ReadFrom(r io.Reader) (n int64, err error) {621	rs, ok := r.(io.ReadSeeker)622	if ok {623		if _, err := rs.Seek(0, io.SeekCurrent); err != nil {624			ok = false // Not all io.Seeker can really seek625		}626	}627	if !ok {628		return io.Copy(struct{ io.Writer }{sw}, r)629	}630631	var readLastByte bool632	pos0 := sw.pos633	for sw.logicalRemaining() > 0 && !readLastByte && err == nil {634		var nf int64 // Size of fragment635		dataStart, dataEnd := sw.sp[0].Offset, sw.sp[0].endOffset()636		if sw.pos < dataStart { // In a hole fragment637			nf = dataStart - sw.pos638			if sw.physicalRemaining() == 0 {639				readLastByte = true640				nf--641			}642			_, err = rs.Seek(nf, io.SeekCurrent)643		} else { // In a data fragment644			nf = dataEnd - sw.pos645			nf, err = io.CopyN(sw.fw, rs, nf)646		}647		sw.pos += nf648		if sw.pos >= dataEnd && len(sw.sp) > 1 {649			sw.sp = sw.sp[1:] // Ensure last fragment always remains650		}651	}652653	// If the last fragment is a hole, then seek to 1-byte before EOF, and654	// read a single byte to ensure the file is the right size.655	if readLastByte && err == nil {656		_, err = mustReadFull(rs, []byte{0})657		sw.pos++658	}659660	n = sw.pos - pos0661	switch {662	case err == io.EOF:663		return n, io.ErrUnexpectedEOF664	case err == ErrWriteTooLong:665		return n, errMissData // Not possible; implies bug in validation logic666	case err != nil:667		return n, err668	case sw.logicalRemaining() == 0 && sw.physicalRemaining() > 0:669		return n, errUnrefData // Not possible; implies bug in validation logic670	default:671		return n, ensureEOF(rs)672	}673}674675func (sw sparseFileWriter) logicalRemaining() int64 {676	return sw.sp[len(sw.sp)-1].endOffset() - sw.pos677}678679func (sw sparseFileWriter) physicalRemaining() int64 {680	return sw.fw.physicalRemaining()681}682683// zeroWriter may only be written with NULs, otherwise it returns errWriteHole.684type zeroWriter struct{}685686func (zeroWriter) Write(b []byte) (int, error) {687	for i, c := range b {688		if c != 0 {689			return i, errWriteHole690		}691	}692	return len(b), nil693}694695// ensureEOF checks whether r is at EOF, reporting ErrWriteTooLong if not so.696func ensureEOF(r io.Reader) error {697	n, err := tryReadFull(r, []byte{0})698	switch {699	case n > 0:700		return ErrWriteTooLong701	case err == io.EOF:702		return nil703	default:704		return err705	}706}

Code quality findings 11

Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_ = realSize
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
sph := append([]sparseEntry{}, hdr.SparseHoles...) // Copy sparse map
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
spb = append(strconv.AppendInt(spb, int64(len(spd)), 10), '\n')
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
spb = append(strconv.AppendInt(spb, s.Offset, 10), '\n')
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
spb = append(strconv.AppendInt(spb, s.Length, 10), '\n')
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
spb = append(spb, zeroBlock[:pad]...)
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
hdr.Name = path.Join(dir, "GNUSparseFile.0", file)
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
name = path.Join(dir, "PaxHeaders.0", file)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
sph := append([]sparseEntry{}, hdr.SparseHoles...) // Copy sparse map
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
spb = append(spb, spHdr[:]...)
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for i, c := range b {

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.