zigmini 1.0.0-beta.3 → 1.1.0-beta.1
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.ko.md +27 -2
- package/README.md +27 -2
- package/assets/agent/gateway-request-rules.md +1 -1
- package/assets/agent/gateway-rules.md +6 -1
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/assets/document-reader/document-reader +0 -0
- package/assets/document-reader/host-selector +0 -0
- package/assets/document-reader/manifest.json +27 -0
- package/docs/manual.ko.md +114 -10
- package/docs/manual.md +117 -11
- package/host/document-reader/DocumentRead.swift +370 -0
- package/host/document-reader/HostSelector.swift +192 -0
- package/host/document-reader/Protocol.swift +647 -0
- package/host/document-reader/main.swift +22 -0
- package/host/omo-bridge/agent-aside-tool.ts +119 -0
- package/host/omo-bridge/agent-aside.ts +522 -0
- package/host/omo-bridge/agent-collaboration-files.ts +282 -0
- package/host/omo-bridge/agent-collaboration.ts +385 -0
- package/host/omo-bridge/agent-control.ts +400 -28
- package/host/omo-bridge/agent-execution.sb +42 -0
- package/host/omo-bridge/agent-execution.ts +158 -0
- package/host/omo-bridge/agent-tool-adapters.ts +388 -0
- package/host/omo-bridge/agent-tool-permissions.ts +96 -0
- package/host/omo-bridge/aside-tabs.ts +41 -0
- package/host/omo-bridge/attachment-read.ts +100 -0
- package/host/omo-bridge/bridge.ts +10 -0
- package/host/omo-bridge/managed-call.ts +46 -0
- package/host/omo-bridge/task-control.ts +39 -5
- package/host/omo-check/patches/attachments-293/README.md +138 -0
- package/host/omo-check/patches/attachments-293/document-content.js +234 -0
- package/host/omo-check/patches/attachments-293/document-content.types.txt +91 -0
- package/host/omo-check/patches/attachments-293/omo-5.1.51-documents.patch +831 -0
- package/host/omo-check/patches/attachments-293/omo-5.1.51-gateway-port.manifest.json +471 -0
- package/host/omo-check/patches/omo-5.1.51-agent-tool-authority.patch +471 -0
- package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +28 -6
- package/host/omo-check/patches/omo-5.1.51-memory-receipts.patch +5 -0
- package/package.json +5 -3
- package/src/agent/api.ts +345 -20
- package/src/agent/artifacts.ts +196 -0
- package/src/agent/aside-target-routes.ts +369 -0
- package/src/agent/aside-targets.ts +215 -0
- package/src/agent/collaboration-admission.ts +47 -0
- package/src/agent/collaboration-read.ts +117 -0
- package/src/agent/collaboration-session.ts +196 -0
- package/src/agent/collaboration-types.ts +258 -0
- package/src/agent/collaboration.ts +399 -0
- package/src/agent/definition.ts +65 -3
- package/src/agent/fields.ts +9 -3
- package/src/agent/file-core.ts +325 -0
- package/src/agent/files.ts +87 -8
- package/src/agent/list.ts +3 -3
- package/src/agent/managed-call.ts +144 -0
- package/src/agent/tool-catalog.ts +184 -0
- package/src/agent/tool-delivery.ts +359 -0
- package/src/agent/tool-permission-types.ts +81 -0
- package/src/agent/tool-permissions.ts +1366 -0
- package/src/attachments/api.ts +161 -0
- package/src/attachments/document-reader.ts +325 -0
- package/src/attachments/host-selector.ts +410 -0
- package/src/attachments/managed-binding.ts +83 -0
- package/src/attachments/prepare.ts +214 -0
- package/src/attachments/selector-port.ts +28 -0
- package/src/attachments/service.ts +729 -0
- package/src/attachments/store.ts +698 -0
- package/src/attachments/types.ts +123 -0
- package/src/cli/doctor.ts +30 -10
- package/src/decisions.ts +109 -10
- package/src/engine/apply-patch.ts +21 -0
- package/src/engine/compat.json +2 -1
- package/src/engine/install.ts +45 -4
- package/src/engine/sandbox-cell.ts +2 -0
- package/src/gc.ts +33 -225
- package/src/housekeeping/api.ts +163 -0
- package/src/housekeeping/audit.ts +61 -0
- package/src/housekeeping/collectors/agent-collaboration.ts +33 -0
- package/src/housekeeping/collectors/agent-drafts.ts +43 -0
- package/src/housekeeping/collectors/agent-file-trash.ts +36 -0
- package/src/housekeeping/collectors/agent-trash.ts +28 -0
- package/src/housekeeping/collectors/attachment-cleanup.ts +35 -0
- package/src/housekeeping/collectors/db-backup.ts +25 -0
- package/src/housekeeping/collectors/docker-build-cache.ts +29 -0
- package/src/housekeeping/collectors/docker-image.ts +135 -0
- package/src/housekeeping/collectors/omo-version.ts +32 -0
- package/src/housekeeping/collectors/orphan-test-process.ts +205 -0
- package/src/housekeeping/collectors/reconcile-work.ts +55 -0
- package/src/housekeeping/collectors/rpc-daemon.ts +39 -0
- package/src/housekeeping/collectors/shared.ts +149 -0
- package/src/housekeeping/collectors/temp-dir.ts +28 -0
- package/src/housekeeping/collectors/update-file.ts +25 -0
- package/src/housekeeping/collectors/worktree.ts +193 -0
- package/src/housekeeping/engine.ts +125 -0
- package/src/housekeeping/reconcile/work.ts +130 -0
- package/src/housekeeping/registry.ts +63 -0
- package/src/housekeeping/trash.ts +59 -0
- package/src/housekeeping/types.ts +31 -0
- package/src/hub.ts +1095 -41
- package/src/main.ts +228 -30
- package/src/message-references.ts +14 -5
- package/src/profile-prompt.ts +13 -1
- package/src/protocol.ts +41 -6
- package/src/security-allow.ts +79 -2
- package/src/security-check.ts +1 -1
- package/src/store.ts +1946 -23
- package/src/test-crash.ts +20 -0
- package/src/work-watch.ts +55 -7
- package/src/work.ts +192 -3
- package/web/DESIGN.md +33 -0
- package/web/agents.d.ts +189 -12
- package/web/agents.js +898 -39
- package/web/app.js +2634 -210
- package/web/attachments.d.ts +91 -0
- package/web/attachments.js +422 -0
- package/web/collaboration.d.ts +87 -0
- package/web/collaboration.js +290 -0
- package/web/decisions.d.ts +39 -2
- package/web/decisions.js +354 -2
- package/web/gc.d.ts +26 -5
- package/web/gc.js +113 -13
- package/web/i18n/agents.d.ts +4 -0
- package/web/i18n/agents.js +200 -8
- package/web/i18n/attachments.js +78 -0
- package/web/i18n/decisions.d.ts +4 -0
- package/web/i18n/decisions.js +68 -0
- package/web/i18n/gc.d.ts +4 -0
- package/web/i18n/gc.js +184 -2
- package/web/i18n/main.js +80 -8
- package/web/i18n/tour.d.ts +4 -0
- package/web/i18n/tour.js +37 -0
- package/web/index.html +20 -3
- package/web/profile.d.ts +2 -2
- package/web/profile.js +11 -6
- package/web/style.css +342 -21
- package/web/tooltip.d.ts +2 -0
- package/web/tooltip.js +12 -0
- package/web/tour.d.ts +57 -0
- package/web/tour.js +450 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CryptoKit
|
|
3
|
+
|
|
4
|
+
// SIZE_OK: the T3 shared-file freeze requires the entire native wire seam in this file.
|
|
5
|
+
// No selector, document parser, authorization, sandbox, or process supervisor lives here.
|
|
6
|
+
enum HelperProtocolError: String, Error {
|
|
7
|
+
case invalidJSON = "invalid_json"
|
|
8
|
+
case invalidShape = "invalid_shape"
|
|
9
|
+
case invalidOperation = "invalid_operation"
|
|
10
|
+
case wrongPrivilege = "wrong_privilege"
|
|
11
|
+
case invalidIdentity = "invalid_identity"
|
|
12
|
+
case invalidBounds = "invalid_bounds"
|
|
13
|
+
case invalidBase64 = "invalid_base64"
|
|
14
|
+
case requestMismatch = "request_mismatch"
|
|
15
|
+
case terminalState = "terminal_state"
|
|
16
|
+
case missingTerminal = "missing_terminal"
|
|
17
|
+
case invalidSequence = "invalid_sequence"
|
|
18
|
+
case invalidSnapshot = "invalid_snapshot"
|
|
19
|
+
case invalidCursor = "invalid_cursor"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
enum HelperPrivilege: String {
|
|
23
|
+
case selector, document
|
|
24
|
+
init(argument: String) throws {
|
|
25
|
+
guard let value = Self(rawValue: argument) else { throw HelperProtocolError.wrongPrivilege }
|
|
26
|
+
self = value
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
enum HelperLimits {
|
|
31
|
+
static let originalBytes = 10 * 1024 * 1024
|
|
32
|
+
static let requestBytes = 13_981_016 + 65_536
|
|
33
|
+
static let streamBytes = 16 * 1024 * 1024
|
|
34
|
+
static let frameBytes = 16 * 1024 * 1024
|
|
35
|
+
static let textBytes = 48 * 1024
|
|
36
|
+
static let preparedTextBytes = 20 * 1024 * 1024
|
|
37
|
+
static let imageBytes = 4 * 1024 * 1024
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
struct NativeIdentity: Codable, Equatable {
|
|
41
|
+
let device: String
|
|
42
|
+
let inode: String
|
|
43
|
+
let size: Int
|
|
44
|
+
let mtimeNs: String
|
|
45
|
+
let ctimeNs: String
|
|
46
|
+
let nlink: Int
|
|
47
|
+
|
|
48
|
+
func validate() throws {
|
|
49
|
+
guard let dev = Int32(device), String(dev) == device,
|
|
50
|
+
let ino = UInt64(inode), String(ino) == inode,
|
|
51
|
+
Self.validNanoseconds(mtimeNs), Self.validNanoseconds(ctimeNs),
|
|
52
|
+
(1...HelperLimits.originalBytes).contains(size), nlink == 1 else {
|
|
53
|
+
throw HelperProtocolError.invalidIdentity
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static func validNanoseconds(_ value: String) -> Bool {
|
|
58
|
+
guard HelperCodec.matches(value, #"^(?:0|-?[1-9][0-9]*)$"#) else { return false }
|
|
59
|
+
let negative = value.hasPrefix("-")
|
|
60
|
+
let magnitude = negative ? String(value.dropFirst()) : value
|
|
61
|
+
let maximum = negative ? "9223372036854775808000000000" : "9223372036854775807999999999"
|
|
62
|
+
return magnitude.count < maximum.count || (magnitude.count == maximum.count && magnitude <= maximum)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// time_t * 1e9 must never be computed in Int64 or Double.
|
|
66
|
+
static func nanoseconds(seconds: Int64, nanoseconds: Int) throws -> String {
|
|
67
|
+
guard (0..<1_000_000_000).contains(nanoseconds) else { throw HelperProtocolError.invalidIdentity }
|
|
68
|
+
let negative = seconds < 0
|
|
69
|
+
let magnitude = seconds.magnitude - (negative && nanoseconds > 0 ? 1 : 0)
|
|
70
|
+
let fraction = negative && nanoseconds > 0 ? 1_000_000_000 - nanoseconds : nanoseconds
|
|
71
|
+
let joined = String(magnitude) + String(repeating: "0", count: 9 - String(fraction).count) + String(fraction)
|
|
72
|
+
let trimmed = joined.drop(while: { $0 == "0" })
|
|
73
|
+
return trimmed.isEmpty ? "0" : (negative ? "-" : "") + trimmed
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
enum SelectorOperation: String, Codable { case list = "selector.list", snapshot = "selector.snapshot" }
|
|
78
|
+
enum SelectorRoot: String, Codable { case home, computer, downloads }
|
|
79
|
+
struct SelectorRequest: Codable {
|
|
80
|
+
let version: Int
|
|
81
|
+
let requestId: String
|
|
82
|
+
let operation: SelectorOperation
|
|
83
|
+
let root: SelectorRoot
|
|
84
|
+
let segments: [String]
|
|
85
|
+
let showHidden: Bool?
|
|
86
|
+
let query: String?
|
|
87
|
+
let afterName: String?
|
|
88
|
+
let expectedIdentity: NativeIdentity?
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
enum DocumentOperation: String, Codable {
|
|
92
|
+
case inspect = "document.inspect", text = "document.text", pages = "document.pages", ocr = "document.ocr"
|
|
93
|
+
}
|
|
94
|
+
enum DocumentFormat: String, Codable { case html, md, txt, pdf }
|
|
95
|
+
enum DocumentEncoding: String, Codable {
|
|
96
|
+
case utf8 = "utf-8", utf16le = "utf-16le", utf16be = "utf-16be", eucKR = "euc-kr", cp949
|
|
97
|
+
}
|
|
98
|
+
struct DocumentOptions: Codable {
|
|
99
|
+
let format: DocumentFormat
|
|
100
|
+
let encoding: DocumentEncoding?
|
|
101
|
+
let firstPage: Int?
|
|
102
|
+
let pageLimit: Int?
|
|
103
|
+
let lineLimit: Int?
|
|
104
|
+
let cursor: DocumentTextCursor?
|
|
105
|
+
init(format: DocumentFormat, encoding: DocumentEncoding?, firstPage: Int?, pageLimit: Int?,
|
|
106
|
+
lineLimit: Int?, cursor: DocumentTextCursor? = nil) {
|
|
107
|
+
self.format = format; self.encoding = encoding; self.firstPage = firstPage
|
|
108
|
+
self.pageLimit = pageLimit; self.lineLimit = lineLimit; self.cursor = cursor
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Internal scalar position only. The gateway owns external opaque cursor authorization.
|
|
112
|
+
struct DocumentTextCursor: Codable, Equatable {
|
|
113
|
+
let originalSha256: String
|
|
114
|
+
let representationSha256: String
|
|
115
|
+
let offset: Int
|
|
116
|
+
func validate() throws {
|
|
117
|
+
guard HelperCodec.matches(originalSha256, "^[0-9a-f]{64}$"),
|
|
118
|
+
HelperCodec.matches(representationSha256, "^[0-9a-f]{64}$"),
|
|
119
|
+
(1..<HelperLimits.preparedTextBytes).contains(offset) else { throw HelperProtocolError.invalidCursor }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
struct DocumentTextRange: Codable {
|
|
123
|
+
let start: Int
|
|
124
|
+
let end: Int
|
|
125
|
+
let totalScalars: Int
|
|
126
|
+
let totalBytes: Int
|
|
127
|
+
}
|
|
128
|
+
struct DocumentRequest: Codable {
|
|
129
|
+
let version: Int
|
|
130
|
+
let requestId: String
|
|
131
|
+
let operation: DocumentOperation
|
|
132
|
+
let originalBase64: String
|
|
133
|
+
let options: DocumentOptions
|
|
134
|
+
func originalBytes() throws -> Data {
|
|
135
|
+
try HelperCodec.base64(originalBase64, maximum: HelperLimits.originalBytes)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private enum Discriminator: String, CodingKey { case operation, type }
|
|
140
|
+
enum HelperRequest: Codable {
|
|
141
|
+
case selector(SelectorRequest), document(DocumentRequest)
|
|
142
|
+
var requestId: String {
|
|
143
|
+
switch self { case .selector(let value): return value.requestId; case .document(let value): return value.requestId }
|
|
144
|
+
}
|
|
145
|
+
init(from decoder: Decoder) throws {
|
|
146
|
+
let operation = try decoder.container(keyedBy: Discriminator.self).decode(String.self, forKey: .operation)
|
|
147
|
+
switch operation {
|
|
148
|
+
case "selector.list", "selector.snapshot": self = .selector(try SelectorRequest(from: decoder))
|
|
149
|
+
case "document.inspect", "document.text", "document.pages", "document.ocr": self = .document(try DocumentRequest(from: decoder))
|
|
150
|
+
default: throw HelperProtocolError.invalidOperation
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
func encode(to encoder: Encoder) throws {
|
|
154
|
+
switch self { case .selector(let value): try value.encode(to: encoder); case .document(let value): try value.encode(to: encoder) }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
enum NativeEntryKind: String, Codable { case directory, file, disabled }
|
|
159
|
+
struct NativeEntry: Codable {
|
|
160
|
+
let name: String
|
|
161
|
+
let kind: NativeEntryKind
|
|
162
|
+
let identity: NativeIdentity?
|
|
163
|
+
let selectable: Bool
|
|
164
|
+
let reason: String?
|
|
165
|
+
}
|
|
166
|
+
struct SelectorListed: Codable {
|
|
167
|
+
let requestId: String
|
|
168
|
+
let entries: [NativeEntry]
|
|
169
|
+
let nextName: String?
|
|
170
|
+
}
|
|
171
|
+
struct SnapshotChunk: Codable {
|
|
172
|
+
let requestId: String
|
|
173
|
+
let index: Int
|
|
174
|
+
let base64: String
|
|
175
|
+
}
|
|
176
|
+
struct SnapshotComplete: Codable {
|
|
177
|
+
let requestId: String
|
|
178
|
+
let before: NativeIdentity
|
|
179
|
+
let after: NativeIdentity
|
|
180
|
+
let bytes: Int
|
|
181
|
+
let sha256: String
|
|
182
|
+
}
|
|
183
|
+
enum PageRepresentation: String, Codable {
|
|
184
|
+
case nativeText = "native-text", renderedImage = "rendered-image", ocrText = "ocr-text", verifiedBlank = "verified-blank"
|
|
185
|
+
}
|
|
186
|
+
struct PageCoverage: Codable {
|
|
187
|
+
let page: Int
|
|
188
|
+
let representation: PageRepresentation
|
|
189
|
+
}
|
|
190
|
+
struct DocumentCoverage: Codable {
|
|
191
|
+
let totalPages: Int
|
|
192
|
+
let pages: [PageCoverage]
|
|
193
|
+
}
|
|
194
|
+
enum ImageMIME: String, Codable { case jpeg = "image/jpeg", png = "image/png" }
|
|
195
|
+
struct HelperImage: Codable {
|
|
196
|
+
let data: String
|
|
197
|
+
let mimeType: ImageMIME
|
|
198
|
+
}
|
|
199
|
+
enum HelperContent: Codable {
|
|
200
|
+
case text(String), image(HelperImage)
|
|
201
|
+
private enum Keys: String, CodingKey { case type, text }
|
|
202
|
+
init(from decoder: Decoder) throws {
|
|
203
|
+
let container = try decoder.container(keyedBy: Keys.self)
|
|
204
|
+
switch try container.decode(String.self, forKey: .type) {
|
|
205
|
+
case "text": self = .text(try container.decode(String.self, forKey: .text))
|
|
206
|
+
case "image": self = .image(try HelperImage(from: decoder))
|
|
207
|
+
default: throw HelperProtocolError.invalidShape
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
func encode(to encoder: Encoder) throws {
|
|
211
|
+
var container = encoder.container(keyedBy: Keys.self)
|
|
212
|
+
switch self {
|
|
213
|
+
case .text(let text):
|
|
214
|
+
try container.encode("text", forKey: .type)
|
|
215
|
+
try container.encode(text, forKey: .text)
|
|
216
|
+
case .image(let image):
|
|
217
|
+
try container.encode("image", forKey: .type)
|
|
218
|
+
try image.encode(to: encoder)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
struct DocumentComplete: Codable {
|
|
223
|
+
let requestId: String
|
|
224
|
+
let originalSha256: String
|
|
225
|
+
let representationSha256: String
|
|
226
|
+
let coverage: DocumentCoverage
|
|
227
|
+
let content: [HelperContent]
|
|
228
|
+
let textRange: DocumentTextRange?
|
|
229
|
+
let nextCursor: DocumentTextCursor?
|
|
230
|
+
init(requestId: String, originalSha256: String, representationSha256: String,
|
|
231
|
+
coverage: DocumentCoverage, content: [HelperContent],
|
|
232
|
+
textRange: DocumentTextRange? = nil, nextCursor: DocumentTextCursor? = nil) {
|
|
233
|
+
self.requestId = requestId; self.originalSha256 = originalSha256
|
|
234
|
+
self.representationSha256 = representationSha256; self.coverage = coverage
|
|
235
|
+
self.content = content; self.textRange = textRange; self.nextCursor = nextCursor
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
struct PreparedTextPage: Codable {
|
|
240
|
+
let page: Int
|
|
241
|
+
let representation: PageRepresentation
|
|
242
|
+
let text: String
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Exact canonical per-page text, already produced by T4. This type does not parse or authorize.
|
|
246
|
+
struct PreparedDocumentText {
|
|
247
|
+
private let pages: [PreparedTextPage]
|
|
248
|
+
let bytes: Int
|
|
249
|
+
let scalars: Int
|
|
250
|
+
let sha256: String
|
|
251
|
+
let totalPages: Int
|
|
252
|
+
|
|
253
|
+
init(pages: [PreparedTextPage], totalPages: Int? = nil) throws {
|
|
254
|
+
let total = totalPages ?? pages.count
|
|
255
|
+
guard (1...300).contains(pages.count), (pages.count...300).contains(total) else { throw HelperProtocolError.invalidBounds }
|
|
256
|
+
var bytes = 0, scalars = 0, previous = 0
|
|
257
|
+
var digest = SHA256()
|
|
258
|
+
for page in pages {
|
|
259
|
+
guard page.page > previous, page.page <= total, !page.text.contains("\0"),
|
|
260
|
+
page.representation != .renderedImage,
|
|
261
|
+
page.representation != .verifiedBlank || page.text.isEmpty else { throw HelperProtocolError.invalidShape }
|
|
262
|
+
bytes += page.text.utf8.count
|
|
263
|
+
guard bytes <= HelperLimits.preparedTextBytes else { throw HelperProtocolError.invalidBounds }
|
|
264
|
+
scalars += page.text.unicodeScalars.count
|
|
265
|
+
digest.update(data: Data(page.text.utf8))
|
|
266
|
+
previous = page.page
|
|
267
|
+
}
|
|
268
|
+
self.pages = pages; self.bytes = bytes; self.scalars = scalars; self.totalPages = total
|
|
269
|
+
self.sha256 = digest.finalize().map({ String(format: "%02x", $0) }).joined()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
func frame(for request: DocumentRequest) throws -> HelperFrame {
|
|
273
|
+
try HelperCodec.validate(.document(request))
|
|
274
|
+
guard request.operation == .text || request.operation == .ocr else { throw HelperProtocolError.invalidOperation }
|
|
275
|
+
let original = SHA256.hash(data: try request.originalBytes()).map({ String(format: "%02x", $0) }).joined()
|
|
276
|
+
let start = request.options.cursor?.offset ?? 0
|
|
277
|
+
if let cursor = request.options.cursor {
|
|
278
|
+
guard cursor.originalSha256 == original, cursor.representationSha256 == sha256,
|
|
279
|
+
start < scalars else { throw HelperProtocolError.invalidCursor }
|
|
280
|
+
}
|
|
281
|
+
var text = "", position = 0, end = start, written = 0, lines = 0
|
|
282
|
+
read: for page in pages {
|
|
283
|
+
for scalar in page.text.unicodeScalars {
|
|
284
|
+
defer { position += 1 }
|
|
285
|
+
if position < start { continue }
|
|
286
|
+
let part = String(scalar)
|
|
287
|
+
guard written + part.utf8.count <= HelperLimits.textBytes,
|
|
288
|
+
lines < (request.options.lineLimit ?? 2000) else { break read }
|
|
289
|
+
text.unicodeScalars.append(scalar)
|
|
290
|
+
written += part.utf8.count
|
|
291
|
+
end += 1
|
|
292
|
+
if scalar.value == 10 { lines += 1 }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
var pageStart = 0
|
|
296
|
+
var coverage: [PageCoverage] = []
|
|
297
|
+
for page in pages {
|
|
298
|
+
let pageEnd = pageStart + page.text.unicodeScalars.count
|
|
299
|
+
let intersects = pageStart < end && pageEnd > start
|
|
300
|
+
let emptyAtBoundary = pageStart == pageEnd && pageStart >= start &&
|
|
301
|
+
(pageStart < end || (pageStart == scalars && end == scalars))
|
|
302
|
+
if intersects || emptyAtBoundary {
|
|
303
|
+
coverage.append(PageCoverage(page: page.page, representation: page.representation))
|
|
304
|
+
}
|
|
305
|
+
pageStart = pageEnd
|
|
306
|
+
}
|
|
307
|
+
let next = end < scalars ? DocumentTextCursor(originalSha256: original, representationSha256: sha256, offset: end) : nil
|
|
308
|
+
let result = HelperFrame.documentComplete(DocumentComplete(requestId: request.requestId, originalSha256: original,
|
|
309
|
+
representationSha256: sha256, coverage: DocumentCoverage(totalPages: totalPages, pages: coverage),
|
|
310
|
+
content: [.text(text)], textRange: DocumentTextRange(start: start, end: end, totalScalars: scalars, totalBytes: bytes),
|
|
311
|
+
nextCursor: next))
|
|
312
|
+
var state = try HelperFrameState(request: .document(request))
|
|
313
|
+
try state.accept(result)
|
|
314
|
+
try state.finish()
|
|
315
|
+
return result
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
enum HelperFailureCode: String, Codable {
|
|
319
|
+
case hostAccessDenied = "host_access_denied", fileChanged = "file_changed", invalidPDF = "invalid_pdf"
|
|
320
|
+
case passwordRequired = "password_required", encodingRequired = "encoding_required"
|
|
321
|
+
case unsupportedEncoding = "unsupported_encoding", ocrFailed = "ocr_failed", processingLimit = "processing_limit"
|
|
322
|
+
}
|
|
323
|
+
struct HelperFailure: Codable {
|
|
324
|
+
let requestId: String
|
|
325
|
+
let code: HelperFailureCode
|
|
326
|
+
}
|
|
327
|
+
enum HelperFrame: Codable {
|
|
328
|
+
case listed(SelectorListed), chunk(SnapshotChunk), snapshotComplete(SnapshotComplete)
|
|
329
|
+
case documentComplete(DocumentComplete), error(HelperFailure)
|
|
330
|
+
var requestId: String {
|
|
331
|
+
switch self {
|
|
332
|
+
case .listed(let value): return value.requestId
|
|
333
|
+
case .chunk(let value): return value.requestId
|
|
334
|
+
case .snapshotComplete(let value): return value.requestId
|
|
335
|
+
case .documentComplete(let value): return value.requestId
|
|
336
|
+
case .error(let value): return value.requestId
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
init(from decoder: Decoder) throws {
|
|
340
|
+
let type = try decoder.container(keyedBy: Discriminator.self).decode(String.self, forKey: .type)
|
|
341
|
+
switch type {
|
|
342
|
+
case "selector.listed": self = .listed(try SelectorListed(from: decoder))
|
|
343
|
+
case "chunk": self = .chunk(try SnapshotChunk(from: decoder))
|
|
344
|
+
case "snapshot.complete": self = .snapshotComplete(try SnapshotComplete(from: decoder))
|
|
345
|
+
case "document.complete": self = .documentComplete(try DocumentComplete(from: decoder))
|
|
346
|
+
case "error": self = .error(try HelperFailure(from: decoder))
|
|
347
|
+
default: throw HelperProtocolError.invalidOperation
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
func encode(to encoder: Encoder) throws {
|
|
351
|
+
var container = encoder.container(keyedBy: Discriminator.self)
|
|
352
|
+
switch self {
|
|
353
|
+
case .listed(let value):
|
|
354
|
+
try container.encode("selector.listed", forKey: .type); try value.encode(to: encoder)
|
|
355
|
+
case .chunk(let value):
|
|
356
|
+
try container.encode("chunk", forKey: .type); try value.encode(to: encoder)
|
|
357
|
+
case .snapshotComplete(let value):
|
|
358
|
+
try container.encode("snapshot.complete", forKey: .type); try value.encode(to: encoder)
|
|
359
|
+
case .documentComplete(let value):
|
|
360
|
+
try container.encode("document.complete", forKey: .type); try value.encode(to: encoder)
|
|
361
|
+
case .error(let value):
|
|
362
|
+
try container.encode("error", forKey: .type); try value.encode(to: encoder)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
enum HelperCodec {
|
|
368
|
+
static func matches(_ value: String, _ pattern: String) -> Bool {
|
|
369
|
+
value.range(of: pattern, options: .regularExpression) == value.startIndex..<value.endIndex
|
|
370
|
+
}
|
|
371
|
+
static func base64(_ value: String, maximum: Int) throws -> Data {
|
|
372
|
+
guard value.utf8.count <= ((maximum + 2) / 3) * 4,
|
|
373
|
+
let decoded = Data(base64Encoded: value), !decoded.isEmpty, decoded.count <= maximum,
|
|
374
|
+
decoded.base64EncodedString() == value else { throw HelperProtocolError.invalidBase64 }
|
|
375
|
+
return decoded
|
|
376
|
+
}
|
|
377
|
+
private static func strict<T: Codable>(_ type: T.Type, data: Data) throws -> T {
|
|
378
|
+
guard String(data: data, encoding: .utf8) != nil else { throw HelperProtocolError.invalidJSON }
|
|
379
|
+
let decoded = try JSONDecoder().decode(type, from: data)
|
|
380
|
+
// Foundation Codable ignores unknown keys. Compare JSON values (not spelling)
|
|
381
|
+
// against its exact encoded shape, recursively, to reject extras and null optionals.
|
|
382
|
+
guard let original = try JSONSerialization.jsonObject(with: data) as? NSDictionary,
|
|
383
|
+
let normalized = try JSONSerialization.jsonObject(with: JSONEncoder().encode(decoded)) as? NSDictionary,
|
|
384
|
+
original.isEqual(normalized) else { throw HelperProtocolError.invalidShape }
|
|
385
|
+
return decoded
|
|
386
|
+
}
|
|
387
|
+
static func validate(_ request: HelperRequest) throws {
|
|
388
|
+
guard matches(request.requestId, #"^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$"#) else {
|
|
389
|
+
throw HelperProtocolError.invalidShape
|
|
390
|
+
}
|
|
391
|
+
switch request {
|
|
392
|
+
case .selector(let value):
|
|
393
|
+
guard value.version == 1, value.segments.count <= 256,
|
|
394
|
+
value.segments.allSatisfy({ !$0.isEmpty && $0 != "." && $0 != ".." && !$0.contains("/") && !$0.contains("\0") }),
|
|
395
|
+
value.query.map({ $0.unicodeScalars.count <= 200 }) ?? true,
|
|
396
|
+
value.afterName.map({ !$0.isEmpty }) ?? true else { throw HelperProtocolError.invalidBounds }
|
|
397
|
+
switch value.operation {
|
|
398
|
+
case .list:
|
|
399
|
+
guard value.showHidden != nil, value.expectedIdentity == nil else { throw HelperProtocolError.invalidShape }
|
|
400
|
+
case .snapshot:
|
|
401
|
+
guard let identity = value.expectedIdentity, value.showHidden == nil,
|
|
402
|
+
value.query == nil, value.afterName == nil else { throw HelperProtocolError.invalidShape }
|
|
403
|
+
try identity.validate()
|
|
404
|
+
}
|
|
405
|
+
case .document(let value):
|
|
406
|
+
guard value.version == 1,
|
|
407
|
+
value.options.firstPage.map({ (1...300).contains($0) }) ?? true,
|
|
408
|
+
value.options.pageLimit.map({ (1...2).contains($0) }) ?? true,
|
|
409
|
+
value.options.lineLimit.map({ (1...2000).contains($0) }) ?? true else { throw HelperProtocolError.invalidBounds }
|
|
410
|
+
_ = try value.originalBytes()
|
|
411
|
+
if let cursor = value.options.cursor {
|
|
412
|
+
guard value.operation == .text || value.operation == .ocr else { throw HelperProtocolError.invalidCursor }
|
|
413
|
+
try cursor.validate()
|
|
414
|
+
let original = SHA256.hash(data: try value.originalBytes()).map({ String(format: "%02x", $0) }).joined()
|
|
415
|
+
guard cursor.originalSha256 == original else { throw HelperProtocolError.invalidCursor }
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
static func decodeRequest(_ data: Data, privilege: HelperPrivilege) throws -> HelperRequest {
|
|
420
|
+
guard data.count <= HelperLimits.requestBytes else { throw HelperProtocolError.invalidBounds }
|
|
421
|
+
let request = try strict(HelperRequest.self, data: data)
|
|
422
|
+
switch (request, privilege) {
|
|
423
|
+
case (.selector, .selector), (.document, .document): break
|
|
424
|
+
default: throw HelperProtocolError.wrongPrivilege
|
|
425
|
+
}
|
|
426
|
+
try validate(request)
|
|
427
|
+
return request
|
|
428
|
+
}
|
|
429
|
+
static func encodeRequest(_ request: HelperRequest) throws -> Data {
|
|
430
|
+
try validate(request)
|
|
431
|
+
let data = try JSONEncoder().encode(request)
|
|
432
|
+
guard data.count <= HelperLimits.requestBytes else { throw HelperProtocolError.invalidBounds }
|
|
433
|
+
return data
|
|
434
|
+
}
|
|
435
|
+
static func readRequest(from input: FileHandle, privilege: HelperPrivilege) throws -> HelperRequest {
|
|
436
|
+
var data = Data()
|
|
437
|
+
while let part = try input.read(upToCount: min(65_536, HelperLimits.requestBytes + 1 - data.count)), !part.isEmpty {
|
|
438
|
+
data.append(part)
|
|
439
|
+
guard data.count <= HelperLimits.requestBytes else { throw HelperProtocolError.invalidBounds }
|
|
440
|
+
}
|
|
441
|
+
return try decodeRequest(data, privilege: privilege)
|
|
442
|
+
}
|
|
443
|
+
static func decodeFrame(_ data: Data) throws -> HelperFrame {
|
|
444
|
+
guard data.count <= HelperLimits.frameBytes else { throw HelperProtocolError.invalidBounds }
|
|
445
|
+
let frame = try strict(HelperFrame.self, data: data)
|
|
446
|
+
try validate(frame)
|
|
447
|
+
return frame
|
|
448
|
+
}
|
|
449
|
+
static func encodeFrame(_ frame: HelperFrame) throws -> Data {
|
|
450
|
+
try validate(frame)
|
|
451
|
+
var data = try JSONEncoder().encode(frame)
|
|
452
|
+
guard data.count <= HelperLimits.frameBytes else { throw HelperProtocolError.invalidBounds }
|
|
453
|
+
data.append(10)
|
|
454
|
+
return data
|
|
455
|
+
}
|
|
456
|
+
static func validate(_ frame: HelperFrame) throws {
|
|
457
|
+
guard matches(frame.requestId, #"^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$"#) else {
|
|
458
|
+
throw HelperProtocolError.invalidShape
|
|
459
|
+
}
|
|
460
|
+
switch frame {
|
|
461
|
+
case .listed(let value):
|
|
462
|
+
guard value.entries.count <= 50, value.nextName.map({ !$0.isEmpty }) ?? true else { throw HelperProtocolError.invalidBounds }
|
|
463
|
+
for entry in value.entries {
|
|
464
|
+
guard !entry.name.isEmpty, entry.reason.map({ !$0.isEmpty }) ?? true else { throw HelperProtocolError.invalidShape }
|
|
465
|
+
try entry.identity?.validate()
|
|
466
|
+
}
|
|
467
|
+
case .chunk(let value):
|
|
468
|
+
guard (0...HelperLimits.originalBytes).contains(value.index) else { throw HelperProtocolError.invalidBounds }
|
|
469
|
+
_ = try base64(value.base64, maximum: HelperLimits.originalBytes)
|
|
470
|
+
case .snapshotComplete(let value):
|
|
471
|
+
try value.before.validate(); try value.after.validate()
|
|
472
|
+
guard (1...HelperLimits.originalBytes).contains(value.bytes), matches(value.sha256, "^[0-9a-f]{64}$") else {
|
|
473
|
+
throw HelperProtocolError.invalidBounds
|
|
474
|
+
}
|
|
475
|
+
case .documentComplete(let value):
|
|
476
|
+
guard matches(value.originalSha256, "^[0-9a-f]{64}$"), matches(value.representationSha256, "^[0-9a-f]{64}$"),
|
|
477
|
+
(1...300).contains(value.coverage.totalPages), value.coverage.pages.count <= 300,
|
|
478
|
+
value.coverage.pages.allSatisfy({ (1...value.coverage.totalPages).contains($0.page) }),
|
|
479
|
+
value.content.count <= 2000 else { throw HelperProtocolError.invalidBounds }
|
|
480
|
+
var textBytes = 0
|
|
481
|
+
var images = 0
|
|
482
|
+
for content in value.content {
|
|
483
|
+
switch content {
|
|
484
|
+
case .text(let text): textBytes += text.utf8.count
|
|
485
|
+
case .image(let image):
|
|
486
|
+
images += 1
|
|
487
|
+
_ = try base64(image.data, maximum: HelperLimits.imageBytes)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
guard textBytes <= HelperLimits.textBytes, images <= 2 else { throw HelperProtocolError.invalidBounds }
|
|
491
|
+
if let range = value.textRange {
|
|
492
|
+
guard range.start >= 0, range.end >= range.start, range.end <= range.totalScalars,
|
|
493
|
+
range.totalScalars <= range.totalBytes, range.totalBytes <= HelperLimits.preparedTextBytes,
|
|
494
|
+
range.totalBytes <= range.totalScalars * 4,
|
|
495
|
+
range.end > range.start || range.totalScalars == 0,
|
|
496
|
+
value.content.count == 1, case .text(let text) = value.content[0],
|
|
497
|
+
text.unicodeScalars.count == range.end - range.start,
|
|
498
|
+
text.utf8.count <= range.totalBytes else { throw HelperProtocolError.invalidBounds }
|
|
499
|
+
let remainingScalars = range.totalScalars - (range.end - range.start)
|
|
500
|
+
let remainingBytes = range.totalBytes - textBytes
|
|
501
|
+
guard remainingBytes >= remainingScalars,
|
|
502
|
+
remainingBytes <= remainingScalars * 4 else { throw HelperProtocolError.invalidBounds }
|
|
503
|
+
if range.end < range.totalScalars {
|
|
504
|
+
guard let cursor = value.nextCursor, cursor.offset == range.end,
|
|
505
|
+
cursor.originalSha256 == value.originalSha256,
|
|
506
|
+
cursor.representationSha256 == value.representationSha256 else { throw HelperProtocolError.invalidCursor }
|
|
507
|
+
try cursor.validate()
|
|
508
|
+
} else {
|
|
509
|
+
guard value.nextCursor == nil else { throw HelperProtocolError.invalidCursor }
|
|
510
|
+
}
|
|
511
|
+
if range.start == 0 && range.end == range.totalScalars {
|
|
512
|
+
let digest = SHA256.hash(data: Data(text.utf8)).map({ String(format: "%02x", $0) }).joined()
|
|
513
|
+
guard value.representationSha256 == digest, text.utf8.count == range.totalBytes else {
|
|
514
|
+
throw HelperProtocolError.invalidSnapshot
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} else if value.nextCursor != nil { throw HelperProtocolError.invalidCursor }
|
|
518
|
+
case .error: break
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
static func readFrames(from input: FileHandle, request: HelperRequest, emit: (HelperFrame) throws -> Void) throws {
|
|
522
|
+
var state = try HelperFrameState(request: request)
|
|
523
|
+
var line = Data()
|
|
524
|
+
var total = 0
|
|
525
|
+
while let part = try input.read(upToCount: 65_536), !part.isEmpty {
|
|
526
|
+
total += part.count
|
|
527
|
+
guard total <= HelperLimits.streamBytes else { throw HelperProtocolError.invalidBounds }
|
|
528
|
+
for byte in part {
|
|
529
|
+
if byte == 10 {
|
|
530
|
+
let frame = try decodeFrame(line)
|
|
531
|
+
try state.accept(frame)
|
|
532
|
+
try emit(frame)
|
|
533
|
+
line.removeAll(keepingCapacity: true)
|
|
534
|
+
} else {
|
|
535
|
+
guard line.count < HelperLimits.frameBytes else { throw HelperProtocolError.invalidBounds }
|
|
536
|
+
line.append(byte)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
guard line.isEmpty else { throw HelperProtocolError.invalidJSON }
|
|
541
|
+
try state.finish()
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
struct HelperFrameState {
|
|
546
|
+
private let request: HelperRequest
|
|
547
|
+
private var terminal = false
|
|
548
|
+
private var nextIndex = 0
|
|
549
|
+
private var bytes = 0
|
|
550
|
+
private var digest = SHA256()
|
|
551
|
+
|
|
552
|
+
init(request: HelperRequest) throws {
|
|
553
|
+
try HelperCodec.validate(request)
|
|
554
|
+
self.request = request
|
|
555
|
+
}
|
|
556
|
+
mutating func accept(_ frame: HelperFrame) throws {
|
|
557
|
+
guard !terminal else { throw HelperProtocolError.terminalState }
|
|
558
|
+
guard frame.requestId == request.requestId else { throw HelperProtocolError.requestMismatch }
|
|
559
|
+
try HelperCodec.validate(frame)
|
|
560
|
+
switch (request, frame) {
|
|
561
|
+
case (_, .error): terminal = true
|
|
562
|
+
case (.selector(let selection), .listed):
|
|
563
|
+
guard selection.operation == .list else { throw HelperProtocolError.invalidSequence }
|
|
564
|
+
terminal = true
|
|
565
|
+
case (.selector(let selection), .chunk(let chunk)):
|
|
566
|
+
guard selection.operation == .snapshot, chunk.index == nextIndex else { throw HelperProtocolError.invalidSequence }
|
|
567
|
+
let data = try HelperCodec.base64(chunk.base64, maximum: HelperLimits.originalBytes)
|
|
568
|
+
guard bytes + data.count <= HelperLimits.originalBytes else { throw HelperProtocolError.invalidBounds }
|
|
569
|
+
bytes += data.count
|
|
570
|
+
nextIndex += 1
|
|
571
|
+
digest.update(data: data)
|
|
572
|
+
case (.selector(let selection), .snapshotComplete(let value)):
|
|
573
|
+
guard selection.operation == .snapshot, nextIndex > 0, bytes == value.bytes,
|
|
574
|
+
value.before == selection.expectedIdentity, value.after == value.before,
|
|
575
|
+
value.bytes == value.before.size,
|
|
576
|
+
value.sha256 == digest.finalize().map({ String(format: "%02x", $0) }).joined() else {
|
|
577
|
+
throw HelperProtocolError.invalidSnapshot
|
|
578
|
+
}
|
|
579
|
+
terminal = true
|
|
580
|
+
case (.document(let document), .documentComplete(let value)):
|
|
581
|
+
let originalDigest = SHA256.hash(data: try document.originalBytes()).map({ String(format: "%02x", $0) }).joined()
|
|
582
|
+
guard value.originalSha256 == originalDigest else { throw HelperProtocolError.invalidSnapshot }
|
|
583
|
+
var lines = 0
|
|
584
|
+
for content in value.content {
|
|
585
|
+
if case .text(let text) = content, !text.isEmpty {
|
|
586
|
+
lines += text.utf8.filter({ $0 == 10 }).count + (text.hasSuffix("\n") ? 0 : 1)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
guard lines <= (document.options.lineLimit ?? 2000) else { throw HelperProtocolError.invalidBounds }
|
|
590
|
+
if document.operation == .text || document.operation == .ocr {
|
|
591
|
+
guard let range = value.textRange, range.start == (document.options.cursor?.offset ?? 0) else {
|
|
592
|
+
throw HelperProtocolError.invalidCursor
|
|
593
|
+
}
|
|
594
|
+
if let cursor = document.options.cursor {
|
|
595
|
+
guard value.representationSha256 == cursor.representationSha256 else { throw HelperProtocolError.invalidCursor }
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
guard value.textRange == nil, value.nextCursor == nil else { throw HelperProtocolError.invalidCursor }
|
|
599
|
+
}
|
|
600
|
+
if document.operation == .pages {
|
|
601
|
+
let first = document.options.firstPage ?? 1
|
|
602
|
+
let end = min(first + (document.options.pageLimit ?? 2), value.coverage.totalPages + 1)
|
|
603
|
+
guard first < end, Set(value.coverage.pages.map(\.page)) == Set(first..<end) else {
|
|
604
|
+
throw HelperProtocolError.invalidBounds
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
terminal = true
|
|
608
|
+
default: throw HelperProtocolError.invalidSequence
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
func finish() throws {
|
|
612
|
+
guard terminal else { throw HelperProtocolError.missingTerminal }
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Entrypoints select their privilege before reading stdin and pass writer.emit to
|
|
617
|
+
// HostSelector.handle(_:emit:) OR DocumentRead.handle(_:emit:), never a fallback.
|
|
618
|
+
// They must call finish() and require normal child exit before publishing success.
|
|
619
|
+
final class HelperFrameWriter {
|
|
620
|
+
private var state: HelperFrameState
|
|
621
|
+
private let output: FileHandle
|
|
622
|
+
private var written = 0
|
|
623
|
+
private var failed = false
|
|
624
|
+
init(request: HelperRequest, output: FileHandle) throws {
|
|
625
|
+
state = try HelperFrameState(request: request)
|
|
626
|
+
self.output = output
|
|
627
|
+
}
|
|
628
|
+
func emit(_ frame: HelperFrame) throws {
|
|
629
|
+
guard !failed else { throw HelperProtocolError.terminalState }
|
|
630
|
+
do {
|
|
631
|
+
var next = state
|
|
632
|
+
try next.accept(frame)
|
|
633
|
+
let data = try HelperCodec.encodeFrame(frame)
|
|
634
|
+
guard written + data.count <= HelperLimits.streamBytes else { throw HelperProtocolError.invalidBounds }
|
|
635
|
+
try output.write(contentsOf: data)
|
|
636
|
+
state = next
|
|
637
|
+
written += data.count
|
|
638
|
+
} catch {
|
|
639
|
+
failed = true
|
|
640
|
+
throw error
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
func finish() throws {
|
|
644
|
+
guard !failed else { throw HelperProtocolError.terminalState }
|
|
645
|
+
try state.finish()
|
|
646
|
+
}
|
|
647
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Darwin
|
|
3
|
+
|
|
4
|
+
// Selector-only executable. DocumentRead and PDFKit/Vision are not linked here.
|
|
5
|
+
signal(SIGPIPE, SIG_IGN)
|
|
6
|
+
do {
|
|
7
|
+
let request = try HelperCodec.readRequest(from: .standardInput, privilege: .selector)
|
|
8
|
+
guard case .selector(let selector) = request else { throw HelperProtocolError.wrongPrivilege }
|
|
9
|
+
let writer = try HelperFrameWriter(request: request, output: .standardOutput)
|
|
10
|
+
do {
|
|
11
|
+
try HostSelector.handle(selector, emit: writer.emit)
|
|
12
|
+
try writer.finish()
|
|
13
|
+
} catch let failure as SelectorFailure {
|
|
14
|
+
try writer.emit(.error(HelperFailure(requestId: selector.requestId, code: failure.code)))
|
|
15
|
+
try writer.finish()
|
|
16
|
+
exit(1)
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// Never print private paths, OS error text, or input on protocol failure.
|
|
20
|
+
try? FileHandle.standardError.write(contentsOf: Data("selector_failed\n".utf8))
|
|
21
|
+
exit(1)
|
|
22
|
+
}
|