Skip to content

Commit b580355

Browse files
authored
Add ArArchiveReader (#1)
* Create Parser
1 parent f41c052 commit b580355

File tree

14 files changed

+20424
-62
lines changed

14 files changed

+20424
-62
lines changed

.github/workflows/buildAndTest.yml

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,28 @@ jobs:
3939
- uses: actions/checkout@v2
4040
- name: Run tests
4141
run: swift test
42-
# TestOnWindows10-x86_64:
43-
# runs-on: windows-latest
44-
# steps:
45-
# - uses: actions/checkout@v2
46-
# - uses: seanmiddleditch/gha-setup-vsdevenv@master
47-
# - name: Install swift-DEVELOPMENT-SNAPSHOT-2021-01-27-a
48-
# run: Install-Binary -Url "https://swift.org/builds/development/windows10/swift-DEVELOPMENT-SNAPSHOT-2021-01-27-a/swift-DEVELOPMENT-SNAPSHOT-2021-01-27-a-windows10.exe" -Name "installer.exe" -ArgumentList ("-q")
49-
# - name: Set Environment Variables
50-
# run: |
51-
# echo "SDKROOT=C:\Library\Developer\Platforms\Windows.platform\Developer\SDKs\Windows.sdk" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
52-
# echo "DEVELOPER_DIR=C:\Library\Developer" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
53-
# - name: Adjust Paths
54-
# run: echo "C:\Library\Developer\Toolchains\unknown-Asserts-development.xctoolchain\usr\bin;C:\Library\Swift-development\bin;C:\Library\icu-67\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
55-
# - name: Install Supporting Files
56-
# shell: cmd
57-
# run: |
58-
# copy "%SDKROOT%\usr\share\ucrt.modulemap" "%UniversalCRTSdkDir%\Include\%UCRTVersion%\ucrt\module.modulemap"
59-
# copy "%SDKROOT%\usr\share\visualc.modulemap" "%VCToolsInstallDir%\include\module.modulemap"
60-
# copy "%SDKROOT%\usr\share\visualc.apinotes" "%VCToolsInstallDir%\include\visualc.apinotes"
61-
# copy "%SDKROOT%\usr\share\winsdk.modulemap" "%UniversalCRTSdkDir%\Include\%UCRTVersion%\um\module.modulemap"
62-
# - name: Test
63-
# run: |
64-
# swift build
65-
# swift test
42+
TestOnWindows10-x86_64:
43+
runs-on: windows-latest
44+
steps:
45+
- uses: actions/checkout@v2
46+
- uses: seanmiddleditch/gha-setup-vsdevenv@master
47+
- name: Install swift-DEVELOPMENT-SNAPSHOT-2021-01-27-a
48+
run: Install-Binary -Url "https://swift.org/builds/development/windows10/swift-DEVELOPMENT-SNAPSHOT-2021-01-27-a/swift-DEVELOPMENT-SNAPSHOT-2021-01-27-a-windows10.exe" -Name "installer.exe" -ArgumentList ("-q")
49+
- name: Set Environment Variables
50+
run: |
51+
echo "SDKROOT=C:\Library\Developer\Platforms\Windows.platform\Developer\SDKs\Windows.sdk" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
52+
echo "DEVELOPER_DIR=C:\Library\Developer" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
53+
- name: Adjust Paths
54+
run: echo "C:\Library\Developer\Toolchains\unknown-Asserts-development.xctoolchain\usr\bin;C:\Library\Swift-development\bin;C:\Library\icu-67\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
55+
- name: Install Supporting Files
56+
shell: cmd
57+
run: |
58+
copy "%SDKROOT%\usr\share\ucrt.modulemap" "%UniversalCRTSdkDir%\Include\%UCRTVersion%\ucrt\module.modulemap"
59+
copy "%SDKROOT%\usr\share\visualc.modulemap" "%VCToolsInstallDir%\include\module.modulemap"
60+
copy "%SDKROOT%\usr\share\visualc.apinotes" "%VCToolsInstallDir%\include\visualc.apinotes"
61+
copy "%SDKROOT%\usr\share\winsdk.modulemap" "%UniversalCRTSdkDir%\Include\%UCRTVersion%\um\module.modulemap"
62+
- name: Test
63+
run: "swift build"
6664
TestBuildingOnMacOS-11_0-ARM64:
6765
runs-on: macos-11.0
6866
steps:

.swiftformat

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,22 @@
1212
--wraparguments before-first
1313
--wrapparameters before-first
1414
--disable trailingClosures, typeSugar
15+
--header "Copyright (c) 2021 Jeff Lebrun \n\n \
16+
Permission is hereby granted, free of charge, to any person obtaining a copy \n \
17+
of this software and associated documentation files (the "Software"), to deal \n \
18+
in the Software without restriction, including without limitation the rights \n \
19+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \n \
20+
copies of the Software, and to permit persons to whom the Software is \n \
21+
furnished to do so, subject to the following conditions: \n\n \
22+
\
23+
The above copyright notice and this permission notice shall be included in all \n \
24+
copies or substantial portions of the Software. \n\n \
25+
\
26+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR \n \
27+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \n \
28+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE \n \
29+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER \n \
30+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, \n \
31+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE \n \
32+
SOFTWARE.
33+
"

README.md

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ArArchiveKit
22

3-
**A simple, 0-dependency (including `Foundation`) Swift package for creating `ar` archives. Inspired by [ar](https://github.com/blakesmith/ar).**
3+
**A simple, 0-dependency Swift package for creating `ar` archives. Inspired by [ar](https://github.com/blakesmith/ar).**
44

55
[![Swift 5.3](https://img.shields.io/badge/Swift-5.3-brightgreen?logo=swift)](https://swift.org)
66
[![SPM Compatible](https://img.shields.io/badge/SPM-compatible-brightgreen.svg)](https://swift.org/package-manager)
@@ -11,25 +11,25 @@
1111
# Table of Contents
1212

1313
<!--ts-->
14-
* [ArArchiveKit](#ararchivekit)
15-
* [Table of Contents](#table-of-contents)
16-
* [Coming Soon](#coming-soon)
17-
* [Installation](#installation)
18-
* [Swift Package Manager](#swift-package-manager)
19-
* [Usage](#usage)
20-
* [Other Platforms](#other-platforms)
21-
* [Contributing](#contributing)
2214

23-
<!-- Added by: lebje, at: Wed Mar 17 17:56:24 EDT 2021 -->
15+
- [ArArchiveKit](#ararchivekit)
16+
- [Table of Contents](#table-of-contents)
17+
- [Installation](#installation)
18+
- [Swift Package Manager](#swift-package-manager)
19+
- [Usage](#usage)
20+
- [Writing Archives](#writing-archives)
21+
- [Reading Archives](#reading-archives)
22+
- [Iteration](#iteration)
23+
- [Subscript](#subscript)
24+
- [Other Platforms](#other-platforms)
25+
- [Contributing](#contributing)
26+
27+
<!-- Added by: lebje, at: Thu Mar 18 21:01:32 EDT 2021 -->
2428

2529
<!--te-->
2630

2731
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
2832

29-
## Coming Soon
30-
31-
- Parsing `ar` archives
32-
3333
## Installation
3434

3535
### Swift Package Manager
@@ -48,7 +48,9 @@ Add this to the `dependencies` array in `Package.swift`:
4848

4949
## Usage
5050

51-
First, initialize your `ArArchiveWriter`:
51+
### Writing Archives
52+
53+
To write archives, you'll need a `ArArchiveWriter`:
5254

5355
```swift
5456
var writer = ArArchiveWriter()
@@ -107,9 +109,55 @@ let data = Data(bytes)
107109
// And write it:
108110
try data.write(to: URL(fileURLWithPath: "myArchive.a"))
109111
```
112+
113+
### Reading Archives
114+
115+
To read archives, you need an `ArArchiveReader`:
116+
117+
```swift
118+
// myData is the bytes of the archive.
119+
let myData: Data = ...
120+
121+
let reader = ArArchiveReader(archive: Array<UInt8>(myData))
122+
```
123+
124+
Once you have your reader, there are several ways you can retrieve the data:
125+
126+
#### Iteration
127+
128+
You can iterate though all the files in the archive like this:
129+
130+
```swift
131+
for (header, data) in reader {
132+
// `data` is `Array<UInt8>` that contains the raw bytes of the file in the archive.
133+
// `header` is the `Header` that describes the `data`.
134+
135+
// if you know `data` is a `String`, then you can use this initializer:
136+
let str = String(data)
137+
}
138+
```
139+
140+
#### Subscript
141+
142+
Accessing data through the `subscript` is useful when you only need to access one or two items in a large archive:
143+
144+
```swift
145+
146+
// The subscript provides you with random access to any file in the archive:
147+
let firstFile = reader[0]
148+
let fifthFile = reader[6]
149+
```
150+
151+
You can also use the overloaded version of the subscript which takes a header - useful for when you have a `Header`, but not the index of that header.
152+
153+
```swift
154+
let header = reader.headers.first(where: { $0.name.contains(".swift") })!
155+
let data = reader[header: header]
156+
```
157+
110158
## Other Platforms
111159

112-
ArArchiveKit doesn't depend on any library or `Foundation` - only the Swift standard library. It should compile on any platform that supports the standard library.
160+
ArArchiveKit doesn't depend on any library, `Foundation`, or `Darwin`/`Glibc` - only the Swift standard library. It should compile on any platform where the standard library compiles.
113161

114162
## Contributing
115163

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) 2021 Jeff Lebrun
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the Software), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all
11+
// copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
// SOFTWARE.
20+
21+
/// `ArArchiveReader` reads `ar` files.
22+
public struct ArArchiveReader {
23+
private var data: [UInt8]
24+
private var currentIndex: Int = 0
25+
26+
/// Used primarily when in a for loop:
27+
///
28+
/// ```swift
29+
/// let bytes = Array<UInt8>(try Data(contentsOf: myURL))
30+
/// let reader = try ArArchiveReader(archive: bytes)
31+
/// for index = in 0..<reader.count {
32+
/// let header = reader.headers[index]
33+
/// let data = reader[header]
34+
/// // Use data and header.
35+
/// }
36+
/// ```
37+
///
38+
39+
/// The headers that describe the files in this archive.
40+
///
41+
/// Use this to find a file in the archive, then use the provided subscript to get the bytes of the file.
42+
///
43+
/// ```swift
44+
/// let bytes = Array<UInt8>(try Data(contentsOf: myURL))
45+
/// let reader = try ArArchiveReader(archive: bytes)
46+
/// let bytes = reader[reader.headers[0]]
47+
/// // Use bytes...
48+
/// ```
49+
///
50+
public var headers: [Header] = []
51+
52+
/// The initializer reads all the `ar` headers in preparation for random access to the header's file contents later.
53+
public init(archive: [UInt8]) throws {
54+
if archive.isEmpty {
55+
throw ArArchiveError.emptyArchive
56+
} else if archive.count < 8 {
57+
// The global header is missing.
58+
throw ArArchiveError.invalidArchive
59+
} else if Array(archive[0...7]) != globalHeader.asciiArray {
60+
// The global header is invalid.
61+
throw ArArchiveError.invalidArchive
62+
}
63+
64+
// Drop the global header from the byte array.
65+
self.data = Array(archive[8...])
66+
67+
var index = 0
68+
69+
// Read all the headers so we can provide random access to the data later.
70+
while index < (self.data.count - 1), (index + (headerSize - 1)) < self.data.count - 1 {
71+
var h = try self.parseHeader(bytes: Array(self.data[index...(index + headerSize - 1)]))
72+
73+
index += headerSize + 1
74+
h.contentLocation = index - 1
75+
index += h.size
76+
77+
self.headers.append(h)
78+
}
79+
}
80+
81+
/// Retrieves the bytes of the item at `index`, where index is the index of the `header` stored in the `headers` property of the reader.
82+
///
83+
/// Internally, the `Header` stored at `index` is used to find the file.
84+
public subscript(index: Int) -> [UInt8] {
85+
Array(self.data[self.headers[index].contentLocation..<self.headers[index].contentLocation + self.headers[index].size])
86+
}
87+
88+
/// Retrieves the bytes of the file described in `header`.
89+
///
90+
/// - Parameter header: The `Header` that describes the file you wish to retrieves.
91+
///
92+
/// `header` MUST be a `Header` contained in the `headers` property of this `ArArchiveReader` or else you will get a "index out of range" error.
93+
public subscript(header header: Header) -> [UInt8] {
94+
Array(self.data[header.contentLocation..<header.contentLocation + header.size])
95+
}
96+
97+
private func parseHeader(bytes: [UInt8]) throws -> Header {
98+
var start = 0
99+
let name = self.readString(from: Array(bytes[start...15]))
100+
101+
start = 16
102+
103+
let modificationTime = self.readInt(from: Array(bytes[start...(start + 11)]))
104+
105+
start += 12
106+
let userID = self.readInt(from: Array(bytes[start...(start + 5)]))
107+
108+
start += 6
109+
110+
let groupID = self.readInt(from: Array(bytes[start...(start + 5)]))
111+
112+
start += 6
113+
114+
let mode = UInt32(String(readString(from: Array(bytes[start...(start + 5)])).dropFirst(3)), radix: 8)
115+
116+
start += 8
117+
118+
let size = self.readInt(from: Array(bytes[start...(start + 7)]))
119+
120+
guard
121+
let mT = modificationTime,
122+
let u = userID,
123+
let g = groupID,
124+
let m = mode,
125+
let s = size
126+
else { throw ArArchiveError.invalidHeader }
127+
128+
var h = Header(name: name, userID: u, groupID: g, mode: m, modificationTime: mT)
129+
h.size = s
130+
return h
131+
}
132+
133+
/// From [blakesmith/r/reader.go: line 62](https://github.com/blakesmith/ar/blob/809d4375e1fb5bb262c159fc3ec2e7a86a8bfd28/reader.go#L62) .
134+
private func readString(from bytes: [UInt8]) -> String {
135+
var i = bytes.count - 1
136+
137+
while i > 0, bytes[i] == 32 /* ASCII space character */ {
138+
i -= 1
139+
}
140+
141+
return String(bytes[0...i].map({ Character(Unicode.Scalar($0)) }))
142+
}
143+
144+
private func readInt(from bytes: [UInt8], radix: Int? = nil) -> Int? {
145+
if let r = radix {
146+
return Int(self.readString(from: bytes), radix: r)
147+
} else {
148+
return Int(self.readString(from: bytes))
149+
}
150+
}
151+
}
152+
153+
extension ArArchiveReader: IteratorProtocol, Sequence {
154+
public typealias Element = (Header, [UInt8])
155+
156+
public mutating func next() -> Element? {
157+
if self.currentIndex > self.headers.count - 1 {
158+
return nil
159+
}
160+
161+
let bytes = self[currentIndex]
162+
let h = self.headers[self.currentIndex]
163+
self.currentIndex += 1
164+
165+
return (h, bytes)
166+
}
167+
}

Sources/ArArchiveKit/ArArchiveWriter.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
// Copyright (c) 2021 Jeff Lebrun
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the Software), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all
11+
// copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
// SOFTWARE.
120

221
/// `ArArchiveWriter` creates `ar` files.
322
public struct ArArchiveWriter {
@@ -16,7 +35,7 @@ public struct ArArchiveWriter {
1635
}
1736

1837
private mutating func addGlobalHeader() {
19-
self.write(Array("!<arch>\n".map({ $0.asciiValue! })))
38+
self.write(globalHeader.asciiArray)
2039
}
2140

2241
private mutating func writeString(_ str: String, size: Int) {

0 commit comments

Comments
 (0)