Skip to content

Commit

Permalink
Add ArArchiveReader (#1)
Browse files Browse the repository at this point in the history
* Create Parser
  • Loading branch information
LebJe authored Mar 19, 2021
1 parent f41c052 commit b580355
Show file tree
Hide file tree
Showing 14 changed files with 20,424 additions and 62 deletions.
46 changes: 22 additions & 24 deletions .github/workflows/buildAndTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,28 @@ jobs:
- uses: actions/checkout@v2
- name: Run tests
run: swift test
# TestOnWindows10-x86_64:
# runs-on: windows-latest
# steps:
# - uses: actions/checkout@v2
# - uses: seanmiddleditch/gha-setup-vsdevenv@master
# - name: Install swift-DEVELOPMENT-SNAPSHOT-2021-01-27-a
# 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")
# - name: Set Environment Variables
# run: |
# echo "SDKROOT=C:\Library\Developer\Platforms\Windows.platform\Developer\SDKs\Windows.sdk" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
# echo "DEVELOPER_DIR=C:\Library\Developer" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
# - name: Adjust Paths
# 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
# - name: Install Supporting Files
# shell: cmd
# run: |
# copy "%SDKROOT%\usr\share\ucrt.modulemap" "%UniversalCRTSdkDir%\Include\%UCRTVersion%\ucrt\module.modulemap"
# copy "%SDKROOT%\usr\share\visualc.modulemap" "%VCToolsInstallDir%\include\module.modulemap"
# copy "%SDKROOT%\usr\share\visualc.apinotes" "%VCToolsInstallDir%\include\visualc.apinotes"
# copy "%SDKROOT%\usr\share\winsdk.modulemap" "%UniversalCRTSdkDir%\Include\%UCRTVersion%\um\module.modulemap"
# - name: Test
# run: |
# swift build
# swift test
TestOnWindows10-x86_64:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: seanmiddleditch/gha-setup-vsdevenv@master
- name: Install swift-DEVELOPMENT-SNAPSHOT-2021-01-27-a
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")
- name: Set Environment Variables
run: |
echo "SDKROOT=C:\Library\Developer\Platforms\Windows.platform\Developer\SDKs\Windows.sdk" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "DEVELOPER_DIR=C:\Library\Developer" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Adjust Paths
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
- name: Install Supporting Files
shell: cmd
run: |
copy "%SDKROOT%\usr\share\ucrt.modulemap" "%UniversalCRTSdkDir%\Include\%UCRTVersion%\ucrt\module.modulemap"
copy "%SDKROOT%\usr\share\visualc.modulemap" "%VCToolsInstallDir%\include\module.modulemap"
copy "%SDKROOT%\usr\share\visualc.apinotes" "%VCToolsInstallDir%\include\visualc.apinotes"
copy "%SDKROOT%\usr\share\winsdk.modulemap" "%UniversalCRTSdkDir%\Include\%UCRTVersion%\um\module.modulemap"
- name: Test
run: "swift build"
TestBuildingOnMacOS-11_0-ARM64:
runs-on: macos-11.0
steps:
Expand Down
19 changes: 19 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,22 @@
--wraparguments before-first
--wrapparameters before-first
--disable trailingClosures, typeSugar
--header "Copyright (c) 2021 Jeff Lebrun \n\n \
Permission is hereby granted, free of charge, to any person obtaining a copy \n \
of this software and associated documentation files (the "Software"), to deal \n \
in the Software without restriction, including without limitation the rights \n \
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \n \
copies of the Software, and to permit persons to whom the Software is \n \
furnished to do so, subject to the following conditions: \n\n \
\
The above copyright notice and this permission notice shall be included in all \n \
copies or substantial portions of the Software. \n\n \
\
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR \n \
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \n \
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE \n \
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER \n \
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, \n \
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE \n \
SOFTWARE.
"
80 changes: 64 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ArArchiveKit

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

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

<!--ts-->
* [ArArchiveKit](#ararchivekit)
* [Table of Contents](#table-of-contents)
* [Coming Soon](#coming-soon)
* [Installation](#installation)
* [Swift Package Manager](#swift-package-manager)
* [Usage](#usage)
* [Other Platforms](#other-platforms)
* [Contributing](#contributing)

<!-- Added by: lebje, at: Wed Mar 17 17:56:24 EDT 2021 -->
- [ArArchiveKit](#ararchivekit)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Swift Package Manager](#swift-package-manager)
- [Usage](#usage)
- [Writing Archives](#writing-archives)
- [Reading Archives](#reading-archives)
- [Iteration](#iteration)
- [Subscript](#subscript)
- [Other Platforms](#other-platforms)
- [Contributing](#contributing)

<!-- Added by: lebje, at: Thu Mar 18 21:01:32 EDT 2021 -->

<!--te-->

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

## Coming Soon

- Parsing `ar` archives

## Installation

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

## Usage

First, initialize your `ArArchiveWriter`:
### Writing Archives

To write archives, you'll need a `ArArchiveWriter`:

```swift
var writer = ArArchiveWriter()
Expand Down Expand Up @@ -107,9 +109,55 @@ let data = Data(bytes)
// And write it:
try data.write(to: URL(fileURLWithPath: "myArchive.a"))
```

### Reading Archives

To read archives, you need an `ArArchiveReader`:

```swift
// myData is the bytes of the archive.
let myData: Data = ...

let reader = ArArchiveReader(archive: Array<UInt8>(myData))
```

Once you have your reader, there are several ways you can retrieve the data:

#### Iteration

You can iterate though all the files in the archive like this:

```swift
for (header, data) in reader {
// `data` is `Array<UInt8>` that contains the raw bytes of the file in the archive.
// `header` is the `Header` that describes the `data`.

// if you know `data` is a `String`, then you can use this initializer:
let str = String(data)
}
```

#### Subscript

Accessing data through the `subscript` is useful when you only need to access one or two items in a large archive:

```swift

// The subscript provides you with random access to any file in the archive:
let firstFile = reader[0]
let fifthFile = reader[6]
```

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.

```swift
let header = reader.headers.first(where: { $0.name.contains(".swift") })!
let data = reader[header: header]
```

## Other Platforms

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.
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.

## Contributing

Expand Down
167 changes: 167 additions & 0 deletions Sources/ArArchiveKit/ArArchiveReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) 2021 Jeff Lebrun
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

/// `ArArchiveReader` reads `ar` files.
public struct ArArchiveReader {
private var data: [UInt8]
private var currentIndex: Int = 0

/// Used primarily when in a for loop:
///
/// ```swift
/// let bytes = Array<UInt8>(try Data(contentsOf: myURL))
/// let reader = try ArArchiveReader(archive: bytes)
/// for index = in 0..<reader.count {
/// let header = reader.headers[index]
/// let data = reader[header]
/// // Use data and header.
/// }
/// ```
///

/// The headers that describe the files in this archive.
///
/// Use this to find a file in the archive, then use the provided subscript to get the bytes of the file.
///
/// ```swift
/// let bytes = Array<UInt8>(try Data(contentsOf: myURL))
/// let reader = try ArArchiveReader(archive: bytes)
/// let bytes = reader[reader.headers[0]]
/// // Use bytes...
/// ```
///
public var headers: [Header] = []

/// The initializer reads all the `ar` headers in preparation for random access to the header's file contents later.
public init(archive: [UInt8]) throws {
if archive.isEmpty {
throw ArArchiveError.emptyArchive
} else if archive.count < 8 {
// The global header is missing.
throw ArArchiveError.invalidArchive
} else if Array(archive[0...7]) != globalHeader.asciiArray {
// The global header is invalid.
throw ArArchiveError.invalidArchive
}

// Drop the global header from the byte array.
self.data = Array(archive[8...])

var index = 0

// Read all the headers so we can provide random access to the data later.
while index < (self.data.count - 1), (index + (headerSize - 1)) < self.data.count - 1 {
var h = try self.parseHeader(bytes: Array(self.data[index...(index + headerSize - 1)]))

index += headerSize + 1
h.contentLocation = index - 1
index += h.size

self.headers.append(h)
}
}

/// Retrieves the bytes of the item at `index`, where index is the index of the `header` stored in the `headers` property of the reader.
///
/// Internally, the `Header` stored at `index` is used to find the file.
public subscript(index: Int) -> [UInt8] {
Array(self.data[self.headers[index].contentLocation..<self.headers[index].contentLocation + self.headers[index].size])
}

/// Retrieves the bytes of the file described in `header`.
///
/// - Parameter header: The `Header` that describes the file you wish to retrieves.
///
/// `header` MUST be a `Header` contained in the `headers` property of this `ArArchiveReader` or else you will get a "index out of range" error.
public subscript(header header: Header) -> [UInt8] {
Array(self.data[header.contentLocation..<header.contentLocation + header.size])
}

private func parseHeader(bytes: [UInt8]) throws -> Header {
var start = 0
let name = self.readString(from: Array(bytes[start...15]))

start = 16

let modificationTime = self.readInt(from: Array(bytes[start...(start + 11)]))

start += 12
let userID = self.readInt(from: Array(bytes[start...(start + 5)]))

start += 6

let groupID = self.readInt(from: Array(bytes[start...(start + 5)]))

start += 6

let mode = UInt32(String(readString(from: Array(bytes[start...(start + 5)])).dropFirst(3)), radix: 8)

start += 8

let size = self.readInt(from: Array(bytes[start...(start + 7)]))

guard
let mT = modificationTime,
let u = userID,
let g = groupID,
let m = mode,
let s = size
else { throw ArArchiveError.invalidHeader }

var h = Header(name: name, userID: u, groupID: g, mode: m, modificationTime: mT)
h.size = s
return h
}

/// From [blakesmith/r/reader.go: line 62](https://github.com/blakesmith/ar/blob/809d4375e1fb5bb262c159fc3ec2e7a86a8bfd28/reader.go#L62) .
private func readString(from bytes: [UInt8]) -> String {
var i = bytes.count - 1

while i > 0, bytes[i] == 32 /* ASCII space character */ {
i -= 1
}

return String(bytes[0...i].map({ Character(Unicode.Scalar($0)) }))
}

private func readInt(from bytes: [UInt8], radix: Int? = nil) -> Int? {
if let r = radix {
return Int(self.readString(from: bytes), radix: r)
} else {
return Int(self.readString(from: bytes))
}
}
}

extension ArArchiveReader: IteratorProtocol, Sequence {
public typealias Element = (Header, [UInt8])

public mutating func next() -> Element? {
if self.currentIndex > self.headers.count - 1 {
return nil
}

let bytes = self[currentIndex]
let h = self.headers[self.currentIndex]
self.currentIndex += 1

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

/// `ArArchiveWriter` creates `ar` files.
public struct ArArchiveWriter {
Expand All @@ -16,7 +35,7 @@ public struct ArArchiveWriter {
}

private mutating func addGlobalHeader() {
self.write(Array("!<arch>\n".map({ $0.asciiValue! })))
self.write(globalHeader.asciiArray)
}

private mutating func writeString(_ str: String, size: Int) {
Expand Down
Loading

0 comments on commit b580355

Please sign in to comment.