1use crate::language::{
4 ACCEPTED_LANGUAGE_CAPABILITY_SET_VERSION, BROAD_PARSER_PACK_ID,
5 LANGUAGE_CAPABILITY_REGISTRY_VERSION, OPTIONAL_GRAMMAR_CATALOG,
6 OPTIONAL_GRAMMAR_CATALOG_RELEASE_REVISION, OPTIONAL_GRAMMAR_CATALOG_VERSION,
7 OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS, accepted_language_capability_digest,
8 language_capability, language_registry_digest,
9};
10use crate::optional_parser_protocol::{
11 PARSER_WORKER_JOB_MEMORY_BYTES, PARSER_WORKER_PROCESS_MEMORY_BYTES,
12};
13use blake3::Hasher;
14use serde::{Deserialize, Deserializer, Serialize};
15use std::collections::{BTreeMap, BTreeSet};
16use std::fmt;
17use thiserror::Error;
18
19pub const OPTIONAL_PARSER_PACK_MANIFEST_SCHEMA_VERSION: u32 = 2;
21pub const OPTIONAL_PARSER_PACK_CAPABILITY_SET_VERSION: u32 = 2;
23pub const OPTIONAL_PARSER_PACK_ID: &str = BROAD_PARSER_PACK_ID;
25pub const OPTIONAL_GRAMMAR_CATALOG_CRATE_SHA256: &str =
27 "44dc94ef7a5f7f4247d88d5acdd26d842c8fc6f5eaf491a970c8e3d8fc9c9287";
28pub const OPTIONAL_GRAMMAR_CATALOG_CRATE_REVISION: &str =
30 "ce9e9c0974731d25b4b9426711a62d544d993368";
31pub const OPTIONAL_GRAMMAR_CATALOG_CRATE_PATH_IN_VCS: &str = "crates/ts-pack-core";
33pub const OPTIONAL_GRAMMAR_CATALOG_RELEASE_TAG: &str = "v1.13.2";
35pub const OPTIONAL_GRAMMAR_CATALOG_SOURCE_BUNDLE_SHA256: &str =
37 "d684799dc664553c9c746d5fe676a5b599f9efcec4cad5450bec7ec5a29574a9";
38pub const OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION: &str = "0.5.0-rc1";
40pub const OPTIONAL_PARSER_PACK_TREE_SITTER_VERSION: &str = "0.26.13";
42pub const OPTIONAL_PARSER_PACK_MINIMUM_ABI: u32 = 13;
44pub const OPTIONAL_PARSER_PACK_MAXIMUM_ABI: u32 = 15;
46pub const OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES: usize = 32 * 1024 * 1024;
48pub const OPTIONAL_PARSER_PACK_ARTIFACT_SCHEMA_VERSION: u32 = 2;
50pub const OPTIONAL_PARSER_PACK_NATIVE_AUDIT_SCHEMA_VERSION: u32 = 3;
52pub const OPTIONAL_PARSER_PACK_NATIVE_IMPORT_POLICY_SCHEMA_VERSION: u32 = 3;
54pub const OPTIONAL_PARSER_PACK_LINUX_RUNTIME_LOADER_BASENAME: &str = "ld-linux-x86-64.so.2";
56pub const OPTIONAL_PARSER_PACK_WINDOWS_BROKER_RUNTIME_FAMILY: &str = "windows-net-framework-clr-v4";
58pub const OPTIONAL_PARSER_PACK_WINDOWS_BROKER_NATIVE_ENTRY_POINT: &str = "0x0000000000000000";
60pub const OPTIONAL_PARSER_PACK_WINDOWS_BROKER_CLR_RUNTIME_HEADER_SIZE: u32 = 72;
62pub const OPTIONAL_PARSER_PACK_WINDOWS_BROKER_PE_LOADER_LIBRARIES: &[&str] = &[];
64pub const OPTIONAL_PARSER_PACK_WINDOWS_BROKER_MANAGED_MODULES: &[&str] =
66 &["advapi32.dll", "kernel32.dll", "userenv.dll"];
67pub const OPTIONAL_PARSER_PACK_PLATFORM_PROOF_SCHEMA_VERSION: u32 = 2;
69pub const OPTIONAL_PARSER_PACK_PROOF_AGGREGATE_SCHEMA_VERSION: u32 = 2;
71pub const OPTIONAL_PARSER_PACK_LINUX_MEMORY_PROBE_BYTES: u64 = 1024 * 1024;
73pub const OPTIONAL_PARSER_PACK_WINDOWS_MINIMUM_MEMORY_PROBE_BYTES: u64 = 16 * 1024 * 1024;
75pub const OPTIONAL_PARSER_PACK_MAX_ARCHIVE_BYTES: u64 = 64 * 1024 * 1024;
77pub const OPTIONAL_PARSER_PACK_MAX_EXPANDED_BYTES: u64 = 512 * 1024 * 1024;
79pub const OPTIONAL_PARSER_PACK_MAX_FILE_BYTES: u64 = 128 * 1024 * 1024;
81pub const OPTIONAL_PARSER_PACK_NATIVE_IMPORT_POLICY_MAX_BYTES: u64 = 1024 * 1024;
83pub const OPTIONAL_PARSER_PACK_MAX_PATH_BYTES: usize = 256;
85pub const OPTIONAL_PARSER_PACK_MAX_FILE_ENTRIES: usize = 512;
87
88const MAX_ACCEPTED_GRAMMARS: usize = 512;
90const MAX_LICENSE_RECORDS: usize = 1_024;
92const MAX_IDENTITY_BYTES: usize = 4_096;
94const MAX_LICENSE_TEXT_BYTES: usize = 256 * 1024;
96const MAX_FIXTURE_SOURCE_BYTES: usize = 64 * 1024;
98const OPTIONAL_PARSER_PACK_COMMON_PAYLOAD_FILES: usize = 6;
100const CAPABILITY_DIGEST_DOMAIN: &str = "projectatlas.optional-parser-capability.v1";
102const MANIFEST_DIGEST_DOMAIN: &str = "projectatlas.optional-parser-pack-manifest.v2";
104
105#[derive(Debug, Error)]
107pub enum OptionalParserPackManifestError {
108 #[error("optional parser-pack manifest is {actual} bytes; maximum is {maximum}")]
110 ManifestTooLarge {
111 actual: usize,
113 maximum: usize,
115 },
116 #[error("invalid optional parser-pack manifest JSON")]
118 InvalidJson {
119 #[source]
121 source: serde_json::Error,
122 },
123 #[error("invalid {field} for {owner}: {reason}")]
125 InvalidField {
126 owner: String,
128 field: &'static str,
130 reason: &'static str,
132 },
133 #[error("optional parser-pack binding {field} is {actual:?}; expected {expected:?}")]
135 BindingMismatch {
136 field: &'static str,
138 expected: String,
140 actual: String,
142 },
143 #[error("optional parser-pack {field} must be strictly sorted and unique")]
145 NotSortedUnique {
146 field: &'static str,
148 },
149 #[error("optional parser-pack {field} count {actual} is outside {minimum}..={maximum}")]
151 CountOutOfBounds {
152 field: &'static str,
154 actual: usize,
156 minimum: usize,
158 maximum: usize,
160 },
161 #[error("grammar {language_id:?} is not a canonical optional language capability")]
163 UnknownOptionalLanguage {
164 language_id: String,
166 },
167 #[error("grammar {language_id:?} overlaps a default-core language capability")]
169 BuiltInOverlap {
170 language_id: String,
172 },
173 #[error("grammar {language_id:?} references unknown license record {license_id:?}")]
175 UnknownLicense {
176 language_id: String,
178 license_id: String,
180 },
181 #[error(
183 "grammar {language_id:?} license {license_id:?} does not match its source repository and revision"
184 )]
185 LicenseSourceMismatch {
186 language_id: String,
188 license_id: String,
190 },
191 #[error("optional parser-pack {field} {value:?} is owned by more than one grammar")]
193 DuplicateRuntimeIdentity {
194 field: &'static str,
196 value: String,
198 },
199 #[error("grammar {language_id:?} ABI claim is outside the consuming runtime window")]
201 AbiMismatch {
202 language_id: String,
204 },
205 #[error("{field} digest mismatch for {owner}")]
207 DigestMismatch {
208 owner: String,
210 field: &'static str,
212 },
213}
214
215#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
217#[serde(transparent)]
218pub struct Sha256Digest(String);
219
220impl Sha256Digest {
221 pub fn new(value: impl Into<String>) -> Result<Self, OptionalParserPackManifestError> {
227 let value = value.into();
228 validate_hex_digest(&value, "sha256")?;
229 Ok(Self(value))
230 }
231
232 #[must_use]
234 pub fn as_str(&self) -> &str {
235 &self.0
236 }
237}
238
239impl<'de> Deserialize<'de> for Sha256Digest {
240 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
241 where
242 D: Deserializer<'de>,
243 {
244 let value = String::deserialize(deserializer)?;
245 Self::new(value).map_err(serde::de::Error::custom)
246 }
247}
248
249impl fmt::Display for Sha256Digest {
250 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
251 formatter.write_str(self.as_str())
252 }
253}
254
255#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
257#[serde(transparent)]
258pub struct Blake3Digest(String);
259
260impl Blake3Digest {
261 pub fn new(value: impl Into<String>) -> Result<Self, OptionalParserPackManifestError> {
267 let value = value.into();
268 validate_hex_digest(&value, "blake3")?;
269 Ok(Self(value))
270 }
271
272 #[must_use]
274 pub fn for_bytes(bytes: &[u8]) -> Self {
275 Self(blake3::hash(bytes).to_hex().to_string())
276 }
277
278 #[must_use]
280 pub fn as_str(&self) -> &str {
281 &self.0
282 }
283}
284
285impl<'de> Deserialize<'de> for Blake3Digest {
286 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
287 where
288 D: Deserializer<'de>,
289 {
290 let value = String::deserialize(deserializer)?;
291 Self::new(value).map_err(serde::de::Error::custom)
292 }
293}
294
295impl fmt::Display for Blake3Digest {
296 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
297 formatter.write_str(self.as_str())
298 }
299}
300
301#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
303#[serde(transparent)]
304pub struct SourceRevision(String);
305
306impl SourceRevision {
307 pub fn new(value: impl Into<String>) -> Result<Self, OptionalParserPackManifestError> {
313 let value = value.into();
314 if value.len() != 40
315 || !value
316 .bytes()
317 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
318 {
319 return Err(invalid_field(
320 "source revision",
321 "revision",
322 "expected 40 lowercase hexadecimal characters",
323 ));
324 }
325 Ok(Self(value))
326 }
327
328 #[must_use]
330 pub fn as_str(&self) -> &str {
331 &self.0
332 }
333}
334
335impl<'de> Deserialize<'de> for SourceRevision {
336 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
337 where
338 D: Deserializer<'de>,
339 {
340 let value = String::deserialize(deserializer)?;
341 Self::new(value).map_err(serde::de::Error::custom)
342 }
343}
344
345impl fmt::Display for SourceRevision {
346 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
347 formatter.write_str(self.as_str())
348 }
349}
350
351#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
353#[serde(transparent)]
354pub struct GrammarExportSymbol(String);
355
356impl GrammarExportSymbol {
357 pub fn new(value: impl Into<String>) -> Result<Self, OptionalParserPackManifestError> {
363 let value = value.into();
364 let mut bytes = value.bytes();
365 let valid_first = bytes
366 .next()
367 .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_');
368 if value.len() > 256
369 || !valid_first
370 || !bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
371 {
372 return Err(invalid_field(
373 &value,
374 "export_symbol",
375 "expected a C identifier of at most 256 ASCII bytes",
376 ));
377 }
378 Ok(Self(value))
379 }
380
381 #[must_use]
383 pub fn as_str(&self) -> &str {
384 &self.0
385 }
386}
387
388impl<'de> Deserialize<'de> for GrammarExportSymbol {
389 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
390 where
391 D: Deserializer<'de>,
392 {
393 let value = String::deserialize(deserializer)?;
394 Self::new(value).map_err(serde::de::Error::custom)
395 }
396}
397
398#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
400#[serde(transparent)]
401pub struct GrammarLibraryStem(String);
402
403impl GrammarLibraryStem {
404 pub fn new(value: impl Into<String>) -> Result<Self, OptionalParserPackManifestError> {
410 let value = value.into();
411 if value.is_empty()
412 || value.len() > 256
413 || !value.bytes().all(|byte| {
414 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
415 })
416 || !value
417 .as_bytes()
418 .first()
419 .is_some_and(u8::is_ascii_alphanumeric)
420 {
421 return Err(invalid_field(
422 &value,
423 "library_stem",
424 "expected a lowercase ASCII basename of at most 256 bytes",
425 ));
426 }
427 Ok(Self(value))
428 }
429
430 #[must_use]
432 pub fn as_str(&self) -> &str {
433 &self.0
434 }
435}
436
437impl<'de> Deserialize<'de> for GrammarLibraryStem {
438 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
439 where
440 D: Deserializer<'de>,
441 {
442 let value = String::deserialize(deserializer)?;
443 Self::new(value).map_err(serde::de::Error::custom)
444 }
445}
446
447#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
449pub enum PackPlatform {
450 #[serde(rename = "x86_64-unknown-linux-gnu")]
452 LinuxX86_64,
453 #[serde(rename = "x86_64-pc-windows-msvc")]
455 WindowsX86_64,
456}
457
458#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
460#[serde(rename_all = "snake_case", tag = "mode")]
461pub enum OptionalParserCapability {
462 Pack {
464 platform: PackPlatform,
466 },
467 BuiltInOnly,
469}
470
471impl OptionalParserCapability {
472 #[must_use]
474 pub fn for_target(os: &str, architecture: &str) -> Self {
475 match (os, architecture) {
476 ("linux", "x86_64") => Self::Pack {
477 platform: PackPlatform::LinuxX86_64,
478 },
479 ("windows", "x86_64") => Self::Pack {
480 platform: PackPlatform::WindowsX86_64,
481 },
482 _ => Self::BuiltInOnly,
483 }
484 }
485
486 #[must_use]
488 pub fn current() -> Self {
489 Self::for_target(std::env::consts::OS, std::env::consts::ARCH)
490 }
491
492 #[must_use]
494 pub const fn pack_platform(self) -> Option<PackPlatform> {
495 match self {
496 Self::Pack { platform } => Some(platform),
497 Self::BuiltInOnly => None,
498 }
499 }
500
501 #[must_use]
503 pub const fn built_in_parsing_available(self) -> bool {
504 true
505 }
506}
507
508impl PackPlatform {
509 pub const ALL: &'static [Self] = &[Self::LinuxX86_64, Self::WindowsX86_64];
511
512 pub const fn as_str(self) -> &'static str {
514 match self {
515 Self::LinuxX86_64 => "x86_64-unknown-linux-gnu",
516 Self::WindowsX86_64 => "x86_64-pc-windows-msvc",
517 }
518 }
519
520 #[must_use]
522 pub const fn worker_file_name(self) -> &'static str {
523 match self {
524 Self::WindowsX86_64 => "projectatlas-parser-worker.exe",
525 Self::LinuxX86_64 => "projectatlas-parser-worker",
526 }
527 }
528
529 #[must_use]
531 pub const fn containment_broker_file_name(self) -> Option<&'static str> {
532 match self {
533 Self::LinuxX86_64 => None,
534 Self::WindowsX86_64 => Some("projectatlas-parser-containment.exe"),
535 }
536 }
537
538 #[must_use]
540 pub fn grammar_library_file_name(self, stem: &GrammarLibraryStem) -> String {
541 match self {
542 Self::LinuxX86_64 => format!("lib{}.so", stem.as_str()),
543 Self::WindowsX86_64 => format!("{}.dll", stem.as_str()),
544 }
545 }
546}
547
548#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
550#[serde(transparent)]
551pub struct PackRelativePath(String);
552
553impl PackRelativePath {
554 pub fn new(value: impl Into<String>) -> Result<Self, OptionalParserPackManifestError> {
560 let value = value.into();
561 let valid_bytes = value
562 .bytes()
563 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'/' | b'.' | b'_' | b'-'));
564 let valid_components = !value.is_empty()
565 && !value.starts_with('/')
566 && !value.ends_with('/')
567 && value
568 .split('/')
569 .all(|component| !component.is_empty() && component != "." && component != "..");
570 if value.len() > OPTIONAL_PARSER_PACK_MAX_PATH_BYTES || !valid_bytes || !valid_components {
571 return Err(invalid_field(
572 &value,
573 "relative_path",
574 "expected a safe slash-separated ASCII path within the pack path bound",
575 ));
576 }
577 Ok(Self(value))
578 }
579
580 #[must_use]
582 pub fn as_str(&self) -> &str {
583 &self.0
584 }
585}
586
587impl<'de> Deserialize<'de> for PackRelativePath {
588 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
589 where
590 D: Deserializer<'de>,
591 {
592 let value = String::deserialize(deserializer)?;
593 Self::new(value).map_err(serde::de::Error::custom)
594 }
595}
596
597#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
599#[serde(rename_all = "kebab-case")]
600pub enum ParserPackCandidateSourceState {
601 Clean,
603 Dirty,
605}
606
607#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
609#[serde(deny_unknown_fields)]
610pub struct ParserPackCandidateIdentity {
611 pub projectatlas_revision: SourceRevision,
613 pub cargo_package_version: String,
615 pub intended_release_version: String,
617 pub cargo_lock_sha256: Sha256Digest,
619 pub rustc_release: String,
621 pub rustc_commit_hash: String,
623 pub source_state: ParserPackCandidateSourceState,
625}
626
627#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
629#[serde(rename_all = "kebab-case")]
630pub enum ParserPackNetworkIsolation {
631 LinuxNetworkNamespace,
633 WindowsPrincipalFirewall,
635 WindowsAppContainer,
637}
638
639impl ParserPackNetworkIsolation {
640 const fn for_construction(platform: PackPlatform) -> Self {
642 match platform {
643 PackPlatform::LinuxX86_64 => Self::LinuxNetworkNamespace,
644 PackPlatform::WindowsX86_64 => Self::WindowsPrincipalFirewall,
645 }
646 }
647
648 const fn for_fresh_runner(platform: PackPlatform) -> Self {
650 match platform {
651 PackPlatform::LinuxX86_64 => Self::LinuxNetworkNamespace,
652 PackPlatform::WindowsX86_64 => Self::WindowsAppContainer,
653 }
654 }
655}
656
657#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
659#[serde(deny_unknown_fields)]
660pub struct ParserPackNetworkDenial {
661 pub mechanism: ParserPackNetworkIsolation,
663 pub dns_denied: bool,
665 pub direct_tcp_denied: bool,
667 pub https_denied: bool,
669}
670
671#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
673#[serde(rename_all = "kebab-case")]
674pub enum ParserPackVerifiedControl {
675 Verified,
677}
678
679#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
681#[serde(deny_unknown_fields)]
682pub struct ParserPackOfflineConstruction {
683 pub cargo_frozen: ParserPackVerifiedControl,
685 pub cargo_offline: ParserPackVerifiedControl,
687 pub dependency_offline: ParserPackVerifiedControl,
689 pub zero_embedded_grammars: ParserPackVerifiedControl,
691 pub language_selector_absent: ParserPackVerifiedControl,
693 pub failed_grammar_override_absent: ParserPackVerifiedControl,
695 pub network_denial: ParserPackNetworkDenial,
697}
698
699#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
701#[serde(deny_unknown_fields)]
702pub struct ParserPackSourceAsset {
703 pub release_tag: String,
705 pub release_revision: SourceRevision,
707 pub name: String,
709 pub sha256: Sha256Digest,
711 pub bytes: u64,
713 pub parsers_manifest_sha256: Sha256Digest,
715}
716
717#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
719#[serde(deny_unknown_fields, tag = "kind", rename_all = "kebab-case")]
720pub enum ParserPackPayloadRole {
721 Worker,
723 ContainmentBroker,
725 AcceptedManifest,
727 FixtureCorpus,
729 ProjectLicense,
731 NativeImportPolicy,
733 NativeAuditReport,
735 GrammarLibrary {
737 language_id: String,
739 },
740}
741
742#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
744#[serde(deny_unknown_fields)]
745pub struct ParserPackPayloadFile {
746 pub path: PackRelativePath,
748 pub role: ParserPackPayloadRole,
750 pub bytes: u64,
752 pub sha256: Sha256Digest,
754}
755
756#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
758#[serde(deny_unknown_fields)]
759pub struct ParserPackPayloadMeasurements {
760 pub files: u32,
762 pub grammar_libraries: u32,
764 pub payload_bytes: u64,
766 pub largest_file_bytes: u64,
768 pub longest_path_bytes: u32,
770}
771
772#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
774#[serde(deny_unknown_fields)]
775pub struct ParserPackNativeAudit {
776 pub policy_sha256: Sha256Digest,
778 pub report_sha256: Sha256Digest,
780 pub audited_libraries: u32,
782 pub forbidden_imports: u32,
784 pub unexpected_dependencies: u32,
786 pub missing_exports: u32,
788 pub unexpected_exports: u32,
790}
791
792#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
794#[serde(deny_unknown_fields)]
795pub struct OptionalParserPackArtifactManifest {
796 pub schema_version: u32,
798 pub pack_id: String,
800 pub projectatlas_version: String,
802 pub platform: PackPlatform,
804 pub candidate: ParserPackCandidateIdentity,
806 pub accepted_manifest_sha256: Sha256Digest,
808 pub capability_set_digest: Blake3Digest,
810 pub fixture_corpus_sha256: Sha256Digest,
812 pub source_asset: ParserPackSourceAsset,
814 pub construction: ParserPackOfflineConstruction,
816 pub native_audit: ParserPackNativeAudit,
818 pub measurements: ParserPackPayloadMeasurements,
820 pub files: Vec<ParserPackPayloadFile>,
822}
823
824#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
826#[serde(deny_unknown_fields)]
827pub struct ParserPackFreshRunner {
828 pub fresh_host: ParserPackVerifiedControl,
830 pub repository_inputs_absent: ParserPackVerifiedControl,
832 pub build_tools_not_invoked: ParserPackVerifiedControl,
834 pub working_directory_outside_pack: ParserPackVerifiedControl,
836 pub ambient_library_paths_cleared: ParserPackVerifiedControl,
838 pub network_denial: ParserPackNetworkDenial,
840}
841
842impl ParserPackFreshRunner {
843 pub fn validate(&self, platform: PackPlatform) -> Result<(), OptionalParserPackManifestError> {
849 validate_fresh_runner(self, platform)
850 }
851}
852
853#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
855#[serde(deny_unknown_fields)]
856pub struct ParserPackGrammarProbe {
857 pub language_id: String,
859 pub worker_probe_passed: bool,
861}
862
863#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
865#[serde(rename_all = "kebab-case")]
866pub enum ParserPackMemoryControl {
867 LinuxCgroupV2,
869 LinuxProcStatus,
871 WindowsJobObject,
873}
874
875#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
877#[serde(deny_unknown_fields)]
878pub struct ParserPackMemoryProbe {
879 pub control: ParserPackMemoryControl,
881 pub process_limit_bytes: u64,
883 pub process_tree_limit_bytes: u64,
885 pub observation_interval_millis: Option<u64>,
887 pub peak_observed_bytes: Option<u64>,
889 pub maximum_observed_overshoot_bytes: Option<u64>,
891 pub limit_enforced: ParserPackVerifiedControl,
893 pub process_tree_cleaned: ParserPackVerifiedControl,
895}
896
897#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
899#[serde(deny_unknown_fields)]
900pub struct OptionalParserPackPlatformProof {
901 pub schema_version: u32,
903 pub pack_id: String,
905 pub platform: PackPlatform,
907 pub candidate: ParserPackCandidateIdentity,
909 pub archive_name: String,
911 pub archive_sha256: Sha256Digest,
913 pub archive_bytes: u64,
915 pub expanded_bytes: u64,
917 pub artifact_manifest_sha256: Sha256Digest,
919 pub accepted_manifest_sha256: Sha256Digest,
921 pub capability_set_digest: Blake3Digest,
923 pub fixture_corpus_sha256: Sha256Digest,
925 pub native_audit_report_sha256: Sha256Digest,
927 pub runner: ParserPackFreshRunner,
929 pub grammars: Vec<ParserPackGrammarProbe>,
931 pub memory: ParserPackMemoryProbe,
933}
934
935#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
937#[serde(deny_unknown_fields)]
938pub struct OptionalParserPackProofAggregate {
939 pub schema_version: u32,
941 pub pack_id: String,
943 pub projectatlas_version: String,
945 pub accepted_manifest_sha256: Sha256Digest,
947 pub capability_set_digest: Blake3Digest,
949 pub fixture_corpus_sha256: Sha256Digest,
951 pub platforms: Vec<OptionalParserPackPlatformProof>,
953}
954
955#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
957#[serde(rename_all = "kebab-case")]
958pub enum ParserPackConsumer {
959 #[serde(rename = "projectatlas-parser-worker")]
961 ProjectAtlasParserWorker,
962}
963
964impl ParserPackConsumer {
965 const fn canonical_name(self) -> &'static str {
967 match self {
968 Self::ProjectAtlasParserWorker => "projectatlas-parser-worker",
969 }
970 }
971}
972
973#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
975#[serde(rename_all = "kebab-case")]
976pub enum BuiltInParserPrecedence {
977 BuiltInAuthoritative,
979}
980
981impl BuiltInParserPrecedence {
982 const fn canonical_name(self) -> &'static str {
984 match self {
985 Self::BuiltInAuthoritative => "built-in-authoritative",
986 }
987 }
988}
989
990#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
992#[serde(deny_unknown_fields)]
993pub struct OptionalParserPackSource {
994 pub package: String,
996 pub version: String,
998 pub cargo_archive: OptionalParserCargoArchive,
1000 pub native_release: OptionalParserNativeRelease,
1002}
1003
1004#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1006#[serde(deny_unknown_fields)]
1007pub struct OptionalParserCargoArchive {
1008 pub sha256: Sha256Digest,
1010 pub vcs_revision: SourceRevision,
1012 pub path_in_vcs: String,
1014}
1015
1016#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1018#[serde(deny_unknown_fields)]
1019pub struct OptionalParserNativeRelease {
1020 pub tag: String,
1022 pub revision: SourceRevision,
1024 pub source_bundle_sha256: Sha256Digest,
1026}
1027
1028#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1030#[serde(deny_unknown_fields)]
1031pub struct OptionalParserPackRuntime {
1032 pub consumer: ParserPackConsumer,
1034 pub projectatlas_version: String,
1036 pub tree_sitter_version: String,
1038 pub minimum_abi: u32,
1040 pub maximum_abi: u32,
1042}
1043
1044#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1046#[serde(deny_unknown_fields)]
1047pub struct OptionalParserPackRegistryBinding {
1048 pub registry_version: u32,
1050 pub registry_digest: Blake3Digest,
1052 pub accepted_set_version: u32,
1054 pub accepted_set_digest: Blake3Digest,
1056}
1057
1058impl OptionalParserPackRegistryBinding {
1059 pub fn current() -> Result<Self, OptionalParserPackManifestError> {
1065 Ok(Self {
1066 registry_version: LANGUAGE_CAPABILITY_REGISTRY_VERSION,
1067 registry_digest: Blake3Digest::new(language_registry_digest())?,
1068 accepted_set_version: ACCEPTED_LANGUAGE_CAPABILITY_SET_VERSION,
1069 accepted_set_digest: Blake3Digest::new(accepted_language_capability_digest())?,
1070 })
1071 }
1072}
1073
1074#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1076#[serde(deny_unknown_fields)]
1077pub struct GrammarSourceProvenance {
1078 pub repository_url: String,
1080 pub revision: SourceRevision,
1082 pub subdirectory: String,
1084 pub compile_input_sha256: Sha256Digest,
1086}
1087
1088#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1090#[serde(deny_unknown_fields)]
1091pub struct GrammarLicense {
1092 pub id: String,
1094 pub repository_url: String,
1096 pub source_path: String,
1098 pub revision: SourceRevision,
1100 pub text: String,
1102 pub text_blake3: Blake3Digest,
1104 pub spdx_expression: Option<String>,
1106}
1107
1108impl GrammarLicense {
1109 #[must_use]
1111 pub fn new(
1112 id: impl Into<String>,
1113 repository_url: impl Into<String>,
1114 source_path: impl Into<String>,
1115 revision: SourceRevision,
1116 text: impl Into<String>,
1117 spdx_expression: Option<String>,
1118 ) -> Self {
1119 let text = text.into();
1120 Self {
1121 id: id.into(),
1122 repository_url: repository_url.into(),
1123 source_path: source_path.into(),
1124 revision,
1125 text_blake3: Blake3Digest::for_bytes(text.as_bytes()),
1126 text,
1127 spdx_expression,
1128 }
1129 }
1130}
1131
1132#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1134#[serde(deny_unknown_fields)]
1135pub struct GrammarAbiExport {
1136 pub minimum_abi: u32,
1138 pub maximum_abi: u32,
1140 pub expected_abi: u32,
1142 pub export_symbol: GrammarExportSymbol,
1144 pub library_stem: GrammarLibraryStem,
1146}
1147
1148#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1150pub enum GrammarFixtureOrigin {
1151 #[serde(rename = "upstream-tree-sitter-corpus")]
1153 UpstreamTreeSitterCorpus,
1154 #[serde(rename = "upstream-language-example")]
1156 UpstreamLanguageExample,
1157 #[serde(rename = "upstream-corpus-error-case")]
1159 UpstreamCorpusErrorCase,
1160 #[serde(rename = "projectatlas-incomplete-upstream-case")]
1162 ProjectAtlasIncompleteUpstreamCase,
1163 #[serde(rename = "projectatlas-incomplete-upstream-example")]
1165 ProjectAtlasIncompleteUpstreamExample,
1166}
1167
1168impl GrammarFixtureOrigin {
1169 const fn canonical_name(self) -> &'static str {
1171 match self {
1172 Self::UpstreamTreeSitterCorpus => "upstream-tree-sitter-corpus",
1173 Self::UpstreamLanguageExample => "upstream-language-example",
1174 Self::UpstreamCorpusErrorCase => "upstream-corpus-error-case",
1175 Self::ProjectAtlasIncompleteUpstreamCase => "projectatlas-incomplete-upstream-case",
1176 Self::ProjectAtlasIncompleteUpstreamExample => {
1177 "projectatlas-incomplete-upstream-example"
1178 }
1179 }
1180 }
1181
1182 const fn is_positive(self) -> bool {
1184 matches!(
1185 self,
1186 Self::UpstreamTreeSitterCorpus | Self::UpstreamLanguageExample
1187 )
1188 }
1189
1190 const fn is_negative(self) -> bool {
1192 matches!(
1193 self,
1194 Self::UpstreamCorpusErrorCase
1195 | Self::ProjectAtlasIncompleteUpstreamCase
1196 | Self::ProjectAtlasIncompleteUpstreamExample
1197 )
1198 }
1199}
1200
1201#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1203#[serde(deny_unknown_fields)]
1204pub struct GrammarFixture {
1205 pub origin: GrammarFixtureOrigin,
1207 pub path: String,
1209 pub case_name: String,
1211 pub source: String,
1213 pub source_blake3: Blake3Digest,
1215}
1216
1217impl GrammarFixture {
1218 #[must_use]
1220 pub fn new(
1221 origin: GrammarFixtureOrigin,
1222 path: impl Into<String>,
1223 case_name: impl Into<String>,
1224 source: impl Into<String>,
1225 ) -> Self {
1226 let source = source.into();
1227 Self {
1228 origin,
1229 path: path.into(),
1230 case_name: case_name.into(),
1231 source_blake3: Blake3Digest::for_bytes(source.as_bytes()),
1232 source,
1233 }
1234 }
1235}
1236
1237#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1239#[serde(deny_unknown_fields)]
1240pub struct GrammarFixtures {
1241 pub positive: GrammarFixture,
1243 pub negative: GrammarFixture,
1245}
1246
1247#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1249#[serde(deny_unknown_fields)]
1250pub struct AcceptedGrammar {
1251 pub language_id: String,
1253 pub source: GrammarSourceProvenance,
1255 pub license_record_ids: Vec<String>,
1257 pub abi_export: GrammarAbiExport,
1259 pub fixtures: GrammarFixtures,
1261 pub required_platforms: Vec<PackPlatform>,
1263 pub built_in_precedence: BuiltInParserPrecedence,
1265 pub capability_digest: Blake3Digest,
1267}
1268
1269impl AcceptedGrammar {
1270 #[must_use]
1272 pub fn new(
1273 language_id: impl Into<String>,
1274 source: GrammarSourceProvenance,
1275 license_record_ids: Vec<String>,
1276 abi_export: GrammarAbiExport,
1277 fixtures: GrammarFixtures,
1278 ) -> Self {
1279 let mut grammar = Self {
1280 language_id: language_id.into(),
1281 source,
1282 license_record_ids,
1283 abi_export,
1284 fixtures,
1285 required_platforms: PackPlatform::ALL.to_vec(),
1286 built_in_precedence: BuiltInParserPrecedence::BuiltInAuthoritative,
1287 capability_digest: Blake3Digest::for_bytes(&[]),
1288 };
1289 grammar.capability_digest = grammar.computed_capability_digest();
1290 grammar
1291 }
1292
1293 #[must_use]
1295 pub fn computed_capability_digest(&self) -> Blake3Digest {
1296 let mut hasher = Hasher::new();
1297 hash_value(&mut hasher, CAPABILITY_DIGEST_DOMAIN);
1298 hash_value(&mut hasher, &self.language_id);
1299 hash_source_provenance(&mut hasher, &self.source);
1300 for license_id in &self.license_record_ids {
1301 hash_value(&mut hasher, license_id);
1302 }
1303 hasher.update(&self.abi_export.minimum_abi.to_le_bytes());
1304 hasher.update(&self.abi_export.maximum_abi.to_le_bytes());
1305 hasher.update(&self.abi_export.expected_abi.to_le_bytes());
1306 hash_value(&mut hasher, self.abi_export.export_symbol.as_str());
1307 hash_value(&mut hasher, self.abi_export.library_stem.as_str());
1308 hash_fixture(&mut hasher, &self.fixtures.positive);
1309 hash_fixture(&mut hasher, &self.fixtures.negative);
1310 for platform in &self.required_platforms {
1311 hash_value(&mut hasher, platform.as_str());
1312 }
1313 hash_value(&mut hasher, self.built_in_precedence.canonical_name());
1314 Blake3Digest(hasher.finalize().to_hex().to_string())
1315 }
1316}
1317
1318#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1320pub struct OptionalParserPackManifest {
1321 schema_version: u32,
1323 pack_id: String,
1325 capability_set_version: u32,
1327 source: OptionalParserPackSource,
1329 runtime: OptionalParserPackRuntime,
1331 registry: OptionalParserPackRegistryBinding,
1333 required_platforms: Vec<PackPlatform>,
1335 licenses: Vec<GrammarLicense>,
1337 grammars: Vec<AcceptedGrammar>,
1339 capability_set_digest: Blake3Digest,
1341}
1342
1343#[derive(Deserialize)]
1344#[serde(deny_unknown_fields)]
1345struct OptionalParserPackManifestWire {
1347 schema_version: u32,
1349 pack_id: String,
1351 capability_set_version: u32,
1353 source: OptionalParserPackSource,
1355 runtime: OptionalParserPackRuntime,
1357 registry: OptionalParserPackRegistryBinding,
1359 required_platforms: Vec<PackPlatform>,
1361 licenses: Vec<GrammarLicense>,
1363 grammars: Vec<AcceptedGrammar>,
1365 capability_set_digest: Blake3Digest,
1367}
1368
1369impl OptionalParserPackManifest {
1370 pub fn new(
1376 source: OptionalParserPackSource,
1377 runtime: OptionalParserPackRuntime,
1378 licenses: Vec<GrammarLicense>,
1379 mut grammars: Vec<AcceptedGrammar>,
1380 ) -> Result<Self, OptionalParserPackManifestError> {
1381 for grammar in &mut grammars {
1382 grammar.capability_digest = grammar.computed_capability_digest();
1383 }
1384 let mut manifest = Self {
1385 schema_version: OPTIONAL_PARSER_PACK_MANIFEST_SCHEMA_VERSION,
1386 pack_id: OPTIONAL_PARSER_PACK_ID.to_string(),
1387 capability_set_version: OPTIONAL_PARSER_PACK_CAPABILITY_SET_VERSION,
1388 source,
1389 runtime,
1390 registry: OptionalParserPackRegistryBinding::current()?,
1391 required_platforms: PackPlatform::ALL.to_vec(),
1392 licenses,
1393 grammars,
1394 capability_set_digest: Blake3Digest::for_bytes(&[]),
1395 };
1396 manifest.capability_set_digest = manifest.computed_capability_set_digest();
1397 manifest.validate()?;
1398 Ok(manifest)
1399 }
1400
1401 #[must_use]
1403 pub const fn schema_version(&self) -> u32 {
1404 self.schema_version
1405 }
1406
1407 #[must_use]
1409 pub fn pack_id(&self) -> &str {
1410 &self.pack_id
1411 }
1412
1413 #[must_use]
1415 pub const fn capability_set_version(&self) -> u32 {
1416 self.capability_set_version
1417 }
1418
1419 #[must_use]
1421 pub const fn source(&self) -> &OptionalParserPackSource {
1422 &self.source
1423 }
1424
1425 #[must_use]
1427 pub const fn runtime(&self) -> &OptionalParserPackRuntime {
1428 &self.runtime
1429 }
1430
1431 #[must_use]
1433 pub const fn registry(&self) -> &OptionalParserPackRegistryBinding {
1434 &self.registry
1435 }
1436
1437 #[must_use]
1439 pub fn required_platforms(&self) -> &[PackPlatform] {
1440 &self.required_platforms
1441 }
1442
1443 #[must_use]
1445 pub fn licenses(&self) -> &[GrammarLicense] {
1446 &self.licenses
1447 }
1448
1449 #[must_use]
1451 pub fn grammars(&self) -> &[AcceptedGrammar] {
1452 &self.grammars
1453 }
1454
1455 #[must_use]
1457 pub const fn capability_set_digest(&self) -> &Blake3Digest {
1458 &self.capability_set_digest
1459 }
1460
1461 pub fn from_json(bytes: &[u8]) -> Result<Self, OptionalParserPackManifestError> {
1467 if bytes.len() > OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES {
1468 return Err(OptionalParserPackManifestError::ManifestTooLarge {
1469 actual: bytes.len(),
1470 maximum: OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES,
1471 });
1472 }
1473 let wire: OptionalParserPackManifestWire = serde_json::from_slice(bytes)
1474 .map_err(|source| OptionalParserPackManifestError::InvalidJson { source })?;
1475 Self::try_from(wire)
1476 }
1477
1478 pub fn validate(&self) -> Result<(), OptionalParserPackManifestError> {
1484 validate_binding(
1485 "schema_version",
1486 &OPTIONAL_PARSER_PACK_MANIFEST_SCHEMA_VERSION,
1487 &self.schema_version,
1488 )?;
1489 validate_binding("pack_id", OPTIONAL_PARSER_PACK_ID, self.pack_id.as_str())?;
1490 validate_binding(
1491 "capability_set_version",
1492 &OPTIONAL_PARSER_PACK_CAPABILITY_SET_VERSION,
1493 &self.capability_set_version,
1494 )?;
1495 validate_source(&self.source)?;
1496 validate_runtime(&self.runtime)?;
1497 validate_registry_binding(&self.registry)?;
1498 validate_required_platforms("manifest", &self.required_platforms)?;
1499 validate_count("licenses", self.licenses.len(), 1, MAX_LICENSE_RECORDS)?;
1500 validate_count(
1501 "grammars",
1502 self.grammars.len(),
1503 OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS,
1504 MAX_ACCEPTED_GRAMMARS,
1505 )?;
1506 if !strictly_sorted_by(&self.licenses, |license| license.id.as_str()) {
1507 return Err(OptionalParserPackManifestError::NotSortedUnique { field: "licenses" });
1508 }
1509 if !strictly_sorted_by(&self.grammars, |grammar| grammar.language_id.as_str()) {
1510 return Err(OptionalParserPackManifestError::NotSortedUnique { field: "grammars" });
1511 }
1512
1513 let mut license_by_id = BTreeMap::new();
1514 for license in &self.licenses {
1515 validate_license(license)?;
1516 license_by_id.insert(license.id.as_str(), license);
1517 }
1518
1519 let mut source_owners = BTreeSet::new();
1520 let mut export_symbols = BTreeSet::new();
1521 let mut library_stems = BTreeSet::new();
1522 for grammar in &self.grammars {
1523 validate_grammar(
1524 grammar,
1525 &self.runtime,
1526 &self.required_platforms,
1527 &license_by_id,
1528 )?;
1529 let source_identity = (
1530 grammar.source.repository_url.as_str(),
1531 grammar.source.revision.as_str(),
1532 grammar.source.subdirectory.as_str(),
1533 );
1534 if !source_owners.insert(source_identity) {
1535 return Err(OptionalParserPackManifestError::DuplicateRuntimeIdentity {
1536 field: "grammar_source",
1537 value: format!(
1538 "{}@{}:{}",
1539 source_identity.0, source_identity.1, source_identity.2
1540 ),
1541 });
1542 }
1543 if !export_symbols.insert(grammar.abi_export.export_symbol.as_str()) {
1544 return Err(OptionalParserPackManifestError::DuplicateRuntimeIdentity {
1545 field: "export_symbol",
1546 value: grammar.abi_export.export_symbol.as_str().to_string(),
1547 });
1548 }
1549 if !library_stems.insert(grammar.abi_export.library_stem.as_str()) {
1550 return Err(OptionalParserPackManifestError::DuplicateRuntimeIdentity {
1551 field: "library_stem",
1552 value: grammar.abi_export.library_stem.as_str().to_string(),
1553 });
1554 }
1555 }
1556
1557 if self.computed_capability_set_digest() != self.capability_set_digest {
1558 return Err(OptionalParserPackManifestError::DigestMismatch {
1559 owner: self.pack_id.clone(),
1560 field: "capability_set_digest",
1561 });
1562 }
1563 Ok(())
1564 }
1565
1566 #[must_use]
1568 pub fn computed_capability_set_digest(&self) -> Blake3Digest {
1569 let mut hasher = Hasher::new();
1570 hash_value(&mut hasher, MANIFEST_DIGEST_DOMAIN);
1571 hasher.update(&self.schema_version.to_le_bytes());
1572 hash_value(&mut hasher, &self.pack_id);
1573 hasher.update(&self.capability_set_version.to_le_bytes());
1574 hash_value(&mut hasher, &self.source.package);
1575 hash_value(&mut hasher, &self.source.version);
1576 hash_value(&mut hasher, self.source.cargo_archive.sha256.as_str());
1577 hash_value(&mut hasher, self.source.cargo_archive.vcs_revision.as_str());
1578 hash_value(&mut hasher, &self.source.cargo_archive.path_in_vcs);
1579 hash_value(&mut hasher, &self.source.native_release.tag);
1580 hash_value(&mut hasher, self.source.native_release.revision.as_str());
1581 hash_value(
1582 &mut hasher,
1583 self.source.native_release.source_bundle_sha256.as_str(),
1584 );
1585 hash_value(&mut hasher, self.runtime.consumer.canonical_name());
1586 hash_value(&mut hasher, &self.runtime.projectatlas_version);
1587 hash_value(&mut hasher, &self.runtime.tree_sitter_version);
1588 hasher.update(&self.runtime.minimum_abi.to_le_bytes());
1589 hasher.update(&self.runtime.maximum_abi.to_le_bytes());
1590 hasher.update(&self.registry.registry_version.to_le_bytes());
1591 hash_value(&mut hasher, self.registry.registry_digest.as_str());
1592 hasher.update(&self.registry.accepted_set_version.to_le_bytes());
1593 hash_value(&mut hasher, self.registry.accepted_set_digest.as_str());
1594 for platform in &self.required_platforms {
1595 hash_value(&mut hasher, platform.as_str());
1596 }
1597 for license in &self.licenses {
1598 hash_value(&mut hasher, &license.id);
1599 hash_value(&mut hasher, &license.repository_url);
1600 hash_value(&mut hasher, &license.source_path);
1601 hash_value(&mut hasher, license.revision.as_str());
1602 hash_value(&mut hasher, license.text_blake3.as_str());
1603 hash_value(
1604 &mut hasher,
1605 license.spdx_expression.as_deref().unwrap_or(""),
1606 );
1607 }
1608 for grammar in &self.grammars {
1609 hash_value(&mut hasher, &grammar.language_id);
1610 hash_value(&mut hasher, grammar.capability_digest.as_str());
1611 }
1612 Blake3Digest(hasher.finalize().to_hex().to_string())
1613 }
1614}
1615
1616impl TryFrom<OptionalParserPackManifestWire> for OptionalParserPackManifest {
1617 type Error = OptionalParserPackManifestError;
1618
1619 fn try_from(wire: OptionalParserPackManifestWire) -> Result<Self, Self::Error> {
1620 let manifest = Self {
1621 schema_version: wire.schema_version,
1622 pack_id: wire.pack_id,
1623 capability_set_version: wire.capability_set_version,
1624 source: wire.source,
1625 runtime: wire.runtime,
1626 registry: wire.registry,
1627 required_platforms: wire.required_platforms,
1628 licenses: wire.licenses,
1629 grammars: wire.grammars,
1630 capability_set_digest: wire.capability_set_digest,
1631 };
1632 manifest.validate()?;
1633 Ok(manifest)
1634 }
1635}
1636
1637impl ParserPackPayloadMeasurements {
1638 pub fn from_files(
1644 files: &[ParserPackPayloadFile],
1645 ) -> Result<Self, OptionalParserPackManifestError> {
1646 validate_count(
1647 "artifact payload files",
1648 files.len(),
1649 1,
1650 OPTIONAL_PARSER_PACK_MAX_FILE_ENTRIES,
1651 )?;
1652 let mut payload_bytes = 0_u64;
1653 let mut largest_file_bytes = 0_u64;
1654 let mut longest_path_bytes = 0_usize;
1655 let mut grammar_libraries = 0_usize;
1656 for file in files {
1657 if file.bytes == 0 || file.bytes > OPTIONAL_PARSER_PACK_MAX_FILE_BYTES {
1658 return Err(invalid_field(
1659 file.path.as_str(),
1660 "bytes",
1661 "expected a non-empty payload file within the per-file byte ceiling",
1662 ));
1663 }
1664 payload_bytes = payload_bytes.checked_add(file.bytes).ok_or_else(|| {
1665 invalid_field(
1666 OPTIONAL_PARSER_PACK_ID,
1667 "payload_bytes",
1668 "payload byte sum overflowed",
1669 )
1670 })?;
1671 if payload_bytes > OPTIONAL_PARSER_PACK_MAX_EXPANDED_BYTES {
1672 return Err(invalid_field(
1673 OPTIONAL_PARSER_PACK_ID,
1674 "payload_bytes",
1675 "payload byte sum exceeds the expanded artifact ceiling",
1676 ));
1677 }
1678 largest_file_bytes = largest_file_bytes.max(file.bytes);
1679 longest_path_bytes = longest_path_bytes.max(file.path.as_str().len());
1680 if matches!(&file.role, ParserPackPayloadRole::GrammarLibrary { .. }) {
1681 grammar_libraries = grammar_libraries.checked_add(1).ok_or_else(|| {
1682 invalid_field(
1683 OPTIONAL_PARSER_PACK_ID,
1684 "grammar_libraries",
1685 "grammar-library count overflowed",
1686 )
1687 })?;
1688 }
1689 }
1690 Ok(Self {
1691 files: u32::try_from(files.len()).map_err(|_error| {
1692 invalid_field(
1693 OPTIONAL_PARSER_PACK_ID,
1694 "files",
1695 "payload-file count cannot be represented",
1696 )
1697 })?,
1698 grammar_libraries: u32::try_from(grammar_libraries).map_err(|_error| {
1699 invalid_field(
1700 OPTIONAL_PARSER_PACK_ID,
1701 "grammar_libraries",
1702 "grammar-library count cannot be represented",
1703 )
1704 })?,
1705 payload_bytes,
1706 largest_file_bytes,
1707 longest_path_bytes: u32::try_from(longest_path_bytes).map_err(|_error| {
1708 invalid_field(
1709 OPTIONAL_PARSER_PACK_ID,
1710 "longest_path_bytes",
1711 "relative-path length cannot be represented",
1712 )
1713 })?,
1714 })
1715 }
1716}
1717
1718impl OptionalParserPackArtifactManifest {
1719 pub fn validate(
1728 &self,
1729 logical: &OptionalParserPackManifest,
1730 ) -> Result<(), OptionalParserPackManifestError> {
1731 logical.validate()?;
1732 validate_binding(
1733 "artifact.schema_version",
1734 &OPTIONAL_PARSER_PACK_ARTIFACT_SCHEMA_VERSION,
1735 &self.schema_version,
1736 )?;
1737 validate_binding("artifact.pack_id", logical.pack_id(), self.pack_id.as_str())?;
1738 validate_binding(
1739 "artifact.projectatlas_version",
1740 logical.runtime().projectatlas_version.as_str(),
1741 self.projectatlas_version.as_str(),
1742 )?;
1743 validate_required_platform("artifact.platform", self.platform)?;
1744 validate_candidate_identity(&self.candidate, false, logical)?;
1745 validate_binding(
1746 "artifact.capability_set_digest",
1747 logical.capability_set_digest(),
1748 &self.capability_set_digest,
1749 )?;
1750 validate_source_asset(&self.source_asset, logical)?;
1751 validate_offline_construction(&self.construction, self.platform)?;
1752 validate_native_audit(&self.native_audit, logical.grammars().len())?;
1753 validate_payload_files(self, logical)?;
1754 let measured = ParserPackPayloadMeasurements::from_files(&self.files)?;
1755 if measured != self.measurements {
1756 return Err(invalid_field(
1757 self.pack_id.as_str(),
1758 "measurements",
1759 "stored artifact measurements differ from the payload inventory",
1760 ));
1761 }
1762 Ok(())
1763 }
1764}
1765
1766impl OptionalParserPackPlatformProof {
1767 pub fn validate(
1773 &self,
1774 logical: &OptionalParserPackManifest,
1775 ) -> Result<(), OptionalParserPackManifestError> {
1776 logical.validate()?;
1777 validate_binding(
1778 "platform_proof.schema_version",
1779 &OPTIONAL_PARSER_PACK_PLATFORM_PROOF_SCHEMA_VERSION,
1780 &self.schema_version,
1781 )?;
1782 validate_binding(
1783 "platform_proof.pack_id",
1784 logical.pack_id(),
1785 self.pack_id.as_str(),
1786 )?;
1787 validate_required_platform("platform_proof.platform", self.platform)?;
1788 validate_candidate_identity(&self.candidate, true, logical)?;
1789 validate_safe_basename("platform proof", "archive_name", &self.archive_name)?;
1790 if self.archive_bytes == 0 || self.archive_bytes > OPTIONAL_PARSER_PACK_MAX_ARCHIVE_BYTES {
1791 return Err(invalid_field(
1792 self.archive_name.as_str(),
1793 "archive_bytes",
1794 "completed archive is empty or exceeds the compressed-byte ceiling",
1795 ));
1796 }
1797 if self.expanded_bytes == 0 || self.expanded_bytes > OPTIONAL_PARSER_PACK_MAX_EXPANDED_BYTES
1798 {
1799 return Err(invalid_field(
1800 self.archive_name.as_str(),
1801 "expanded_bytes",
1802 "expanded artifact is empty or exceeds the expanded-byte ceiling",
1803 ));
1804 }
1805 validate_binding(
1806 "platform_proof.capability_set_digest",
1807 logical.capability_set_digest(),
1808 &self.capability_set_digest,
1809 )?;
1810 validate_fresh_runner(&self.runner, self.platform)?;
1811 validate_grammar_probes(&self.grammars, logical)?;
1812 validate_memory_probe(&self.memory, self.platform)?;
1813 Ok(())
1814 }
1815}
1816
1817impl OptionalParserPackProofAggregate {
1818 pub fn validate(
1824 &self,
1825 logical: &OptionalParserPackManifest,
1826 ) -> Result<(), OptionalParserPackManifestError> {
1827 logical.validate()?;
1828 validate_binding(
1829 "proof_aggregate.schema_version",
1830 &OPTIONAL_PARSER_PACK_PROOF_AGGREGATE_SCHEMA_VERSION,
1831 &self.schema_version,
1832 )?;
1833 validate_binding(
1834 "proof_aggregate.pack_id",
1835 logical.pack_id(),
1836 self.pack_id.as_str(),
1837 )?;
1838 validate_binding(
1839 "proof_aggregate.projectatlas_version",
1840 logical.runtime().projectatlas_version.as_str(),
1841 self.projectatlas_version.as_str(),
1842 )?;
1843 validate_binding(
1844 "proof_aggregate.capability_set_digest",
1845 logical.capability_set_digest(),
1846 &self.capability_set_digest,
1847 )?;
1848 let platforms = self
1849 .platforms
1850 .iter()
1851 .map(|proof| proof.platform)
1852 .collect::<Vec<_>>();
1853 validate_required_platforms("proof aggregate", &platforms)?;
1854 let mut archive_names = BTreeSet::new();
1855 let mut archive_digests = BTreeSet::new();
1856 let first_candidate = self.platforms.first().map(|proof| &proof.candidate);
1857 for proof in &self.platforms {
1858 proof.validate(logical)?;
1859 validate_binding(
1860 "proof_aggregate.accepted_manifest_sha256",
1861 &self.accepted_manifest_sha256,
1862 &proof.accepted_manifest_sha256,
1863 )?;
1864 validate_binding(
1865 "proof_aggregate.capability_set_digest",
1866 &self.capability_set_digest,
1867 &proof.capability_set_digest,
1868 )?;
1869 validate_binding(
1870 "proof_aggregate.fixture_corpus_sha256",
1871 &self.fixture_corpus_sha256,
1872 &proof.fixture_corpus_sha256,
1873 )?;
1874 if first_candidate.is_some_and(|candidate| candidate != &proof.candidate) {
1875 return Err(invalid_field(
1876 self.pack_id.as_str(),
1877 "candidate",
1878 "platform proofs were not built from one exact candidate identity",
1879 ));
1880 }
1881 if !archive_names.insert(proof.archive_name.as_str()) {
1882 return Err(OptionalParserPackManifestError::DuplicateRuntimeIdentity {
1883 field: "archive_name",
1884 value: proof.archive_name.clone(),
1885 });
1886 }
1887 if !archive_digests.insert(proof.archive_sha256.as_str()) {
1888 return Err(OptionalParserPackManifestError::DuplicateRuntimeIdentity {
1889 field: "archive_sha256",
1890 value: proof.archive_sha256.as_str().to_string(),
1891 });
1892 }
1893 }
1894 Ok(())
1895 }
1896}
1897
1898fn validate_candidate_identity(
1900 candidate: &ParserPackCandidateIdentity,
1901 require_release_candidate: bool,
1902 logical: &OptionalParserPackManifest,
1903) -> Result<(), OptionalParserPackManifestError> {
1904 validate_identity(
1905 "candidate",
1906 "cargo_package_version",
1907 &candidate.cargo_package_version,
1908 )?;
1909 validate_binding(
1910 "candidate.intended_release_version",
1911 logical.runtime().projectatlas_version.as_str(),
1912 candidate.intended_release_version.as_str(),
1913 )?;
1914 validate_identity("candidate", "rustc_release", &candidate.rustc_release)?;
1915 validate_hex_value(
1916 "candidate rustc commit hash",
1917 "rustc_commit_hash",
1918 &candidate.rustc_commit_hash,
1919 40,
1920 )?;
1921 if require_release_candidate {
1922 validate_binding(
1923 "candidate.cargo_package_version",
1924 logical.runtime().projectatlas_version.as_str(),
1925 candidate.cargo_package_version.as_str(),
1926 )?;
1927 if candidate.source_state != ParserPackCandidateSourceState::Clean {
1928 return Err(invalid_field(
1929 candidate.projectatlas_revision.as_str(),
1930 "source_state",
1931 "fresh-runner proof requires one exact clean candidate commit",
1932 ));
1933 }
1934 }
1935 Ok(())
1936}
1937
1938fn validate_source_asset(
1940 asset: &ParserPackSourceAsset,
1941 logical: &OptionalParserPackManifest,
1942) -> Result<(), OptionalParserPackManifestError> {
1943 validate_binding(
1944 "source_asset.release_tag",
1945 logical.source().native_release.tag.as_str(),
1946 asset.release_tag.as_str(),
1947 )?;
1948 validate_binding(
1949 "source_asset.release_revision",
1950 &logical.source().native_release.revision,
1951 &asset.release_revision,
1952 )?;
1953 validate_safe_basename("source asset", "name", &asset.name)?;
1954 if asset.bytes == 0 || asset.bytes > OPTIONAL_PARSER_PACK_MAX_ARCHIVE_BYTES {
1955 return Err(invalid_field(
1956 asset.name.as_str(),
1957 "bytes",
1958 "source asset is empty or exceeds the acquisition ceiling",
1959 ));
1960 }
1961 Ok(())
1962}
1963
1964fn validate_offline_construction(
1966 construction: &ParserPackOfflineConstruction,
1967 platform: PackPlatform,
1968) -> Result<(), OptionalParserPackManifestError> {
1969 validate_network_denial(
1970 &construction.network_denial,
1971 platform,
1972 ParserPackNetworkIsolation::for_construction(platform),
1973 )
1974}
1975
1976fn validate_network_denial(
1978 denial: &ParserPackNetworkDenial,
1979 platform: PackPlatform,
1980 expected: ParserPackNetworkIsolation,
1981) -> Result<(), OptionalParserPackManifestError> {
1982 if denial.mechanism != expected {
1983 return Err(invalid_field(
1984 platform.as_str(),
1985 "network_isolation",
1986 "physical network-isolation mechanism does not match the platform",
1987 ));
1988 }
1989 if !denial.dns_denied || !denial.direct_tcp_denied || !denial.https_denied {
1990 return Err(invalid_field(
1991 platform.as_str(),
1992 "network_denial",
1993 "DNS, direct TCP, and HTTPS canaries must all be denied",
1994 ));
1995 }
1996 Ok(())
1997}
1998
1999fn validate_native_audit(
2001 audit: &ParserPackNativeAudit,
2002 expected_grammars: usize,
2003) -> Result<(), OptionalParserPackManifestError> {
2004 let audited = usize::try_from(audit.audited_libraries).map_err(|_error| {
2005 invalid_field(
2006 OPTIONAL_PARSER_PACK_ID,
2007 "audited_libraries",
2008 "native audit count cannot be represented",
2009 )
2010 })?;
2011 validate_binding(
2012 "native_audit.audited_libraries",
2013 &expected_grammars,
2014 &audited,
2015 )?;
2016 if audit.forbidden_imports != 0
2017 || audit.unexpected_dependencies != 0
2018 || audit.missing_exports != 0
2019 || audit.unexpected_exports != 0
2020 {
2021 return Err(invalid_field(
2022 OPTIONAL_PARSER_PACK_ID,
2023 "native_audit",
2024 "closed import, dependency, and export audit must have zero violations",
2025 ));
2026 }
2027 Ok(())
2028}
2029
2030fn validate_payload_files(
2032 artifact: &OptionalParserPackArtifactManifest,
2033 logical: &OptionalParserPackManifest,
2034) -> Result<(), OptionalParserPackManifestError> {
2035 let platform_fixed_files = OPTIONAL_PARSER_PACK_COMMON_PAYLOAD_FILES
2036 + usize::from(artifact.platform.containment_broker_file_name().is_some());
2037 let expected_files = logical
2038 .grammars()
2039 .len()
2040 .checked_add(platform_fixed_files)
2041 .ok_or_else(|| {
2042 invalid_field(
2043 logical.pack_id(),
2044 "files",
2045 "expected payload-file count overflowed",
2046 )
2047 })?;
2048 validate_count(
2049 "artifact payload files",
2050 artifact.files.len(),
2051 expected_files,
2052 expected_files,
2053 )?;
2054 if !strictly_sorted_by(&artifact.files, |file| file.path.as_str()) {
2055 return Err(OptionalParserPackManifestError::NotSortedUnique {
2056 field: "artifact payload paths",
2057 });
2058 }
2059 let grammar_by_id = logical
2060 .grammars()
2061 .iter()
2062 .map(|grammar| (grammar.language_id.as_str(), grammar))
2063 .collect::<BTreeMap<_, _>>();
2064 let mut fixed_roles = BTreeSet::new();
2065 let mut grammar_ids = BTreeSet::new();
2066 for file in &artifact.files {
2067 if file.path.as_str() == "artifact-manifest.json" {
2068 return Err(invalid_field(
2069 file.path.as_str(),
2070 "files",
2071 "artifact manifest must not recursively list itself",
2072 ));
2073 }
2074 let expected_path = match &file.role {
2075 ParserPackPayloadRole::Worker => {
2076 fixed_roles.insert("worker");
2077 artifact.platform.worker_file_name().to_string()
2078 }
2079 ParserPackPayloadRole::ContainmentBroker => {
2080 fixed_roles.insert("containment-broker");
2081 artifact
2082 .platform
2083 .containment_broker_file_name()
2084 .ok_or_else(|| {
2085 invalid_field(
2086 artifact.platform.as_str(),
2087 "files",
2088 "platform does not admit a runtime-containment broker",
2089 )
2090 })?
2091 .to_string()
2092 }
2093 ParserPackPayloadRole::AcceptedManifest => {
2094 fixed_roles.insert("accepted-manifest");
2095 validate_binding(
2096 "artifact.accepted_manifest_sha256",
2097 &artifact.accepted_manifest_sha256,
2098 &file.sha256,
2099 )?;
2100 "accepted-capabilities.json".to_string()
2101 }
2102 ParserPackPayloadRole::FixtureCorpus => {
2103 fixed_roles.insert("fixture-corpus");
2104 validate_binding(
2105 "artifact.fixture_corpus_sha256",
2106 &artifact.fixture_corpus_sha256,
2107 &file.sha256,
2108 )?;
2109 "optional-parser-pack-corpus.json".to_string()
2110 }
2111 ParserPackPayloadRole::ProjectLicense => {
2112 fixed_roles.insert("project-license");
2113 "LICENSE".to_string()
2114 }
2115 ParserPackPayloadRole::NativeImportPolicy => {
2116 if file.bytes > OPTIONAL_PARSER_PACK_NATIVE_IMPORT_POLICY_MAX_BYTES {
2117 return Err(invalid_field(
2118 file.path.as_str(),
2119 "bytes",
2120 "native-import policy exceeds its pre-containment byte ceiling",
2121 ));
2122 }
2123 fixed_roles.insert("native-import-policy");
2124 validate_binding(
2125 "artifact.native_audit.policy_sha256",
2126 &artifact.native_audit.policy_sha256,
2127 &file.sha256,
2128 )?;
2129 "native-import-policy.json".to_string()
2130 }
2131 ParserPackPayloadRole::NativeAuditReport => {
2132 fixed_roles.insert("native-audit-report");
2133 validate_binding(
2134 "artifact.native_audit.report_sha256",
2135 &artifact.native_audit.report_sha256,
2136 &file.sha256,
2137 )?;
2138 "native-audit-report.json".to_string()
2139 }
2140 ParserPackPayloadRole::GrammarLibrary { language_id } => {
2141 validate_record_id("artifact grammar", language_id)?;
2142 if !grammar_ids.insert(language_id.as_str()) {
2143 return Err(OptionalParserPackManifestError::DuplicateRuntimeIdentity {
2144 field: "artifact grammar",
2145 value: language_id.clone(),
2146 });
2147 }
2148 let grammar = grammar_by_id.get(language_id.as_str()).ok_or_else(|| {
2149 OptionalParserPackManifestError::UnknownOptionalLanguage {
2150 language_id: language_id.clone(),
2151 }
2152 })?;
2153 format!(
2154 "lib/{}",
2155 artifact
2156 .platform
2157 .grammar_library_file_name(&grammar.abi_export.library_stem)
2158 )
2159 }
2160 };
2161 validate_binding(
2162 "artifact payload path",
2163 expected_path.as_str(),
2164 file.path.as_str(),
2165 )?;
2166 }
2167 validate_count(
2168 "artifact fixed payload roles",
2169 fixed_roles.len(),
2170 platform_fixed_files,
2171 platform_fixed_files,
2172 )?;
2173 if grammar_by_id
2174 .keys()
2175 .copied()
2176 .ne(grammar_ids.iter().copied())
2177 {
2178 return Err(invalid_field(
2179 artifact.pack_id.as_str(),
2180 "grammar_set",
2181 "artifact grammar identities differ from the accepted logical manifest",
2182 ));
2183 }
2184 Ok(())
2185}
2186
2187fn validate_fresh_runner(
2189 runner: &ParserPackFreshRunner,
2190 platform: PackPlatform,
2191) -> Result<(), OptionalParserPackManifestError> {
2192 validate_network_denial(
2193 &runner.network_denial,
2194 platform,
2195 ParserPackNetworkIsolation::for_fresh_runner(platform),
2196 )
2197}
2198
2199fn validate_grammar_probes(
2201 probes: &[ParserPackGrammarProbe],
2202 logical: &OptionalParserPackManifest,
2203) -> Result<(), OptionalParserPackManifestError> {
2204 validate_count(
2205 "platform proof grammars",
2206 probes.len(),
2207 logical.grammars().len(),
2208 logical.grammars().len(),
2209 )?;
2210 if !strictly_sorted_by(probes, |probe| probe.language_id.as_str()) {
2211 return Err(OptionalParserPackManifestError::NotSortedUnique {
2212 field: "platform proof grammars",
2213 });
2214 }
2215 for (probe, grammar) in probes.iter().zip(logical.grammars()) {
2216 validate_binding(
2217 "platform proof language_id",
2218 grammar.language_id.as_str(),
2219 probe.language_id.as_str(),
2220 )?;
2221 if !probe.worker_probe_passed {
2222 return Err(invalid_field(
2223 probe.language_id.as_str(),
2224 "worker_probe_passed",
2225 "manifest approval, native loading, ABI, positive, and negative fixtures must pass",
2226 ));
2227 }
2228 }
2229 Ok(())
2230}
2231
2232fn validate_memory_probe(
2234 probe: &ParserPackMemoryProbe,
2235 platform: PackPlatform,
2236) -> Result<(), OptionalParserPackManifestError> {
2237 if probe.process_limit_bytes == 0
2238 || probe.process_limit_bytes > PARSER_WORKER_PROCESS_MEMORY_BYTES
2239 || probe.process_tree_limit_bytes < probe.process_limit_bytes
2240 || probe.process_tree_limit_bytes > PARSER_WORKER_JOB_MEMORY_BYTES
2241 {
2242 return Err(invalid_field(
2243 platform.as_str(),
2244 "memory_limits",
2245 "probe limits must be non-zero, ordered, and no stronger than the runtime ceilings",
2246 ));
2247 }
2248
2249 let sampled = match (platform, probe.control) {
2250 (PackPlatform::LinuxX86_64, ParserPackMemoryControl::LinuxProcStatus) => true,
2251 (PackPlatform::LinuxX86_64, ParserPackMemoryControl::LinuxCgroupV2)
2252 | (PackPlatform::WindowsX86_64, ParserPackMemoryControl::WindowsJobObject) => false,
2253 _ => {
2254 return Err(invalid_field(
2255 platform.as_str(),
2256 "memory.control",
2257 "memory control does not match the platform proof",
2258 ));
2259 }
2260 };
2261
2262 let expected_process_limit = match platform {
2263 PackPlatform::LinuxX86_64 => OPTIONAL_PARSER_PACK_LINUX_MEMORY_PROBE_BYTES,
2264 PackPlatform::WindowsX86_64 => OPTIONAL_PARSER_PACK_WINDOWS_MINIMUM_MEMORY_PROBE_BYTES,
2265 };
2266 if probe.process_limit_bytes != expected_process_limit
2267 || probe.process_tree_limit_bytes != expected_process_limit
2268 {
2269 return Err(invalid_field(
2270 platform.as_str(),
2271 "memory_limits",
2272 "memory probe must use the exact closed release-verification ceiling",
2273 ));
2274 }
2275
2276 if sampled {
2277 let Some(interval) = probe.observation_interval_millis else {
2278 return Err(invalid_field(
2279 platform.as_str(),
2280 "memory.observation_interval_millis",
2281 "sampled Linux RSS proof requires the declared observation interval",
2282 ));
2283 };
2284 let Some(peak) = probe.peak_observed_bytes else {
2285 return Err(invalid_field(
2286 platform.as_str(),
2287 "memory.peak_observed_bytes",
2288 "sampled Linux RSS proof requires the hosted peak observation",
2289 ));
2290 };
2291 let Some(overshoot) = probe.maximum_observed_overshoot_bytes else {
2292 return Err(invalid_field(
2293 platform.as_str(),
2294 "memory.maximum_observed_overshoot_bytes",
2295 "sampled Linux RSS proof requires the hosted maximum overshoot",
2296 ));
2297 };
2298 if interval == 0
2299 || peak < probe.process_limit_bytes
2300 || overshoot != peak.saturating_sub(probe.process_limit_bytes)
2301 {
2302 return Err(invalid_field(
2303 platform.as_str(),
2304 "memory.sampled_measurement",
2305 "sampled Linux RSS interval, peak, and overshoot are inconsistent",
2306 ));
2307 }
2308 } else if probe.observation_interval_millis.is_some()
2309 || probe.peak_observed_bytes.is_some()
2310 || probe.maximum_observed_overshoot_bytes.is_some()
2311 {
2312 return Err(invalid_field(
2313 platform.as_str(),
2314 "memory.hard_limit_measurement",
2315 "kernel-hard controls must not claim sampled RSS interval or overshoot values",
2316 ));
2317 }
2318 Ok(())
2319}
2320
2321fn validate_required_platform(
2323 owner: &'static str,
2324 platform: PackPlatform,
2325) -> Result<(), OptionalParserPackManifestError> {
2326 if !PackPlatform::ALL.contains(&platform) {
2327 return Err(invalid_field(
2328 owner,
2329 "platform",
2330 "platform is not part of the optional-pack artifact target set",
2331 ));
2332 }
2333 Ok(())
2334}
2335
2336fn validate_safe_basename(
2338 owner: &str,
2339 field: &'static str,
2340 value: &str,
2341) -> Result<(), OptionalParserPackManifestError> {
2342 if value.is_empty()
2343 || value.len() > OPTIONAL_PARSER_PACK_MAX_PATH_BYTES
2344 || !value
2345 .bytes()
2346 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
2347 {
2348 return Err(invalid_field(
2349 owner,
2350 field,
2351 "expected a safe ASCII basename within the pack path bound",
2352 ));
2353 }
2354 Ok(())
2355}
2356
2357fn validate_hex_value(
2359 owner: &str,
2360 field: &'static str,
2361 value: &str,
2362 expected_len: usize,
2363) -> Result<(), OptionalParserPackManifestError> {
2364 if value.len() != expected_len
2365 || !value
2366 .bytes()
2367 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2368 {
2369 return Err(invalid_field(
2370 owner,
2371 field,
2372 "expected canonical lowercase hexadecimal",
2373 ));
2374 }
2375 Ok(())
2376}
2377
2378fn validate_source(
2380 source: &OptionalParserPackSource,
2381) -> Result<(), OptionalParserPackManifestError> {
2382 validate_binding(
2383 "source.package",
2384 OPTIONAL_GRAMMAR_CATALOG,
2385 source.package.as_str(),
2386 )?;
2387 validate_binding(
2388 "source.version",
2389 OPTIONAL_GRAMMAR_CATALOG_VERSION,
2390 source.version.as_str(),
2391 )?;
2392 validate_binding(
2393 "source.cargo_archive.sha256",
2394 OPTIONAL_GRAMMAR_CATALOG_CRATE_SHA256,
2395 source.cargo_archive.sha256.as_str(),
2396 )?;
2397 validate_binding(
2398 "source.cargo_archive.vcs_revision",
2399 OPTIONAL_GRAMMAR_CATALOG_CRATE_REVISION,
2400 source.cargo_archive.vcs_revision.as_str(),
2401 )?;
2402 validate_binding(
2403 "source.cargo_archive.path_in_vcs",
2404 OPTIONAL_GRAMMAR_CATALOG_CRATE_PATH_IN_VCS,
2405 source.cargo_archive.path_in_vcs.as_str(),
2406 )?;
2407 validate_binding(
2408 "source.native_release.tag",
2409 OPTIONAL_GRAMMAR_CATALOG_RELEASE_TAG,
2410 source.native_release.tag.as_str(),
2411 )?;
2412 validate_binding(
2413 "source.native_release.revision",
2414 OPTIONAL_GRAMMAR_CATALOG_RELEASE_REVISION,
2415 source.native_release.revision.as_str(),
2416 )?;
2417 validate_binding(
2418 "source.native_release.source_bundle_sha256",
2419 OPTIONAL_GRAMMAR_CATALOG_SOURCE_BUNDLE_SHA256,
2420 source.native_release.source_bundle_sha256.as_str(),
2421 )
2422}
2423
2424fn validate_runtime(
2426 runtime: &OptionalParserPackRuntime,
2427) -> Result<(), OptionalParserPackManifestError> {
2428 validate_binding(
2429 "runtime.projectatlas_version",
2430 OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION,
2431 runtime.projectatlas_version.as_str(),
2432 )?;
2433 validate_binding(
2434 "runtime.tree_sitter_version",
2435 OPTIONAL_PARSER_PACK_TREE_SITTER_VERSION,
2436 runtime.tree_sitter_version.as_str(),
2437 )?;
2438 validate_binding(
2439 "runtime.minimum_abi",
2440 &OPTIONAL_PARSER_PACK_MINIMUM_ABI,
2441 &runtime.minimum_abi,
2442 )?;
2443 validate_binding(
2444 "runtime.maximum_abi",
2445 &OPTIONAL_PARSER_PACK_MAXIMUM_ABI,
2446 &runtime.maximum_abi,
2447 )
2448}
2449
2450fn validate_registry_binding(
2452 binding: &OptionalParserPackRegistryBinding,
2453) -> Result<(), OptionalParserPackManifestError> {
2454 let current = OptionalParserPackRegistryBinding::current()?;
2455 validate_binding(
2456 "registry.registry_version",
2457 ¤t.registry_version,
2458 &binding.registry_version,
2459 )?;
2460 validate_binding(
2461 "registry.registry_digest",
2462 current.registry_digest.as_str(),
2463 binding.registry_digest.as_str(),
2464 )?;
2465 validate_binding(
2466 "registry.accepted_set_version",
2467 ¤t.accepted_set_version,
2468 &binding.accepted_set_version,
2469 )?;
2470 validate_binding(
2471 "registry.accepted_set_digest",
2472 current.accepted_set_digest.as_str(),
2473 binding.accepted_set_digest.as_str(),
2474 )
2475}
2476
2477fn validate_license(license: &GrammarLicense) -> Result<(), OptionalParserPackManifestError> {
2479 validate_record_id("license", &license.id)?;
2480 validate_https_url(&license.id, &license.repository_url)?;
2481 validate_relative_path(&license.id, "source_path", &license.source_path, false)?;
2482 if license.text.is_empty() || license.text.len() > MAX_LICENSE_TEXT_BYTES {
2483 return Err(invalid_field(
2484 &license.id,
2485 "text",
2486 "expected 1..=262144 exact UTF-8 bytes",
2487 ));
2488 }
2489 if Blake3Digest::for_bytes(license.text.as_bytes()) != license.text_blake3 {
2490 return Err(OptionalParserPackManifestError::DigestMismatch {
2491 owner: license.id.clone(),
2492 field: "text_blake3",
2493 });
2494 }
2495 if let Some(expression) = &license.spdx_expression {
2496 validate_identity(&license.id, "spdx_expression", expression)?;
2497 }
2498 Ok(())
2499}
2500
2501fn validate_grammar(
2503 grammar: &AcceptedGrammar,
2504 runtime: &OptionalParserPackRuntime,
2505 required_platforms: &[PackPlatform],
2506 license_by_id: &BTreeMap<&str, &GrammarLicense>,
2507) -> Result<(), OptionalParserPackManifestError> {
2508 let Some(capability) = language_capability(&grammar.language_id) else {
2509 return Err(OptionalParserPackManifestError::UnknownOptionalLanguage {
2510 language_id: grammar.language_id.clone(),
2511 });
2512 };
2513 if capability.id != grammar.language_id {
2514 return Err(OptionalParserPackManifestError::UnknownOptionalLanguage {
2515 language_id: grammar.language_id.clone(),
2516 });
2517 }
2518 if capability.optional_pack != Some(OPTIONAL_PARSER_PACK_ID) {
2519 return Err(OptionalParserPackManifestError::BuiltInOverlap {
2520 language_id: grammar.language_id.clone(),
2521 });
2522 }
2523 validate_https_url(&grammar.language_id, &grammar.source.repository_url)?;
2524 validate_relative_path(
2525 &grammar.language_id,
2526 "subdirectory",
2527 &grammar.source.subdirectory,
2528 true,
2529 )?;
2530 if grammar.license_record_ids.is_empty() {
2531 return Err(invalid_field(
2532 &grammar.language_id,
2533 "license_record_ids",
2534 "expected at least one applicable exact license record",
2535 ));
2536 }
2537 if !strictly_sorted_by(&grammar.license_record_ids, String::as_str) {
2538 return Err(OptionalParserPackManifestError::NotSortedUnique {
2539 field: "grammar.license_record_ids",
2540 });
2541 }
2542 for license_id in &grammar.license_record_ids {
2543 let Some(license) = license_by_id.get(license_id.as_str()) else {
2544 return Err(OptionalParserPackManifestError::UnknownLicense {
2545 language_id: grammar.language_id.clone(),
2546 license_id: license_id.clone(),
2547 });
2548 };
2549 if license.repository_url != grammar.source.repository_url
2550 || license.revision != grammar.source.revision
2551 {
2552 return Err(OptionalParserPackManifestError::LicenseSourceMismatch {
2553 language_id: grammar.language_id.clone(),
2554 license_id: license_id.clone(),
2555 });
2556 }
2557 }
2558 if grammar.abi_export.minimum_abi == 0
2559 || grammar.abi_export.maximum_abi < grammar.abi_export.minimum_abi
2560 || grammar.abi_export.expected_abi < grammar.abi_export.minimum_abi
2561 || grammar.abi_export.expected_abi > grammar.abi_export.maximum_abi
2562 || grammar.abi_export.minimum_abi != runtime.minimum_abi
2563 || grammar.abi_export.maximum_abi != runtime.maximum_abi
2564 {
2565 return Err(OptionalParserPackManifestError::AbiMismatch {
2566 language_id: grammar.language_id.clone(),
2567 });
2568 }
2569 validate_fixture(&grammar.language_id, "positive", &grammar.fixtures.positive)?;
2570 validate_fixture(&grammar.language_id, "negative", &grammar.fixtures.negative)?;
2571 if !grammar.fixtures.positive.origin.is_positive()
2572 || !grammar.fixtures.negative.origin.is_negative()
2573 {
2574 return Err(invalid_field(
2575 &grammar.language_id,
2576 "fixtures.origin",
2577 "expected a natural upstream positive and an upstream-error or incomplete-editor-state negative",
2578 ));
2579 }
2580 if grammar.fixtures.positive.source_blake3 == grammar.fixtures.negative.source_blake3 {
2581 return Err(invalid_field(
2582 &grammar.language_id,
2583 "fixtures",
2584 "positive and negative fixture source must be distinct",
2585 ));
2586 }
2587 validate_required_platforms(&grammar.language_id, &grammar.required_platforms)?;
2588 if grammar.required_platforms != required_platforms {
2589 return Err(invalid_field(
2590 &grammar.language_id,
2591 "required_platforms",
2592 "grammar and manifest platform sets differ",
2593 ));
2594 }
2595 if grammar.built_in_precedence != BuiltInParserPrecedence::BuiltInAuthoritative {
2596 return Err(invalid_field(
2597 &grammar.language_id,
2598 "built_in_precedence",
2599 "default-core ownership must remain authoritative",
2600 ));
2601 }
2602 if grammar.computed_capability_digest() != grammar.capability_digest {
2603 return Err(OptionalParserPackManifestError::DigestMismatch {
2604 owner: grammar.language_id.clone(),
2605 field: "capability_digest",
2606 });
2607 }
2608 Ok(())
2609}
2610
2611fn validate_fixture(
2613 owner: &str,
2614 role: &'static str,
2615 fixture: &GrammarFixture,
2616) -> Result<(), OptionalParserPackManifestError> {
2617 validate_relative_path(owner, "fixture.path", &fixture.path, false)?;
2618 validate_identity(owner, "fixture.case_name", &fixture.case_name)?;
2619 if fixture.source.is_empty() || fixture.source.len() > MAX_FIXTURE_SOURCE_BYTES {
2620 return Err(invalid_field(
2621 owner,
2622 role,
2623 "expected 1..=65536 exact UTF-8 source bytes",
2624 ));
2625 }
2626 if Blake3Digest::for_bytes(fixture.source.as_bytes()) != fixture.source_blake3 {
2627 return Err(OptionalParserPackManifestError::DigestMismatch {
2628 owner: owner.to_string(),
2629 field: "fixture.source_blake3",
2630 });
2631 }
2632 Ok(())
2633}
2634
2635fn validate_required_platforms(
2637 owner: &str,
2638 platforms: &[PackPlatform],
2639) -> Result<(), OptionalParserPackManifestError> {
2640 if platforms != PackPlatform::ALL {
2641 return Err(invalid_field(
2642 owner,
2643 "required_platforms",
2644 "expected the complete canonical optional-pack artifact target set",
2645 ));
2646 }
2647 Ok(())
2648}
2649
2650fn validate_count(
2652 field: &'static str,
2653 actual: usize,
2654 minimum: usize,
2655 maximum: usize,
2656) -> Result<(), OptionalParserPackManifestError> {
2657 if !(minimum..=maximum).contains(&actual) {
2658 return Err(OptionalParserPackManifestError::CountOutOfBounds {
2659 field,
2660 actual,
2661 minimum,
2662 maximum,
2663 });
2664 }
2665 Ok(())
2666}
2667
2668fn validate_binding<T>(
2670 field: &'static str,
2671 expected: &T,
2672 actual: &T,
2673) -> Result<(), OptionalParserPackManifestError>
2674where
2675 T: Eq + fmt::Display + ?Sized,
2676{
2677 if expected != actual {
2678 return Err(OptionalParserPackManifestError::BindingMismatch {
2679 field,
2680 expected: expected.to_string(),
2681 actual: actual.to_string(),
2682 });
2683 }
2684 Ok(())
2685}
2686
2687fn validate_hex_digest(
2689 value: &str,
2690 field: &'static str,
2691) -> Result<(), OptionalParserPackManifestError> {
2692 if value.len() != 64
2693 || !value
2694 .bytes()
2695 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2696 {
2697 return Err(invalid_field(
2698 "digest",
2699 field,
2700 "expected 64 lowercase hexadecimal characters",
2701 ));
2702 }
2703 Ok(())
2704}
2705
2706fn validate_identity(
2708 owner: &str,
2709 field: &'static str,
2710 value: &str,
2711) -> Result<(), OptionalParserPackManifestError> {
2712 if value.is_empty()
2713 || value.len() > MAX_IDENTITY_BYTES
2714 || value.trim() != value
2715 || value.chars().any(char::is_control)
2716 {
2717 return Err(invalid_field(
2718 owner,
2719 field,
2720 "expected non-empty, unpadded, control-free bounded text",
2721 ));
2722 }
2723 Ok(())
2724}
2725
2726fn validate_record_id(owner: &str, value: &str) -> Result<(), OptionalParserPackManifestError> {
2728 validate_identity(owner, "id", value)?;
2729 if !value.bytes().all(|byte| {
2730 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_' | b'-')
2731 }) || !value
2732 .as_bytes()
2733 .first()
2734 .is_some_and(u8::is_ascii_alphanumeric)
2735 {
2736 return Err(invalid_field(
2737 owner,
2738 "id",
2739 "expected a lowercase ASCII record identifier",
2740 ));
2741 }
2742 Ok(())
2743}
2744
2745fn validate_https_url(owner: &str, value: &str) -> Result<(), OptionalParserPackManifestError> {
2747 validate_identity(owner, "repository_url", value)?;
2748 if !value.starts_with("https://") || value.contains(['?', '#']) {
2749 return Err(invalid_field(
2750 owner,
2751 "repository_url",
2752 "expected an HTTPS repository URL without query or fragment",
2753 ));
2754 }
2755 Ok(())
2756}
2757
2758fn validate_relative_path(
2760 owner: &str,
2761 field: &'static str,
2762 value: &str,
2763 allow_root: bool,
2764) -> Result<(), OptionalParserPackManifestError> {
2765 validate_identity(owner, field, value)?;
2766 if allow_root && value == "." {
2767 return Ok(());
2768 }
2769 if value.starts_with('/')
2770 || value.ends_with('/')
2771 || value.contains('\\')
2772 || value
2773 .split('/')
2774 .any(|part| part.is_empty() || matches!(part, "." | ".."))
2775 {
2776 return Err(invalid_field(
2777 owner,
2778 field,
2779 "expected a normalized repository-relative slash path",
2780 ));
2781 }
2782 Ok(())
2783}
2784
2785fn invalid_field(
2787 owner: &str,
2788 field: &'static str,
2789 reason: &'static str,
2790) -> OptionalParserPackManifestError {
2791 OptionalParserPackManifestError::InvalidField {
2792 owner: owner.to_string(),
2793 field,
2794 reason,
2795 }
2796}
2797
2798fn strictly_sorted_by<T>(values: &[T], key: impl Fn(&T) -> &str) -> bool {
2800 values.windows(2).all(|pair| key(&pair[0]) < key(&pair[1]))
2801}
2802
2803fn hash_source_provenance(hasher: &mut Hasher, source: &GrammarSourceProvenance) {
2805 hash_value(hasher, &source.repository_url);
2806 hash_value(hasher, source.revision.as_str());
2807 hash_value(hasher, &source.subdirectory);
2808 hash_value(hasher, source.compile_input_sha256.as_str());
2809}
2810
2811fn hash_fixture(hasher: &mut Hasher, fixture: &GrammarFixture) {
2813 hash_value(hasher, fixture.origin.canonical_name());
2814 hash_value(hasher, &fixture.path);
2815 hash_value(hasher, &fixture.case_name);
2816 hash_value(hasher, fixture.source_blake3.as_str());
2817}
2818
2819fn hash_value(hasher: &mut Hasher, value: &str) {
2821 hasher.update(&(value.len() as u64).to_le_bytes());
2822 hasher.update(value.as_bytes());
2823}
2824
2825#[cfg(test)]
2826mod tests {
2827 use super::*;
2828 use crate::language::language_documentation_rows;
2829 use std::error::Error;
2830 use std::io;
2831
2832 const RELEASE_REVISION: &str = "6258abac30304283763a0d2dc8a48cb87fbcf438";
2833 const CRATE_VCS_REVISION: &str = "ce9e9c0974731d25b4b9426711a62d544d993368";
2834 const CRATE_SHA256: &str = "44dc94ef7a5f7f4247d88d5acdd26d842c8fc6f5eaf491a970c8e3d8fc9c9287";
2835 const SOURCE_BUNDLE_SHA256: &str =
2836 "d684799dc664553c9c746d5fe676a5b599f9efcec4cad5450bec7ec5a29574a9";
2837 const REPOSITORY_URL: &str = "https://example.invalid/optional-grammars.git";
2838
2839 fn require(condition: bool, message: &'static str) -> Result<(), Box<dyn Error>> {
2841 if condition {
2842 Ok(())
2843 } else {
2844 Err(io::Error::other(message).into())
2845 }
2846 }
2847
2848 #[test]
2849 fn capability_authority_keeps_macos_arm64_builtin_only() -> Result<(), Box<dyn Error>> {
2850 let macos_arm64 = OptionalParserCapability::for_target("macos", "aarch64");
2851 require(
2852 macos_arm64 == OptionalParserCapability::BuiltInOnly
2853 && macos_arm64.pack_platform().is_none()
2854 && macos_arm64.built_in_parsing_available(),
2855 "macOS arm64 optional-parser capability drifted from built-in-only",
2856 )?;
2857 require(
2858 OptionalParserCapability::for_target("linux", "x86_64")
2859 == OptionalParserCapability::Pack {
2860 platform: PackPlatform::LinuxX86_64,
2861 }
2862 && OptionalParserCapability::for_target("windows", "x86_64")
2863 == OptionalParserCapability::Pack {
2864 platform: PackPlatform::WindowsX86_64,
2865 },
2866 "supported optional-parser platform capability drifted",
2867 )
2868 }
2869
2870 fn test_manifest() -> Result<OptionalParserPackManifest, Box<dyn Error>> {
2871 let revision = SourceRevision::new(RELEASE_REVISION)?;
2872 let licenses = vec![
2873 GrammarLicense::new(
2874 "apache-root",
2875 REPOSITORY_URL,
2876 "LICENSE-APACHE",
2877 revision.clone(),
2878 "Apache License\nVersion 2.0 fixture text.",
2879 Some("Apache-2.0".to_string()),
2880 ),
2881 GrammarLicense::new(
2882 "mit-root",
2883 REPOSITORY_URL,
2884 "LICENSE-MIT",
2885 revision.clone(),
2886 "MIT License\n\nPermission is hereby granted for the fixture.",
2887 Some("MIT".to_string()),
2888 ),
2889 ];
2890 let mut grammars = language_documentation_rows()
2891 .iter()
2892 .filter(|capability| capability.optional_pack.is_some())
2893 .take(OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS)
2894 .enumerate()
2895 .map(|(index, capability)| {
2896 let ordinal = index + 1;
2897 let license_record_ids = if index == 0 {
2898 vec!["apache-root".to_string(), "mit-root".to_string()]
2899 } else {
2900 vec!["mit-root".to_string()]
2901 };
2902 Ok(AcceptedGrammar::new(
2903 capability.id,
2904 GrammarSourceProvenance {
2905 repository_url: REPOSITORY_URL.to_string(),
2906 revision: revision.clone(),
2907 subdirectory: format!("grammars/{ordinal}"),
2908 compile_input_sha256: Sha256Digest::new(format!("{ordinal:064x}"))?,
2909 },
2910 license_record_ids,
2911 GrammarAbiExport {
2912 minimum_abi: 13,
2913 maximum_abi: 15,
2914 expected_abi: 15,
2915 export_symbol: GrammarExportSymbol::new(format!(
2916 "tree_sitter_optional_{ordinal}"
2917 ))?,
2918 library_stem: GrammarLibraryStem::new(format!(
2919 "tree-sitter-optional-{ordinal}"
2920 ))?,
2921 },
2922 GrammarFixtures {
2923 positive: GrammarFixture::new(
2924 GrammarFixtureOrigin::UpstreamTreeSitterCorpus,
2925 format!("fixtures/{ordinal}/positive.txt"),
2926 format!("natural positive {}", capability.id),
2927 format!("natural positive {} source\n", capability.id),
2928 ),
2929 negative: GrammarFixture::new(
2930 GrammarFixtureOrigin::ProjectAtlasIncompleteUpstreamCase,
2931 format!("fixtures/{ordinal}/negative.txt"),
2932 format!("natural negative {} incomplete", capability.id),
2933 format!("natural negative {} source ?\n", capability.id),
2934 ),
2935 },
2936 ))
2937 })
2938 .collect::<Result<Vec<_>, OptionalParserPackManifestError>>()?;
2939 grammars.sort_by(|left, right| left.language_id.cmp(&right.language_id));
2940 OptionalParserPackManifest::new(
2941 OptionalParserPackSource {
2942 package: OPTIONAL_GRAMMAR_CATALOG.to_string(),
2943 version: OPTIONAL_GRAMMAR_CATALOG_VERSION.to_string(),
2944 cargo_archive: OptionalParserCargoArchive {
2945 sha256: Sha256Digest::new(CRATE_SHA256)?,
2946 vcs_revision: SourceRevision::new(CRATE_VCS_REVISION)?,
2947 path_in_vcs: OPTIONAL_GRAMMAR_CATALOG_CRATE_PATH_IN_VCS.to_string(),
2948 },
2949 native_release: OptionalParserNativeRelease {
2950 tag: OPTIONAL_GRAMMAR_CATALOG_RELEASE_TAG.to_string(),
2951 revision,
2952 source_bundle_sha256: Sha256Digest::new(SOURCE_BUNDLE_SHA256)?,
2953 },
2954 },
2955 OptionalParserPackRuntime {
2956 consumer: ParserPackConsumer::ProjectAtlasParserWorker,
2957 projectatlas_version: OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION.to_string(),
2958 tree_sitter_version: OPTIONAL_PARSER_PACK_TREE_SITTER_VERSION.to_string(),
2959 minimum_abi: 13,
2960 maximum_abi: 15,
2961 },
2962 licenses,
2963 grammars,
2964 )
2965 .map_err(Into::into)
2966 }
2967
2968 fn test_construction_network_denial(platform: PackPlatform) -> ParserPackNetworkDenial {
2969 ParserPackNetworkDenial {
2970 mechanism: ParserPackNetworkIsolation::for_construction(platform),
2971 dns_denied: true,
2972 direct_tcp_denied: true,
2973 https_denied: true,
2974 }
2975 }
2976
2977 fn test_fresh_runner_network_denial(platform: PackPlatform) -> ParserPackNetworkDenial {
2978 ParserPackNetworkDenial {
2979 mechanism: ParserPackNetworkIsolation::for_fresh_runner(platform),
2980 dns_denied: true,
2981 direct_tcp_denied: true,
2982 https_denied: true,
2983 }
2984 }
2985
2986 fn test_candidate(
2987 state: ParserPackCandidateSourceState,
2988 cargo_package_version: &str,
2989 ) -> Result<ParserPackCandidateIdentity, OptionalParserPackManifestError> {
2990 Ok(ParserPackCandidateIdentity {
2991 projectatlas_revision: SourceRevision::new(CRATE_VCS_REVISION)?,
2992 cargo_package_version: cargo_package_version.to_string(),
2993 intended_release_version: OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION.to_string(),
2994 cargo_lock_sha256: Sha256Digest::new(format!("{:064x}", 31))?,
2995 rustc_release: "1.88.0".to_string(),
2996 rustc_commit_hash: "01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf".to_string(),
2997 source_state: state,
2998 })
2999 }
3000
3001 fn test_artifact(
3002 logical: &OptionalParserPackManifest,
3003 platform: PackPlatform,
3004 ) -> Result<OptionalParserPackArtifactManifest, Box<dyn Error>> {
3005 let mut files = vec![
3006 ParserPackPayloadFile {
3007 path: PackRelativePath::new(platform.worker_file_name())?,
3008 role: ParserPackPayloadRole::Worker,
3009 bytes: 10,
3010 sha256: Sha256Digest::new(format!("{:064x}", 1))?,
3011 },
3012 ParserPackPayloadFile {
3013 path: PackRelativePath::new("accepted-capabilities.json")?,
3014 role: ParserPackPayloadRole::AcceptedManifest,
3015 bytes: 11,
3016 sha256: Sha256Digest::new(format!("{:064x}", 21))?,
3017 },
3018 ParserPackPayloadFile {
3019 path: PackRelativePath::new("optional-parser-pack-corpus.json")?,
3020 role: ParserPackPayloadRole::FixtureCorpus,
3021 bytes: 12,
3022 sha256: Sha256Digest::new(format!("{:064x}", 22))?,
3023 },
3024 ParserPackPayloadFile {
3025 path: PackRelativePath::new("LICENSE")?,
3026 role: ParserPackPayloadRole::ProjectLicense,
3027 bytes: 13,
3028 sha256: Sha256Digest::new(format!("{:064x}", 4))?,
3029 },
3030 ParserPackPayloadFile {
3031 path: PackRelativePath::new("native-import-policy.json")?,
3032 role: ParserPackPayloadRole::NativeImportPolicy,
3033 bytes: 14,
3034 sha256: Sha256Digest::new(format!("{:064x}", 25))?,
3035 },
3036 ParserPackPayloadFile {
3037 path: PackRelativePath::new("native-audit-report.json")?,
3038 role: ParserPackPayloadRole::NativeAuditReport,
3039 bytes: 15,
3040 sha256: Sha256Digest::new(format!("{:064x}", 26))?,
3041 },
3042 ];
3043 if let Some(broker_name) = platform.containment_broker_file_name() {
3044 files.push(ParserPackPayloadFile {
3045 path: PackRelativePath::new(broker_name)?,
3046 role: ParserPackPayloadRole::ContainmentBroker,
3047 bytes: 16,
3048 sha256: Sha256Digest::new(format!("{:064x}", 28))?,
3049 });
3050 }
3051 for (index, grammar) in logical.grammars().iter().enumerate() {
3052 files.push(ParserPackPayloadFile {
3053 path: PackRelativePath::new(format!(
3054 "lib/{}",
3055 platform.grammar_library_file_name(&grammar.abi_export.library_stem)
3056 ))?,
3057 role: ParserPackPayloadRole::GrammarLibrary {
3058 language_id: grammar.language_id.clone(),
3059 },
3060 bytes: u64::try_from(index)?.saturating_add(100),
3061 sha256: Sha256Digest::new(format!("{:064x}", index + 100))?,
3062 });
3063 }
3064 files.sort_by(|left, right| left.path.cmp(&right.path));
3065 let measurements = ParserPackPayloadMeasurements::from_files(&files)?;
3066 Ok(OptionalParserPackArtifactManifest {
3067 schema_version: OPTIONAL_PARSER_PACK_ARTIFACT_SCHEMA_VERSION,
3068 pack_id: logical.pack_id().to_string(),
3069 projectatlas_version: logical.runtime().projectatlas_version.clone(),
3070 platform,
3071 candidate: test_candidate(ParserPackCandidateSourceState::Dirty, "0.3.26")?,
3072 accepted_manifest_sha256: Sha256Digest::new(format!("{:064x}", 21))?,
3073 capability_set_digest: logical.capability_set_digest().clone(),
3074 fixture_corpus_sha256: Sha256Digest::new(format!("{:064x}", 22))?,
3075 source_asset: ParserPackSourceAsset {
3076 release_tag: logical.source().native_release.tag.clone(),
3077 release_revision: logical.source().native_release.revision.clone(),
3078 name: format!("parsers-{}.tar.zst", platform.as_str()),
3079 sha256: Sha256Digest::new(format!("{:064x}", 23))?,
3080 bytes: 1_024,
3081 parsers_manifest_sha256: Sha256Digest::new(format!("{:064x}", 24))?,
3082 },
3083 construction: ParserPackOfflineConstruction {
3084 cargo_frozen: ParserPackVerifiedControl::Verified,
3085 cargo_offline: ParserPackVerifiedControl::Verified,
3086 dependency_offline: ParserPackVerifiedControl::Verified,
3087 zero_embedded_grammars: ParserPackVerifiedControl::Verified,
3088 language_selector_absent: ParserPackVerifiedControl::Verified,
3089 failed_grammar_override_absent: ParserPackVerifiedControl::Verified,
3090 network_denial: test_construction_network_denial(platform),
3091 },
3092 native_audit: ParserPackNativeAudit {
3093 policy_sha256: Sha256Digest::new(format!("{:064x}", 25))?,
3094 report_sha256: Sha256Digest::new(format!("{:064x}", 26))?,
3095 audited_libraries: u32::try_from(logical.grammars().len())?,
3096 forbidden_imports: 0,
3097 unexpected_dependencies: 0,
3098 missing_exports: 0,
3099 unexpected_exports: 0,
3100 },
3101 measurements,
3102 files,
3103 })
3104 }
3105
3106 fn test_platform_proof(
3107 logical: &OptionalParserPackManifest,
3108 platform: PackPlatform,
3109 ordinal: usize,
3110 ) -> Result<OptionalParserPackPlatformProof, Box<dyn Error>> {
3111 Ok(OptionalParserPackPlatformProof {
3112 schema_version: OPTIONAL_PARSER_PACK_PLATFORM_PROOF_SCHEMA_VERSION,
3113 pack_id: logical.pack_id().to_string(),
3114 platform,
3115 candidate: test_candidate(
3116 ParserPackCandidateSourceState::Clean,
3117 OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION,
3118 )?,
3119 archive_name: format!("projectatlas-broad-parser-{}.tar.zst", platform.as_str()),
3120 archive_sha256: Sha256Digest::new(format!("{:064x}", ordinal + 40))?,
3121 archive_bytes: 1_024,
3122 expanded_bytes: 4_096,
3123 artifact_manifest_sha256: Sha256Digest::new(format!("{:064x}", ordinal + 50))?,
3124 accepted_manifest_sha256: Sha256Digest::new(format!("{:064x}", 21))?,
3125 capability_set_digest: logical.capability_set_digest().clone(),
3126 fixture_corpus_sha256: Sha256Digest::new(format!("{:064x}", 22))?,
3127 native_audit_report_sha256: Sha256Digest::new(format!("{:064x}", ordinal + 60))?,
3128 runner: ParserPackFreshRunner {
3129 fresh_host: ParserPackVerifiedControl::Verified,
3130 repository_inputs_absent: ParserPackVerifiedControl::Verified,
3131 build_tools_not_invoked: ParserPackVerifiedControl::Verified,
3132 working_directory_outside_pack: ParserPackVerifiedControl::Verified,
3133 ambient_library_paths_cleared: ParserPackVerifiedControl::Verified,
3134 network_denial: test_fresh_runner_network_denial(platform),
3135 },
3136 grammars: logical
3137 .grammars()
3138 .iter()
3139 .map(|grammar| ParserPackGrammarProbe {
3140 language_id: grammar.language_id.clone(),
3141 worker_probe_passed: true,
3142 })
3143 .collect(),
3144 memory: match platform {
3145 PackPlatform::LinuxX86_64 => ParserPackMemoryProbe {
3146 control: ParserPackMemoryControl::LinuxProcStatus,
3147 process_limit_bytes: OPTIONAL_PARSER_PACK_LINUX_MEMORY_PROBE_BYTES,
3148 process_tree_limit_bytes: OPTIONAL_PARSER_PACK_LINUX_MEMORY_PROBE_BYTES,
3149 observation_interval_millis: Some(20),
3150 peak_observed_bytes: Some(1024 * 1024 + 4096),
3151 maximum_observed_overshoot_bytes: Some(4096),
3152 limit_enforced: ParserPackVerifiedControl::Verified,
3153 process_tree_cleaned: ParserPackVerifiedControl::Verified,
3154 },
3155 PackPlatform::WindowsX86_64 => ParserPackMemoryProbe {
3156 control: ParserPackMemoryControl::WindowsJobObject,
3157 process_limit_bytes: OPTIONAL_PARSER_PACK_WINDOWS_MINIMUM_MEMORY_PROBE_BYTES,
3158 process_tree_limit_bytes:
3159 OPTIONAL_PARSER_PACK_WINDOWS_MINIMUM_MEMORY_PROBE_BYTES,
3160 observation_interval_millis: None,
3161 peak_observed_bytes: None,
3162 maximum_observed_overshoot_bytes: None,
3163 limit_enforced: ParserPackVerifiedControl::Verified,
3164 process_tree_cleaned: ParserPackVerifiedControl::Verified,
3165 },
3166 },
3167 })
3168 }
3169
3170 #[test]
3171 fn valid_manifest_round_trips_with_complete_optional_floor() -> Result<(), Box<dyn Error>> {
3172 let manifest = test_manifest()?;
3173 manifest.validate()?;
3174 require(
3175 manifest.grammars.len() >= OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS,
3176 "test manifest did not cover the accepted optional grammar floor",
3177 )?;
3178 require(
3179 manifest
3180 .grammars
3181 .iter()
3182 .any(|grammar| grammar.license_record_ids.len() == 2),
3183 "valid dual-licensed grammar was not retained as a license-record set",
3184 )?;
3185 let encoded = serde_json::to_vec(&manifest)?;
3186 let decoded = OptionalParserPackManifest::from_json(&encoded)?;
3187 require(
3188 decoded == manifest,
3189 "validated manifest JSON did not round-trip",
3190 )?;
3191 Ok(())
3192 }
3193
3194 #[test]
3195 fn external_release_json_rejects_unknown_fields() -> Result<(), Box<dyn Error>> {
3196 let manifest = test_manifest()?;
3197
3198 let mut logical_root = serde_json::to_value(&manifest)?;
3199 logical_root
3200 .as_object_mut()
3201 .ok_or_else(|| io::Error::other("logical manifest is not an object"))?
3202 .insert("unmodeled_claim".to_string(), serde_json::Value::Bool(true));
3203 require(
3204 OptionalParserPackManifest::from_json(&serde_json::to_vec(&logical_root)?).is_err(),
3205 "logical manifest accepted an unknown root field",
3206 )?;
3207
3208 let mut logical_nested = serde_json::to_value(&manifest)?;
3209 logical_nested["grammars"][0]["fixtures"]["positive"]
3210 .as_object_mut()
3211 .ok_or_else(|| io::Error::other("positive fixture is not an object"))?
3212 .insert("unmodeled_claim".to_string(), serde_json::Value::Bool(true));
3213 require(
3214 OptionalParserPackManifest::from_json(&serde_json::to_vec(&logical_nested)?).is_err(),
3215 "logical manifest accepted an unknown nested field",
3216 )?;
3217
3218 let artifact = test_artifact(&manifest, PackPlatform::LinuxX86_64)?;
3219 let mut artifact_json = serde_json::to_value(&artifact)?;
3220 artifact_json["candidate"]
3221 .as_object_mut()
3222 .ok_or_else(|| io::Error::other("artifact candidate is not an object"))?
3223 .insert("unmodeled_claim".to_string(), serde_json::Value::Bool(true));
3224 require(
3225 serde_json::from_value::<OptionalParserPackArtifactManifest>(artifact_json).is_err(),
3226 "artifact manifest accepted an unknown nested field",
3227 )?;
3228
3229 let platform_proof = test_platform_proof(&manifest, PackPlatform::LinuxX86_64, 0)?;
3230 let mut proof_json = serde_json::to_value(&platform_proof)?;
3231 proof_json
3232 .as_object_mut()
3233 .ok_or_else(|| io::Error::other("platform proof is not an object"))?
3234 .insert("unmodeled_claim".to_string(), serde_json::Value::Bool(true));
3235 require(
3236 serde_json::from_value::<OptionalParserPackPlatformProof>(proof_json).is_err(),
3237 "platform proof accepted an unknown root field",
3238 )?;
3239
3240 let aggregate = OptionalParserPackProofAggregate {
3241 schema_version: OPTIONAL_PARSER_PACK_PROOF_AGGREGATE_SCHEMA_VERSION,
3242 pack_id: manifest.pack_id().to_string(),
3243 projectatlas_version: manifest.runtime().projectatlas_version.clone(),
3244 accepted_manifest_sha256: platform_proof.accepted_manifest_sha256.clone(),
3245 capability_set_digest: manifest.capability_set_digest().clone(),
3246 fixture_corpus_sha256: platform_proof.fixture_corpus_sha256.clone(),
3247 platforms: vec![platform_proof],
3248 };
3249 let mut aggregate_json = serde_json::to_value(aggregate)?;
3250 aggregate_json
3251 .as_object_mut()
3252 .ok_or_else(|| io::Error::other("proof aggregate is not an object"))?
3253 .insert("unmodeled_claim".to_string(), serde_json::Value::Bool(true));
3254 require(
3255 serde_json::from_value::<OptionalParserPackProofAggregate>(aggregate_json).is_err(),
3256 "proof aggregate accepted an unknown root field",
3257 )?;
3258 Ok(())
3259 }
3260
3261 #[test]
3262 fn manifest_rejects_built_in_overlap_and_capability_shrinkage() -> Result<(), Box<dyn Error>> {
3263 let mut overlap = test_manifest()?;
3264 overlap.grammars[0].language_id = "rust".to_string();
3265 overlap
3266 .grammars
3267 .sort_by(|left, right| left.language_id.cmp(&right.language_id));
3268 require(
3269 matches!(
3270 overlap.validate(),
3271 Err(OptionalParserPackManifestError::BuiltInOverlap { .. })
3272 ),
3273 "default-core grammar overlap was accepted",
3274 )?;
3275
3276 let mut too_small = test_manifest()?;
3277 too_small
3278 .grammars
3279 .truncate(OPTIONAL_PACK_MINIMUM_ADDITIONAL_GRAMMARS - 1);
3280 require(
3281 matches!(
3282 too_small.validate(),
3283 Err(OptionalParserPackManifestError::CountOutOfBounds {
3284 field: "grammars",
3285 ..
3286 })
3287 ),
3288 "accepted grammar floor shrinkage was accepted",
3289 )?;
3290 Ok(())
3291 }
3292
3293 #[test]
3294 fn manifest_rejects_nondeterminism_unknown_licenses_and_runtime_collisions()
3295 -> Result<(), Box<dyn Error>> {
3296 let mut unsorted = test_manifest()?;
3297 unsorted.grammars.swap(0, 1);
3298 require(
3299 matches!(
3300 unsorted.validate(),
3301 Err(OptionalParserPackManifestError::NotSortedUnique { field: "grammars" })
3302 ),
3303 "unsorted grammar rows were accepted",
3304 )?;
3305
3306 let mut missing_license = test_manifest()?;
3307 missing_license.grammars[0].license_record_ids = vec!["missing".to_string()];
3308 require(
3309 matches!(
3310 missing_license.validate(),
3311 Err(OptionalParserPackManifestError::UnknownLicense { .. })
3312 ),
3313 "unknown license reference was accepted",
3314 )?;
3315
3316 let mut duplicate_symbol = test_manifest()?;
3317 duplicate_symbol.grammars[1].abi_export.export_symbol = duplicate_symbol.grammars[0]
3318 .abi_export
3319 .export_symbol
3320 .clone();
3321 duplicate_symbol.grammars[1].capability_digest =
3322 duplicate_symbol.grammars[1].computed_capability_digest();
3323 require(
3324 matches!(
3325 duplicate_symbol.validate(),
3326 Err(OptionalParserPackManifestError::DuplicateRuntimeIdentity {
3327 field: "export_symbol",
3328 ..
3329 })
3330 ),
3331 "duplicate grammar export symbol was accepted",
3332 )?;
3333 Ok(())
3334 }
3335
3336 #[test]
3337 fn manifest_rejects_tampered_content_abi_and_registry_binding() -> Result<(), Box<dyn Error>> {
3338 let mut tampered_license = test_manifest()?;
3339 tampered_license.licenses[0].text.push_str("\ntampered");
3340 require(
3341 matches!(
3342 tampered_license.validate(),
3343 Err(OptionalParserPackManifestError::DigestMismatch {
3344 field: "text_blake3",
3345 ..
3346 })
3347 ),
3348 "tampered exact license text was accepted",
3349 )?;
3350
3351 let mut invalid_abi = test_manifest()?;
3352 invalid_abi.grammars[0].abi_export.expected_abi = 16;
3353 invalid_abi.grammars[0].capability_digest =
3354 invalid_abi.grammars[0].computed_capability_digest();
3355 require(
3356 matches!(
3357 invalid_abi.validate(),
3358 Err(OptionalParserPackManifestError::AbiMismatch { .. })
3359 ),
3360 "grammar ABI outside the runtime window was accepted",
3361 )?;
3362
3363 let mut wrong_fixture_role = test_manifest()?;
3364 wrong_fixture_role.grammars[0].fixtures.negative.origin =
3365 GrammarFixtureOrigin::UpstreamTreeSitterCorpus;
3366 wrong_fixture_role.grammars[0].capability_digest =
3367 wrong_fixture_role.grammars[0].computed_capability_digest();
3368 require(
3369 matches!(
3370 wrong_fixture_role.validate(),
3371 Err(OptionalParserPackManifestError::InvalidField {
3372 field: "fixtures.origin",
3373 ..
3374 })
3375 ),
3376 "a natural-positive origin was accepted for a negative fixture",
3377 )?;
3378
3379 let mut stale_registry = test_manifest()?;
3380 stale_registry.registry.registry_version += 1;
3381 require(
3382 matches!(
3383 stale_registry.validate(),
3384 Err(OptionalParserPackManifestError::BindingMismatch {
3385 field: "registry.registry_version",
3386 ..
3387 })
3388 ),
3389 "stale language-registry binding was accepted",
3390 )?;
3391 Ok(())
3392 }
3393
3394 #[test]
3395 fn validated_loader_identities_reject_abbreviated_or_unsafe_values() {
3396 assert!(SourceRevision::new("6258abac").is_err());
3397 assert!(Sha256Digest::new("ABCDEF").is_err());
3398 assert!(GrammarExportSymbol::new("../tree_sitter_bad").is_err());
3399 assert!(GrammarLibraryStem::new("tree-sitter.dll").is_err());
3400 assert!(PackRelativePath::new("../artifact-manifest.json").is_err());
3401 assert!(PackRelativePath::new("lib\\tree_sitter_bad.dll").is_err());
3402 assert!(PackRelativePath::new("/absolute/path").is_err());
3403 }
3404
3405 #[test]
3406 fn artifact_manifest_requires_exact_payload_and_closed_audit() -> Result<(), Box<dyn Error>> {
3407 let logical = test_manifest()?;
3408 let artifact = test_artifact(&logical, PackPlatform::WindowsX86_64)?;
3409 artifact.validate(&logical)?;
3410 require(
3411 artifact.files.iter().any(|file| {
3412 matches!(&file.role, ParserPackPayloadRole::ContainmentBroker)
3413 && file.path.as_str() == "projectatlas-parser-containment.exe"
3414 }),
3415 "Windows artifact did not retain its runtime-containment broker",
3416 )?;
3417 require(
3418 usize::try_from(artifact.measurements.grammar_libraries)? == logical.grammars().len(),
3419 "artifact did not retain the exact accepted grammar count",
3420 )?;
3421
3422 let mut wrong_construction_isolation = artifact.clone();
3423 wrong_construction_isolation
3424 .construction
3425 .network_denial
3426 .mechanism = ParserPackNetworkIsolation::WindowsAppContainer;
3427 require(
3428 wrong_construction_isolation.validate(&logical).is_err(),
3429 "Windows construction accepted its fresh-verification isolation mechanism",
3430 )?;
3431 let mut cross_platform_construction_isolation = artifact.clone();
3432 cross_platform_construction_isolation
3433 .construction
3434 .network_denial
3435 .mechanism = ParserPackNetworkIsolation::LinuxNetworkNamespace;
3436 require(
3437 cross_platform_construction_isolation
3438 .validate(&logical)
3439 .is_err(),
3440 "Windows construction accepted a Linux network namespace",
3441 )?;
3442
3443 let mut recursive_manifest = artifact.clone();
3444 let worker = recursive_manifest
3445 .files
3446 .iter_mut()
3447 .find(|file| matches!(&file.role, ParserPackPayloadRole::Worker))
3448 .ok_or_else(|| io::Error::other("worker payload missing"))?;
3449 worker.path = PackRelativePath::new("artifact-manifest.json")?;
3450 recursive_manifest
3451 .files
3452 .sort_by(|left, right| left.path.cmp(&right.path));
3453 require(
3454 recursive_manifest.validate(&logical).is_err(),
3455 "artifact manifest was allowed to list itself",
3456 )?;
3457
3458 let mut missing_broker = artifact.clone();
3459 missing_broker
3460 .files
3461 .retain(|file| !matches!(&file.role, ParserPackPayloadRole::ContainmentBroker));
3462 missing_broker.measurements =
3463 ParserPackPayloadMeasurements::from_files(&missing_broker.files)?;
3464 require(
3465 missing_broker.validate(&logical).is_err(),
3466 "Windows artifact without its runtime-containment broker was accepted",
3467 )?;
3468
3469 let mut forbidden_import = artifact.clone();
3470 forbidden_import.native_audit.forbidden_imports = 1;
3471 require(
3472 forbidden_import.validate(&logical).is_err(),
3473 "artifact with a forbidden native import was accepted",
3474 )?;
3475
3476 let mut detached_audit_report = artifact.clone();
3477 let report = detached_audit_report
3478 .files
3479 .iter_mut()
3480 .find(|file| matches!(&file.role, ParserPackPayloadRole::NativeAuditReport))
3481 .ok_or_else(|| io::Error::other("native audit report payload missing"))?;
3482 report.sha256 = Sha256Digest::new(format!("{:064x}", 27))?;
3483 require(
3484 detached_audit_report.validate(&logical).is_err(),
3485 "artifact audit claim was allowed to detach from its packaged report",
3486 )?;
3487
3488 let mut oversized_policy = artifact.clone();
3489 let policy = oversized_policy
3490 .files
3491 .iter_mut()
3492 .find(|file| matches!(&file.role, ParserPackPayloadRole::NativeImportPolicy))
3493 .ok_or_else(|| io::Error::other("native-import policy payload missing"))?;
3494 policy.bytes = OPTIONAL_PARSER_PACK_NATIVE_IMPORT_POLICY_MAX_BYTES + 1;
3495 oversized_policy.measurements =
3496 ParserPackPayloadMeasurements::from_files(&oversized_policy.files)?;
3497 require(
3498 matches!(
3499 oversized_policy.validate(&logical),
3500 Err(OptionalParserPackManifestError::InvalidField { field: "bytes", .. })
3501 ),
3502 "oversized native-import policy was accepted",
3503 )?;
3504
3505 let mut network_available = artifact;
3506 network_available.construction.network_denial.https_denied = false;
3507 require(
3508 network_available.validate(&logical).is_err(),
3509 "artifact constructed with reachable HTTPS was accepted",
3510 )?;
3511 Ok(())
3512 }
3513
3514 #[test]
3515 fn linux_artifact_rejects_a_windows_containment_broker() -> Result<(), Box<dyn Error>> {
3516 let logical = test_manifest()?;
3517 let mut artifact = test_artifact(&logical, PackPlatform::LinuxX86_64)?;
3518 for mechanism in [
3519 ParserPackNetworkIsolation::WindowsPrincipalFirewall,
3520 ParserPackNetworkIsolation::WindowsAppContainer,
3521 ] {
3522 let mut wrong_isolation = artifact.clone();
3523 wrong_isolation.construction.network_denial.mechanism = mechanism;
3524 require(
3525 wrong_isolation.validate(&logical).is_err(),
3526 "Linux construction accepted a Windows isolation mechanism",
3527 )?;
3528 }
3529 require(
3530 artifact
3531 .files
3532 .iter()
3533 .all(|file| !matches!(&file.role, ParserPackPayloadRole::ContainmentBroker)),
3534 "Linux test artifact unexpectedly contained a broker",
3535 )?;
3536 artifact.files.push(ParserPackPayloadFile {
3537 path: PackRelativePath::new("projectatlas-parser-containment.exe")?,
3538 role: ParserPackPayloadRole::ContainmentBroker,
3539 bytes: 16,
3540 sha256: Sha256Digest::new(format!("{:064x}", 28))?,
3541 });
3542 artifact
3543 .files
3544 .sort_by(|left, right| left.path.cmp(&right.path));
3545 artifact.measurements = ParserPackPayloadMeasurements::from_files(&artifact.files)?;
3546 require(
3547 artifact.validate(&logical).is_err(),
3548 "Linux artifact accepted a Windows runtime-containment broker",
3549 )?;
3550 Ok(())
3551 }
3552
3553 #[test]
3554 fn proof_aggregate_requires_one_clean_identical_platform_set() -> Result<(), Box<dyn Error>> {
3555 let logical = test_manifest()?;
3556 let platforms = PackPlatform::ALL
3557 .iter()
3558 .copied()
3559 .enumerate()
3560 .map(|(ordinal, platform)| test_platform_proof(&logical, platform, ordinal))
3561 .collect::<Result<Vec<_>, _>>()?;
3562 let aggregate = OptionalParserPackProofAggregate {
3563 schema_version: OPTIONAL_PARSER_PACK_PROOF_AGGREGATE_SCHEMA_VERSION,
3564 pack_id: logical.pack_id().to_string(),
3565 projectatlas_version: logical.runtime().projectatlas_version.clone(),
3566 accepted_manifest_sha256: platforms[0].accepted_manifest_sha256.clone(),
3567 capability_set_digest: logical.capability_set_digest().clone(),
3568 fixture_corpus_sha256: platforms[0].fixture_corpus_sha256.clone(),
3569 platforms,
3570 };
3571 aggregate.validate(&logical)?;
3572
3573 let mut wrong_fresh_isolation = aggregate.clone();
3574 wrong_fresh_isolation.platforms[1]
3575 .runner
3576 .network_denial
3577 .mechanism = ParserPackNetworkIsolation::WindowsPrincipalFirewall;
3578 require(
3579 wrong_fresh_isolation.validate(&logical).is_err(),
3580 "Windows fresh verification accepted its construction isolation mechanism",
3581 )?;
3582 let mut cross_platform_fresh_isolation = aggregate.clone();
3583 cross_platform_fresh_isolation.platforms[1]
3584 .runner
3585 .network_denial
3586 .mechanism = ParserPackNetworkIsolation::LinuxNetworkNamespace;
3587 require(
3588 cross_platform_fresh_isolation.validate(&logical).is_err(),
3589 "Windows fresh verification accepted a Linux network namespace",
3590 )?;
3591 for mechanism in [
3592 ParserPackNetworkIsolation::WindowsPrincipalFirewall,
3593 ParserPackNetworkIsolation::WindowsAppContainer,
3594 ] {
3595 let mut linux_wrong_isolation = aggregate.clone();
3596 linux_wrong_isolation.platforms[0]
3597 .runner
3598 .network_denial
3599 .mechanism = mechanism;
3600 require(
3601 linux_wrong_isolation.validate(&logical).is_err(),
3602 "Linux fresh verification accepted a Windows isolation mechanism",
3603 )?;
3604 }
3605
3606 let mut failed_probe = aggregate.clone();
3607 failed_probe.platforms[0].grammars[0].worker_probe_passed = false;
3608 require(
3609 failed_probe.validate(&logical).is_err(),
3610 "aggregate accepted one failed grammar/platform probe",
3611 )?;
3612
3613 let mut dirty_candidate = aggregate.clone();
3614 dirty_candidate.platforms[0].candidate.source_state = ParserPackCandidateSourceState::Dirty;
3615 require(
3616 dirty_candidate.validate(&logical).is_err(),
3617 "aggregate accepted a dirty candidate proof",
3618 )?;
3619
3620 let mut false_overshoot = aggregate.clone();
3621 false_overshoot.platforms[0]
3622 .memory
3623 .maximum_observed_overshoot_bytes = Some(0);
3624 require(
3625 false_overshoot.validate(&logical).is_err(),
3626 "aggregate accepted an inconsistent sampled RSS overshoot",
3627 )?;
3628
3629 let mut mismatched_memory_control = aggregate.clone();
3630 mismatched_memory_control.platforms[1].memory.control =
3631 ParserPackMemoryControl::LinuxCgroupV2;
3632 require(
3633 mismatched_memory_control.validate(&logical).is_err(),
3634 "aggregate accepted a memory control from another platform",
3635 )?;
3636
3637 let mut impossible_windows_probe = aggregate.clone();
3638 impossible_windows_probe.platforms[1]
3639 .memory
3640 .process_limit_bytes =
3641 OPTIONAL_PARSER_PACK_WINDOWS_MINIMUM_MEMORY_PROBE_BYTES.saturating_sub(1);
3642 require(
3643 impossible_windows_probe.validate(&logical).is_err(),
3644 "aggregate accepted a Windows probe below the broker's configured floor",
3645 )?;
3646
3647 let mut missing_platform = aggregate;
3648 missing_platform.platforms.pop();
3649 require(
3650 missing_platform.validate(&logical).is_err(),
3651 "aggregate accepted a missing required platform",
3652 )?;
3653 Ok(())
3654 }
3655}