| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| package tar |
|
|
| import ( |
| "errors" |
| "fmt" |
| "internal/godebug" |
| "io/fs" |
| "maps" |
| "math" |
| "path" |
| "reflect" |
| "strconv" |
| "strings" |
| "time" |
| ) |
|
|
| |
| |
| |
|
|
| var tarinsecurepath = godebug.New("tarinsecurepath") |
|
|
| var ( |
| ErrHeader = errors.New("archive/tar: invalid tar header") |
| ErrWriteTooLong = errors.New("archive/tar: write too long") |
| ErrFieldTooLong = errors.New("archive/tar: header field too long") |
| ErrWriteAfterClose = errors.New("archive/tar: write after close") |
| ErrInsecurePath = errors.New("archive/tar: insecure file path") |
| errMissData = errors.New("archive/tar: sparse file references non-existent data") |
| errUnrefData = errors.New("archive/tar: sparse file contains unreferenced data") |
| errWriteHole = errors.New("archive/tar: write non-NUL byte in sparse hole") |
| errSparseTooLong = errors.New("archive/tar: sparse map too long") |
| ) |
|
|
| type headerError []string |
|
|
| func (he headerError) Error() string { |
| const prefix = "archive/tar: cannot encode header" |
| var ss []string |
| for _, s := range he { |
| if s != "" { |
| ss = append(ss, s) |
| } |
| } |
| if len(ss) == 0 { |
| return prefix |
| } |
| return fmt.Sprintf("%s: %v", prefix, strings.Join(ss, "; and ")) |
| } |
|
|
| |
| const ( |
| |
| TypeReg = '0' |
|
|
| |
| TypeRegA = '\x00' |
|
|
| |
| TypeLink = '1' |
| TypeSymlink = '2' |
| TypeChar = '3' |
| TypeBlock = '4' |
| TypeDir = '5' |
| TypeFifo = '6' |
|
|
| |
| TypeCont = '7' |
|
|
| |
| |
| |
| TypeXHeader = 'x' |
|
|
| |
| |
| |
| |
| TypeXGlobalHeader = 'g' |
|
|
| |
| TypeGNUSparse = 'S' |
|
|
| |
| |
| |
| TypeGNULongName = 'L' |
| TypeGNULongLink = 'K' |
| ) |
|
|
| |
| const ( |
| paxNone = "" |
| paxPath = "path" |
| paxLinkpath = "linkpath" |
| paxSize = "size" |
| paxUid = "uid" |
| paxGid = "gid" |
| paxUname = "uname" |
| paxGname = "gname" |
| paxMtime = "mtime" |
| paxAtime = "atime" |
| paxCtime = "ctime" |
| paxCharset = "charset" |
| paxComment = "comment" |
|
|
| paxSchilyXattr = "SCHILY.xattr." |
|
|
| |
| paxGNUSparse = "GNU.sparse." |
| paxGNUSparseNumBlocks = "GNU.sparse.numblocks" |
| paxGNUSparseOffset = "GNU.sparse.offset" |
| paxGNUSparseNumBytes = "GNU.sparse.numbytes" |
| paxGNUSparseMap = "GNU.sparse.map" |
| paxGNUSparseName = "GNU.sparse.name" |
| paxGNUSparseMajor = "GNU.sparse.major" |
| paxGNUSparseMinor = "GNU.sparse.minor" |
| paxGNUSparseSize = "GNU.sparse.size" |
| paxGNUSparseRealSize = "GNU.sparse.realsize" |
| ) |
|
|
| |
| |
| |
| |
| var basicKeys = map[string]bool{ |
| paxPath: true, paxLinkpath: true, paxSize: true, paxUid: true, paxGid: true, |
| paxUname: true, paxGname: true, paxMtime: true, paxAtime: true, paxCtime: true, |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| type Header struct { |
| |
| |
| |
| Typeflag byte |
|
|
| Name string |
| Linkname string |
|
|
| Size int64 |
| Mode int64 |
| Uid int |
| Gid int |
| Uname string |
| Gname string |
|
|
| |
| |
| |
| |
| |
| ModTime time.Time |
| AccessTime time.Time |
| ChangeTime time.Time |
|
|
| Devmajor int64 |
| Devminor int64 |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Xattrs map[string]string |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| PAXRecords map[string]string |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Format Format |
| } |
|
|
| |
| type sparseEntry struct{ Offset, Length int64 } |
|
|
| func (s sparseEntry) endOffset() int64 { return s.Offset + s.Length } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| type ( |
| sparseDatas []sparseEntry |
| sparseHoles []sparseEntry |
| ) |
|
|
| |
| |
| func validateSparseEntries(sp []sparseEntry, size int64) bool { |
| |
| |
| if size < 0 { |
| return false |
| } |
| var pre sparseEntry |
| for _, cur := range sp { |
| switch { |
| case cur.Offset < 0 || cur.Length < 0: |
| return false |
| case cur.Offset > math.MaxInt64-cur.Length: |
| return false |
| case cur.endOffset() > size: |
| return false |
| case pre.endOffset() > cur.Offset: |
| return false |
| } |
| pre = cur |
| } |
| return true |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| func alignSparseEntries(src []sparseEntry, size int64) []sparseEntry { |
| dst := src[:0] |
| for _, s := range src { |
| pos, end := s.Offset, s.endOffset() |
| pos += blockPadding(+pos) |
| if end != size { |
| end -= blockPadding(-end) |
| } |
| if pos < end { |
| dst = append(dst, sparseEntry{Offset: pos, Length: end - pos}) |
| } |
| } |
| return dst |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| func invertSparseEntries(src []sparseEntry, size int64) []sparseEntry { |
| dst := src[:0] |
| var pre sparseEntry |
| for _, cur := range src { |
| if cur.Length == 0 { |
| continue |
| } |
| pre.Length = cur.Offset - pre.Offset |
| if pre.Length > 0 { |
| dst = append(dst, pre) |
| } |
| pre.Offset = cur.endOffset() |
| } |
| pre.Length = size - pre.Offset |
| return append(dst, pre) |
| } |
|
|
| |
| |
| |
| |
| type fileState interface { |
| logicalRemaining() int64 |
| physicalRemaining() int64 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| func (h Header) allowedFormats() (format Format, paxHdrs map[string]string, err error) { |
| format = FormatUSTAR | FormatPAX | FormatGNU |
| paxHdrs = make(map[string]string) |
|
|
| var whyNoUSTAR, whyNoPAX, whyNoGNU string |
| var preferPAX bool |
| verifyString := func(s string, size int, name, paxKey string) { |
| |
| |
| |
| tooLong := len(s) > size |
| allowLongGNU := paxKey == paxPath || paxKey == paxLinkpath |
| if hasNUL(s) || (tooLong && !allowLongGNU) { |
| whyNoGNU = fmt.Sprintf("GNU cannot encode %s=%q", name, s) |
| format.mustNotBe(FormatGNU) |
| } |
| if !isASCII(s) || tooLong { |
| canSplitUSTAR := paxKey == paxPath |
| if _, _, ok := splitUSTARPath(s); !canSplitUSTAR || !ok { |
| whyNoUSTAR = fmt.Sprintf("USTAR cannot encode %s=%q", name, s) |
| format.mustNotBe(FormatUSTAR) |
| } |
| if paxKey == paxNone { |
| whyNoPAX = fmt.Sprintf("PAX cannot encode %s=%q", name, s) |
| format.mustNotBe(FormatPAX) |
| } else { |
| paxHdrs[paxKey] = s |
| } |
| } |
| if v, ok := h.PAXRecords[paxKey]; ok && v == s { |
| paxHdrs[paxKey] = v |
| } |
| } |
| verifyNumeric := func(n int64, size int, name, paxKey string) { |
| if !fitsInBase256(size, n) { |
| whyNoGNU = fmt.Sprintf("GNU cannot encode %s=%d", name, n) |
| format.mustNotBe(FormatGNU) |
| } |
| if !fitsInOctal(size, n) { |
| whyNoUSTAR = fmt.Sprintf("USTAR cannot encode %s=%d", name, n) |
| format.mustNotBe(FormatUSTAR) |
| if paxKey == paxNone { |
| whyNoPAX = fmt.Sprintf("PAX cannot encode %s=%d", name, n) |
| format.mustNotBe(FormatPAX) |
| } else { |
| paxHdrs[paxKey] = strconv.FormatInt(n, 10) |
| } |
| } |
| if v, ok := h.PAXRecords[paxKey]; ok && v == strconv.FormatInt(n, 10) { |
| paxHdrs[paxKey] = v |
| } |
| } |
| verifyTime := func(ts time.Time, size int, name, paxKey string) { |
| if ts.IsZero() { |
| return |
| } |
| if !fitsInBase256(size, ts.Unix()) { |
| whyNoGNU = fmt.Sprintf("GNU cannot encode %s=%v", name, ts) |
| format.mustNotBe(FormatGNU) |
| } |
| isMtime := paxKey == paxMtime |
| fitsOctal := fitsInOctal(size, ts.Unix()) |
| if (isMtime && !fitsOctal) || !isMtime { |
| whyNoUSTAR = fmt.Sprintf("USTAR cannot encode %s=%v", name, ts) |
| format.mustNotBe(FormatUSTAR) |
| } |
| needsNano := ts.Nanosecond() != 0 |
| if !isMtime || !fitsOctal || needsNano { |
| preferPAX = true |
| if paxKey == paxNone { |
| whyNoPAX = fmt.Sprintf("PAX cannot encode %s=%v", name, ts) |
| format.mustNotBe(FormatPAX) |
| } else { |
| paxHdrs[paxKey] = formatPAXTime(ts) |
| } |
| } |
| if v, ok := h.PAXRecords[paxKey]; ok && v == formatPAXTime(ts) { |
| paxHdrs[paxKey] = v |
| } |
| } |
|
|
| |
| var blk block |
| v7 := blk.toV7() |
| ustar := blk.toUSTAR() |
| gnu := blk.toGNU() |
| verifyString(h.Name, len(v7.name()), "Name", paxPath) |
| verifyString(h.Linkname, len(v7.linkName()), "Linkname", paxLinkpath) |
| verifyString(h.Uname, len(ustar.userName()), "Uname", paxUname) |
| verifyString(h.Gname, len(ustar.groupName()), "Gname", paxGname) |
| verifyNumeric(h.Mode, len(v7.mode()), "Mode", paxNone) |
| verifyNumeric(int64(h.Uid), len(v7.uid()), "Uid", paxUid) |
| verifyNumeric(int64(h.Gid), len(v7.gid()), "Gid", paxGid) |
| verifyNumeric(h.Size, len(v7.size()), "Size", paxSize) |
| verifyNumeric(h.Devmajor, len(ustar.devMajor()), "Devmajor", paxNone) |
| verifyNumeric(h.Devminor, len(ustar.devMinor()), "Devminor", paxNone) |
| verifyTime(h.ModTime, len(v7.modTime()), "ModTime", paxMtime) |
| verifyTime(h.AccessTime, len(gnu.accessTime()), "AccessTime", paxAtime) |
| verifyTime(h.ChangeTime, len(gnu.changeTime()), "ChangeTime", paxCtime) |
|
|
| |
| var whyOnlyPAX, whyOnlyGNU string |
| switch h.Typeflag { |
| case TypeReg, TypeChar, TypeBlock, TypeFifo, TypeGNUSparse: |
| |
| if strings.HasSuffix(h.Name, "/") { |
| return FormatUnknown, nil, headerError{"filename may not have trailing slash"} |
| } |
| case TypeXHeader, TypeGNULongName, TypeGNULongLink: |
| return FormatUnknown, nil, headerError{"cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers"} |
| case TypeXGlobalHeader: |
| h2 := Header{Name: h.Name, Typeflag: h.Typeflag, Xattrs: h.Xattrs, PAXRecords: h.PAXRecords, Format: h.Format} |
| if !reflect.DeepEqual(h, h2) { |
| return FormatUnknown, nil, headerError{"only PAXRecords should be set for TypeXGlobalHeader"} |
| } |
| whyOnlyPAX = "only PAX supports TypeXGlobalHeader" |
| format.mayOnlyBe(FormatPAX) |
| } |
| if !isHeaderOnlyType(h.Typeflag) && h.Size < 0 { |
| return FormatUnknown, nil, headerError{"negative size on header-only type"} |
| } |
|
|
| |
| if len(h.Xattrs) > 0 { |
| for k, v := range h.Xattrs { |
| paxHdrs[paxSchilyXattr+k] = v |
| } |
| whyOnlyPAX = "only PAX supports Xattrs" |
| format.mayOnlyBe(FormatPAX) |
| } |
| if len(h.PAXRecords) > 0 { |
| for k, v := range h.PAXRecords { |
| switch _, exists := paxHdrs[k]; { |
| case exists: |
| continue |
| case h.Typeflag == TypeXGlobalHeader: |
| paxHdrs[k] = v |
| case !basicKeys[k] && !strings.HasPrefix(k, paxGNUSparse): |
| paxHdrs[k] = v |
| } |
| } |
| whyOnlyPAX = "only PAX supports PAXRecords" |
| format.mayOnlyBe(FormatPAX) |
| } |
| for k, v := range paxHdrs { |
| if !validPAXRecord(k, v) { |
| return FormatUnknown, nil, headerError{fmt.Sprintf("invalid PAX record: %q", k+" = "+v)} |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| if wantFormat := h.Format; wantFormat != FormatUnknown { |
| if wantFormat.has(FormatPAX) && !preferPAX { |
| wantFormat.mayBe(FormatUSTAR) |
| } |
| format.mayOnlyBe(wantFormat) |
| } |
| if format == FormatUnknown { |
| switch h.Format { |
| case FormatUSTAR: |
| err = headerError{"Format specifies USTAR", whyNoUSTAR, whyOnlyPAX, whyOnlyGNU} |
| case FormatPAX: |
| err = headerError{"Format specifies PAX", whyNoPAX, whyOnlyGNU} |
| case FormatGNU: |
| err = headerError{"Format specifies GNU", whyNoGNU, whyOnlyPAX} |
| default: |
| err = headerError{whyNoUSTAR, whyNoPAX, whyNoGNU, whyOnlyPAX, whyOnlyGNU} |
| } |
| } |
| return format, paxHdrs, err |
| } |
|
|
| |
| func (h *Header) FileInfo() fs.FileInfo { |
| return headerFileInfo{h} |
| } |
|
|
| |
| type headerFileInfo struct { |
| h *Header |
| } |
|
|
| func (fi headerFileInfo) Size() int64 { return fi.h.Size } |
| func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() } |
| func (fi headerFileInfo) ModTime() time.Time { return fi.h.ModTime } |
| func (fi headerFileInfo) Sys() any { return fi.h } |
|
|
| |
| func (fi headerFileInfo) Name() string { |
| if fi.IsDir() { |
| return path.Base(path.Clean(fi.h.Name)) |
| } |
| return path.Base(fi.h.Name) |
| } |
|
|
| |
| func (fi headerFileInfo) Mode() (mode fs.FileMode) { |
| |
| mode = fs.FileMode(fi.h.Mode).Perm() |
|
|
| |
| if fi.h.Mode&c_ISUID != 0 { |
| mode |= fs.ModeSetuid |
| } |
| if fi.h.Mode&c_ISGID != 0 { |
| mode |= fs.ModeSetgid |
| } |
| if fi.h.Mode&c_ISVTX != 0 { |
| mode |= fs.ModeSticky |
| } |
|
|
| |
| switch m := fs.FileMode(fi.h.Mode) &^ 07777; m { |
| case c_ISDIR: |
| mode |= fs.ModeDir |
| case c_ISFIFO: |
| mode |= fs.ModeNamedPipe |
| case c_ISLNK: |
| mode |= fs.ModeSymlink |
| case c_ISBLK: |
| mode |= fs.ModeDevice |
| case c_ISCHR: |
| mode |= fs.ModeDevice |
| mode |= fs.ModeCharDevice |
| case c_ISSOCK: |
| mode |= fs.ModeSocket |
| } |
|
|
| switch fi.h.Typeflag { |
| case TypeSymlink: |
| mode |= fs.ModeSymlink |
| case TypeChar: |
| mode |= fs.ModeDevice |
| mode |= fs.ModeCharDevice |
| case TypeBlock: |
| mode |= fs.ModeDevice |
| case TypeDir: |
| mode |= fs.ModeDir |
| case TypeFifo: |
| mode |= fs.ModeNamedPipe |
| } |
|
|
| return mode |
| } |
|
|
| func (fi headerFileInfo) String() string { |
| return fs.FormatFileInfo(fi) |
| } |
|
|
| |
| var sysStat func(fi fs.FileInfo, h *Header, doNameLookups bool) error |
|
|
| const ( |
| |
| |
| c_ISUID = 04000 |
| c_ISGID = 02000 |
| c_ISVTX = 01000 |
|
|
| |
| |
| c_ISDIR = 040000 |
| c_ISFIFO = 010000 |
| c_ISREG = 0100000 |
| c_ISLNK = 0120000 |
| c_ISBLK = 060000 |
| c_ISCHR = 020000 |
| c_ISSOCK = 0140000 |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func FileInfoHeader(fi fs.FileInfo, link string) (*Header, error) { |
| if fi == nil { |
| return nil, errors.New("archive/tar: FileInfo is nil") |
| } |
| fm := fi.Mode() |
| h := &Header{ |
| Name: fi.Name(), |
| ModTime: fi.ModTime(), |
| Mode: int64(fm.Perm()), |
| } |
| switch { |
| case fm.IsRegular(): |
| h.Typeflag = TypeReg |
| h.Size = fi.Size() |
| case fi.IsDir(): |
| h.Typeflag = TypeDir |
| h.Name += "/" |
| case fm&fs.ModeSymlink != 0: |
| h.Typeflag = TypeSymlink |
| h.Linkname = link |
| case fm&fs.ModeDevice != 0: |
| if fm&fs.ModeCharDevice != 0 { |
| h.Typeflag = TypeChar |
| } else { |
| h.Typeflag = TypeBlock |
| } |
| case fm&fs.ModeNamedPipe != 0: |
| h.Typeflag = TypeFifo |
| case fm&fs.ModeSocket != 0: |
| return nil, fmt.Errorf("archive/tar: sockets not supported") |
| default: |
| return nil, fmt.Errorf("archive/tar: unknown file mode %v", fm) |
| } |
| if fm&fs.ModeSetuid != 0 { |
| h.Mode |= c_ISUID |
| } |
| if fm&fs.ModeSetgid != 0 { |
| h.Mode |= c_ISGID |
| } |
| if fm&fs.ModeSticky != 0 { |
| h.Mode |= c_ISVTX |
| } |
| |
| |
| if sys, ok := fi.Sys().(*Header); ok { |
| |
| |
| h.Uid = sys.Uid |
| h.Gid = sys.Gid |
| h.Uname = sys.Uname |
| h.Gname = sys.Gname |
| h.AccessTime = sys.AccessTime |
| h.ChangeTime = sys.ChangeTime |
| h.Xattrs = maps.Clone(sys.Xattrs) |
| if sys.Typeflag == TypeLink { |
| |
| h.Typeflag = TypeLink |
| h.Size = 0 |
| h.Linkname = sys.Linkname |
| } |
| h.PAXRecords = maps.Clone(sys.PAXRecords) |
| } |
| var doNameLookups = true |
| if iface, ok := fi.(FileInfoNames); ok { |
| doNameLookups = false |
| var err error |
| h.Gname, err = iface.Gname() |
| if err != nil { |
| return nil, err |
| } |
| h.Uname, err = iface.Uname() |
| if err != nil { |
| return nil, err |
| } |
| } |
| if sysStat != nil { |
| return h, sysStat(fi, h, doNameLookups) |
| } |
| return h, nil |
| } |
|
|
| |
| |
| |
| type FileInfoNames interface { |
| fs.FileInfo |
| |
| Uname() (string, error) |
| |
| Gname() (string, error) |
| } |
|
|
| |
| |
| func isHeaderOnlyType(flag byte) bool { |
| switch flag { |
| case TypeLink, TypeSymlink, TypeChar, TypeBlock, TypeDir, TypeFifo: |
| return true |
| default: |
| return false |
| } |
| } |
|
|