From ef162c4b4fa1820c119cd4c19c416760f8a1bc01 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Mon, 23 Jul 2018 09:25:06 +0300 Subject: [PATCH] [TAR] Correctly handle gnu format when used for incremental backups Previously, in such cases the resulting entries were having incorrect file names instead of having ctime/atime. --- Sources/TAR/TarEntryInfo.swift | 73 +++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/Sources/TAR/TarEntryInfo.swift b/Sources/TAR/TarEntryInfo.swift index 8a1197e7..9d140b39 100644 --- a/Sources/TAR/TarEntryInfo.swift +++ b/Sources/TAR/TarEntryInfo.swift @@ -111,7 +111,7 @@ public struct TarEntryInfo: ContainerEntryInfo { // the alternative, which was used in previous versions, is to throw an error. if let posixAttributes = byteReader.tarInt(maxLength: 8, radix: 8) { - // Sometimes file mode also contains unix type, so we need to filter it out. + // Sometimes file mode field also contains unix type, so we need to filter it out. self.permissions = Permissions(rawValue: UInt32(truncatingIfNeeded: posixAttributes) & 0xFFF) } else { self.permissions = nil @@ -160,13 +160,17 @@ public struct TarEntryInfo: ContainerEntryInfo { // Linked file name let linkName = byteReader.tarCString(maxLength: 100) - // There are two POSIX-like formats: pre-POSIX used by GNU tools (aka "old-GNU") and POSIX (aka "ustar"). - // They differ in `magic` field value and how other fields are padded (either SPACEs or NULLs). - // Padding is taken care of in Data extension functions in "ByteReader+Tar.swift" file. - // Here we deal with magic. First one is "old-GNU", second is "ustar", third is for compatiblity with strange - // implementations of "ustar", which use SPACEs instead of NULLs. + // There are two different formats utilizing this section of TAR header: GNU format and POSIX (aka "ustar"; + // also PAX containers can also be considered POSIX). They differ in the value of magic field as well as what + // comes after deviceMinorNumber field. While "ustar" format may contain prefix for file name, GNU format + // uses this place for storing atime/ctime and fields related to sparse-files. In practice, these fields are + // rarely used by GNU tar and only present if "incremental backups" options were used. Thus, GNU format TAR + // container can often be incorrectly considered as having prefix field containing only NULLs. let magic = byteReader.uint64() + var gnuAtime: Int? + var gnuCtime: Int? + if magic == 0x0020207261747375 || magic == 0x3030007261747375 || magic == 0x3030207261747375 { self.hasRecognizedMagic = true let uname = byteReader.tarCString(maxLength: 32) @@ -175,22 +179,33 @@ public struct TarEntryInfo: ContainerEntryInfo { let gname = byteReader.tarCString(maxLength: 32) self.ownerGroupName = (local?.gname ?? global?.gname) ?? gname - deviceMajorNumber = byteReader.tarInt(maxLength: 8) - deviceMinorNumber = byteReader.tarInt(maxLength: 8) - let prefix = byteReader.tarCString(maxLength: 155) - if prefix != "" { - if prefix.last == "/" { - name = prefix + name - } else { - name = prefix + "/" + name + self.deviceMajorNumber = byteReader.tarInt(maxLength: 8) + self.deviceMinorNumber = byteReader.tarInt(maxLength: 8) + + if magic == 0x00_20_20_72_61_74_73_75 { // GNU format. + // GNU format mostly is identical to POSIX format and in the common situations can be considered as + // having prefix containing only NULLs. However, in the case of incremental backups produced by GNU tar + // this part of the TAR header is used for storing a lot of different properties. For now, we are only + // reading atime and ctime. + + gnuAtime = byteReader.tarInt(maxLength: 12, radix: 8) + gnuCtime = byteReader.tarInt(maxLength: 12, radix: 8) + } else { + let prefix = byteReader.tarCString(maxLength: 155) + if prefix != "" { + if prefix.last == "/" { + name = prefix + name + } else { + name = prefix + "/" + name + } } } } else { self.hasRecognizedMagic = false - ownerUserName = local?.uname ?? global?.uname - ownerGroupName = local?.gname ?? global?.gname - deviceMajorNumber = nil - deviceMinorNumber = nil + self.ownerUserName = local?.uname ?? global?.uname + self.ownerGroupName = local?.gname ?? global?.gname + self.deviceMajorNumber = nil + self.deviceMinorNumber = nil } // Set `name` and `linkName` to values from PAX or GNU format if possible. @@ -199,27 +214,31 @@ public struct TarEntryInfo: ContainerEntryInfo { // Set additional properties from PAX extended headers. if let atime = local?.atime ?? global?.atime { - accessTime = Date(timeIntervalSince1970: atime) + self.accessTime = Date(timeIntervalSince1970: atime) + } else if let gnuAtime = gnuAtime { + self.accessTime = Date(timeIntervalSince1970: TimeInterval(gnuAtime)) } else { - accessTime = nil + self.accessTime = nil } if let ctime = local?.ctime ?? global?.ctime { - creationTime = Date(timeIntervalSince1970: ctime) + self.creationTime = Date(timeIntervalSince1970: ctime) + } else if let gnuCtime = gnuCtime { + self.creationTime = Date(timeIntervalSince1970: TimeInterval(gnuCtime)) } else { - creationTime = nil + self.creationTime = nil } - charset = local?.charset ?? global?.charset - comment = local?.comment ?? global?.comment + self.charset = local?.charset ?? global?.charset + self.comment = local?.comment ?? global?.comment if let localUnknownRecords = local?.unknownRecords { if let globalUnknownRecords = global?.unknownRecords { - unknownExtendedHeaderRecords = globalUnknownRecords.merging(localUnknownRecords) { $1 } + self.unknownExtendedHeaderRecords = globalUnknownRecords.merging(localUnknownRecords) { $1 } } else { - unknownExtendedHeaderRecords = localUnknownRecords + self.unknownExtendedHeaderRecords = localUnknownRecords } } else { - unknownExtendedHeaderRecords = global?.unknownRecords + self.unknownExtendedHeaderRecords = global?.unknownRecords } }