-
Notifications
You must be signed in to change notification settings - Fork 6
/
Light-Swift-Untar.swift
160 lines (146 loc) · 6.82 KB
/
Light-Swift-Untar.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import Foundation
public typealias Closure = (Double) -> Void
enum UntarError: Error, LocalizedError {
case notFound(file: String)
case corruptFile(type: UnicodeScalar)
public var errorDescription: String? {
switch self {
case let .notFound(file: file): return "Source file \(file) not found"
case let .corruptFile(type: type): return "Invalid block type \(type) found"
}
}
}
public extension FileManager {
// MARK: - Definitions
private static var tarBlockSize: UInt64 = 512
private static var tarTypePosition: UInt64 = 156
private static var tarNamePosition: UInt64 = 0
private static var tarNameSize: UInt64 = 100
private static var tarSizePosition: UInt64 = 124
private static var tarSizeSize: UInt64 = 12
private static var tarMaxBlockLoadInMemory: UInt64 = 100
// MARK: - Private Methods
private func createFilesAndDirectories(path: String, tarObject: Any, size: UInt64,
progress progressClosure: Closure?) throws -> Bool {
try createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
var location: UInt64 = 0
while location < size {
var blockCount: UInt64 = 1
if let closure = progressClosure { closure(Double(location) / Double(size)) }
let type = self.type(object: tarObject, offset: location)
switch type {
case "0": // File
let name = self.name(object: tarObject, offset: location)
let filePath = URL(fileURLWithPath: path).appendingPathComponent(name).path
let size = self.size(object: tarObject, offset: location)
if size == 0 { try "".write(toFile: filePath, atomically: true, encoding: .utf8) }
else {
blockCount += (size - 1) / FileManager.tarBlockSize + 1 // size / tarBlockSize rounded up
writeFileData(object: tarObject, location: location + FileManager.tarBlockSize,
length: size, path: filePath)
}
case "5": // Directory
let name = self.name(object: tarObject, offset: location)
let directoryPath = URL(fileURLWithPath: path).appendingPathComponent(name).path
try createDirectory(atPath: directoryPath, withIntermediateDirectories: true,
attributes: nil)
case "\0": break // Null block
case "x": blockCount += 1 // Extra header block
case "1": fallthrough
case "2": fallthrough
case "3": fallthrough
case "4": fallthrough
case "6": fallthrough
case "7": fallthrough
case "g": // Not a file nor directory
let size = self.size(object: tarObject, offset: location)
blockCount += UInt64(ceil(Double(size) / Double(FileManager.tarBlockSize)))
default: throw UntarError.corruptFile(type: type) // Not a tar type
}
location += blockCount * FileManager.tarBlockSize
}
return true
}
private func type(object: Any, offset: UInt64) -> UnicodeScalar {
let typeData = data(object: object, location: offset + FileManager.tarTypePosition, length: 1)!
return UnicodeScalar([UInt8](typeData)[0])
}
private func name(object: Any, offset: UInt64) -> String {
var nameSize = FileManager.tarNameSize
for i in 0...FileManager.tarNameSize {
let char = String(data: data(object: object, location: offset + FileManager.tarNamePosition + i, length: 1)!, encoding: .ascii)!
if (char == "\0") {
nameSize = i
break
}
}
return String(data: data(object: object, location: offset + FileManager.tarNamePosition, length: nameSize)!, encoding: .utf8)!
}
private func size(object: Any, offset: UInt64) -> UInt64 {
let sizeData = data(object: object, location: offset + FileManager.tarSizePosition,
length: FileManager.tarSizeSize)!
let sizeString = String(data: sizeData, encoding: .ascii)!
return strtoull(sizeString, nil, 8) // Size is an octal number, convert to decimal
}
private func writeFileData(object: Any, location _loc: UInt64, length _len: UInt64,
path: String) {
if let data = object as? Data {
createFile(atPath: path, contents: data.subdata(in: Int(_loc) ..< Int(_loc + _len)),
attributes: nil)
} else if let fileHandle = object as? FileHandle {
if NSData().write(toFile: path, atomically: false) {
let destinationFile = FileHandle(forWritingAtPath: path)!
fileHandle.seek(toFileOffset: _loc)
let maxSize = FileManager.tarMaxBlockLoadInMemory * FileManager.tarBlockSize
var length = _len, location = _loc
while length > maxSize {
autoreleasepool { // Needed to prevent heap overflow when reading large files
destinationFile.write(fileHandle.readData(ofLength: Int(maxSize)))
}
location += maxSize
length -= maxSize
}
autoreleasepool { // Needed to prevent heap overflow when reading large files
destinationFile.write(fileHandle.readData(ofLength: Int(length)))
}
destinationFile.closeFile()
}
}
}
private func data(object: Any, location: UInt64, length: UInt64) -> Data? {
if let data = object as? Data {
return data.subdata(in: Int(location) ..< Int(location + length))
} else if let fileHandle = object as? FileHandle {
fileHandle.seek(toFileOffset: location)
return autoreleasepool { // Needed to prevent heap overflow when reading large files
fileHandle.readData(ofLength: Int(length))
}
}
return nil
}
// MARK: - Public Methods
// Return true when no error for convenience
@discardableResult func createFilesAndDirectories(path: String, tarData: Data,
progress: Closure? = nil) throws -> Bool {
try createFilesAndDirectories(path: path, tarObject: tarData, size: UInt64(tarData.count),
progress: progress)
}
@discardableResult func createFilesAndDirectories(url: URL, tarData: Data,
progress: Closure? = nil) throws -> Bool {
try createFilesAndDirectories(path: url.path, tarData: tarData, progress: progress)
}
@discardableResult func createFilesAndDirectories(path: String, tarPath: String,
progress: Closure? = nil) throws -> Bool {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: tarPath) {
let attributes = try fileManager.attributesOfItem(atPath: tarPath)
let size = attributes[.size] as! UInt64
let fileHandle = FileHandle(forReadingAtPath: tarPath)!
let result = try createFilesAndDirectories(path: path, tarObject: fileHandle, size: size,
progress: progress)
fileHandle.closeFile()
return result
}
throw UntarError.notFound(file: tarPath)
}
}