@hasna/recordings 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +349 -69
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1919 -98
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1973 -236
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Darwin
|
|
3
|
+
import Foundation
|
|
4
|
+
import RecordingsUpdateProtocol
|
|
5
|
+
import RecordingsVerifierLauncher
|
|
6
|
+
|
|
7
|
+
struct StagedFile: Sendable {
|
|
8
|
+
let path: String
|
|
9
|
+
let sha256: String
|
|
10
|
+
let byteCount: Int64
|
|
11
|
+
let data: Data?
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
struct StagedUpdate: Sendable {
|
|
15
|
+
let transactionID: UUID
|
|
16
|
+
let directory: String
|
|
17
|
+
let archive: StagedFile
|
|
18
|
+
let manifest: StagedFile
|
|
19
|
+
let envelope: StagedFile
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
final class RootUpdateState {
|
|
23
|
+
let rootPath: String
|
|
24
|
+
private let rootDescriptor: Int32
|
|
25
|
+
|
|
26
|
+
init(path: String = RecordingsUpdateConstants.stateRoot) throws {
|
|
27
|
+
guard geteuid() == 0 else { throw IngestError.brokerNotRoot }
|
|
28
|
+
guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(to: path) else {
|
|
29
|
+
throw IngestError.unsafeStateRoot
|
|
30
|
+
}
|
|
31
|
+
var metadata = stat()
|
|
32
|
+
guard lstat(path, &metadata) == 0,
|
|
33
|
+
(metadata.st_mode & S_IFMT) == S_IFDIR,
|
|
34
|
+
metadata.st_uid == 0,
|
|
35
|
+
(metadata.st_mode & 0o077) == 0
|
|
36
|
+
else {
|
|
37
|
+
throw IngestError.unsafeStateRoot
|
|
38
|
+
}
|
|
39
|
+
rootDescriptor = Darwin.open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
|
|
40
|
+
guard rootDescriptor >= 0 else { throw IngestError.unsafeStateRoot }
|
|
41
|
+
var openedMetadata = stat()
|
|
42
|
+
guard fstat(rootDescriptor, &openedMetadata) == 0,
|
|
43
|
+
openedMetadata.st_dev == metadata.st_dev,
|
|
44
|
+
openedMetadata.st_ino == metadata.st_ino,
|
|
45
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(rootDescriptor)
|
|
46
|
+
else {
|
|
47
|
+
Darwin.close(rootDescriptor)
|
|
48
|
+
throw IngestError.unsafeStateRoot
|
|
49
|
+
}
|
|
50
|
+
rootPath = path
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
deinit { Darwin.close(rootDescriptor) }
|
|
54
|
+
|
|
55
|
+
func withExclusiveTransactionLock<T>(_ operation: () throws -> T) throws -> T {
|
|
56
|
+
let lock = openat(
|
|
57
|
+
rootDescriptor,
|
|
58
|
+
".transactions.lock",
|
|
59
|
+
O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW,
|
|
60
|
+
0o600
|
|
61
|
+
)
|
|
62
|
+
guard lock >= 0 else { throw IngestError.transactionLockUnavailable }
|
|
63
|
+
defer { Darwin.close(lock) }
|
|
64
|
+
var metadata = stat()
|
|
65
|
+
guard fstat(lock, &metadata) == 0,
|
|
66
|
+
(metadata.st_mode & S_IFMT) == S_IFREG,
|
|
67
|
+
metadata.st_uid == 0,
|
|
68
|
+
(metadata.st_mode & 0o077) == 0,
|
|
69
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(lock),
|
|
70
|
+
flock(lock, LOCK_EX) == 0
|
|
71
|
+
else {
|
|
72
|
+
throw IngestError.transactionLockUnavailable
|
|
73
|
+
}
|
|
74
|
+
defer { flock(lock, LOCK_UN) }
|
|
75
|
+
return try operation()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func ensureFreeSpace(requiredBytes: UInt64) throws {
|
|
79
|
+
var filesystem = statfs()
|
|
80
|
+
guard fstatfs(rootDescriptor, &filesystem) == 0,
|
|
81
|
+
filesystem.f_bavail > 0,
|
|
82
|
+
filesystem.f_bsize > 0
|
|
83
|
+
else {
|
|
84
|
+
throw IngestError.insufficientStagingCapacity
|
|
85
|
+
}
|
|
86
|
+
let (available, overflow) = UInt64(filesystem.f_bavail)
|
|
87
|
+
.multipliedReportingOverflow(by: UInt64(filesystem.f_bsize))
|
|
88
|
+
let safetyReserve: UInt64 = 2 * 1024 * 1024 * 1024
|
|
89
|
+
guard !overflow,
|
|
90
|
+
requiredBytes <= UInt64.max - safetyReserve,
|
|
91
|
+
available >= requiredBytes + safetyReserve
|
|
92
|
+
else {
|
|
93
|
+
throw IngestError.insufficientStagingCapacity
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func ensureTransactionQuota() throws {
|
|
98
|
+
let names = try FileManager.default.contentsOfDirectory(atPath: rootPath)
|
|
99
|
+
for name in names where name.hasPrefix("transaction-") {
|
|
100
|
+
guard let identifier = UUID(uuidString: String(name.dropFirst("transaction-".count))),
|
|
101
|
+
"transaction-" + identifier.uuidString.lowercased() == name
|
|
102
|
+
else {
|
|
103
|
+
throw IngestError.unsafeRetainedTransaction
|
|
104
|
+
}
|
|
105
|
+
// Startup recovery must resolve and prune every previous transaction
|
|
106
|
+
// before the listener opens. One broker process admits one transaction.
|
|
107
|
+
throw IngestError.transactionQuotaExceeded
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func removeTransactionDirectory(id: UUID) throws {
|
|
112
|
+
let leaf = "transaction-\(id.uuidString.lowercased())"
|
|
113
|
+
let status = leaf.withCString {
|
|
114
|
+
recordings_remove_directory_tree_at(rootDescriptor, $0)
|
|
115
|
+
}
|
|
116
|
+
guard status == 0 || status == ENOENT else { throw IngestError.couldNotCleanup }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func createTransactionDirectory(id: UUID) throws -> String {
|
|
120
|
+
let leaf = "transaction-\(id.uuidString.lowercased())"
|
|
121
|
+
guard mkdirat(rootDescriptor, leaf, 0o700) == 0 else {
|
|
122
|
+
throw IngestError.couldNotCreateTransaction
|
|
123
|
+
}
|
|
124
|
+
guard fsync(rootDescriptor) == 0 else { throw IngestError.couldNotSync }
|
|
125
|
+
let descriptor = openat(rootDescriptor, leaf, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
|
|
126
|
+
guard descriptor >= 0 else { throw IngestError.couldNotCreateTransaction }
|
|
127
|
+
defer { Darwin.close(descriptor) }
|
|
128
|
+
var metadata = stat()
|
|
129
|
+
guard fstat(descriptor, &metadata) == 0,
|
|
130
|
+
(metadata.st_mode & S_IFMT) == S_IFDIR,
|
|
131
|
+
metadata.st_uid == 0,
|
|
132
|
+
(metadata.st_mode & 0o077) == 0,
|
|
133
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
|
|
134
|
+
else {
|
|
135
|
+
throw IngestError.couldNotCreateTransaction
|
|
136
|
+
}
|
|
137
|
+
return rootPath + "/" + leaf
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
struct ArtifactIngestor {
|
|
142
|
+
static let maximumArchiveBytes: Int64 = 256 * 1024 * 1024
|
|
143
|
+
static let maximumManifestBytes: Int64 = 16 * 1024 * 1024
|
|
144
|
+
static let maximumEnvelopeBytes: Int64 = 1024 * 1024
|
|
145
|
+
static let maximumTransactionFootprintBytes: UInt64 = 2 * 1024 * 1024 * 1024
|
|
146
|
+
|
|
147
|
+
let state: RootUpdateState
|
|
148
|
+
|
|
149
|
+
func stage(archive: FileHandle, manifest: FileHandle, envelope: FileHandle) throws -> StagedUpdate {
|
|
150
|
+
try state.ensureFreeSpace(
|
|
151
|
+
requiredBytes: Self.maximumTransactionFootprintBytes
|
|
152
|
+
)
|
|
153
|
+
let retainedDescriptors = try [archive, manifest, envelope].map { handle -> Int32 in
|
|
154
|
+
let retained = fcntl(handle.fileDescriptor, F_DUPFD_CLOEXEC, 10)
|
|
155
|
+
guard retained >= 0, lseek(retained, 0, SEEK_CUR) >= 0 else {
|
|
156
|
+
if retained >= 0 { Darwin.close(retained) }
|
|
157
|
+
throw IngestError.invalidSourceDescriptor
|
|
158
|
+
}
|
|
159
|
+
return retained
|
|
160
|
+
}
|
|
161
|
+
defer { retainedDescriptors.forEach { Darwin.close($0) } }
|
|
162
|
+
let transactionID = UUID()
|
|
163
|
+
let directory = try state.createTransactionDirectory(id: transactionID)
|
|
164
|
+
do {
|
|
165
|
+
let stagedArchive = try copyDescriptor(
|
|
166
|
+
retainedDescriptors[0],
|
|
167
|
+
to: directory + "/artifact.pkg",
|
|
168
|
+
maximumBytes: Self.maximumArchiveBytes,
|
|
169
|
+
retainData: false
|
|
170
|
+
)
|
|
171
|
+
let stagedManifest = try copyDescriptor(
|
|
172
|
+
retainedDescriptors[1],
|
|
173
|
+
to: directory + "/manifest.json",
|
|
174
|
+
maximumBytes: Self.maximumManifestBytes,
|
|
175
|
+
retainData: true
|
|
176
|
+
)
|
|
177
|
+
let stagedEnvelope = try copyDescriptor(
|
|
178
|
+
retainedDescriptors[2],
|
|
179
|
+
to: directory + "/envelope.json",
|
|
180
|
+
maximumBytes: Self.maximumEnvelopeBytes,
|
|
181
|
+
retainData: true
|
|
182
|
+
)
|
|
183
|
+
return StagedUpdate(
|
|
184
|
+
transactionID: transactionID,
|
|
185
|
+
directory: directory,
|
|
186
|
+
archive: stagedArchive,
|
|
187
|
+
manifest: stagedManifest,
|
|
188
|
+
envelope: stagedEnvelope
|
|
189
|
+
)
|
|
190
|
+
} catch {
|
|
191
|
+
// Nothing in this transaction has executed. Remove only the exact
|
|
192
|
+
// broker-generated UUID using the descriptor-relative root primitive.
|
|
193
|
+
do {
|
|
194
|
+
try state.removeTransactionDirectory(id: transactionID)
|
|
195
|
+
} catch {
|
|
196
|
+
throw error
|
|
197
|
+
}
|
|
198
|
+
throw error
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private func copyDescriptor(
|
|
203
|
+
_ sourceDescriptor: Int32,
|
|
204
|
+
to destination: String,
|
|
205
|
+
maximumBytes: Int64,
|
|
206
|
+
retainData: Bool
|
|
207
|
+
) throws -> StagedFile {
|
|
208
|
+
var before = stat()
|
|
209
|
+
guard fstat(sourceDescriptor, &before) == 0,
|
|
210
|
+
(before.st_mode & S_IFMT) == S_IFREG,
|
|
211
|
+
before.st_size > 0
|
|
212
|
+
else {
|
|
213
|
+
throw IngestError.invalidSourceDescriptor
|
|
214
|
+
}
|
|
215
|
+
guard before.st_size <= maximumBytes else { throw IngestError.sourceTooLarge }
|
|
216
|
+
|
|
217
|
+
let output = Darwin.open(
|
|
218
|
+
destination,
|
|
219
|
+
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW,
|
|
220
|
+
0o600
|
|
221
|
+
)
|
|
222
|
+
guard output >= 0 else { throw IngestError.couldNotCreateStagingFile }
|
|
223
|
+
var shouldUnlink = true
|
|
224
|
+
defer {
|
|
225
|
+
Darwin.close(output)
|
|
226
|
+
if shouldUnlink { Darwin.unlink(destination) }
|
|
227
|
+
}
|
|
228
|
+
guard DarwinACLValidator.descriptorHasNoExtendedACL(output) else {
|
|
229
|
+
throw IngestError.couldNotCreateStagingFile
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
var hasher = SHA256()
|
|
233
|
+
var captured = retainData ? Data() : nil
|
|
234
|
+
if retainData { captured?.reserveCapacity(Int(before.st_size)) }
|
|
235
|
+
var offset: off_t = 0
|
|
236
|
+
var buffer = [UInt8](repeating: 0, count: 1024 * 1024)
|
|
237
|
+
while offset < before.st_size {
|
|
238
|
+
let requestCount = min(buffer.count, Int(before.st_size - offset))
|
|
239
|
+
let count = buffer.withUnsafeMutableBytes {
|
|
240
|
+
pread(sourceDescriptor, $0.baseAddress, requestCount, offset)
|
|
241
|
+
}
|
|
242
|
+
guard count > 0 else { throw IngestError.sourceChangedDuringCopy }
|
|
243
|
+
let data = Data(buffer.prefix(count))
|
|
244
|
+
hasher.update(data: data)
|
|
245
|
+
captured?.append(data)
|
|
246
|
+
try writeAll(data, to: output)
|
|
247
|
+
offset += off_t(count)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var trailingByte: UInt8 = 0
|
|
251
|
+
guard pread(sourceDescriptor, &trailingByte, 1, offset) == 0 else {
|
|
252
|
+
throw IngestError.sourceChangedDuringCopy
|
|
253
|
+
}
|
|
254
|
+
var after = stat()
|
|
255
|
+
guard fstat(sourceDescriptor, &after) == 0,
|
|
256
|
+
before.st_dev == after.st_dev,
|
|
257
|
+
before.st_ino == after.st_ino,
|
|
258
|
+
before.st_size == after.st_size,
|
|
259
|
+
before.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
|
|
260
|
+
before.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
|
|
261
|
+
before.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
|
|
262
|
+
before.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec
|
|
263
|
+
else {
|
|
264
|
+
throw IngestError.sourceChangedDuringCopy
|
|
265
|
+
}
|
|
266
|
+
guard fsync(output) == 0,
|
|
267
|
+
fchmod(output, 0o400) == 0,
|
|
268
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(output),
|
|
269
|
+
fsync(output) == 0
|
|
270
|
+
else {
|
|
271
|
+
throw IngestError.couldNotSync
|
|
272
|
+
}
|
|
273
|
+
let parent = Darwin.open(
|
|
274
|
+
URL(fileURLWithPath: destination).deletingLastPathComponent().path,
|
|
275
|
+
O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW
|
|
276
|
+
)
|
|
277
|
+
guard parent >= 0 else { throw IngestError.couldNotSync }
|
|
278
|
+
defer { Darwin.close(parent) }
|
|
279
|
+
guard DarwinACLValidator.descriptorHasNoExtendedACL(parent),
|
|
280
|
+
fsync(parent) == 0
|
|
281
|
+
else {
|
|
282
|
+
throw IngestError.couldNotSync
|
|
283
|
+
}
|
|
284
|
+
shouldUnlink = false
|
|
285
|
+
return StagedFile(
|
|
286
|
+
path: destination,
|
|
287
|
+
sha256: hasher.finalize().map { String(format: "%02x", $0) }.joined(),
|
|
288
|
+
byteCount: before.st_size,
|
|
289
|
+
data: captured
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private func writeAll(_ data: Data, to descriptor: Int32) throws {
|
|
294
|
+
try data.withUnsafeBytes { bytes in
|
|
295
|
+
guard let base = bytes.baseAddress else { return }
|
|
296
|
+
var written = 0
|
|
297
|
+
while written < bytes.count {
|
|
298
|
+
let count = Darwin.write(descriptor, base.advanced(by: written), bytes.count - written)
|
|
299
|
+
if count < 0, errno == EINTR { continue }
|
|
300
|
+
guard count > 0 else { throw IngestError.couldNotWrite }
|
|
301
|
+
written += count
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
enum IngestError: Error, CustomStringConvertible {
|
|
308
|
+
case brokerNotRoot
|
|
309
|
+
case unsafeStateRoot
|
|
310
|
+
case couldNotCreateTransaction
|
|
311
|
+
case invalidSourceDescriptor
|
|
312
|
+
case sourceTooLarge
|
|
313
|
+
case sourceChangedDuringCopy
|
|
314
|
+
case couldNotCreateStagingFile
|
|
315
|
+
case couldNotWrite
|
|
316
|
+
case couldNotSync
|
|
317
|
+
case transactionLockUnavailable
|
|
318
|
+
case insufficientStagingCapacity
|
|
319
|
+
case couldNotCleanup
|
|
320
|
+
case unsafeRetainedTransaction
|
|
321
|
+
case transactionQuotaExceeded
|
|
322
|
+
|
|
323
|
+
var description: String {
|
|
324
|
+
switch self {
|
|
325
|
+
case .brokerNotRoot: "The update broker is not running as root"
|
|
326
|
+
case .unsafeStateRoot: "The root-owned update state directory is missing or unsafe"
|
|
327
|
+
case .couldNotCreateTransaction: "Could not create a protected update transaction"
|
|
328
|
+
case .invalidSourceDescriptor: "The update input is not a regular file descriptor"
|
|
329
|
+
case .sourceTooLarge: "The update input exceeds its size limit"
|
|
330
|
+
case .sourceChangedDuringCopy: "The update input changed while it was being copied"
|
|
331
|
+
case .couldNotCreateStagingFile: "Could not create a protected staging file"
|
|
332
|
+
case .couldNotWrite: "Could not write the protected staging file"
|
|
333
|
+
case .couldNotSync: "Could not durably synchronize update state"
|
|
334
|
+
case .transactionLockUnavailable: "The protected update transaction lock is unavailable"
|
|
335
|
+
case .insufficientStagingCapacity: "The protected update volume lacks the required safety reserve"
|
|
336
|
+
case .couldNotCleanup: "Could not remove a protected update transaction"
|
|
337
|
+
case .unsafeRetainedTransaction: "A retained update transaction name is unsafe"
|
|
338
|
+
case .transactionQuotaExceeded: "A protected update transaction is already retained"
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|