@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,561 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Darwin
|
|
3
|
+
import Foundation
|
|
4
|
+
import RecordingsUpdateProtocol
|
|
5
|
+
import Security
|
|
6
|
+
|
|
7
|
+
struct CandidateValidationResult: Sendable {
|
|
8
|
+
let treeSHA256: String
|
|
9
|
+
let applicationIdentifier: String
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// Cross-runtime release material is ordered by its unsigned UTF-8 bytes.
|
|
13
|
+
///
|
|
14
|
+
/// Swift's native `String` comparison is normalization-aware, while the
|
|
15
|
+
/// artifact tooling deliberately compares the encoded bytes. Keep this in
|
|
16
|
+
/// lockstep with `compareUnsignedUtf8` in `scripts/macos_artifact.ts`.
|
|
17
|
+
enum CanonicalReleaseOrder {
|
|
18
|
+
static func unsignedUTF8Precedes(_ left: String, _ right: String) -> Bool {
|
|
19
|
+
left.utf8.lexicographicallyPrecedes(right.utf8)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static func sorted(_ values: [String]) -> [String] {
|
|
23
|
+
values.sorted(by: unsignedUTF8Precedes)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
enum ReleaseCodeValidator {
|
|
28
|
+
static func validateProtectedComponents(payload: ReleaseEnvelopePayload) throws {
|
|
29
|
+
try validateRegularDigest(
|
|
30
|
+
path: RecordingsUpdateConstants.brokerExecutablePath,
|
|
31
|
+
expected: payload.updateBrokerSHA256
|
|
32
|
+
)
|
|
33
|
+
try validateStaticCode(
|
|
34
|
+
path: RecordingsUpdateConstants.brokerExecutablePath,
|
|
35
|
+
requirementText: payload.updateBrokerDesignatedRequirement,
|
|
36
|
+
expectedTeam: payload.signingTeamIdentifier
|
|
37
|
+
)
|
|
38
|
+
try validateRegularDigest(
|
|
39
|
+
path: RecordingsUpdateConstants.artifactVerifierPath,
|
|
40
|
+
expected: payload.artifactVerifierSHA256
|
|
41
|
+
)
|
|
42
|
+
try validateStaticCode(
|
|
43
|
+
path: RecordingsUpdateConstants.artifactVerifierPath,
|
|
44
|
+
requirementText: payload.artifactVerifierDesignatedRequirement,
|
|
45
|
+
expectedTeam: payload.signingTeamIdentifier
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static func validateCandidate(
|
|
50
|
+
at applicationPath: String,
|
|
51
|
+
payload: ReleaseEnvelopePayload,
|
|
52
|
+
policy: BrokerPolicy
|
|
53
|
+
) throws -> CandidateValidationResult {
|
|
54
|
+
let treeDigest = try CanonicalTree.digest(at: applicationPath)
|
|
55
|
+
guard treeDigest == payload.candidateTreeSHA256 else {
|
|
56
|
+
throw CodeValidationError.treeDigestMismatch
|
|
57
|
+
}
|
|
58
|
+
try validateStaticCode(
|
|
59
|
+
path: applicationPath,
|
|
60
|
+
requirementText: payload.applicationDesignatedRequirement,
|
|
61
|
+
expectedTeam: payload.signingTeamIdentifier
|
|
62
|
+
)
|
|
63
|
+
let clientPath = applicationPath + "/" + RecordingsUpdateConstants.updateClientRelativePath
|
|
64
|
+
try validateRegularDigest(path: clientPath, expected: payload.updateClientSHA256)
|
|
65
|
+
try validateStaticCode(
|
|
66
|
+
path: clientPath,
|
|
67
|
+
requirementText: payload.updateClientDesignatedRequirement,
|
|
68
|
+
expectedTeam: payload.signingTeamIdentifier
|
|
69
|
+
)
|
|
70
|
+
let actualUpdateClientArchitectures = try readArchitectures(clientPath)
|
|
71
|
+
|
|
72
|
+
let informationPath = applicationPath + "/Contents/Info.plist"
|
|
73
|
+
let informationData = try readBoundedRegularFile(path: informationPath, maximumBytes: 1024 * 1024)
|
|
74
|
+
guard let propertyList = try PropertyListSerialization.propertyList(
|
|
75
|
+
from: informationData,
|
|
76
|
+
options: [],
|
|
77
|
+
format: nil
|
|
78
|
+
) as? [String: Any],
|
|
79
|
+
let identifier = propertyList["CFBundleIdentifier"] as? String,
|
|
80
|
+
let shortVersion = propertyList["CFBundleShortVersionString"] as? String,
|
|
81
|
+
let buildVersion = propertyList["CFBundleVersion"] as? String,
|
|
82
|
+
let executable = propertyList["CFBundleExecutable"] as? String,
|
|
83
|
+
let minimumOS = propertyList["LSMinimumSystemVersion"] as? String
|
|
84
|
+
else {
|
|
85
|
+
throw CodeValidationError.invalidApplicationMetadata
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let provenancePath =
|
|
89
|
+
applicationPath + "/Contents/Resources/recordings-build-provenance.json"
|
|
90
|
+
let provenanceData = try readBoundedRegularFile(
|
|
91
|
+
path: provenancePath,
|
|
92
|
+
maximumBytes: 64 * 1024
|
|
93
|
+
)
|
|
94
|
+
let provenance: ReleaseBuildProvenance
|
|
95
|
+
let provenanceObject: [String: Any]
|
|
96
|
+
do {
|
|
97
|
+
provenance = try JSONDecoder().decode(ReleaseBuildProvenance.self, from: provenanceData)
|
|
98
|
+
guard let object = try JSONSerialization.jsonObject(with: provenanceData) as? [String: Any]
|
|
99
|
+
else {
|
|
100
|
+
throw CodeValidationError.invalidBuildProvenance
|
|
101
|
+
}
|
|
102
|
+
provenanceObject = object
|
|
103
|
+
} catch {
|
|
104
|
+
throw CodeValidationError.invalidBuildProvenance
|
|
105
|
+
}
|
|
106
|
+
let expectedProvenanceFields: Set<String> = [
|
|
107
|
+
"schema_version",
|
|
108
|
+
"bundle_id",
|
|
109
|
+
"bundle_version",
|
|
110
|
+
"bundle_build_version",
|
|
111
|
+
"git_sha",
|
|
112
|
+
"architectures",
|
|
113
|
+
"team_id",
|
|
114
|
+
"minimum_macos",
|
|
115
|
+
"companion",
|
|
116
|
+
]
|
|
117
|
+
let expectedCompanionFields: Set<String> = ["version", "sha256", "architectures"]
|
|
118
|
+
guard Set(provenanceObject.keys) == expectedProvenanceFields,
|
|
119
|
+
let companionObject = provenanceObject["companion"] as? [String: Any],
|
|
120
|
+
Set(companionObject.keys) == expectedCompanionFields
|
|
121
|
+
else {
|
|
122
|
+
throw CodeValidationError.invalidBuildProvenance
|
|
123
|
+
}
|
|
124
|
+
let executablePath = applicationPath + "/Contents/MacOS/Recordings"
|
|
125
|
+
let actualArchitectures = try readArchitectures(executablePath)
|
|
126
|
+
let companionPath = applicationPath + "/Contents/Helpers/recordings"
|
|
127
|
+
let actualCompanionSHA256 = try sha256RegularFile(path: companionPath)
|
|
128
|
+
let actualCompanionArchitectures = try readArchitectures(companionPath)
|
|
129
|
+
let localOnlyFieldNames: Set<String> = [
|
|
130
|
+
"artifact_policy",
|
|
131
|
+
"approved_target",
|
|
132
|
+
"approved_target_identity_kind",
|
|
133
|
+
"approved_target_identity_sha256",
|
|
134
|
+
"builder_identity_kind",
|
|
135
|
+
"builder_identity_sha256",
|
|
136
|
+
"non_notarized",
|
|
137
|
+
]
|
|
138
|
+
do {
|
|
139
|
+
try CandidateMetadataPolicy.validate(
|
|
140
|
+
application: CandidateApplicationMetadata(
|
|
141
|
+
bundleIdentifier: identifier,
|
|
142
|
+
shortVersion: shortVersion,
|
|
143
|
+
buildVersion: buildVersion,
|
|
144
|
+
executable: executable,
|
|
145
|
+
minimumOSVersion: minimumOS,
|
|
146
|
+
architectures: actualArchitectures
|
|
147
|
+
),
|
|
148
|
+
updateClientArchitectures: actualUpdateClientArchitectures,
|
|
149
|
+
companion: CandidateCompanionMetadata(
|
|
150
|
+
sha256: actualCompanionSHA256,
|
|
151
|
+
architectures: actualCompanionArchitectures
|
|
152
|
+
),
|
|
153
|
+
provenance: CandidateBuildProvenanceMetadata(
|
|
154
|
+
schemaVersion: provenance.schemaVersion,
|
|
155
|
+
containsLocalOnlyFields:
|
|
156
|
+
!localOnlyFieldNames.isDisjoint(with: Set(provenanceObject.keys)),
|
|
157
|
+
bundleIdentifier: provenance.bundleIdentifier,
|
|
158
|
+
bundleVersion: provenance.bundleVersion,
|
|
159
|
+
bundleBuildVersion: provenance.bundleBuildVersion,
|
|
160
|
+
sourceCommit: provenance.sourceCommit,
|
|
161
|
+
teamIdentifier: provenance.teamIdentifier,
|
|
162
|
+
minimumMacOS: provenance.minimumMacOS,
|
|
163
|
+
architectures: provenance.architectures,
|
|
164
|
+
companionVersion: provenance.companion.version,
|
|
165
|
+
companionSHA256: provenance.companion.sha256,
|
|
166
|
+
companionArchitectures: provenance.companion.architectures
|
|
167
|
+
),
|
|
168
|
+
expected: CandidateReleaseMetadataExpectation(
|
|
169
|
+
applicationIdentifier: policy.applicationIdentifier,
|
|
170
|
+
applicationExecutable: "Recordings",
|
|
171
|
+
version: payload.version,
|
|
172
|
+
build: payload.build,
|
|
173
|
+
sourceCommit: payload.sourceCommit,
|
|
174
|
+
signingTeamIdentifier: payload.signingTeamIdentifier,
|
|
175
|
+
minimumOSVersion: payload.minimumOSVersion,
|
|
176
|
+
architectures: payload.architectures
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
} catch let error as CandidateMetadataPolicyError {
|
|
180
|
+
throw mapMetadataPolicyError(error)
|
|
181
|
+
}
|
|
182
|
+
return CandidateValidationResult(treeSHA256: treeDigest, applicationIdentifier: identifier)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static func validateRegularDigest(path: String, expected: String) throws {
|
|
186
|
+
guard try sha256RegularFile(path: path) == expected else {
|
|
187
|
+
throw CodeValidationError.fileDigestMismatch
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static func sha256RegularFile(
|
|
192
|
+
path: String,
|
|
193
|
+
requireRootOwnedAncestry: Bool = true
|
|
194
|
+
) throws -> String {
|
|
195
|
+
if requireRootOwnedAncestry {
|
|
196
|
+
let parentDirectory = URL(fileURLWithPath: path).deletingLastPathComponent().path
|
|
197
|
+
guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
|
|
198
|
+
to: parentDirectory
|
|
199
|
+
) else {
|
|
200
|
+
throw CodeValidationError.unsafeRegularFile
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
|
|
204
|
+
guard descriptor >= 0 else { throw CodeValidationError.unsafeRegularFile }
|
|
205
|
+
defer { Darwin.close(descriptor) }
|
|
206
|
+
var before = stat()
|
|
207
|
+
guard fstat(descriptor, &before) == 0,
|
|
208
|
+
(before.st_mode & S_IFMT) == S_IFREG,
|
|
209
|
+
before.st_uid == 0,
|
|
210
|
+
(before.st_mode & 0o022) == 0,
|
|
211
|
+
before.st_nlink == 1,
|
|
212
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(descriptor),
|
|
213
|
+
before.st_size >= 0
|
|
214
|
+
else {
|
|
215
|
+
throw CodeValidationError.unsafeRegularFile
|
|
216
|
+
}
|
|
217
|
+
var hasher = SHA256()
|
|
218
|
+
var offset: off_t = 0
|
|
219
|
+
var buffer = [UInt8](repeating: 0, count: 1024 * 1024)
|
|
220
|
+
while offset < before.st_size {
|
|
221
|
+
let requested = min(buffer.count, Int(before.st_size - offset))
|
|
222
|
+
let count = buffer.withUnsafeMutableBytes {
|
|
223
|
+
pread(descriptor, $0.baseAddress, requested, offset)
|
|
224
|
+
}
|
|
225
|
+
if count < 0, errno == EINTR { continue }
|
|
226
|
+
guard count > 0 else { throw CodeValidationError.fileChanged }
|
|
227
|
+
hasher.update(data: Data(buffer.prefix(count)))
|
|
228
|
+
offset += off_t(count)
|
|
229
|
+
}
|
|
230
|
+
var after = stat()
|
|
231
|
+
guard fstat(descriptor, &after) == 0,
|
|
232
|
+
before.st_dev == after.st_dev,
|
|
233
|
+
before.st_ino == after.st_ino,
|
|
234
|
+
before.st_size == after.st_size,
|
|
235
|
+
before.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
|
|
236
|
+
before.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
|
|
237
|
+
before.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
|
|
238
|
+
before.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec,
|
|
239
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
|
|
240
|
+
else {
|
|
241
|
+
throw CodeValidationError.fileChanged
|
|
242
|
+
}
|
|
243
|
+
return hasher.finalize().map { String(format: "%02x", $0) }.joined()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private static func validateStaticCode(
|
|
247
|
+
path: String,
|
|
248
|
+
requirementText: String,
|
|
249
|
+
expectedTeam: String
|
|
250
|
+
) throws {
|
|
251
|
+
let parentDirectory = URL(fileURLWithPath: path).deletingLastPathComponent().path
|
|
252
|
+
guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
|
|
253
|
+
to: parentDirectory
|
|
254
|
+
) else {
|
|
255
|
+
throw CodeValidationError.unsafeTreeEntry
|
|
256
|
+
}
|
|
257
|
+
var pathMetadata = stat()
|
|
258
|
+
guard lstat(path, &pathMetadata) == 0,
|
|
259
|
+
DarwinACLValidator.pathHasNoExtendedACL(
|
|
260
|
+
path,
|
|
261
|
+
directory: (pathMetadata.st_mode & S_IFMT) == S_IFDIR
|
|
262
|
+
)
|
|
263
|
+
else {
|
|
264
|
+
throw CodeValidationError.unsafeTreeEntry
|
|
265
|
+
}
|
|
266
|
+
var staticCode: SecStaticCode?
|
|
267
|
+
let createStatus = SecStaticCodeCreateWithPath(
|
|
268
|
+
URL(fileURLWithPath: path) as CFURL,
|
|
269
|
+
[],
|
|
270
|
+
&staticCode
|
|
271
|
+
)
|
|
272
|
+
guard createStatus == errSecSuccess, let staticCode else {
|
|
273
|
+
throw CodeValidationError.codeIdentityUnavailable(createStatus)
|
|
274
|
+
}
|
|
275
|
+
var requirement: SecRequirement?
|
|
276
|
+
let requirementStatus = SecRequirementCreateWithString(
|
|
277
|
+
requirementText as CFString,
|
|
278
|
+
[],
|
|
279
|
+
&requirement
|
|
280
|
+
)
|
|
281
|
+
guard requirementStatus == errSecSuccess, let requirement else {
|
|
282
|
+
throw CodeValidationError.invalidDesignatedRequirement
|
|
283
|
+
}
|
|
284
|
+
let validityStatus = SecStaticCodeCheckValidity(
|
|
285
|
+
staticCode,
|
|
286
|
+
SecCSFlags(rawValue: kSecCSStrictValidate | kSecCSCheckAllArchitectures | kSecCSCheckNestedCode),
|
|
287
|
+
requirement
|
|
288
|
+
)
|
|
289
|
+
guard validityStatus == errSecSuccess else {
|
|
290
|
+
throw CodeValidationError.codeIdentityRejected(validityStatus)
|
|
291
|
+
}
|
|
292
|
+
var information: CFDictionary?
|
|
293
|
+
let informationStatus = SecCodeCopySigningInformation(
|
|
294
|
+
staticCode,
|
|
295
|
+
SecCSFlags(rawValue: kSecCSSigningInformation),
|
|
296
|
+
&information
|
|
297
|
+
)
|
|
298
|
+
guard informationStatus == errSecSuccess,
|
|
299
|
+
let values = information as? [String: Any],
|
|
300
|
+
values[kSecCodeInfoTeamIdentifier as String] as? String == expectedTeam
|
|
301
|
+
else {
|
|
302
|
+
throw CodeValidationError.codeIdentityRejected(informationStatus)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private static func readArchitectures(_ path: String) throws -> [String] {
|
|
307
|
+
guard DarwinACLValidator.pathHasNoExtendedACL(path, directory: false) else {
|
|
308
|
+
throw CodeValidationError.architectureCheckFailed
|
|
309
|
+
}
|
|
310
|
+
let result: BoundedProcessResult
|
|
311
|
+
do {
|
|
312
|
+
result = try BoundedProcessRunner.run(
|
|
313
|
+
executablePath: "/usr/bin/lipo",
|
|
314
|
+
arguments: ["-archs", path],
|
|
315
|
+
environment: [
|
|
316
|
+
"PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
317
|
+
"HOME": "/var/empty",
|
|
318
|
+
"TMPDIR": "/tmp",
|
|
319
|
+
],
|
|
320
|
+
maximumOutputBytes: 4_096,
|
|
321
|
+
timeout: 5
|
|
322
|
+
)
|
|
323
|
+
} catch {
|
|
324
|
+
throw CodeValidationError.architectureCheckFailed
|
|
325
|
+
}
|
|
326
|
+
guard result.exitedNormally,
|
|
327
|
+
result.terminationStatus == 0,
|
|
328
|
+
result.standardOutput.count <= 4_096,
|
|
329
|
+
let text = String(data: result.standardOutput, encoding: .utf8)
|
|
330
|
+
else {
|
|
331
|
+
throw CodeValidationError.architectureCheckFailed
|
|
332
|
+
}
|
|
333
|
+
let actual = text.split(whereSeparator: \.isWhitespace).map(String.init)
|
|
334
|
+
guard !actual.isEmpty, Set(actual).count == actual.count else {
|
|
335
|
+
throw CodeValidationError.architectureCheckFailed
|
|
336
|
+
}
|
|
337
|
+
return CanonicalReleaseOrder.sorted(actual)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private static func mapMetadataPolicyError(
|
|
341
|
+
_ error: CandidateMetadataPolicyError
|
|
342
|
+
) -> CodeValidationError {
|
|
343
|
+
switch error {
|
|
344
|
+
case .applicationIdentifierMismatch,
|
|
345
|
+
.applicationVersionMismatch,
|
|
346
|
+
.applicationBuildMismatch,
|
|
347
|
+
.applicationExecutableMismatch,
|
|
348
|
+
.applicationMinimumOSMismatch,
|
|
349
|
+
.applicationArchitectureMismatch,
|
|
350
|
+
.updateClientArchitectureMismatch,
|
|
351
|
+
.companionArchitectureMismatch:
|
|
352
|
+
return .invalidApplicationMetadata
|
|
353
|
+
case .provenanceSchemaMismatch,
|
|
354
|
+
.localOnlyProvenanceRejected,
|
|
355
|
+
.provenanceIdentifierMismatch,
|
|
356
|
+
.provenanceVersionMismatch,
|
|
357
|
+
.provenanceBuildMismatch,
|
|
358
|
+
.provenanceSourceMismatch,
|
|
359
|
+
.provenanceTeamMismatch,
|
|
360
|
+
.provenanceMinimumOSMismatch,
|
|
361
|
+
.provenanceArchitectureMismatch,
|
|
362
|
+
.companionDigestMismatch,
|
|
363
|
+
.provenanceCompanionVersionMismatch:
|
|
364
|
+
return .invalidBuildProvenance
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private static func readBoundedRegularFile(path: String, maximumBytes: Int64) throws -> Data {
|
|
369
|
+
let parentDirectory = URL(fileURLWithPath: path).deletingLastPathComponent().path
|
|
370
|
+
guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
|
|
371
|
+
to: parentDirectory
|
|
372
|
+
) else {
|
|
373
|
+
throw CodeValidationError.unsafeRegularFile
|
|
374
|
+
}
|
|
375
|
+
let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
|
|
376
|
+
guard descriptor >= 0 else { throw CodeValidationError.unsafeRegularFile }
|
|
377
|
+
defer { Darwin.close(descriptor) }
|
|
378
|
+
var metadata = stat()
|
|
379
|
+
guard fstat(descriptor, &metadata) == 0,
|
|
380
|
+
(metadata.st_mode & S_IFMT) == S_IFREG,
|
|
381
|
+
metadata.st_uid == 0,
|
|
382
|
+
(metadata.st_mode & 0o022) == 0,
|
|
383
|
+
metadata.st_nlink == 1,
|
|
384
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(descriptor),
|
|
385
|
+
metadata.st_size > 0,
|
|
386
|
+
metadata.st_size <= maximumBytes
|
|
387
|
+
else {
|
|
388
|
+
throw CodeValidationError.unsafeRegularFile
|
|
389
|
+
}
|
|
390
|
+
var data = Data(count: Int(metadata.st_size))
|
|
391
|
+
var offset: off_t = 0
|
|
392
|
+
try data.withUnsafeMutableBytes { bytes in
|
|
393
|
+
guard let base = bytes.baseAddress else { return }
|
|
394
|
+
while offset < metadata.st_size {
|
|
395
|
+
let count = pread(descriptor, base.advanced(by: Int(offset)), Int(metadata.st_size - offset), offset)
|
|
396
|
+
if count < 0, errno == EINTR { continue }
|
|
397
|
+
guard count > 0 else { throw CodeValidationError.fileChanged }
|
|
398
|
+
offset += off_t(count)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
var trailingByte: UInt8 = 0
|
|
402
|
+
guard pread(descriptor, &trailingByte, 1, offset) == 0 else {
|
|
403
|
+
throw CodeValidationError.fileChanged
|
|
404
|
+
}
|
|
405
|
+
var after = stat()
|
|
406
|
+
guard fstat(descriptor, &after) == 0,
|
|
407
|
+
metadata.st_dev == after.st_dev,
|
|
408
|
+
metadata.st_ino == after.st_ino,
|
|
409
|
+
metadata.st_mode == after.st_mode,
|
|
410
|
+
metadata.st_uid == after.st_uid,
|
|
411
|
+
metadata.st_gid == after.st_gid,
|
|
412
|
+
metadata.st_nlink == after.st_nlink,
|
|
413
|
+
metadata.st_size == after.st_size,
|
|
414
|
+
metadata.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
|
|
415
|
+
metadata.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
|
|
416
|
+
metadata.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
|
|
417
|
+
metadata.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec,
|
|
418
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
|
|
419
|
+
else {
|
|
420
|
+
throw CodeValidationError.fileChanged
|
|
421
|
+
}
|
|
422
|
+
return data
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private struct ReleaseBuildProvenance: Codable {
|
|
427
|
+
let schemaVersion: Int
|
|
428
|
+
let bundleIdentifier: String
|
|
429
|
+
let bundleVersion: String
|
|
430
|
+
let bundleBuildVersion: String
|
|
431
|
+
let sourceCommit: String
|
|
432
|
+
let architectures: [String]
|
|
433
|
+
let teamIdentifier: String
|
|
434
|
+
let minimumMacOS: String
|
|
435
|
+
let companion: Companion
|
|
436
|
+
|
|
437
|
+
struct Companion: Codable {
|
|
438
|
+
let version: String
|
|
439
|
+
let sha256: String
|
|
440
|
+
let architectures: [String]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
enum CodingKeys: String, CodingKey {
|
|
444
|
+
case schemaVersion = "schema_version"
|
|
445
|
+
case bundleIdentifier = "bundle_id"
|
|
446
|
+
case bundleVersion = "bundle_version"
|
|
447
|
+
case bundleBuildVersion = "bundle_build_version"
|
|
448
|
+
case sourceCommit = "git_sha"
|
|
449
|
+
case architectures
|
|
450
|
+
case teamIdentifier = "team_id"
|
|
451
|
+
case minimumMacOS = "minimum_macos"
|
|
452
|
+
case companion
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
enum CanonicalTree {
|
|
457
|
+
static func digest(
|
|
458
|
+
at root: String,
|
|
459
|
+
modeOverrides: [String: UInt16] = [:]
|
|
460
|
+
) throws -> String {
|
|
461
|
+
let parent = URL(fileURLWithPath: root).deletingLastPathComponent().path
|
|
462
|
+
guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(to: parent) else {
|
|
463
|
+
throw CodeValidationError.unsafeTreeEntry
|
|
464
|
+
}
|
|
465
|
+
var records: [String] = []
|
|
466
|
+
try collectRecords(
|
|
467
|
+
path: root,
|
|
468
|
+
root: root,
|
|
469
|
+
modeOverrides: modeOverrides,
|
|
470
|
+
records: &records
|
|
471
|
+
)
|
|
472
|
+
return SHA256.hash(data: Data(records.joined(separator: "\n").utf8))
|
|
473
|
+
.map { String(format: "%02x", $0) }
|
|
474
|
+
.joined()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private static func collectRecords(
|
|
478
|
+
path: String,
|
|
479
|
+
root: String,
|
|
480
|
+
modeOverrides: [String: UInt16],
|
|
481
|
+
records: inout [String]
|
|
482
|
+
) throws {
|
|
483
|
+
var metadata = stat()
|
|
484
|
+
guard lstat(path, &metadata) == 0 else { throw CodeValidationError.unsafeTreeEntry }
|
|
485
|
+
let relative = path == root ? "." : String(path.dropFirst(root.count + 1))
|
|
486
|
+
let recordedMode: mode_t
|
|
487
|
+
if let modeOverride = modeOverrides[relative] {
|
|
488
|
+
recordedMode = mode_t(modeOverride)
|
|
489
|
+
} else {
|
|
490
|
+
recordedMode = metadata.st_mode & 0o777
|
|
491
|
+
}
|
|
492
|
+
let mode = String(recordedMode, radix: 8)
|
|
493
|
+
switch metadata.st_mode & S_IFMT {
|
|
494
|
+
case S_IFDIR:
|
|
495
|
+
guard metadata.st_uid == 0,
|
|
496
|
+
(metadata.st_mode & 0o022) == 0,
|
|
497
|
+
DarwinACLValidator.pathHasNoExtendedACL(path, directory: true)
|
|
498
|
+
else {
|
|
499
|
+
throw CodeValidationError.unsafeTreeEntry
|
|
500
|
+
}
|
|
501
|
+
records.append("d\0\(relative)\0\(mode)")
|
|
502
|
+
let children = try FileManager.default.contentsOfDirectory(atPath: path)
|
|
503
|
+
for child in CanonicalReleaseOrder.sorted(children) {
|
|
504
|
+
guard child != ".", child != "..", !child.contains("/") else {
|
|
505
|
+
throw CodeValidationError.unsafeTreeEntry
|
|
506
|
+
}
|
|
507
|
+
try collectRecords(
|
|
508
|
+
path: path + "/" + child,
|
|
509
|
+
root: root,
|
|
510
|
+
modeOverrides: modeOverrides,
|
|
511
|
+
records: &records
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
case S_IFREG:
|
|
515
|
+
guard metadata.st_uid == 0,
|
|
516
|
+
(metadata.st_mode & 0o022) == 0,
|
|
517
|
+
metadata.st_nlink == 1,
|
|
518
|
+
DarwinACLValidator.pathHasNoExtendedACL(path, directory: false)
|
|
519
|
+
else {
|
|
520
|
+
throw CodeValidationError.unsafeTreeEntry
|
|
521
|
+
}
|
|
522
|
+
let digest = try ReleaseCodeValidator.sha256RegularFile(
|
|
523
|
+
path: path,
|
|
524
|
+
requireRootOwnedAncestry: false
|
|
525
|
+
)
|
|
526
|
+
records.append("f\0\(relative)\0\(mode)\0\(metadata.st_size)\0\(digest)")
|
|
527
|
+
default:
|
|
528
|
+
throw CodeValidationError.unsafeTreeEntry
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
enum CodeValidationError: Error, CustomStringConvertible {
|
|
534
|
+
case unsafeRegularFile
|
|
535
|
+
case fileChanged
|
|
536
|
+
case fileDigestMismatch
|
|
537
|
+
case unsafeTreeEntry
|
|
538
|
+
case treeDigestMismatch
|
|
539
|
+
case invalidApplicationMetadata
|
|
540
|
+
case invalidBuildProvenance
|
|
541
|
+
case architectureCheckFailed
|
|
542
|
+
case invalidDesignatedRequirement
|
|
543
|
+
case codeIdentityUnavailable(OSStatus)
|
|
544
|
+
case codeIdentityRejected(OSStatus)
|
|
545
|
+
|
|
546
|
+
var description: String {
|
|
547
|
+
switch self {
|
|
548
|
+
case .unsafeRegularFile: "A release component is not a safe root-owned regular file"
|
|
549
|
+
case .fileChanged: "A release component changed during validation"
|
|
550
|
+
case .fileDigestMismatch: "A release component digest does not match the signed envelope"
|
|
551
|
+
case .unsafeTreeEntry: "The candidate contains a link, special file, unsafe owner, mode, or hard link"
|
|
552
|
+
case .treeDigestMismatch: "The candidate tree digest does not match the signed envelope"
|
|
553
|
+
case .invalidApplicationMetadata: "The candidate application metadata is invalid"
|
|
554
|
+
case .invalidBuildProvenance: "The candidate build provenance does not match the signed envelope"
|
|
555
|
+
case .architectureCheckFailed: "The candidate application architectures do not match the signed envelope"
|
|
556
|
+
case .invalidDesignatedRequirement: "A signed designated requirement could not be compiled"
|
|
557
|
+
case let .codeIdentityUnavailable(status): "A release code identity is unavailable (\(status))"
|
|
558
|
+
case let .codeIdentityRejected(status): "A release code identity was rejected (\(status))"
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Darwin
|
|
2
|
+
import Foundation
|
|
3
|
+
import RecordingsVerifierLauncher
|
|
4
|
+
|
|
5
|
+
enum DarwinACLValidator {
|
|
6
|
+
static func descriptorHasNoExtendedACL(_ descriptor: Int32) -> Bool {
|
|
7
|
+
recordings_descriptor_has_no_extended_acl(descriptor) == 1
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static func pathHasNoExtendedACL(_ path: String, directory: Bool) -> Bool {
|
|
11
|
+
let flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | (directory ? O_DIRECTORY : 0)
|
|
12
|
+
let descriptor = Darwin.open(path, flags)
|
|
13
|
+
guard descriptor >= 0 else { return false }
|
|
14
|
+
defer { Darwin.close(descriptor) }
|
|
15
|
+
return descriptorHasNoExtendedACL(descriptor)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static func descriptorIsSafeRootOwnedDirectory(
|
|
19
|
+
_ descriptor: Int32,
|
|
20
|
+
exactPath: String
|
|
21
|
+
) -> Bool {
|
|
22
|
+
var metadata = stat()
|
|
23
|
+
guard fstat(descriptor, &metadata) == 0,
|
|
24
|
+
(metadata.st_mode & S_IFMT) == S_IFDIR,
|
|
25
|
+
metadata.st_uid == 0,
|
|
26
|
+
descriptorHasNoExtendedACL(descriptor)
|
|
27
|
+
else {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
let permissions = metadata.st_mode & 0o777
|
|
31
|
+
if (permissions & 0o022) == 0 { return true }
|
|
32
|
+
return exactPath == "/Applications" &&
|
|
33
|
+
metadata.st_gid == 80 &&
|
|
34
|
+
permissions == 0o775
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static func rootOwnedDirectoryAncestryHasNoExtendedACL(to directoryPath: String) -> Bool {
|
|
38
|
+
let standardized = URL(fileURLWithPath: directoryPath).standardized.path
|
|
39
|
+
guard standardized.hasPrefix("/") else { return false }
|
|
40
|
+
let flags = O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW
|
|
41
|
+
var descriptor = Darwin.open("/", flags)
|
|
42
|
+
guard descriptor >= 0 else { return false }
|
|
43
|
+
defer { Darwin.close(descriptor) }
|
|
44
|
+
|
|
45
|
+
var currentPath = "/"
|
|
46
|
+
guard descriptorIsSafeRootOwnedDirectory(descriptor, exactPath: currentPath) else {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
let components = URL(fileURLWithPath: standardized).pathComponents.dropFirst()
|
|
50
|
+
for component in components {
|
|
51
|
+
let child = component.withCString { openat(descriptor, $0, flags) }
|
|
52
|
+
currentPath = currentPath == "/"
|
|
53
|
+
? "/" + component
|
|
54
|
+
: currentPath + "/" + component
|
|
55
|
+
guard child >= 0,
|
|
56
|
+
descriptorIsSafeRootOwnedDirectory(child, exactPath: currentPath)
|
|
57
|
+
else {
|
|
58
|
+
if child >= 0 { Darwin.close(child) }
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
Darwin.close(descriptor)
|
|
62
|
+
descriptor = child
|
|
63
|
+
}
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum HostOSProductVersionQueryError: Error {
|
|
4
|
+
case unavailable
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/// Reads the live macOS ProductVersion without a shell, search path, or inherited
|
|
8
|
+
/// environment. Any launch, exit-status, encoding, size, or shape anomaly fails closed.
|
|
9
|
+
enum HostOSProductVersionReader {
|
|
10
|
+
static func read() throws -> String {
|
|
11
|
+
let result: BoundedProcessResult
|
|
12
|
+
do {
|
|
13
|
+
result = try BoundedProcessRunner.run(
|
|
14
|
+
executablePath: "/usr/bin/sw_vers",
|
|
15
|
+
arguments: ["-productVersion"],
|
|
16
|
+
environment: [
|
|
17
|
+
"PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
18
|
+
"HOME": "/var/empty",
|
|
19
|
+
"TMPDIR": "/tmp",
|
|
20
|
+
],
|
|
21
|
+
maximumOutputBytes: 65,
|
|
22
|
+
timeout: 2
|
|
23
|
+
)
|
|
24
|
+
} catch {
|
|
25
|
+
throw HostOSProductVersionQueryError.unavailable
|
|
26
|
+
}
|
|
27
|
+
let data = result.standardOutput
|
|
28
|
+
guard result.exitedNormally,
|
|
29
|
+
result.terminationStatus == 0,
|
|
30
|
+
!data.isEmpty,
|
|
31
|
+
data.count <= 65,
|
|
32
|
+
let text = String(data: data, encoding: .utf8)
|
|
33
|
+
else {
|
|
34
|
+
throw HostOSProductVersionQueryError.unavailable
|
|
35
|
+
}
|
|
36
|
+
let value: String
|
|
37
|
+
if text.hasSuffix("\n") {
|
|
38
|
+
value = String(text.dropLast())
|
|
39
|
+
} else {
|
|
40
|
+
value = text
|
|
41
|
+
}
|
|
42
|
+
guard !value.isEmpty,
|
|
43
|
+
value.utf8.count <= 64,
|
|
44
|
+
!value.contains(where: { $0.isWhitespace || $0.isNewline })
|
|
45
|
+
else {
|
|
46
|
+
throw HostOSProductVersionQueryError.unavailable
|
|
47
|
+
}
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
}
|