Compare commits

..
17 Commits
Author SHA1 Message Date
Nikolay Edigaryev fb954b7cc1 tart list: support -q (or --quiet) for automation purposes (#293) 2022-11-02 11:49:45 -04:00
Fedor KorotkovandNikolay Edigaryev 8cbcd2285b Document Cirrus Runners (#288)
* Document Cirrus Runners

Fixes #237

* Update README.md

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>

* Remove redundant mention

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2022-10-27 10:17:13 -04:00
Pete Goldsmith 3d0d889c99 Remove whitespace (#285) 2022-10-22 08:17:42 -04:00
Nikolay Edigaryev 0e77f14dd7 OCI: always read channel until end (#284) 2022-10-18 11:44:14 -04:00
Nikolay Edigaryev 39e1b84423 Use URLSession.dataTask() with delegate instead of URLSession.bytes() (#282)
* Use URLSession.dataTask() with delegate instead of URLSession.bytes()

* Use URLSession.shared instead of creating a new one each time
2022-10-18 08:54:28 -04:00
Fedor Korotkov af7530ee50 Relax status code acceptance (#281)
* Relax status code acceptance

Fixes #273

* added a comment
2022-10-17 21:42:25 +04:00
Nikolay Edigaryev 36bb68a72d tart run: deprecate --with-softnet and introduce --net-softnet (#274) 2022-10-14 18:38:39 +04:00
Nikolay Edigaryev afd707eedf OCI reference parser: allow dashes in host name (#275) 2022-10-14 16:36:25 +04:00
Nikolay Edigaryev c67efb88f3 Fix tart prune --cache-budget logic (#272) 2022-10-14 01:43:37 +04:00
Nikolay Edigaryev 667de2a199 Monitor Softnet process and throw if it terminates prematurely (#270) 2022-10-12 20:00:13 +04:00
Nikolay Edigaryev 44650e9713 Improve RemoteName parser (#225) (#269)
* Improve RemoteName parser

* Remove Parsing import

* Permit namespace components to contain separators, but no more than one

* Add testNoPathTraversal
2022-10-11 22:59:44 +04:00
Nikolay Edigaryev 62ee42de3b Plug URLSession.bytes() memory leak (#267) 2022-10-10 18:38:57 +04:00
Nikolay Edigaryev 89301d114e Log cache pruning to $TART_HOME/tart.log (#265)
* Log cache pruning to $TART_HOME/tart.log

* Log zero capacities
2022-10-07 23:01:42 +04:00
Nikolay Edigaryev dbb33d0651 Don't download IPSWs into memory (#262) 2022-10-06 20:33:34 +04:00
Nikolay Edigaryev 166e3e570f Ditch AsyncHTTPClient in favor of URLSession (#260) 2022-10-05 00:39:36 +04:00
Nikolay Edigaryev 7a2c20ba30 tart create: support fetching URLs specified in the --from-ipsw option (#256)
* tart create: support fetching URLs specified in the --from-ipsw option

* Use x-amz-meta-digest-sha256 header to cache IPSWs
2022-09-27 23:36:18 +04:00
Nikolay Edigaryev e90d53eceb tart delete: allow removing multiple VMs at once (#257) 2022-09-27 10:30:44 -04:00
38 changed files with 1802 additions and 302 deletions
+28 -73
View File
@@ -1,12 +1,12 @@
{
"pins" : [
{
"identity" : "async-http-client",
"identity" : "antlr4",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/async-http-client",
"location" : "https://github.com/antlr/antlr4",
"state" : {
"revision" : "df87a860fdc41a595d5ca67f74cde9adbccc099a",
"version" : "1.11.4"
"branch" : "dev",
"revision" : "2703a8516c0fb7fe92db6b9c40e0113f577646d2"
}
},
{
@@ -18,6 +18,15 @@
"revision" : "772883073d044bc754d401cabb6574624eb3778f"
}
},
{
"identity" : "puppy",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sushichop/Puppy",
"state" : {
"revision" : "3e8d87f714f14244878752a6bb71ea465119f8a1",
"version" : "0.5.1"
}
},
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
@@ -36,6 +45,15 @@
"version" : "1.1.2"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms",
"state" : {
"branch" : "main",
"revision" : "f05e450f0b909c0e80670a47516c4b9700b9e5da"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
@@ -46,12 +64,12 @@
}
},
{
"identity" : "swift-case-paths",
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "ce9c0d897db8a840c39de64caaa9b60119cf4be8",
"version" : "0.8.1"
"revision" : "f504716c27d2e5d4144fa4794b12129301d17729",
"version" : "1.0.3"
}
},
{
@@ -59,53 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
"version" : "1.4.2"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "124119f0bb12384cef35aa041d7c3a686108722d",
"version" : "2.40.0"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "8eea84ec6144167354387ef9244b0939f5852dc8",
"version" : "1.11.0"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "108ac15087ea9b79abb6f6742699cf31de0e8772",
"version" : "1.22.0"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "1750873bce84b4129b5303655cce2c3d35b9ed3a",
"version" : "2.19.0"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "1a4692acb88156e3da1b0c6732a8a38b2a744166",
"version" : "1.12.0"
"revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c",
"version" : "1.4.4"
}
},
{
@@ -117,15 +90,6 @@
"version" : "1.0.2"
}
},
{
"identity" : "swift-parsing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-parsing",
"state" : {
"revision" : "28d32e9ace1c4c43f5e5a177be837a202494c2d5",
"version" : "0.9.2"
}
},
{
"identity" : "swiftdate",
"kind" : "remoteSourceControl",
@@ -134,15 +98,6 @@
"revision" : "6190d0cefff3013e77ed567e6b074f324e5c5bf5",
"version" : "6.3.1"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "50a70a9d3583fe228ce672e8923010c8df2deddd",
"version" : "0.2.1"
}
}
],
"version" : 2
+16 -6
View File
@@ -12,21 +12,31 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.2"),
.package(url: "https://github.com/mhdhejazi/Dynamic", branch: "master"),
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.9.2"),
.package(url: "https://github.com/swift-server/async-http-client", from: "1.11.4"),
.package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0"),
.package(url: "https://github.com/malcommac/SwiftDate", from: "6.3.1")
.package(url: "https://github.com/apple/swift-async-algorithms", branch: "main"),
.package(url: "https://github.com/malcommac/SwiftDate", from: "6.3.1"),
.package(url: "https://github.com/sushichop/Puppy", from: "0.5.1"),
.package(url: "https://github.com/antlr/antlr4", branch: "dev"),
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.0")),
],
targets: [
.executableTarget(name: "tart", dependencies: [
.product(name: "Algorithms", package: "swift-algorithms"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "Dynamic", package: "Dynamic"),
.product(name: "Parsing", package: "swift-parsing"),
.product(name: "SwiftDate", package: "SwiftDate"),
.product(name: "Puppy", package: "Puppy"),
.product(name: "Antlr4Static", package: "Antlr4"),
.product(name: "Atomics", package: "swift-atomics"),
], exclude: [
"OCI/Reference/Makefile",
"OCI/Reference/Reference.g4",
"OCI/Reference/Generated/Reference.interp",
"OCI/Reference/Generated/Reference.tokens",
"OCI/Reference/Generated/ReferenceLexer.interp",
"OCI/Reference/Generated/ReferenceLexer.tokens",
]),
.testTarget(name: "TartTests", dependencies: ["tart"])
]
)
+33 -5
View File
@@ -28,14 +28,42 @@ Try running a Tart VM on your Apple Silicon device running macOS Monterey or lat
```shell
brew install cirruslabs/cli/tart
tart clone ghcr.io/cirruslabs/macos-monterey-base:latest monterey-base
tart run monterey-base
tart clone ghcr.io/cirruslabs/macos-ventura-base:latest ventura-base
tart run ventura-base
```
![tart VM view app](Resources/TartScreenshot.png)
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/TartScreenshot.png"/>
## CI Integration
Tart already powers several CI services mentioned above including our own [Cirrus CI](https://cirrus-ci.org/guide/macOS/) which offers unlimited concurrency with per-second billing.
For services that haven't leveraged Tart yet, we offer fully managed runners via a monthly subscription.
*Cirrus Runners* is the fastest way to get your current CI workflows to benefit from Apple Silicon hardware. No need to manage infrastructure or migrate to another CI provider.
Please read down below about currently supported services.
### Managed runners for your CI-as-a-service
At the moment Cirrus Runners only supports GitHub Actions, but we are actively working on adding more options.
Please [email us](mailto:hello@cirruslabs.org) if you are interested in a particular one.
#### GitHub Actions
Configuring Cirrus Runners for GitHub Actions is as simple as installing [Cirrus Runners App](https://github.com/apps/cirrus-runners).
After successful installation and subscription configuration, use any of [Ventura images managed by us](https://github.com/cirruslabs/macos-image-templates) in `runs-on`:
```yaml
name: Test Suite
jobs:
test:
runs-on: ghcr.io/cirruslabs/macos-ventura-xcode:latest
```
When workflows are executing you'll see Cirrus on-demand runners on your organization's settings page at `https://github.com/organizations/<ORGANIZATION>/settings/actions/runners`.
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/TartGHARunners.png"/>
### Self-hosted CI
Tart itself is only responsible for managing virtual machines, but we've built Tart support into a tool called Cirrus CLI
also developed by Cirrus Labs. [Cirrus CLI](https://github.com/cirruslabs/cirrus-cli) is a command line tool with
one configuration format to execute common CI steps (run a script, cache a folder, etc.) locally or in any CI system.
@@ -64,14 +92,14 @@ brew install cirruslabs/cli/cirrus
cirrus run
```
![Cirrus CLI Run](Resources/TartCirrusCLI.gif)
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/TartCirrusCLI.gif"/>
[Cirrus CI](https://cirrus-ci.org/) already leverages Tart to power its macOS cloud infrastructure. The `.cirrus.yml`
config from above will just work in Cirrus CI and your tasks will be executed inside Tart VMs in our cloud.
**Note:** Cirrus CI only allows [images managed and regularly updated by us](https://github.com/orgs/cirruslabs/packages?tab=packages&q=macos).
### Retrieving artifacts from within Tart VMs
#### Retrieving artifacts from within Tart VMs
In many cases there is a need to retrieve particular files or a folder from within a Tart virtual machine.
For example, the below `.cirrus.yml` configuration defines a single task that builds a `tart` binary and
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:23728bb5438c88b3d0826170a5fa4b7aaad0fbd94a4769c8d492c9f75b57ba81
size 155885
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a43f541b1ab0b57ae2060d371cba5dbb1f5c80b89c76434b7154d8144f66e61
size 205325
oid sha256:7a4929ca4e02d4968904749028ada7072704d2a52cccb69335e1596452c822c7
size 1359834
+9 -3
View File
@@ -9,7 +9,7 @@ struct Create: AsyncParsableCommand {
@Argument(help: "VM name")
var name: String
@Option(help: ArgumentHelp("create a macOS VM using path to the IPSW file (or \"latest\") to fetch the latest appropriate IPSW", valueName: "path"))
@Option(help: ArgumentHelp("create a macOS VM using path to the IPSW file or URL (or \"latest\", to fetch the latest supported IPSW automatically)", valueName: "path"))
var fromIPSW: String?
@Flag(help: "create a Linux VM")
@@ -34,11 +34,17 @@ struct Create: AsyncParsableCommand {
try await withTaskCancellationHandler(operation: {
if let fromIPSW = fromIPSW {
let ipswURL: URL
if fromIPSW == "latest" {
_ = try await VM(vmDir: tmpVMDir, ipswURL: nil, diskSizeGB: diskSize)
ipswURL = try await VM.latestIPSWURL()
} else if fromIPSW.starts(with: "http://") || fromIPSW.starts(with: "https://") {
ipswURL = URL(string: fromIPSW)!
} else {
_ = try await VM(vmDir: tmpVMDir, ipswURL: URL(fileURLWithPath: fromIPSW), diskSizeGB: diskSize)
ipswURL = URL(fileURLWithPath: fromIPSW)
}
_ = try await VM(vmDir: tmpVMDir, ipswURL: ipswURL, diskSizeGB: diskSize)
}
if linux {
+4 -2
View File
@@ -6,11 +6,13 @@ struct Delete: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Delete a VM")
@Argument(help: "VM name")
var name: String
var name: [String]
func run() async throws {
do {
try VMStorageHelper.delete(name)
for it in name {
try VMStorageHelper.delete(it)
}
Foundation.exit(0)
} catch {
+11 -2
View File
@@ -5,9 +5,14 @@ import SwiftUI
struct List: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "List created VMs")
@Flag(name: [.short, .long], help: ArgumentHelp("Only display VM names"))
var quiet: Bool = false
func run() async throws {
do {
print("Source\tName")
if !quiet {
print("Source\tName")
}
displayTable("local", try VMStorageLocal().list())
displayTable("oci", try VMStorageOCI().list().map { (name, vmDir, _) in (name, vmDir) })
@@ -22,7 +27,11 @@ struct List: AsyncParsableCommand {
private func displayTable(_ source: String, _ vms: [(String, VMDirectory)]) {
for (name, _) in vms.sorted(by: { left, right in left.0 < right.0 }) {
print("\(source)\t\(name)")
if quiet {
print(name)
} else {
print("\(source)\t\(name)")
}
}
}
}
+17 -10
View File
@@ -63,21 +63,25 @@ struct Prune: AsyncParsableCommand {
let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()]
let prunables: [Prunable] = try prunableStorages
.flatMap { try $0.prunables() }
.sorted { try $0.accessDate() < $1.accessDate() }
.sorted { try $0.accessDate() > $1.accessDate() }
let cacheUsedBytes = try prunables.map { try $0.sizeBytes() }.reduce(0, +)
var cacheReclaimedBytes: Int = 0
var cacheBudgetBytes = cacheBudgetBytes
var prunablesToDelete: [Prunable] = []
var it = prunables.makeIterator()
for prunable in prunables {
let prunableSizeBytes = UInt64(try prunable.sizeBytes())
while (cacheUsedBytes - cacheReclaimedBytes) > cacheBudgetBytes {
guard let prunable = it.next() else {
break
if prunableSizeBytes <= cacheBudgetBytes {
// Don't mark for deletion as
// there's a budget available
cacheBudgetBytes -= prunableSizeBytes
} else {
// Mark for deletion
prunablesToDelete.append(prunable)
}
cacheReclaimedBytes -= try prunable.sizeBytes()
try prunable.delete()
}
try prunablesToDelete.forEach { try $0.delete() }
}
static func pruneReclaim(reclaimBytes: UInt64) throws {
@@ -103,6 +107,9 @@ struct Prune: AsyncParsableCommand {
cacheReclaimedBytes += try prunable.sizeBytes()
try prunable.delete()
puppy.info("deleting \(prunable.url)...")
}
puppy.info("reclaimed \(cacheReclaimedBytes) bytes")
}
}
+11 -2
View File
@@ -37,7 +37,8 @@ struct Run: AsyncParsableCommand {
+ "Note that this feature is experimental and there may be bugs present when using VNC."))
var vncExperimental: Bool = false
@Flag var withSoftnet: Bool = false
@Flag(help: ArgumentHelp(visibility: .private))
var withSoftnet: Bool = false
@Option(help: ArgumentHelp("""
Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"ubuntu.iso:ro\")
@@ -64,6 +65,10 @@ struct Run: AsyncParsableCommand {
""", valueName: "interface name"))
var netBridged: String?
@Flag(help: ArgumentHelp("Use software networking instead of the default shared (NAT) networking",
discussion: "Learn how to configure Softnet for use with Tart here: https://github.com/cirruslabs/softnet"))
var netSoftnet: Bool = false
func validate() throws {
if vnc && vncExperimental {
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
@@ -73,6 +78,10 @@ struct Run: AsyncParsableCommand {
throw ValidationError("--with-softnet and --net-bridged are mutually exclusive")
}
if netBridged != nil && netSoftnet {
throw ValidationError("--net-bridged and --net-softnet are mutually exclusive")
}
if graphics && noGraphics {
throw ValidationError("--graphics and --no-graphics are mutually exclusive")
}
@@ -145,7 +154,7 @@ struct Run: AsyncParsableCommand {
}
func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? {
if withSoftnet {
if withSoftnet || netSoftnet {
let config = try VMConfig.init(fromURL: vmDir.configURL)
return try Softnet(vmMACAddress: config.macAddress.string)
+46
View File
@@ -0,0 +1,46 @@
import Foundation
import AsyncAlgorithms
class Fetcher: NSObject, URLSessionTaskDelegate, URLSessionDelegate, URLSessionDataDelegate {
let responseCh = AsyncThrowingChannel<URLResponse, Error>()
let dataCh = AsyncThrowingChannel<Data, Error>()
func fetch(_ request: URLRequest) async throws -> (AsyncThrowingChannel<Data, Error>, URLResponse) {
let task = URLSession.shared.dataTask(with: request)
task.delegate = self
task.resume()
// Wait for the response and only then return
var iter = responseCh.makeAsyncIterator()
let response = try await iter.next()!
return (dataCh, response)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) async -> URLSession.ResponseDisposition {
await responseCh.send(response)
return .allow
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let sema = DispatchSemaphore(value: 0)
Task {
await dataCh.send(data)
sema.signal()
}
sema.wait()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
// Premature termination
responseCh.fail(error)
dataCh.fail(error)
} else {
dataCh.finish()
}
}
}
+2 -2
View File
@@ -9,8 +9,8 @@ class IPSWCache: PrunableStorage {
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
}
func locationFor(image: VZMacOSRestoreImage) -> URL {
baseURL.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false)
func locationFor(fileName: String) -> URL {
baseURL.appendingPathComponent(fileName, isDirectory: false)
}
func prunables() throws -> [Prunable] {
+2 -2
View File
@@ -2,6 +2,6 @@ import Virtualization
protocol Network {
func attachment() -> VZNetworkDeviceAttachment
func run() throws
func stop() throws
func run(_ sema: DispatchSemaphore) throws
func stop() async throws
}
+2 -2
View File
@@ -12,11 +12,11 @@ class NetworkBridged: Network {
VZBridgedNetworkDeviceAttachment(interface: interface)
}
func run() throws {
func run(_ sema: DispatchSemaphore) throws {
// no-op, only used for Softnet
}
func stop() throws {
func stop() async throws {
// no-op, only used for Softnet
}
}
+2 -2
View File
@@ -6,11 +6,11 @@ class NetworkShared: Network {
VZNATNetworkDeviceAttachment()
}
func run() throws {
func run(_ sema: DispatchSemaphore) throws {
// no-op, only used for Softnet
}
func stop() throws {
func stop() async throws {
// no-op, only used for Softnet
}
}
+28 -4
View File
@@ -1,12 +1,16 @@
import Foundation
import Virtualization
import Atomics
enum SoftnetError: Error {
case InitializationFailed(why: String)
case RuntimeFailed(why: String)
}
class Softnet: Network {
private let process = Process()
private var monitorTask: Task<Void, Error>? = nil
private let monitorTaskFinished = ManagedAtomic<Bool>(false)
let vmFD: Int32
@@ -35,13 +39,33 @@ class Softnet: Network {
process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false)
}
func run() throws {
func run(_ sema: DispatchSemaphore) throws {
try process.run()
monitorTask = Task {
// Wait for the Softnet to finish
process.waitUntilExit()
// Signal to the caller that the Softnet has finished
sema.signal()
// Signal to ourselves that the Softnet has finished
monitorTaskFinished.store(true, ordering: .sequentiallyConsistent)
}
}
func stop() throws {
process.interrupt()
process.waitUntilExit()
func stop() async throws {
if monitorTaskFinished.load(ordering: .sequentiallyConsistent) {
// Consume the monitor task's value to ensure the task has finished
_ = try await monitorTask?.value
throw SoftnetError.RuntimeFailed(why: "Softnet process terminated prematurely")
} else {
process.interrupt()
// Consume the monitor task's value to ensure the task has finished
_ = try await monitorTask?.value
}
}
private func setSocketBuffers(_ fd: Int32, _ sizeBytes: Int) throws {
@@ -0,0 +1,37 @@
token literal names:
null
':'
'/'
'.'
'-'
'@'
'_'
null
null
token symbolic names:
null
null
null
null
null
null
null
DIGIT
LETTER
rule names:
root
host
port
host_component
namespace
namespace_component
reference
tag
separator
name
atn:
[4, 1, 8, 95, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 1, 0, 1, 0, 1, 0, 3, 0, 24, 8, 0, 1, 0, 1, 0, 1, 0, 3, 0, 29, 8, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 5, 1, 36, 8, 1, 10, 1, 12, 1, 39, 9, 1, 1, 2, 4, 2, 42, 8, 2, 11, 2, 12, 2, 43, 1, 3, 1, 3, 1, 3, 5, 3, 49, 8, 3, 10, 3, 12, 3, 52, 9, 3, 1, 4, 1, 4, 1, 4, 5, 4, 57, 8, 4, 10, 4, 12, 4, 60, 9, 4, 1, 5, 1, 5, 3, 5, 64, 8, 5, 4, 5, 66, 8, 5, 11, 5, 12, 5, 67, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 77, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 83, 8, 7, 10, 7, 12, 7, 86, 9, 7, 1, 8, 1, 8, 1, 9, 4, 9, 91, 8, 9, 11, 9, 12, 9, 92, 1, 9, 0, 0, 10, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 0, 2, 2, 0, 3, 4, 6, 6, 1, 0, 7, 8, 95, 0, 20, 1, 0, 0, 0, 2, 32, 1, 0, 0, 0, 4, 41, 1, 0, 0, 0, 6, 45, 1, 0, 0, 0, 8, 53, 1, 0, 0, 0, 10, 65, 1, 0, 0, 0, 12, 76, 1, 0, 0, 0, 14, 78, 1, 0, 0, 0, 16, 87, 1, 0, 0, 0, 18, 90, 1, 0, 0, 0, 20, 23, 3, 2, 1, 0, 21, 22, 5, 1, 0, 0, 22, 24, 3, 4, 2, 0, 23, 21, 1, 0, 0, 0, 23, 24, 1, 0, 0, 0, 24, 25, 1, 0, 0, 0, 25, 26, 5, 2, 0, 0, 26, 28, 3, 8, 4, 0, 27, 29, 3, 12, 6, 0, 28, 27, 1, 0, 0, 0, 28, 29, 1, 0, 0, 0, 29, 30, 1, 0, 0, 0, 30, 31, 5, 0, 0, 1, 31, 1, 1, 0, 0, 0, 32, 37, 3, 6, 3, 0, 33, 34, 5, 3, 0, 0, 34, 36, 3, 6, 3, 0, 35, 33, 1, 0, 0, 0, 36, 39, 1, 0, 0, 0, 37, 35, 1, 0, 0, 0, 37, 38, 1, 0, 0, 0, 38, 3, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 40, 42, 5, 7, 0, 0, 41, 40, 1, 0, 0, 0, 42, 43, 1, 0, 0, 0, 43, 41, 1, 0, 0, 0, 43, 44, 1, 0, 0, 0, 44, 5, 1, 0, 0, 0, 45, 50, 3, 18, 9, 0, 46, 47, 5, 4, 0, 0, 47, 49, 3, 18, 9, 0, 48, 46, 1, 0, 0, 0, 49, 52, 1, 0, 0, 0, 50, 48, 1, 0, 0, 0, 50, 51, 1, 0, 0, 0, 51, 7, 1, 0, 0, 0, 52, 50, 1, 0, 0, 0, 53, 58, 3, 10, 5, 0, 54, 55, 5, 2, 0, 0, 55, 57, 3, 10, 5, 0, 56, 54, 1, 0, 0, 0, 57, 60, 1, 0, 0, 0, 58, 56, 1, 0, 0, 0, 58, 59, 1, 0, 0, 0, 59, 9, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 61, 63, 3, 18, 9, 0, 62, 64, 3, 16, 8, 0, 63, 62, 1, 0, 0, 0, 63, 64, 1, 0, 0, 0, 64, 66, 1, 0, 0, 0, 65, 61, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 65, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68, 11, 1, 0, 0, 0, 69, 70, 5, 1, 0, 0, 70, 77, 3, 14, 7, 0, 71, 72, 5, 5, 0, 0, 72, 73, 3, 18, 9, 0, 73, 74, 5, 1, 0, 0, 74, 75, 3, 18, 9, 0, 75, 77, 1, 0, 0, 0, 76, 69, 1, 0, 0, 0, 76, 71, 1, 0, 0, 0, 77, 13, 1, 0, 0, 0, 78, 84, 3, 18, 9, 0, 79, 80, 3, 16, 8, 0, 80, 81, 3, 18, 9, 0, 81, 83, 1, 0, 0, 0, 82, 79, 1, 0, 0, 0, 83, 86, 1, 0, 0, 0, 84, 82, 1, 0, 0, 0, 84, 85, 1, 0, 0, 0, 85, 15, 1, 0, 0, 0, 86, 84, 1, 0, 0, 0, 87, 88, 7, 0, 0, 0, 88, 17, 1, 0, 0, 0, 89, 91, 7, 1, 0, 0, 90, 89, 1, 0, 0, 0, 91, 92, 1, 0, 0, 0, 92, 90, 1, 0, 0, 0, 92, 93, 1, 0, 0, 0, 93, 19, 1, 0, 0, 0, 11, 23, 28, 37, 43, 50, 58, 63, 67, 76, 84, 92]
@@ -0,0 +1,14 @@
T__0=1
T__1=2
T__2=3
T__3=4
T__4=5
T__5=6
DIGIT=7
LETTER=8
':'=1
'/'=2
'.'=3
'-'=4
'@'=5
'_'=6
@@ -0,0 +1,167 @@
// Generated from java-escape by ANTLR 4.11.1
import Antlr4
/**
* This class provides an empty implementation of {@link ReferenceListener},
* which can be extended to create a listener which only needs to handle a subset
* of the available methods.
*/
open class ReferenceBaseListener: ReferenceListener {
public init() { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterRoot(_ ctx: ReferenceParser.RootContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitRoot(_ ctx: ReferenceParser.RootContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterHost(_ ctx: ReferenceParser.HostContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitHost(_ ctx: ReferenceParser.HostContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterPort(_ ctx: ReferenceParser.PortContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitPort(_ ctx: ReferenceParser.PortContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterHost_component(_ ctx: ReferenceParser.Host_componentContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitHost_component(_ ctx: ReferenceParser.Host_componentContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterNamespace(_ ctx: ReferenceParser.NamespaceContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitNamespace(_ ctx: ReferenceParser.NamespaceContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterNamespace_component(_ ctx: ReferenceParser.Namespace_componentContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitNamespace_component(_ ctx: ReferenceParser.Namespace_componentContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterReference(_ ctx: ReferenceParser.ReferenceContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitReference(_ ctx: ReferenceParser.ReferenceContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterTag(_ ctx: ReferenceParser.TagContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitTag(_ ctx: ReferenceParser.TagContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterSeparator(_ ctx: ReferenceParser.SeparatorContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitSeparator(_ ctx: ReferenceParser.SeparatorContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterName(_ ctx: ReferenceParser.NameContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitName(_ ctx: ReferenceParser.NameContext) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func enterEveryRule(_ ctx: ParserRuleContext) throws { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func exitEveryRule(_ ctx: ParserRuleContext) throws { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func visitTerminal(_ node: TerminalNode) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
open func visitErrorNode(_ node: ErrorNode) { }
}
@@ -0,0 +1,41 @@
token literal names:
null
':'
'/'
'.'
'-'
'@'
'_'
null
null
token symbolic names:
null
null
null
null
null
null
null
DIGIT
LETTER
rule names:
T__0
T__1
T__2
T__3
T__4
T__5
DIGIT
LETTER
channel names:
DEFAULT_TOKEN_CHANNEL
HIDDEN
mode names:
DEFAULT_MODE
atn:
[4, 0, 8, 33, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 0, 0, 8, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 1, 0, 2, 1, 0, 48, 57, 2, 0, 65, 90, 97, 122, 32, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 1, 17, 1, 0, 0, 0, 3, 19, 1, 0, 0, 0, 5, 21, 1, 0, 0, 0, 7, 23, 1, 0, 0, 0, 9, 25, 1, 0, 0, 0, 11, 27, 1, 0, 0, 0, 13, 29, 1, 0, 0, 0, 15, 31, 1, 0, 0, 0, 17, 18, 5, 58, 0, 0, 18, 2, 1, 0, 0, 0, 19, 20, 5, 47, 0, 0, 20, 4, 1, 0, 0, 0, 21, 22, 5, 46, 0, 0, 22, 6, 1, 0, 0, 0, 23, 24, 5, 45, 0, 0, 24, 8, 1, 0, 0, 0, 25, 26, 5, 64, 0, 0, 26, 10, 1, 0, 0, 0, 27, 28, 5, 95, 0, 0, 28, 12, 1, 0, 0, 0, 29, 30, 7, 0, 0, 0, 30, 14, 1, 0, 0, 0, 31, 32, 7, 1, 0, 0, 32, 16, 1, 0, 0, 0, 1, 0, 0]
@@ -0,0 +1,90 @@
// Generated from java-escape by ANTLR 4.11.1
import Antlr4
open class ReferenceLexer: Lexer {
internal static var _decisionToDFA: [DFA] = {
var decisionToDFA = [DFA]()
let length = ReferenceLexer._ATN.getNumberOfDecisions()
for i in 0..<length {
decisionToDFA.append(DFA(ReferenceLexer._ATN.getDecisionState(i)!, i))
}
return decisionToDFA
}()
internal static let _sharedContextCache = PredictionContextCache()
public
static let T__0=1, T__1=2, T__2=3, T__3=4, T__4=5, T__5=6, DIGIT=7, LETTER=8
public
static let channelNames: [String] = [
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
]
public
static let modeNames: [String] = [
"DEFAULT_MODE"
]
public
static let ruleNames: [String] = [
"T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "DIGIT", "LETTER"
]
private static let _LITERAL_NAMES: [String?] = [
nil, "':'", "'/'", "'.'", "'-'", "'@'", "'_'"
]
private static let _SYMBOLIC_NAMES: [String?] = [
nil, nil, nil, nil, nil, nil, nil, "DIGIT", "LETTER"
]
public
static let VOCABULARY = Vocabulary(_LITERAL_NAMES, _SYMBOLIC_NAMES)
override open
func getVocabulary() -> Vocabulary {
return ReferenceLexer.VOCABULARY
}
public
required init(_ input: CharStream) {
RuntimeMetaData.checkVersion("4.11.1", RuntimeMetaData.VERSION)
super.init(input)
_interp = LexerATNSimulator(self, ReferenceLexer._ATN, ReferenceLexer._decisionToDFA, ReferenceLexer._sharedContextCache)
}
override open
func getGrammarFileName() -> String { return "Reference.g4" }
override open
func getRuleNames() -> [String] { return ReferenceLexer.ruleNames }
override open
func getSerializedATN() -> [Int] { return ReferenceLexer._serializedATN }
override open
func getChannelNames() -> [String] { return ReferenceLexer.channelNames }
override open
func getModeNames() -> [String] { return ReferenceLexer.modeNames }
override open
func getATN() -> ATN { return ReferenceLexer._ATN }
static let _serializedATN:[Int] = [
4,0,8,33,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,
2,7,7,7,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,6,1,6,1,7,1,
7,0,0,8,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,1,0,2,1,0,48,57,2,0,65,90,97,
122,32,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,
1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,1,17,1,0,0,0,3,19,1,0,0,0,5,21,1,0,0,
0,7,23,1,0,0,0,9,25,1,0,0,0,11,27,1,0,0,0,13,29,1,0,0,0,15,31,1,0,0,0,
17,18,5,58,0,0,18,2,1,0,0,0,19,20,5,47,0,0,20,4,1,0,0,0,21,22,5,46,0,0,
22,6,1,0,0,0,23,24,5,45,0,0,24,8,1,0,0,0,25,26,5,64,0,0,26,10,1,0,0,0,
27,28,5,95,0,0,28,12,1,0,0,0,29,30,7,0,0,0,30,14,1,0,0,0,31,32,7,1,0,0,
32,16,1,0,0,0,1,0,0
]
public
static let _ATN: ATN = try! ATNDeserializer().deserialize(_serializedATN)
}
@@ -0,0 +1,14 @@
T__0=1
T__1=2
T__2=3
T__3=4
T__4=5
T__5=6
DIGIT=7
LETTER=8
':'=1
'/'=2
'.'=3
'-'=4
'@'=5
'_'=6
@@ -0,0 +1,129 @@
// Generated from java-escape by ANTLR 4.11.1
import Antlr4
/**
* This interface defines a complete listener for a parse tree produced by
* {@link ReferenceParser}.
*/
public protocol ReferenceListener: ParseTreeListener {
/**
* Enter a parse tree produced by {@link ReferenceParser#root}.
- Parameters:
- ctx: the parse tree
*/
func enterRoot(_ ctx: ReferenceParser.RootContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#root}.
- Parameters:
- ctx: the parse tree
*/
func exitRoot(_ ctx: ReferenceParser.RootContext)
/**
* Enter a parse tree produced by {@link ReferenceParser#host}.
- Parameters:
- ctx: the parse tree
*/
func enterHost(_ ctx: ReferenceParser.HostContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#host}.
- Parameters:
- ctx: the parse tree
*/
func exitHost(_ ctx: ReferenceParser.HostContext)
/**
* Enter a parse tree produced by {@link ReferenceParser#port}.
- Parameters:
- ctx: the parse tree
*/
func enterPort(_ ctx: ReferenceParser.PortContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#port}.
- Parameters:
- ctx: the parse tree
*/
func exitPort(_ ctx: ReferenceParser.PortContext)
/**
* Enter a parse tree produced by {@link ReferenceParser#host_component}.
- Parameters:
- ctx: the parse tree
*/
func enterHost_component(_ ctx: ReferenceParser.Host_componentContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#host_component}.
- Parameters:
- ctx: the parse tree
*/
func exitHost_component(_ ctx: ReferenceParser.Host_componentContext)
/**
* Enter a parse tree produced by {@link ReferenceParser#namespace}.
- Parameters:
- ctx: the parse tree
*/
func enterNamespace(_ ctx: ReferenceParser.NamespaceContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#namespace}.
- Parameters:
- ctx: the parse tree
*/
func exitNamespace(_ ctx: ReferenceParser.NamespaceContext)
/**
* Enter a parse tree produced by {@link ReferenceParser#namespace_component}.
- Parameters:
- ctx: the parse tree
*/
func enterNamespace_component(_ ctx: ReferenceParser.Namespace_componentContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#namespace_component}.
- Parameters:
- ctx: the parse tree
*/
func exitNamespace_component(_ ctx: ReferenceParser.Namespace_componentContext)
/**
* Enter a parse tree produced by {@link ReferenceParser#reference}.
- Parameters:
- ctx: the parse tree
*/
func enterReference(_ ctx: ReferenceParser.ReferenceContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#reference}.
- Parameters:
- ctx: the parse tree
*/
func exitReference(_ ctx: ReferenceParser.ReferenceContext)
/**
* Enter a parse tree produced by {@link ReferenceParser#tag}.
- Parameters:
- ctx: the parse tree
*/
func enterTag(_ ctx: ReferenceParser.TagContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#tag}.
- Parameters:
- ctx: the parse tree
*/
func exitTag(_ ctx: ReferenceParser.TagContext)
/**
* Enter a parse tree produced by {@link ReferenceParser#separator}.
- Parameters:
- ctx: the parse tree
*/
func enterSeparator(_ ctx: ReferenceParser.SeparatorContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#separator}.
- Parameters:
- ctx: the parse tree
*/
func exitSeparator(_ ctx: ReferenceParser.SeparatorContext)
/**
* Enter a parse tree produced by {@link ReferenceParser#name}.
- Parameters:
- ctx: the parse tree
*/
func enterName(_ ctx: ReferenceParser.NameContext)
/**
* Exit a parse tree produced by {@link ReferenceParser#name}.
- Parameters:
- ctx: the parse tree
*/
func exitName(_ ctx: ReferenceParser.NameContext)
}
@@ -0,0 +1,799 @@
// Generated from java-escape by ANTLR 4.11.1
import Antlr4
open class ReferenceParser: Parser {
internal static var _decisionToDFA: [DFA] = {
var decisionToDFA = [DFA]()
let length = ReferenceParser._ATN.getNumberOfDecisions()
for i in 0..<length {
decisionToDFA.append(DFA(ReferenceParser._ATN.getDecisionState(i)!, i))
}
return decisionToDFA
}()
internal static let _sharedContextCache = PredictionContextCache()
public
enum Tokens: Int {
case EOF = -1, T__0 = 1, T__1 = 2, T__2 = 3, T__3 = 4, T__4 = 5, T__5 = 6,
DIGIT = 7, LETTER = 8
}
public
static let RULE_root = 0, RULE_host = 1, RULE_port = 2, RULE_host_component = 3,
RULE_namespace = 4, RULE_namespace_component = 5, RULE_reference = 6,
RULE_tag = 7, RULE_separator = 8, RULE_name = 9
public
static let ruleNames: [String] = [
"root", "host", "port", "host_component", "namespace", "namespace_component",
"reference", "tag", "separator", "name"
]
private static let _LITERAL_NAMES: [String?] = [
nil, "':'", "'/'", "'.'", "'-'", "'@'", "'_'"
]
private static let _SYMBOLIC_NAMES: [String?] = [
nil, nil, nil, nil, nil, nil, nil, "DIGIT", "LETTER"
]
public
static let VOCABULARY = Vocabulary(_LITERAL_NAMES, _SYMBOLIC_NAMES)
override open
func getGrammarFileName() -> String { return "java-escape" }
override open
func getRuleNames() -> [String] { return ReferenceParser.ruleNames }
override open
func getSerializedATN() -> [Int] { return ReferenceParser._serializedATN }
override open
func getATN() -> ATN { return ReferenceParser._ATN }
override open
func getVocabulary() -> Vocabulary {
return ReferenceParser.VOCABULARY
}
override public
init(_ input:TokenStream) throws {
RuntimeMetaData.checkVersion("4.11.1", RuntimeMetaData.VERSION)
try super.init(input)
_interp = ParserATNSimulator(self,ReferenceParser._ATN,ReferenceParser._decisionToDFA, ReferenceParser._sharedContextCache)
}
public class RootContext: ParserRuleContext {
open
func host() -> HostContext? {
return getRuleContext(HostContext.self, 0)
}
open
func namespace() -> NamespaceContext? {
return getRuleContext(NamespaceContext.self, 0)
}
open
func EOF() -> TerminalNode? {
return getToken(ReferenceParser.Tokens.EOF.rawValue, 0)
}
open
func port() -> PortContext? {
return getRuleContext(PortContext.self, 0)
}
open
func reference() -> ReferenceContext? {
return getRuleContext(ReferenceContext.self, 0)
}
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_root
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterRoot(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitRoot(self)
}
}
}
@discardableResult
open func root() throws -> RootContext {
var _localctx: RootContext
_localctx = RootContext(_ctx, getState())
try enterRule(_localctx, 0, ReferenceParser.RULE_root)
var _la: Int = 0
defer {
try! exitRule()
}
do {
try enterOuterAlt(_localctx, 1)
setState(20)
try host()
setState(23)
try _errHandler.sync(self)
_la = try _input.LA(1)
if (_la == ReferenceParser.Tokens.T__0.rawValue) {
setState(21)
try match(ReferenceParser.Tokens.T__0.rawValue)
setState(22)
try port()
}
setState(25)
try match(ReferenceParser.Tokens.T__1.rawValue)
setState(26)
try namespace()
setState(28)
try _errHandler.sync(self)
_la = try _input.LA(1)
if (_la == ReferenceParser.Tokens.T__0.rawValue || _la == ReferenceParser.Tokens.T__4.rawValue) {
setState(27)
try reference()
}
setState(30)
try match(ReferenceParser.Tokens.EOF.rawValue)
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class HostContext: ParserRuleContext {
open
func host_component() -> [Host_componentContext] {
return getRuleContexts(Host_componentContext.self)
}
open
func host_component(_ i: Int) -> Host_componentContext? {
return getRuleContext(Host_componentContext.self, i)
}
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_host
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterHost(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitHost(self)
}
}
}
@discardableResult
open func host() throws -> HostContext {
var _localctx: HostContext
_localctx = HostContext(_ctx, getState())
try enterRule(_localctx, 2, ReferenceParser.RULE_host)
var _la: Int = 0
defer {
try! exitRule()
}
do {
try enterOuterAlt(_localctx, 1)
setState(32)
try host_component()
setState(37)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (_la == ReferenceParser.Tokens.T__2.rawValue) {
setState(33)
try match(ReferenceParser.Tokens.T__2.rawValue)
setState(34)
try host_component()
setState(39)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class PortContext: ParserRuleContext {
open
func DIGIT() -> [TerminalNode] {
return getTokens(ReferenceParser.Tokens.DIGIT.rawValue)
}
open
func DIGIT(_ i:Int) -> TerminalNode? {
return getToken(ReferenceParser.Tokens.DIGIT.rawValue, i)
}
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_port
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterPort(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitPort(self)
}
}
}
@discardableResult
open func port() throws -> PortContext {
var _localctx: PortContext
_localctx = PortContext(_ctx, getState())
try enterRule(_localctx, 4, ReferenceParser.RULE_port)
var _la: Int = 0
defer {
try! exitRule()
}
do {
try enterOuterAlt(_localctx, 1)
setState(41)
try _errHandler.sync(self)
_la = try _input.LA(1)
repeat {
setState(40)
try match(ReferenceParser.Tokens.DIGIT.rawValue)
setState(43);
try _errHandler.sync(self)
_la = try _input.LA(1)
} while (_la == ReferenceParser.Tokens.DIGIT.rawValue)
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class Host_componentContext: ParserRuleContext {
open
func name() -> [NameContext] {
return getRuleContexts(NameContext.self)
}
open
func name(_ i: Int) -> NameContext? {
return getRuleContext(NameContext.self, i)
}
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_host_component
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterHost_component(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitHost_component(self)
}
}
}
@discardableResult
open func host_component() throws -> Host_componentContext {
var _localctx: Host_componentContext
_localctx = Host_componentContext(_ctx, getState())
try enterRule(_localctx, 6, ReferenceParser.RULE_host_component)
var _la: Int = 0
defer {
try! exitRule()
}
do {
try enterOuterAlt(_localctx, 1)
setState(45)
try name()
setState(50)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (_la == ReferenceParser.Tokens.T__3.rawValue) {
setState(46)
try match(ReferenceParser.Tokens.T__3.rawValue)
setState(47)
try name()
setState(52)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class NamespaceContext: ParserRuleContext {
open
func namespace_component() -> [Namespace_componentContext] {
return getRuleContexts(Namespace_componentContext.self)
}
open
func namespace_component(_ i: Int) -> Namespace_componentContext? {
return getRuleContext(Namespace_componentContext.self, i)
}
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_namespace
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterNamespace(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitNamespace(self)
}
}
}
@discardableResult
open func namespace() throws -> NamespaceContext {
var _localctx: NamespaceContext
_localctx = NamespaceContext(_ctx, getState())
try enterRule(_localctx, 8, ReferenceParser.RULE_namespace)
var _la: Int = 0
defer {
try! exitRule()
}
do {
try enterOuterAlt(_localctx, 1)
setState(53)
try namespace_component()
setState(58)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (_la == ReferenceParser.Tokens.T__1.rawValue) {
setState(54)
try match(ReferenceParser.Tokens.T__1.rawValue)
setState(55)
try namespace_component()
setState(60)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class Namespace_componentContext: ParserRuleContext {
open
func name() -> [NameContext] {
return getRuleContexts(NameContext.self)
}
open
func name(_ i: Int) -> NameContext? {
return getRuleContext(NameContext.self, i)
}
open
func separator() -> [SeparatorContext] {
return getRuleContexts(SeparatorContext.self)
}
open
func separator(_ i: Int) -> SeparatorContext? {
return getRuleContext(SeparatorContext.self, i)
}
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_namespace_component
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterNamespace_component(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitNamespace_component(self)
}
}
}
@discardableResult
open func namespace_component() throws -> Namespace_componentContext {
var _localctx: Namespace_componentContext
_localctx = Namespace_componentContext(_ctx, getState())
try enterRule(_localctx, 10, ReferenceParser.RULE_namespace_component)
var _la: Int = 0
defer {
try! exitRule()
}
do {
try enterOuterAlt(_localctx, 1)
setState(65)
try _errHandler.sync(self)
_la = try _input.LA(1)
repeat {
setState(61)
try name()
setState(63)
try _errHandler.sync(self)
_la = try _input.LA(1)
if ((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 88) != 0) {
setState(62)
try separator()
}
setState(67);
try _errHandler.sync(self)
_la = try _input.LA(1)
} while (_la == ReferenceParser.Tokens.DIGIT.rawValue || _la == ReferenceParser.Tokens.LETTER.rawValue)
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class ReferenceContext: ParserRuleContext {
open
func tag() -> TagContext? {
return getRuleContext(TagContext.self, 0)
}
open
func name() -> [NameContext] {
return getRuleContexts(NameContext.self)
}
open
func name(_ i: Int) -> NameContext? {
return getRuleContext(NameContext.self, i)
}
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_reference
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterReference(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitReference(self)
}
}
}
@discardableResult
open func reference() throws -> ReferenceContext {
var _localctx: ReferenceContext
_localctx = ReferenceContext(_ctx, getState())
try enterRule(_localctx, 12, ReferenceParser.RULE_reference)
defer {
try! exitRule()
}
do {
setState(76)
try _errHandler.sync(self)
switch (ReferenceParser.Tokens(rawValue: try _input.LA(1))!) {
case .T__0:
try enterOuterAlt(_localctx, 1)
setState(69)
try match(ReferenceParser.Tokens.T__0.rawValue)
setState(70)
try tag()
break
case .T__4:
try enterOuterAlt(_localctx, 2)
setState(71)
try match(ReferenceParser.Tokens.T__4.rawValue)
setState(72)
try name()
setState(73)
try match(ReferenceParser.Tokens.T__0.rawValue)
setState(74)
try name()
break
default:
throw ANTLRException.recognition(e: NoViableAltException(self))
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class TagContext: ParserRuleContext {
open
func name() -> [NameContext] {
return getRuleContexts(NameContext.self)
}
open
func name(_ i: Int) -> NameContext? {
return getRuleContext(NameContext.self, i)
}
open
func separator() -> [SeparatorContext] {
return getRuleContexts(SeparatorContext.self)
}
open
func separator(_ i: Int) -> SeparatorContext? {
return getRuleContext(SeparatorContext.self, i)
}
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_tag
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterTag(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitTag(self)
}
}
}
@discardableResult
open func tag() throws -> TagContext {
var _localctx: TagContext
_localctx = TagContext(_ctx, getState())
try enterRule(_localctx, 14, ReferenceParser.RULE_tag)
var _la: Int = 0
defer {
try! exitRule()
}
do {
try enterOuterAlt(_localctx, 1)
setState(78)
try name()
setState(84)
try _errHandler.sync(self)
_la = try _input.LA(1)
while ((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 88) != 0) {
setState(79)
try separator()
setState(80)
try name()
setState(86)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class SeparatorContext: ParserRuleContext {
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_separator
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterSeparator(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitSeparator(self)
}
}
}
@discardableResult
open func separator() throws -> SeparatorContext {
var _localctx: SeparatorContext
_localctx = SeparatorContext(_ctx, getState())
try enterRule(_localctx, 16, ReferenceParser.RULE_separator)
var _la: Int = 0
defer {
try! exitRule()
}
do {
try enterOuterAlt(_localctx, 1)
setState(87)
_la = try _input.LA(1)
if (!((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 88) != 0)) {
try _errHandler.recoverInline(self)
}
else {
_errHandler.reportMatch(self)
try consume()
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class NameContext: ParserRuleContext {
open
func LETTER() -> [TerminalNode] {
return getTokens(ReferenceParser.Tokens.LETTER.rawValue)
}
open
func LETTER(_ i:Int) -> TerminalNode? {
return getToken(ReferenceParser.Tokens.LETTER.rawValue, i)
}
open
func DIGIT() -> [TerminalNode] {
return getTokens(ReferenceParser.Tokens.DIGIT.rawValue)
}
open
func DIGIT(_ i:Int) -> TerminalNode? {
return getToken(ReferenceParser.Tokens.DIGIT.rawValue, i)
}
override open
func getRuleIndex() -> Int {
return ReferenceParser.RULE_name
}
override open
func enterRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.enterName(self)
}
}
override open
func exitRule(_ listener: ParseTreeListener) {
if let listener = listener as? ReferenceListener {
listener.exitName(self)
}
}
}
@discardableResult
open func name() throws -> NameContext {
var _localctx: NameContext
_localctx = NameContext(_ctx, getState())
try enterRule(_localctx, 18, ReferenceParser.RULE_name)
var _la: Int = 0
defer {
try! exitRule()
}
do {
var _alt:Int
try enterOuterAlt(_localctx, 1)
setState(90);
try _errHandler.sync(self)
_alt = 1;
repeat {
switch (_alt) {
case 1:
setState(89)
_la = try _input.LA(1)
if (!(_la == ReferenceParser.Tokens.DIGIT.rawValue || _la == ReferenceParser.Tokens.LETTER.rawValue)) {
try _errHandler.recoverInline(self)
}
else {
_errHandler.reportMatch(self)
try consume()
}
break
default:
throw ANTLRException.recognition(e: NoViableAltException(self))
}
setState(92);
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,10,_ctx)
} while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER)
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
static let _serializedATN:[Int] = [
4,1,8,95,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,
7,2,8,7,8,2,9,7,9,1,0,1,0,1,0,3,0,24,8,0,1,0,1,0,1,0,3,0,29,8,0,1,0,1,
0,1,1,1,1,1,1,5,1,36,8,1,10,1,12,1,39,9,1,1,2,4,2,42,8,2,11,2,12,2,43,
1,3,1,3,1,3,5,3,49,8,3,10,3,12,3,52,9,3,1,4,1,4,1,4,5,4,57,8,4,10,4,12,
4,60,9,4,1,5,1,5,3,5,64,8,5,4,5,66,8,5,11,5,12,5,67,1,6,1,6,1,6,1,6,1,
6,1,6,1,6,3,6,77,8,6,1,7,1,7,1,7,1,7,5,7,83,8,7,10,7,12,7,86,9,7,1,8,1,
8,1,9,4,9,91,8,9,11,9,12,9,92,1,9,0,0,10,0,2,4,6,8,10,12,14,16,18,0,2,
2,0,3,4,6,6,1,0,7,8,95,0,20,1,0,0,0,2,32,1,0,0,0,4,41,1,0,0,0,6,45,1,0,
0,0,8,53,1,0,0,0,10,65,1,0,0,0,12,76,1,0,0,0,14,78,1,0,0,0,16,87,1,0,0,
0,18,90,1,0,0,0,20,23,3,2,1,0,21,22,5,1,0,0,22,24,3,4,2,0,23,21,1,0,0,
0,23,24,1,0,0,0,24,25,1,0,0,0,25,26,5,2,0,0,26,28,3,8,4,0,27,29,3,12,6,
0,28,27,1,0,0,0,28,29,1,0,0,0,29,30,1,0,0,0,30,31,5,0,0,1,31,1,1,0,0,0,
32,37,3,6,3,0,33,34,5,3,0,0,34,36,3,6,3,0,35,33,1,0,0,0,36,39,1,0,0,0,
37,35,1,0,0,0,37,38,1,0,0,0,38,3,1,0,0,0,39,37,1,0,0,0,40,42,5,7,0,0,41,
40,1,0,0,0,42,43,1,0,0,0,43,41,1,0,0,0,43,44,1,0,0,0,44,5,1,0,0,0,45,50,
3,18,9,0,46,47,5,4,0,0,47,49,3,18,9,0,48,46,1,0,0,0,49,52,1,0,0,0,50,48,
1,0,0,0,50,51,1,0,0,0,51,7,1,0,0,0,52,50,1,0,0,0,53,58,3,10,5,0,54,55,
5,2,0,0,55,57,3,10,5,0,56,54,1,0,0,0,57,60,1,0,0,0,58,56,1,0,0,0,58,59,
1,0,0,0,59,9,1,0,0,0,60,58,1,0,0,0,61,63,3,18,9,0,62,64,3,16,8,0,63,62,
1,0,0,0,63,64,1,0,0,0,64,66,1,0,0,0,65,61,1,0,0,0,66,67,1,0,0,0,67,65,
1,0,0,0,67,68,1,0,0,0,68,11,1,0,0,0,69,70,5,1,0,0,70,77,3,14,7,0,71,72,
5,5,0,0,72,73,3,18,9,0,73,74,5,1,0,0,74,75,3,18,9,0,75,77,1,0,0,0,76,69,
1,0,0,0,76,71,1,0,0,0,77,13,1,0,0,0,78,84,3,18,9,0,79,80,3,16,8,0,80,81,
3,18,9,0,81,83,1,0,0,0,82,79,1,0,0,0,83,86,1,0,0,0,84,82,1,0,0,0,84,85,
1,0,0,0,85,15,1,0,0,0,86,84,1,0,0,0,87,88,7,0,0,0,88,17,1,0,0,0,89,91,
7,1,0,0,90,89,1,0,0,0,91,92,1,0,0,0,92,90,1,0,0,0,92,93,1,0,0,0,93,19,
1,0,0,0,11,23,28,37,43,50,58,63,67,76,84,92
]
public
static let _ATN = try! ATNDeserializer().deserialize(_serializedATN)
}
+5
View File
@@ -0,0 +1,5 @@
all: clean
antlr -o Generated -Dlanguage=Swift Reference.g4
clean:
rm -rf Generated
+14
View File
@@ -0,0 +1,14 @@
grammar Reference;
root: host (':' port)? '/' namespace reference? EOF;
host: host_component ('.' host_component)*;
port: DIGIT+;
host_component: name ('-' name)*;
namespace: namespace_component ('/' namespace_component)*;
namespace_component: (name separator?)+;
reference: (':' tag) | ('@' name ':' name);
tag: name (separator name)*;
separator: '.' | '-' | '_';
name: (LETTER | DIGIT)+;
DIGIT: [0-9];
LETTER: [A-Za-z];
+102 -93
View File
@@ -1,28 +1,42 @@
import Foundation
import NIOCore
import NIOHTTP1
import AsyncHTTPClient
import Algorithms
import NIOPosix
import AsyncAlgorithms
enum RegistryError: Error {
case UnexpectedHTTPStatusCode(when: String, code: UInt, details: String = "")
case UnexpectedHTTPStatusCode(when: String, code: Int, details: String = "")
case MissingLocationHeader
case AuthFailed(why: String, details: String = "")
case MalformedHeader(why: String)
}
extension HTTPClientResponse.Body {
func readTextResponse() async throws -> String? {
let data = try await readResponse()
return String(decoding: data, as: UTF8.self)
}
enum HTTPMethod: String {
case GET = "GET"
case POST = "POST"
case PUT = "PUT"
case PATCH = "PATCH"
}
func readResponse() async throws -> Data {
enum HTTPCode: Int {
case Ok = 200
case Created = 201
case Accepted = 202
case Unauthorized = 401
}
extension Data {
func asText() -> String {
String(decoding: self, as: UTF8.self)
}
}
extension AsyncThrowingChannel<Data, Error> {
func asData() async throws -> Data {
var result = Data()
for try await part in self {
result.append(Data(buffer: part))
for try await chunk in self {
result += chunk
}
return result
}
}
@@ -78,14 +92,6 @@ struct TokenResponse: Decodable, Authentication {
}
class Registry {
private let httpClient = HTTPClient(
eventLoopGroupProvider: .shared(MultiThreadedEventLoopGroup(numberOfThreads: 1))
)
deinit {
try! httpClient.syncShutdown()
}
let baseURL: URL
let namespace: String
let credentialsProviders: [CredentialsProvider]
@@ -114,43 +120,41 @@ class Registry {
}
func ping() async throws {
let response = try await endpointRequest(.GET, "/v2/")
if response.status != .ok {
throw RegistryError.UnexpectedHTTPStatusCode(when: "doing ping", code: response.status.code)
let (_, response) = try await dataRequest(.GET, endpointURL("/v2/"))
if response.statusCode != HTTPCode.Ok.rawValue {
throw RegistryError.UnexpectedHTTPStatusCode(when: "doing ping", code: response.statusCode)
}
}
func pushManifest(reference: String, manifest: OCIManifest) async throws -> String {
let manifestJSON = try manifest.toJSON()
let response = try await endpointRequest(.PUT, "\(namespace)/manifests/\(reference)",
let (data, response) = try await dataRequest(.PUT, endpointURL("\(namespace)/manifests/\(reference)"),
headers: ["Content-Type": manifest.mediaType],
body: manifestJSON)
if response.status != .created {
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing manifest", code: response.status.code,
details: try await response.body.readTextResponse() ?? "")
if response.statusCode != HTTPCode.Created.rawValue {
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing manifest", code: response.statusCode,
details: data.asText())
}
return Digest.hash(manifestJSON)
}
public func pullManifest(reference: String) async throws -> (OCIManifest, Data) {
let response = try await endpointRequest(.GET, "\(namespace)/manifests/\(reference)",
let (data, response) = try await dataRequest(.GET, endpointURL("\(namespace)/manifests/\(reference)"),
headers: ["Accept": ociManifestMediaType])
if response.status != .ok {
let body = try await response.body.readTextResponse()
throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling manifest", code: response.status.code,
details: body ?? "")
if response.statusCode != HTTPCode.Ok.rawValue {
throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling manifest", code: response.statusCode,
details: data.asText())
}
let manifestData = try await response.body.readResponse()
let manifest = try OCIManifest(fromJSON: manifestData)
let manifest = try OCIManifest(fromJSON: data)
return (manifest, manifestData)
return (manifest, data)
}
private func uploadLocationFromResponse(_ response: HTTPClientResponse) throws -> URLComponents {
guard let uploadLocationRaw = response.headers.first(name: "Location") else {
private func uploadLocationFromResponse(_ response: HTTPURLResponse) throws -> URLComponents {
guard let uploadLocationRaw = response.value(forHTTPHeaderField: "Location") else {
throw RegistryError.MissingLocationHeader
}
@@ -163,12 +167,11 @@ class Registry {
public func pushBlob(fromData: Data, chunkSizeMb: Int = 0) async throws -> String {
// Initiate a blob upload
let postResponse = try await endpointRequest(.POST, "\(namespace)/blobs/uploads/",
let (data, postResponse) = try await dataRequest(.POST, endpointURL("\(namespace)/blobs/uploads/"),
headers: ["Content-Length": "0"])
if postResponse.status != .accepted {
let body = try await postResponse.body.readTextResponse()
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (POST)", code: postResponse.status.code,
details: body ?? "")
if postResponse.statusCode != HTTPCode.Accepted.rawValue {
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (POST)", code: postResponse.statusCode,
details: data.asText())
}
// Figure out where to upload the blob
@@ -178,7 +181,7 @@ class Registry {
if chunkSizeMb == 0 {
// monolithic upload
let response = try await rawRequest(
let (data, response) = try await dataRequest(
.PUT,
uploadLocation,
headers: [
@@ -187,10 +190,9 @@ class Registry {
parameters: ["digest": digest],
body: fromData
)
if response.status != .created {
let body = try await response.body.readTextResponse()
if response.statusCode != HTTPCode.Created.rawValue {
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (PUT) to \(uploadLocation)",
code: response.status.code, details: body ?? "")
code: response.statusCode, details: data.asText())
}
return digest
}
@@ -200,7 +202,7 @@ class Registry {
let chunks = fromData.chunks(ofCount: chunkSizeMb == 0 ? fromData.count : chunkSizeMb * 1_000_000)
for (index, chunk) in chunks.enumerated() {
let lastChunk = index == (chunks.count - 1)
let response = try await rawRequest(
let (data, response) = try await dataRequest(
lastChunk ? .PUT : .PATCH,
uploadLocation,
headers: [
@@ -210,11 +212,10 @@ class Registry {
parameters: lastChunk ? ["digest": digest] : [:],
body: chunk
)
let expectedStatus: HTTPResponseStatus = lastChunk ? .created : .accepted
if response.status != expectedStatus {
let body = try await response.body.readTextResponse()
// always accept both statuses since AWS ECR is not following specification
if response.statusCode != HTTPCode.Created.rawValue && response.statusCode != HTTPCode.Accepted.rawValue {
throw RegistryError.UnexpectedHTTPStatusCode(when: "streaming blob to \(uploadLocation)",
code: response.status.code, details: body ?? "")
code: response.statusCode, details: data.asText())
}
uploadedBytes += chunk.count
// Update location for the next chunk
@@ -224,42 +225,49 @@ class Registry {
return digest
}
public func pullBlob(_ digest: String, handler: (ByteBuffer) throws -> Void) async throws {
let response = try await endpointRequest(.GET, "\(namespace)/blobs/\(digest)")
if response.status != .ok {
let body = try await response.body.readTextResponse()
throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling blob", code: response.status.code,
details: body ?? "")
public func pullBlob(_ digest: String, handler: (Data) throws -> Void) async throws {
let (channel, response) = try await channelRequest(.GET, endpointURL("\(namespace)/blobs/\(digest)"))
if response.statusCode != HTTPCode.Ok.rawValue {
let body = try await channel.asData().asText()
throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling blob", code: response.statusCode,
details: body)
}
for try await part in response.body {
for try await part in channel {
try Task.checkCancellation()
try handler(part)
try handler(Data(part))
}
}
private func endpointRequest(
_ method: HTTPMethod,
_ endpoint: String,
headers: Dictionary<String, String> = Dictionary(),
parameters: Dictionary<String, String> = Dictionary(),
body: Data? = nil
) async throws -> HTTPClientResponse {
private func endpointURL(_ endpoint: String) -> URLComponents {
let url = URL(string: endpoint, relativeTo: baseURL)!
let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
return try await rawRequest(method, urlComponents, headers: headers, parameters: parameters, body: body)
return URLComponents(url: url, resolvingAgainstBaseURL: true)!
}
private func rawRequest(
private func dataRequest(
_ method: HTTPMethod,
_ urlComponents: URLComponents,
headers: Dictionary<String, String> = Dictionary(),
parameters: Dictionary<String, String> = Dictionary(),
body: Data? = nil,
doAuth: Bool = true
) async throws -> (Data, HTTPURLResponse) {
let (channel, response) = try await channelRequest(method, urlComponents,
headers: headers, parameters: parameters, body: body, doAuth: doAuth)
return (try await channel.asData(), response)
}
private func channelRequest(
_ method: HTTPMethod,
_ urlComponents: URLComponents,
headers: Dictionary<String, String> = Dictionary(),
parameters: Dictionary<String, String> = Dictionary(),
body: Data? = nil,
doAuth: Bool = true
) async throws -> HTTPClientResponse {
) async throws -> (AsyncThrowingChannel<Data, Error>, HTTPURLResponse) {
var urlComponents = urlComponents
if urlComponents.queryItems == nil && !parameters.isEmpty {
@@ -269,14 +277,14 @@ class Registry {
URLQueryItem(name: key, value: value)
})
var request = HTTPClientRequest(url: urlComponents.string!)
request.method = method
var request = URLRequest(url: urlComponents.url!)
request.httpMethod = method.rawValue
for (key, value) in headers {
request.headers.add(name: key, value: value)
request.addValue(value, forHTTPHeaderField: key)
}
if body != nil {
request.headers.add(name: "Content-Length", value: "\(body!.count)")
request.body = HTTPClientRequest.Body.bytes(body!)
if let body = body {
request.addValue("\(body.count)", forHTTPHeaderField: "Content-Length")
request.httpBody = body
}
// Invalidate token if it has expired
@@ -284,19 +292,20 @@ class Registry {
currentAuthToken = nil
}
var response = try await authAwareRequest(request: request)
var (channel, response) = try await authAwareRequest(request: request)
if doAuth && response.status == .unauthorized {
if doAuth && response.statusCode == HTTPCode.Unauthorized.rawValue {
_ = try await channel.asData()
try await auth(response: response)
response = try await authAwareRequest(request: request)
(channel, response) = try await authAwareRequest(request: request)
}
return response
return (channel, response)
}
private func auth(response: HTTPClientResponse) async throws {
private func auth(response: HTTPURLResponse) async throws {
// Process WWW-Authenticate header
guard let wwwAuthenticateRaw = response.headers.first(name: "WWW-Authenticate") else {
guard let wwwAuthenticateRaw = response.value(forHTTPHeaderField: "WWW-Authenticate") else {
throw RegistryError.AuthFailed(why: "got HTTP 401, but WWW-Authenticate header is missing")
}
@@ -345,15 +354,13 @@ class Registry {
headers["Authorization"] = "Basic \(encodedCredentials!)"
}
let response = try await rawRequest(.GET, authenticateURL, headers: headers, doAuth: false)
if response.status != .ok {
let body = try await response.body.readTextResponse() ?? ""
throw RegistryError.AuthFailed(why: "received unexpected HTTP status code \(response.status.code) "
+ "while retrieving an authentication token", details: body)
let (data, response) = try await dataRequest(.GET, authenticateURL, headers: headers, doAuth: false)
if response.statusCode != HTTPCode.Ok.rawValue {
throw RegistryError.AuthFailed(why: "received unexpected HTTP status code \(response.statusCode) "
+ "while retrieving an authentication token", details: data.asText())
}
let bodyData = try await response.body.readResponse()
currentAuthToken = try TokenResponse.parse(fromData: bodyData)
currentAuthToken = try TokenResponse.parse(fromData: data)
}
private func lookupCredentials(host: String) throws -> (String, String)? {
@@ -365,14 +372,16 @@ class Registry {
return nil
}
private func authAwareRequest(request: HTTPClientRequest) async throws -> HTTPClientResponse {
private func authAwareRequest(request: URLRequest) async throws -> (AsyncThrowingChannel<Data, Error>, HTTPURLResponse) {
var request = request
if let token = currentAuthToken {
let (name, value) = token.header()
request.headers.add(name: name, value: value)
request.addValue(value, forHTTPHeaderField: name)
}
return try await httpClient.execute(request, deadline: .distantFuture)
let (channel, response) = try await Fetcher().fetch(request)
return (channel, response as! HTTPURLResponse)
}
}
+64 -42
View File
@@ -1,5 +1,5 @@
import Foundation
import Parsing
import Antlr4
struct Reference: Comparable, Hashable, CustomStringConvertible {
enum ReferenceType: Comparable {
@@ -46,6 +46,39 @@ struct Reference: Comparable, Hashable, CustomStringConvertible {
}
}
class ReferenceCollector: ReferenceBaseListener {
var host: String? = nil
var port: String? = nil
var namespace: String? = nil
var reference: String? = nil
override func exitHost(_ ctx: ReferenceParser.HostContext) {
host = ctx.getText()
}
override func exitPort(_ ctx: ReferenceParser.PortContext) {
port = ctx.getText()
}
override func exitNamespace(_ ctx: ReferenceParser.NamespaceContext) {
namespace = ctx.getText()
}
override func exitReference(_ ctx: ReferenceParser.ReferenceContext) {
reference = ctx.getText()
}
}
class ErrorCollector: BaseErrorListener {
var error: String? = nil
override func syntaxError<T>(_ recognizer: Recognizer<T>, _ offendingSymbol: AnyObject?, _ line: Int, _ charPositionInLine: Int, _ msg: String, _ e: AnyObject?) {
if error == nil {
error = "\(msg) (character \(charPositionInLine + 1))"
}
}
}
struct RemoteName: Comparable, Hashable, CustomStringConvertible {
var host: String
var namespace: String
@@ -58,51 +91,40 @@ struct RemoteName: Comparable, Hashable, CustomStringConvertible {
}
init(_ name: String) throws {
let csNormal = [
UInt8(ascii: "a")...UInt8(ascii: "z"),
UInt8(ascii: "A")...UInt8(ascii: "Z"),
UInt8(ascii: "0")...UInt8(ascii: "9"),
].asCharacterSet().union(CharacterSet(charactersIn: "_-."))
let errorCollector = ErrorCollector()
let inputStream = ANTLRInputStream(Array(name.unicodeScalars), name.count)
let lexer = ReferenceLexer(inputStream)
lexer.removeErrorListeners()
lexer.addErrorListener(errorCollector)
let csHex = [
UInt8(ascii: "a")...UInt8(ascii: "f"),
UInt8(ascii: "0")...UInt8(ascii: "9"),
].asCharacterSet()
let tokenStream = CommonTokenStream(lexer)
let parser = try ReferenceParser(tokenStream)
parser.removeErrorListeners()
parser.addErrorListener(errorCollector)
let parser = Parse {
Consumed {
csNormal
Optionally {
":"
Digits()
}
}
"/"
csNormal.union(CharacterSet(charactersIn: "/"))
Optionally {
OneOf {
Parse {
":"
csNormal.map {
Reference(tag: String($0))
}
}
Parse {
"@sha256:"
csHex.map {
Reference(digest: "sha256:" + String($0))
}
}
}
}
End()
let referenceCollector = ReferenceCollector()
try ParseTreeWalker().walk(referenceCollector, try parser.root())
if let error = errorCollector.error {
throw RuntimeError("failed to parse remote name: \(error)")
}
let result = try parser.parse(name)
host = String(result.0)
namespace = String(result.1)
reference = result.2 ?? Reference(tag: "latest")
host = referenceCollector.host!
if let port = referenceCollector.port {
host += ":" + port
}
namespace = referenceCollector.namespace!
if let reference = referenceCollector.reference {
if reference.starts(with: "@sha256:") {
self.reference = Reference(digest: String(reference.dropFirst(1)))
} else if reference.starts(with: ":") {
self.reference = Reference(tag: String(reference.dropFirst(1)))
} else {
throw RuntimeError("failed to parse remote name: unknown reference format")
}
} else {
self.reference = Reference(tag: "latest")
}
}
static func <(lhs: RemoteName, rhs: RemoteName) -> Bool {
+4 -4
View File
@@ -24,7 +24,7 @@ let passphrases = [
"act",
"action",
"actor",
" actress",
"actress",
"actual",
"adapt",
"add",
@@ -829,7 +829,7 @@ let passphrases = [
"grow",
"grunt",
"guard",
" guess",
"guess",
"guide",
"guilt",
"guitar",
@@ -1476,7 +1476,7 @@ let passphrases = [
"retire",
"retreat",
"return",
" reunion",
"reunion",
"reveal",
"review",
"reward",
@@ -1764,7 +1764,7 @@ let passphrases = [
"swim",
"swing",
"switch",
" sword",
"sword",
"symbol",
"symptom",
"syrup",
+1
View File
@@ -5,6 +5,7 @@ protocol PrunableStorage {
}
protocol Prunable {
var url: URL { get }
func delete() throws
func accessDate() throws -> Date
func sizeBytes() throws -> Int
+15
View File
@@ -1,5 +1,14 @@
import ArgumentParser
import Foundation
import Puppy
var puppy = Puppy.default
class LogFormatter: LogFormattable {
func formatMessage(_ level: LogLevel, message: String, tag: String, function: String, file: String, line: UInt, swiftLogInfo: [String: String], label: String, date: Date, threadID: UInt64) -> String {
"\(date) \(level) \(message)"
}
}
@main
struct Root: AsyncParsableCommand {
@@ -36,6 +45,12 @@ struct Root: AsyncParsableCommand {
// Set line-buffered output for stdout
setlinebuf(stdout)
// Initialize file logger
let logFileURL = try Config().tartHomeDir.appendingPathComponent("tart.log")
let fileLogger = try FileLogger("org.cirruslabs.tart", fileURL: logFileURL)
fileLogger.format = LogFormatter()
puppy.add(fileLogger)
// Parse and run command
do {
var command = try parseAsRoot()
+4
View File
@@ -1,6 +1,10 @@
import Foundation
extension URL: Prunable {
var url: URL {
self
}
func delete() throws {
try FileManager.default.removeItem(at: self)
}
+57 -35
View File
@@ -1,5 +1,6 @@
import Foundation
import Virtualization
import AsyncAlgorithms
struct UnsupportedRestoreImageError: Error {
}
@@ -60,44 +61,61 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
virtualMachine.delegate = self
}
static func retrieveLatestIPSW() async throws -> URL {
static func retrieveIPSW(remoteURL: URL) async throws -> URL {
// Check if we already have this IPSW in cache
let (channel, response) = try await Fetcher().fetch(URLRequest(url: remoteURL))
if let hash = (response as! HTTPURLResponse).value(forHTTPHeaderField: "x-amz-meta-digest-sha256") {
let ipswLocation = try IPSWCache().locationFor(fileName: "sha256:\(hash).ipsw")
if FileManager.default.fileExists(atPath: ipswLocation.path) {
defaultLogger.appendNewLine("Using cached *.ipsw file...")
try ipswLocation.updateAccessDate()
return ipswLocation
}
}
// Download the IPSW
defaultLogger.appendNewLine("Fetching \(remoteURL.lastPathComponent)...")
let progress = Progress(totalUnitCount: response.expectedContentLength)
ProgressObserver(progress).log(defaultLogger)
let temporaryLocation = try Config().tartTmpDir.appendingPathComponent(UUID().uuidString + ".ipsw")
FileManager.default.createFile(atPath: temporaryLocation.path, contents: nil)
let lock = try FileLock(lockURL: temporaryLocation)
try lock.lock()
let fileHandle = try FileHandle(forWritingTo: temporaryLocation)
let digest = Digest()
for try await chunk in channel {
let chunkAsData = Data(chunk)
fileHandle.write(chunkAsData)
digest.update(chunkAsData)
progress.completedUnitCount += Int64(chunk.count)
}
try fileHandle.close()
let finalLocation = try IPSWCache().locationFor(fileName: digest.finalize() + ".ipsw")
return try FileManager.default.replaceItemAt(finalLocation, withItemAt: temporaryLocation)!
}
static func latestIPSWURL() async throws -> URL {
defaultLogger.appendNewLine("Looking up the latest supported IPSW...")
let image = try await withCheckedThrowingContinuation { continuation in
VZMacOSRestoreImage.fetchLatestSupported() { result in
continuation.resume(with: result)
}
}
let expectedIPSWLocation = try IPSWCache().locationFor(image: image)
if FileManager.default.fileExists(atPath: expectedIPSWLocation.path) {
defaultLogger.appendNewLine("Using cached *.ipsw file...")
try expectedIPSWLocation.updateAccessDate()
return expectedIPSWLocation
}
defaultLogger.appendNewLine("Fetching \(expectedIPSWLocation.lastPathComponent)...")
let data: Data = try await withCheckedThrowingContinuation { continuation in
let downloadedTask = URLSession.shared.dataTask(with: image.url) { data, response, error in
if error != nil {
continuation.resume(throwing: error!)
return
}
if (data == nil) {
continuation.resume(throwing: DownloadFailed())
return
}
continuation.resume(returning: data!)
}
ProgressObserver(downloadedTask.progress).log(defaultLogger)
downloadedTask.resume()
}
try data.write(to: expectedIPSWLocation, options: [.atomic])
return expectedIPSWLocation
return image.url
}
var inFinalState: Bool {
get {
virtualMachine.state == VZVirtualMachine.State.stopped ||
@@ -109,12 +127,16 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
init(
vmDir: VMDirectory,
ipswURL: URL?,
ipswURL: URL,
diskSizeGB: UInt16,
network: Network = NetworkShared(),
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = []
) async throws {
let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
var ipswURL = ipswURL
if !ipswURL.isFileURL {
ipswURL = try await VM.retrieveIPSW(remoteURL: ipswURL)
}
// Load the restore image and try to get the requirements
// that match both the image and our platform
@@ -158,7 +180,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Run automated installation
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
DispatchQueue.main.async {
DispatchQueue.main.async { [ipswURL] in
let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL)
defaultLogger.appendNewLine("Installing OS...")
@@ -187,7 +209,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
func run(_ recovery: Bool) async throws {
try network.run()
try network.run(sema)
DispatchQueue.main.sync {
Task {
@@ -217,7 +239,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
}
try network.stop()
try await network.stop()
}
static func craftConfiguration(
+5 -6
View File
@@ -35,8 +35,8 @@ extension VMDirectory {
throw OCIError.FailedToCreateVmFile
}
let configFile = try FileHandle(forWritingTo: configURL)
try await registry.pullBlob(configLayers.first!.digest) { buffer in
configFile.write(Data(buffer: buffer))
try await registry.pullBlob(configLayers.first!.digest) { data in
configFile.write(data)
}
try configFile.close()
@@ -70,8 +70,7 @@ extension VMDirectory {
ProgressObserver(progress).log(defaultLogger)
for diskLayer in diskLayers {
try await registry.pullBlob(diskLayer.digest) { buffer in
let data = Data(buffer: buffer)
try await registry.pullBlob(diskLayer.digest) { data in
try filter.write(data)
progress.completedUnitCount += Int64(data.count)
}
@@ -92,8 +91,8 @@ extension VMDirectory {
throw OCIError.FailedToCreateVmFile
}
let nvram = try FileHandle(forWritingTo: nvramURL)
try await registry.pullBlob(nvramLayers.first!.digest) { buffer in
nvram.write(Data(buffer: buffer))
try await registry.pullBlob(nvramLayers.first!.digest) { data in
nvram.write(data)
}
try nvram.close()
}
+4
View File
@@ -28,6 +28,10 @@ struct VMDirectory: Prunable {
baseURL.lastPathComponent
}
var url: URL {
baseURL
}
static func temporary() throws -> VMDirectory {
let tmpDir = try Config().tartTmpDir.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: false)
+12 -1
View File
@@ -159,11 +159,22 @@ class VMStorageOCI: PrunableStorage {
let requiredCapacityBytes = UInt64(uncompressedDiskSize + 128 * 1024 * 1024)
let attrs = try Config().tartCacheDir.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey, .volumeAvailableCapacityKey])
let availableCapacityBytes = max(UInt64(attrs.volumeAvailableCapacityForImportantUsage!), UInt64(attrs.volumeAvailableCapacity!))
let capacityImportant = attrs.volumeAvailableCapacityForImportantUsage!
let capacityAvailable = attrs.volumeAvailableCapacity!
let availableCapacityBytes = max(UInt64(capacityImportant), UInt64(capacityAvailable))
if capacityImportant == 0 || capacityAvailable == 0 {
puppy.warning("important capacity \(capacityImportant) bytes, "
+ "available capacity is \(capacityAvailable) bytes")
}
// There is a suspicious that occasionally capacity is returned as zero which can't be true.
// Let's validate to avoid unnecessary pruning.
if 0 < availableCapacityBytes && availableCapacityBytes < requiredCapacityBytes {
puppy.info("pruning cache to accommodate \(name) with a disk of size \(uncompressedDiskSize) bytes ("
+ "available capacity is \(availableCapacityBytes) bytes, required capacity "
+ "is \(requiredCapacityBytes) bytes)")
try Prune.pruneReclaim(reclaimBytes: requiredCapacityBytes - availableCapacityBytes)
}
}
+4 -4
View File
@@ -34,8 +34,8 @@ final class RegistryTests: XCTestCase {
// Pull it
var pulledBlob = Data()
try await registry.pullBlob(pushedBlobDigest) { buffer in
pulledBlob.append(Data(buffer: buffer))
try await registry.pullBlob(pushedBlobDigest) { data in
pulledBlob.append(data)
}
// Ensure that both blobs are identical
@@ -52,8 +52,8 @@ final class RegistryTests: XCTestCase {
// Pull it
var pulledLargeBlob = Data()
try await registry.pullBlob(largeBlobDigest) { buffer in
pulledLargeBlob.append(Data(buffer: buffer))
try await registry.pullBlob(largeBlobDigest) { data in
pulledLargeBlob.append(data)
}
// Ensure that both blobs are identical
+4
View File
@@ -44,4 +44,8 @@ final class RemoteNameTests: XCTestCase {
// Port must be specified when ":" is used
XCTAssertEqual(try? RemoteName("127.0.0.1:/a/b").host, nil)
}
func testNoPathTraversal() throws {
XCTAssertEqual(try? RemoteName("ghcr.io/a/../b/c:latest"), nil)
}
}