@runeditrun/rer 0.2.1 → 0.2.4
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 +21 -4
- package/cli/README.md +21 -0
- package/cli/auth-copy.mjs +25 -18
- package/cli/bounded.mjs +13 -4
- package/cli/container.mjs +11 -10
- package/cli/contract.mjs +2 -2
- package/cli/demo-host.mjs +47 -0
- package/cli/demo.mjs +47 -0
- package/cli/dependencies.mjs +26 -4
- package/cli/deploy-command.mjs +94 -0
- package/cli/deploy-inputs.mjs +55 -0
- package/cli/deploy-observation.mjs +28 -0
- package/cli/deploy-owner.mjs +76 -0
- package/cli/deploy-readiness.mjs +16 -0
- package/cli/deploy.mjs +99 -17
- package/cli/detached-server.mjs +1 -1
- package/cli/door.mjs +1 -1
- package/cli/export.mjs +1 -1
- package/cli/hosted-output.mjs +2 -1
- package/cli/inspect.mjs +24 -32
- package/cli/lifecycle.mjs +31 -8
- package/cli/local.mjs +7 -0
- package/cli/main.mjs +1 -1
- package/cli/managed-server.mjs +27 -18
- package/cli/parse.mjs +9 -2
- package/cli/pnpm-probe.mjs +17 -0
- package/cli/pnpm-runtime.mjs +22 -22
- package/cli/portability-policy.mjs +55 -0
- package/cli/portability-worker.mjs +8 -4
- package/cli/portability.mjs +24 -5
- package/cli/project-pnpm.mjs +67 -0
- package/cli/release.mjs +35 -0
- package/cli/scheduled.mjs +2 -2
- package/cli/server-registry.mjs +2 -2
- package/cli/setup-input.mjs +1 -1
- package/cli/site-check.mjs +15 -6
- package/cli/site-data.mjs +6 -5
- package/cli/site-references.mjs +153 -3
- package/global/CHANGELOG.md +60 -0
- package/global/README.md +21 -4
- package/global/cli/install.mjs +26 -9
- package/global/cli/path-link.mjs +30 -0
- package/global/cli/status.mjs +1 -1
- package/global/cli/uninstall.mjs +5 -1
- package/global/launch/capacity.mjs +1 -1
- package/global/launch/facts.mjs +12 -6
- package/global/launch/install-pinned.mjs +30 -2
- package/global/launch/prepare.mjs +20 -8
- package/global/launch/state.mjs +54 -1
- package/global/main.mjs +43 -20
- package/global/registry.mjs +2 -2
- package/global/update/command.mjs +2 -1
- package/modules/container/build.mjs +4 -2
- package/modules/container/deploy-input.mjs +5 -9
- package/modules/container/host.mjs +1 -1
- package/modules/container/ingress.mjs +4 -1
- package/modules/container/migrations.mjs +2 -1
- package/modules/container/worker-engine.mjs +3 -1
- package/modules/demo-host/README.md +30 -0
- package/modules/demo-host/capacity.mjs +19 -0
- package/modules/demo-host/claim.mjs +21 -0
- package/modules/demo-host/container.mjs +74 -0
- package/modules/demo-host/pages.mjs +56 -0
- package/modules/demo-host/plan.mjs +52 -0
- package/modules/demo-host/worker.mjs +72 -0
- package/modules/dependencies/auth.mjs +2 -1
- package/modules/deploy-kit/README.md +52 -7
- package/modules/deploy-kit/d1-import.mjs +35 -0
- package/modules/deploy-kit/deploy.mjs +4 -3
- package/modules/deploy-kit/diagnostics.mjs +11 -1
- package/modules/deploy-kit/env-file.mjs +27 -0
- package/modules/deploy-kit/health-request.mjs +13 -3
- package/modules/deploy-kit/health.mjs +35 -15
- package/modules/deploy-kit/migration-ledger.mjs +85 -0
- package/modules/deploy-kit/migrations.mjs +48 -16
- package/modules/deploy-kit/owner-cli.mjs +42 -8
- package/modules/deploy-kit/owner-remove.mjs +108 -0
- package/modules/deploy-kit/owner-secret.mjs +94 -0
- package/modules/deploy-kit/owner.mjs +124 -19
- package/modules/deploy-kit/prepare.mjs +3 -1
- package/modules/deploy-kit/provider.mjs +18 -2
- package/modules/erasure/README.md +2 -3
- package/modules/job-queue/config.mjs +4 -2
- package/modules/operator-auth/accounts/settings-page.mjs +8 -3
- package/modules/operator-auth/csp.mjs +26 -0
- package/modules/operator-auth/dev/badge.mjs +1 -22
- package/modules/operator-auth/identity/oidc/README.md +18 -0
- package/modules/operator-auth/identity/oidc/config.mjs +4 -2
- package/modules/operator-auth/identity/oidc/index.d.mts +1 -1
- package/modules/operator-auth/identity/oidc/index.mjs +28 -9
- package/modules/operator-auth/identity/oidc/network.mjs +9 -4
- package/modules/operator-auth/identity/oidc/store.mjs +19 -3
- package/modules/operator-auth/index.d.mts +2 -0
- package/modules/operator-auth/page.mjs +2 -2
- package/modules/operator-auth/routes.mjs +2 -2
- package/modules/portability/README.md +37 -0
- package/modules/portability/bridge.mjs +14 -4
- package/modules/portability/catalogue.mjs +9 -2
- package/modules/portability/import.mjs +4 -2
- package/modules/portability/index.d.mts +4 -0
- package/modules/portability/index.mjs +1 -0
- package/modules/portability/lines.mjs +3 -1
- package/modules/release/produce.mjs +1 -1
- package/modules/release/source.mjs +5 -2
- package/onboarding/adapter-labels.mjs +1 -0
- package/onboarding/adapter.mjs +9 -4
- package/onboarding/commands.mjs +3 -1
- package/onboarding/dependency-environment.mjs +33 -0
- package/onboarding/local/owner.mjs +1 -1
- package/onboarding/operator-installer.mjs +10 -4
- package/onboarding/redaction.mjs +22 -3
- package/package.json +6 -1
- package/scripts/publish-check/README.md +22 -1
- package/scripts/publish-check/c2pa.py +121 -0
- package/scripts/publish-check/content_identities.py +3 -1
- package/scripts/publish-check/controller_metadata.py +31 -0
- package/scripts/publish-check/history_scan.py +13 -3
- package/scripts/publish-check/media.py +283 -0
- package/scripts/publish-check/png_text.py +34 -0
- package/scripts/publish-check/public_hygiene.py +39 -4
- package/scripts/publish-check/publication-config.json +7 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Find the pixel and sample data inside an image or video file.
|
|
2
|
+
|
|
3
|
+
A file is media only when its bytes walk as one complete container, from the
|
|
4
|
+
signature to the end of the file, allowing only NUL padding after it. The path
|
|
5
|
+
and extension never decide, so text renamed .gif, or a valid image with text
|
|
6
|
+
appended, is not media. Each walker returns where its container ends and the
|
|
7
|
+
(start, end) spans of its pixel or sample data (compressed samples and colour
|
|
8
|
+
tables), or None. Everything outside those spans, such as comments, XMP and
|
|
9
|
+
tags, is still text to inspect.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def gif(data):
|
|
14
|
+
if data[:6] not in (b'GIF87a', b'GIF89a') or len(data) < 13:
|
|
15
|
+
return None
|
|
16
|
+
# Colour tables are pixel data too: palette bytes can spell an address.
|
|
17
|
+
offset = 13 + ((3 << ((data[10] & 7) + 1)) if data[10] & 0x80 else 0)
|
|
18
|
+
samples = [(13, offset)]
|
|
19
|
+
while offset < len(data):
|
|
20
|
+
kind = data[offset]
|
|
21
|
+
if kind == 0x3B:
|
|
22
|
+
return offset + 1, samples
|
|
23
|
+
if kind == 0x21:
|
|
24
|
+
offset += 2
|
|
25
|
+
elif kind == 0x2C:
|
|
26
|
+
if offset + 10 > len(data):
|
|
27
|
+
return None
|
|
28
|
+
flags = data[offset + 9]
|
|
29
|
+
# Descriptor, local colour table, then the LZW minimum code size.
|
|
30
|
+
table = offset + 10 + ((3 << ((flags & 7) + 1)) if flags & 0x80 else 0)
|
|
31
|
+
samples.append((offset + 10, table))
|
|
32
|
+
offset = table + 1
|
|
33
|
+
else:
|
|
34
|
+
return None
|
|
35
|
+
# Data sub-blocks: a length byte, that many bytes, ending at length zero.
|
|
36
|
+
start = offset
|
|
37
|
+
while True:
|
|
38
|
+
if offset >= len(data):
|
|
39
|
+
return None
|
|
40
|
+
size = data[offset]
|
|
41
|
+
offset += 1 + size
|
|
42
|
+
if not size:
|
|
43
|
+
break
|
|
44
|
+
if kind == 0x2C:
|
|
45
|
+
samples.append((start, offset))
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def png(data):
|
|
50
|
+
if not data.startswith(b'\x89PNG\r\n\x1a\n'):
|
|
51
|
+
return None
|
|
52
|
+
offset = 8
|
|
53
|
+
samples = []
|
|
54
|
+
while offset + 12 <= len(data):
|
|
55
|
+
length = int.from_bytes(data[offset:offset + 4], 'big')
|
|
56
|
+
kind = data[offset + 4:offset + 8]
|
|
57
|
+
if kind in (b'PLTE', b'IDAT', b'fdAT'):
|
|
58
|
+
samples.append((offset + 8, offset + 8 + length))
|
|
59
|
+
offset += 12 + length
|
|
60
|
+
if kind == b'IEND':
|
|
61
|
+
return offset, samples
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def jpeg(data):
|
|
66
|
+
# A phone stores more images after the first one's end (MPF depth and
|
|
67
|
+
# gain maps): each is walked, and their samples are pixels too.
|
|
68
|
+
walked = jpeg_image(data, 0)
|
|
69
|
+
if walked is None:
|
|
70
|
+
return None
|
|
71
|
+
end, samples = walked
|
|
72
|
+
while data.startswith(b'\xff\xd8', end) and (following := jpeg_image(data, end)):
|
|
73
|
+
end, more = following
|
|
74
|
+
samples += more
|
|
75
|
+
return end, samples
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def jpeg_image(data, offset):
|
|
79
|
+
if not data.startswith(b'\xff\xd8', offset):
|
|
80
|
+
return None
|
|
81
|
+
offset += 2
|
|
82
|
+
samples = []
|
|
83
|
+
while offset + 2 <= len(data):
|
|
84
|
+
if data[offset] != 0xFF:
|
|
85
|
+
return None
|
|
86
|
+
marker = data[offset + 1]
|
|
87
|
+
if marker == 0xFF:
|
|
88
|
+
offset += 1
|
|
89
|
+
continue
|
|
90
|
+
if marker == 0xD9:
|
|
91
|
+
return offset + 2, samples
|
|
92
|
+
if 0xD0 <= marker <= 0xD7 or marker == 0x01:
|
|
93
|
+
offset += 2
|
|
94
|
+
continue
|
|
95
|
+
if offset + 4 > len(data):
|
|
96
|
+
return None
|
|
97
|
+
offset += 2 + int.from_bytes(data[offset + 2:offset + 4], 'big')
|
|
98
|
+
if marker == 0xDA:
|
|
99
|
+
# Entropy-coded data runs to the next marker other than a stuffed
|
|
100
|
+
# zero or a restart marker.
|
|
101
|
+
start = offset
|
|
102
|
+
while True:
|
|
103
|
+
offset = data.find(b'\xff', offset)
|
|
104
|
+
if offset < 0 or offset + 1 >= len(data):
|
|
105
|
+
return None
|
|
106
|
+
if data[offset + 1] == 0 or 0xD0 <= data[offset + 1] <= 0xD7:
|
|
107
|
+
offset += 2
|
|
108
|
+
else:
|
|
109
|
+
break
|
|
110
|
+
samples.append((start, offset))
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
WEBP_SAMPLES = (b'VP8 ', b'VP8L', b'ALPH', b'ANMF')
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def webp(data):
|
|
118
|
+
if len(data) < 20 or data[:4] != b'RIFF' or data[8:12] != b'WEBP':
|
|
119
|
+
return None
|
|
120
|
+
end = int.from_bytes(data[4:8], 'little') + 8
|
|
121
|
+
offset = 12
|
|
122
|
+
samples = []
|
|
123
|
+
while offset + 8 <= end:
|
|
124
|
+
size = int.from_bytes(data[offset + 4:offset + 8], 'little')
|
|
125
|
+
if data[offset:offset + 4] in WEBP_SAMPLES:
|
|
126
|
+
samples.append((offset + 8, offset + 8 + size))
|
|
127
|
+
offset += 8 + size + (size & 1)
|
|
128
|
+
return (end, samples) if offset == end else None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Major brands of MP4, QuickTime, 3GPP and HEIF/AVIF files.
|
|
132
|
+
ISO_BRANDS = {b'isom', b'iso2', b'iso4', b'iso5', b'iso6', b'mp41', b'mp42', b'avc1', b'dash',
|
|
133
|
+
b'M4V ', b'M4A ', b'M4VH', b'M4VP', b'qt ', b'3gp4', b'3gp5', b'3gp6', b'3g2a',
|
|
134
|
+
b'mif1', b'msf1', b'avif', b'avis', b'heic', b'heix', b'heim', b'heis', b'hevc', b'hevx'}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def iso_media(data):
|
|
138
|
+
# MP4 and AVIF share ISO base media boxes, opened by a file-type box with
|
|
139
|
+
# an explicit size and a known major brand.
|
|
140
|
+
if len(data) < 16 or data[4:8] != b'ftyp' or data[8:12] not in ISO_BRANDS:
|
|
141
|
+
return None
|
|
142
|
+
if int.from_bytes(data[:4], 'big') < 16:
|
|
143
|
+
return None
|
|
144
|
+
offset = 0
|
|
145
|
+
kinds, samples = set(), []
|
|
146
|
+
while offset + 8 <= len(data):
|
|
147
|
+
size = int.from_bytes(data[offset:offset + 4], 'big')
|
|
148
|
+
kind = data[offset + 4:offset + 8]
|
|
149
|
+
header = 8
|
|
150
|
+
if size == 1:
|
|
151
|
+
if offset + 16 > len(data):
|
|
152
|
+
return None
|
|
153
|
+
size, header = int.from_bytes(data[offset + 8:offset + 16], 'big'), 16
|
|
154
|
+
elif size == 0:
|
|
155
|
+
# Only a last box after the file type may run to the end.
|
|
156
|
+
size = len(data) - offset
|
|
157
|
+
if size < header:
|
|
158
|
+
return None
|
|
159
|
+
kinds.add(kind)
|
|
160
|
+
if kind == b'mdat':
|
|
161
|
+
samples.append((offset + header, offset + size))
|
|
162
|
+
offset += size
|
|
163
|
+
# A movie has its moov box; a still image (HEIF/AVIF) has meta.
|
|
164
|
+
if offset != len(data) or not kinds & {b'moov', b'meta'}:
|
|
165
|
+
return None
|
|
166
|
+
return offset, samples
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def ebml_number(data, offset, marker):
|
|
170
|
+
# A variable-length EBML number; the marker bit stays in IDs, not sizes.
|
|
171
|
+
if offset >= len(data) or not data[offset]:
|
|
172
|
+
return None, offset
|
|
173
|
+
length = 9 - data[offset].bit_length()
|
|
174
|
+
if offset + length > len(data):
|
|
175
|
+
return None, offset
|
|
176
|
+
value = int.from_bytes(data[offset:offset + length], 'big')
|
|
177
|
+
if not marker:
|
|
178
|
+
value &= (1 << (7 * length)) - 1
|
|
179
|
+
if value == (1 << (7 * length)) - 1:
|
|
180
|
+
value = -1
|
|
181
|
+
return value, offset + length
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
SEGMENT = 0x18538067
|
|
185
|
+
CLUSTER = 0x1F43B675
|
|
186
|
+
# Segment children: a Cluster of unknown size ends at the next one of these.
|
|
187
|
+
SEGMENT_CHILDREN = {CLUSTER, 0x114D9B74, 0x1549A966, 0x1654AE6B, 0x1C53BB6B,
|
|
188
|
+
0x1941A469, 0x1043A770, 0x1254C367}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def ebml_element(data, offset):
|
|
192
|
+
element, content = ebml_number(data, offset, True)
|
|
193
|
+
size, content = ebml_number(data, content, False)
|
|
194
|
+
return element, content, size
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def open_cluster(data, offset, end):
|
|
198
|
+
# A live Cluster of unknown size runs to the next Segment child.
|
|
199
|
+
start = offset
|
|
200
|
+
while offset < end:
|
|
201
|
+
element, content, size = ebml_element(data, offset)
|
|
202
|
+
if element in SEGMENT_CHILDREN:
|
|
203
|
+
break
|
|
204
|
+
if element is None or size is None or size == -1:
|
|
205
|
+
return None
|
|
206
|
+
offset = content + size
|
|
207
|
+
return offset - start
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def webm_segment(data, offset, end):
|
|
211
|
+
# Each Cluster's blocks are the samples; other Segment children are not.
|
|
212
|
+
samples = []
|
|
213
|
+
while offset < end:
|
|
214
|
+
element, content, size = ebml_element(data, offset)
|
|
215
|
+
if element is None or size is None:
|
|
216
|
+
return None
|
|
217
|
+
if size == -1:
|
|
218
|
+
size = open_cluster(data, content, end) if element == CLUSTER else None
|
|
219
|
+
if size is None:
|
|
220
|
+
return None
|
|
221
|
+
if element == CLUSTER:
|
|
222
|
+
samples.append((content, content + size))
|
|
223
|
+
offset = content + size
|
|
224
|
+
return samples if offset == end else None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def webm(data):
|
|
228
|
+
if not data.startswith(b'\x1a\x45\xdf\xa3') or b'webm' not in data[:64]:
|
|
229
|
+
return None
|
|
230
|
+
offset, samples = 0, []
|
|
231
|
+
while offset < len(data):
|
|
232
|
+
element, content, size = ebml_element(data, offset)
|
|
233
|
+
if element is None or size is None:
|
|
234
|
+
return None
|
|
235
|
+
# Only a live Segment may leave its size unknown: it runs to the end.
|
|
236
|
+
if size == -1:
|
|
237
|
+
if element != SEGMENT:
|
|
238
|
+
return None
|
|
239
|
+
size = len(data) - content
|
|
240
|
+
if element == SEGMENT:
|
|
241
|
+
segment = webm_segment(data, content, content + size)
|
|
242
|
+
if segment is None:
|
|
243
|
+
return None
|
|
244
|
+
samples += segment
|
|
245
|
+
offset = content + size
|
|
246
|
+
return (offset, samples) if offset == len(data) else None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def ico(data):
|
|
250
|
+
if len(data) < 6 or data[:4] != b'\0\0\1\0':
|
|
251
|
+
return None
|
|
252
|
+
count = int.from_bytes(data[4:6], 'little')
|
|
253
|
+
images = []
|
|
254
|
+
for index in range(count):
|
|
255
|
+
entry = 6 + 16 * index
|
|
256
|
+
if entry + 16 > len(data):
|
|
257
|
+
return None
|
|
258
|
+
images.append((int.from_bytes(data[entry + 12:entry + 16], 'little'),
|
|
259
|
+
int.from_bytes(data[entry + 8:entry + 12], 'little')))
|
|
260
|
+
# Image data follows the directory with no gap. An embedded PNG keeps its
|
|
261
|
+
# own compressed samples; bitmap pixels are stored as they are.
|
|
262
|
+
offset = 6 + 16 * count
|
|
263
|
+
samples = []
|
|
264
|
+
for start, size in sorted(images):
|
|
265
|
+
if start != offset or not size:
|
|
266
|
+
return None
|
|
267
|
+
image = png(data[start:start + size])
|
|
268
|
+
if image and image[0] == size:
|
|
269
|
+
samples += [(start + a, start + b) for a, b in image[1]]
|
|
270
|
+
offset += size
|
|
271
|
+
return (offset, samples) if count else None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
FORMATS = (gif, png, jpeg, webp, iso_media, webm, ico)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def sample_spans(data):
|
|
278
|
+
"""The pixel and sample spans of a complete media file, or None."""
|
|
279
|
+
for walk in FORMATS:
|
|
280
|
+
walked = walk(data)
|
|
281
|
+
if walked and walked[0] <= len(data) and not data[walked[0]:].strip(b'\0'):
|
|
282
|
+
return [(start, min(end, walked[0])) for start, end in walked[1]]
|
|
283
|
+
return None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Separate uncompressed PNG text fields from chunk framing and CRC bytes."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def text_chunks(data):
|
|
5
|
+
# Detect the container by bytes, never by a publication path or extension.
|
|
6
|
+
if not data.startswith(b'\x89PNG\r\n\x1a\n'):
|
|
7
|
+
return
|
|
8
|
+
offset = 8
|
|
9
|
+
while offset + 12 <= len(data):
|
|
10
|
+
start = offset
|
|
11
|
+
length = int.from_bytes(data[offset:offset + 4], 'big')
|
|
12
|
+
end = offset + 8 + length
|
|
13
|
+
if end + 4 > len(data):
|
|
14
|
+
return
|
|
15
|
+
kind = data[offset + 4:offset + 8]
|
|
16
|
+
payload = data[offset + 8:end]
|
|
17
|
+
offset = end + 4
|
|
18
|
+
# Checksums do not confer an exemption: inspect a framed text field
|
|
19
|
+
# even if its CRC is wrong. Non-text chunks keep raw-byte inspection.
|
|
20
|
+
if kind == b'tEXt':
|
|
21
|
+
fields = payload.split(b'\0', 1)
|
|
22
|
+
if len(fields) == 2:
|
|
23
|
+
yield start, offset, [field.decode('latin-1').encode('utf-8') for field in fields]
|
|
24
|
+
elif kind == b'iTXt':
|
|
25
|
+
fields = payload.split(b'\0', 1)
|
|
26
|
+
if len(fields) != 2 or len(fields[1]) < 2:
|
|
27
|
+
continue
|
|
28
|
+
keyword, body = fields
|
|
29
|
+
fields = body[2:].split(b'\0', 2)
|
|
30
|
+
# The compression method is ignored when the flag is zero.
|
|
31
|
+
if body[0] == 0 and len(fields) == 3:
|
|
32
|
+
yield start, offset, [keyword.decode('latin-1').encode('utf-8'), *fields]
|
|
33
|
+
if kind == b'IEND':
|
|
34
|
+
return
|
|
@@ -8,6 +8,9 @@ from provenance import provenance_text
|
|
|
8
8
|
from hygiene_config import EMAIL, LOCAL_PATH, public_email
|
|
9
9
|
from font_licence import without_required_notices
|
|
10
10
|
from trust_bundle import anchor_values
|
|
11
|
+
from png_text import text_chunks
|
|
12
|
+
from media import sample_spans
|
|
13
|
+
from c2pa import certificate_spans
|
|
11
14
|
URI_AUTHORITY = re.compile(r'''(?i)(?a:(?P<scheme>[a-z][a-z0-9+.-]*:))?//(?P<authority>[^/?#\s<>"']+)''')
|
|
12
15
|
# Match raw, complete commit trailer lines; decoded or embedded prose cannot
|
|
13
16
|
# acquire trailer privileges. Only the address span is omitted from email checks.
|
|
@@ -47,7 +50,7 @@ def email_userinfo(match):
|
|
|
47
50
|
def private_emails(text, config):
|
|
48
51
|
# URL paths, queries and fragments remain visible to email inspection.
|
|
49
52
|
email_text = URI_AUTHORITY.sub(email_userinfo, without_required_notices(text))
|
|
50
|
-
email_text = re.sub(r'(?i)\b(?a:git@github\.com:)(?=[A-Za-z0-9])', '', email_text)
|
|
53
|
+
email_text = re.sub(r'(?i)\b(?a:git@github\.com:)(?=[A-Za-z0-9]|\$\{[A-Za-z_$][A-Za-z0-9_.$]*\})', '', email_text)
|
|
51
54
|
# A package manager's patch filename joins the name and version with an at sign.
|
|
52
55
|
version = r'(?:v?[0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9-]+)?|[a-f0-9]{40})(?:\.patch)?(?![A-Za-z0-9_.-])'
|
|
53
56
|
email_text = re.sub(r'(?m)^\s*-?\s*uses:\s*[\"\']?[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[^\s\"\']+', '', email_text)
|
|
@@ -92,11 +95,43 @@ def certificate_metadata(data, bundle, emails, config):
|
|
|
92
95
|
return not Counter(emails) - inside
|
|
93
96
|
|
|
94
97
|
|
|
98
|
+
def without_spans(data, spans):
|
|
99
|
+
# Each span becomes one space: text on either side stays apart, and no
|
|
100
|
+
# long blank run is left for the line-anchored patterns to rescan.
|
|
101
|
+
kept, offset = [], 0
|
|
102
|
+
for start, end in sorted(spans):
|
|
103
|
+
kept.append(data[offset:start])
|
|
104
|
+
offset = max(offset, end)
|
|
105
|
+
kept.append(data[offset:])
|
|
106
|
+
return b' '.join(kept)
|
|
107
|
+
|
|
108
|
+
|
|
95
109
|
def inspect(data, path, config, resolver, *, commit_message=False):
|
|
96
|
-
|
|
110
|
+
chunks = list(text_chunks(data))
|
|
111
|
+
# Image and video samples are not text: they can spell an address or a
|
|
112
|
+
# path by chance. Comments, XMP, tags and other metadata remain text.
|
|
113
|
+
spans = sample_spans(data) or []
|
|
114
|
+
# Names in a C2PA manifest's signing certificates are certificate metadata.
|
|
115
|
+
spans += certificate_spans(data)
|
|
116
|
+
rules, provenance = set(), False
|
|
117
|
+
for start, end, fields in chunks:
|
|
118
|
+
spans.append((start, end))
|
|
119
|
+
for field in fields:
|
|
120
|
+
# A metadata value is text, not a standalone source manifest or
|
|
121
|
+
# trust bundle, even when the containing image has a JSON name.
|
|
122
|
+
field_rules, field_provenance = inspect_text(field, '@png-text', config, resolver, binary_content=False)
|
|
123
|
+
rules.update(field_rules)
|
|
124
|
+
provenance |= field_provenance
|
|
125
|
+
# Keep the original binary classification for content outside text chunks.
|
|
126
|
+
# CRC-adjacent bytes must not also create a synthetic raw mailbox match.
|
|
127
|
+
raw_rules, raw_provenance = inspect_text(without_spans(data, spans), path, config, resolver,
|
|
128
|
+
commit_message=commit_message, binary_content=binary(data))
|
|
129
|
+
rules.update(raw_rules)
|
|
130
|
+
provenance |= raw_provenance
|
|
131
|
+
return rules, provenance
|
|
97
132
|
|
|
98
133
|
|
|
99
|
-
def inspect_text(data, path, config, resolver, *, commit_message=False):
|
|
134
|
+
def inspect_text(data, path, config, resolver, *, commit_message=False, binary_content=None):
|
|
100
135
|
# NUL removal covers UTF-16/32 ASCII content, matching the secret scanner.
|
|
101
136
|
text = decoded_text(data)
|
|
102
137
|
rules = set()
|
|
@@ -111,7 +146,7 @@ def inspect_text(data, path, config, resolver, *, commit_message=False):
|
|
|
111
146
|
if emails and not (bundle and certificate_metadata(data, bundle, emails, config)):
|
|
112
147
|
# Inside binary data a mailbox is judged by its own shape: a host
|
|
113
148
|
# name with mixed-case letters inside a label is not one.
|
|
114
|
-
if not binary(data) or any(stored_host(email) for email in emails):
|
|
149
|
+
if not (binary(data) if binary_content is None else binary_content) or any(stored_host(email) for email in emails):
|
|
115
150
|
rules.add('hygiene.email')
|
|
116
151
|
ref_text, provenance = provenance_text(text, path, config.get('declaredSources', frozenset()))
|
|
117
152
|
stated, ambiguous = reference_sets(ref_text, path, config.get('localPaths', frozenset()), config.get('projectIdentity'))
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
{ "repository": "just-every/run-oncall", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
23
23
|
{ "repository": "just-every/run-spool", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
24
24
|
{ "repository": "just-every/run-happytohelp", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
25
|
-
{ "repository": "just-every/run-auth", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." }
|
|
25
|
+
{ "repository": "just-every/run-auth", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
26
|
+
{ "repository": "just-every/run-chest", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
27
|
+
{ "repository": "just-every/run-purse", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
28
|
+
{ "repository": "just-every/run-easel", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
29
|
+
{ "repository": "just-every/run-willow", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
30
|
+
{ "repository": "just-every/run-pier", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
31
|
+
{ "repository": "just-every/run-linen", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." }
|
|
26
32
|
]
|
|
27
33
|
}
|