diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index fbed3457..93f82b24 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -17,6 +17,8 @@ examine_globs = [ "crates/labcolors-core/src/constraints/exact.rs", "crates/labcolors-core/src/constraints/family.rs", "crates/labcolors-core/src/family.rs", + "crates/labcolors-core/src/family_artifact.rs", + "crates/labcolors-core/src/family_artifact/raw_bitmap24.rs", "crates/labcolors-core/src/glow.rs", "crates/labcolors-core/src/material.rs", "crates/labcolors-core/src/numerical_plan.rs", diff --git a/crates/labcolors-core/src/family.rs b/crates/labcolors-core/src/family.rs index 9e231135..1aec78b6 100644 --- a/crates/labcolors-core/src/family.rs +++ b/crates/labcolors-core/src/family.rs @@ -158,31 +158,57 @@ impl FamilyMembershipMeasurementV2 { } } +#[cfg(test)] pub(crate) fn canonical_family_image_digest_v2( output_profile: OutputProfileId, members: &[ColorSignal], ) -> Result { - if members - .iter() - .copied() - .any(|member| member.output_profile() != output_profile) - || members - .windows(2) - .any(|pair| family_image_member_key(pair[0]) >= family_image_member_key(pair[1])) - { - return Err(CanonicalFamilyImageErrorV2::NonCanonicalAdmittedImage); - } - let count = members.len() as u64; + canonical_family_image_digest_from_ordered_members_v2( + output_profile, + members.len() as u64, + members.iter().copied(), + ) + .map_err(|_| CanonicalFamilyImageErrorV2::NonCanonicalAdmittedImage) +} + +/// Один canonical preimage для slice-oracle и streaming artifact codec-ов. +/// +/// `expected_count` входит в preimage до самих members, поэтому +/// расхождение с фактическим потоком является отдельным +/// certificate violation, а не другим образом того же множества. +pub(crate) fn canonical_family_image_digest_from_ordered_members_v2( + output_profile: OutputProfileId, + expected_count: u64, + members: impl IntoIterator, +) -> Result { let mut hasher = Hasher::new(); hasher.update(FAMILY_IMAGE_DOMAIN_V2); hasher.update(&[ FAMILY_IMAGE_ENCODING_RELEASE_V2, output_profile_tag(output_profile) as u8, ]); - hasher.update(&count.to_be_bytes()); - for member in members.iter().copied() { + hasher.update(&expected_count.to_be_bytes()); + let mut previous = None; + let mut actual_count = 0_u64; + for member in members { + let key = family_image_member_key(member); + if member.output_profile() != output_profile + || previous.is_some_and(|previous| previous >= key) + { + return Err(CanonicalFamilyImageStreamErrorV2::NonCanonicalAdmittedImage); + } + actual_count = actual_count + .checked_add(1) + .ok_or(CanonicalFamilyImageStreamErrorV2::NonCanonicalAdmittedImage)?; hasher.update(&[output_profile_tag(member.output_profile()) as u8]); hasher.update(&member.srgb8().bytes()); + previous = Some(key); + } + if actual_count != expected_count { + return Err(CanonicalFamilyImageStreamErrorV2::MemberCountMismatch { + expected: expected_count, + actual: actual_count, + }); } Ok(CanonicalFamilyImageDigestV2(*hasher.finalize().as_bytes())) } @@ -216,7 +242,14 @@ pub(crate) struct FamilyMembershipPassV1; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct FamilyMembershipViolationV1; +#[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum CanonicalFamilyImageErrorV2 { NonCanonicalAdmittedImage, } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CanonicalFamilyImageStreamErrorV2 { + NonCanonicalAdmittedImage, + MemberCountMismatch { expected: u64, actual: u64 }, +} diff --git a/crates/labcolors-core/src/family_artifact.rs b/crates/labcolors-core/src/family_artifact.rs index 29c81d9e..543b5147 100644 --- a/crates/labcolors-core/src/family_artifact.rs +++ b/crates/labcolors-core/src/family_artifact.rs @@ -2,25 +2,35 @@ //! //! Semantic release описывает семейство независимо от хранения. Artifact //! receipt отдельно связывает envelope, codec и payload. Loader допускает bytes -//! до появления executable view; V5b2p намеренно не содержит production codec. +//! в allocation-free executable RawBitmap24 view, не материализуя второе множество. #![cfg_attr( not(test), expect( dead_code, - reason = "V5b2p is a private loader precursor; V5b2a supplies its first production codec and caller" + reason = "V5b2a keeps the exact artifact loader private until the public family provider cutover" ) )] use core::fmt; +mod raw_bitmap24; + +#[cfg(test)] +pub(crate) use raw_bitmap24::PAYLOAD_LEN_V1 as RAW_BITMAP24_PAYLOAD_LEN_V1; +#[cfg(test)] +pub(crate) use raw_bitmap24::encode_for_test as encode_raw_bitmap24_family_artifact_v2_for_test; + use crate::family::{ - CanonicalFamilyImageDigestV2, CanonicalFamilyImageErrorV2, FamilyDeclarationV2, - FamilyDefinitionDigestV2, FamilyMembershipMeasurementV2, FamilyMembershipPassV1, - FamilyMembershipViolationV1, SemanticFamilyReleaseIdV2, canonical_family_image_digest_v2, - semantic_family_release_id_v2, + CanonicalFamilyImageDigestV2, FamilyDeclarationV2, FamilyDefinitionDigestV2, + FamilyMembershipMeasurementV2, FamilyMembershipPassV1, FamilyMembershipViolationV1, + SemanticFamilyReleaseIdV2, semantic_family_release_id_v2, }; -use crate::lcs_occurrence::{ColorSignal, OutputProfileId}; +#[cfg(test)] +use crate::family::{CanonicalFamilyImageErrorV2, canonical_family_image_digest_v2}; +use crate::lcs_occurrence::ColorSignal; +#[cfg(test)] +use crate::lcs_occurrence::OutputProfileId; use crate::sha256::Hasher; const MAGIC_V2: &[u8; 8] = b"LCFAM2\0\0"; @@ -29,8 +39,10 @@ const SIGNAL_DOMAIN_SRGB8_D65_V1: u8 = 1; // Полная кардинальность sRGB8: большее exact-множество обязано повторить сигнал. const MAX_SRGB8_MEMBER_COUNT_V1: u64 = 1 << 24; const SIGNAL_ORDINAL_RGB_BIG_ENDIAN_V1: u8 = 1; -const PROOF_RELEASE_FIXTURE_EXACT_IMAGE_V1: u8 = 1; -const VERIFIER_RELEASE_FIXTURE_REPLAY_V1: u8 = 1; +// Loader не минтит и не перепроверяет proof: он принимает только +// certificate, точно равный trusted expected value от внешнего registry. +const PROOF_RELEASE_EXPECTED_EXACT_IMAGE_V1: u8 = 1; +const VERIFIER_RELEASE_EXPECTED_REPLAY_V1: u8 = 1; // magic + 6 release/domain tags + 6 SHA-256 identities + 2 u64 lengths + receipt. // Любое изменение certificate layout обязано синхронно менять этот размер. const HEADER_LEN_V2: usize = 254; @@ -256,6 +268,16 @@ impl EncodedFamilyArtifactV2 { self.0 } + #[cfg(test)] + pub(crate) fn allocation_ptr_for_test(&self) -> *const u8 { + self.0.as_ptr() + } + + #[cfg(test)] + pub(crate) fn payload_byte_for_test(&self, index: usize) -> u8 { + self.0[HEADER_LEN_V2 + index] + } + #[cfg(test)] pub(crate) fn from_raw_bytes_for_test(bytes: Vec) -> Self { Self(bytes.into_boxed_slice()) @@ -314,6 +336,33 @@ impl EncodedFamilyArtifactV2 { pub(crate) fn truncate_inside_header_for_test(self) -> Self { Self(self.0[..HEADER_LEN_V2 - 1].into()) } + + #[cfg(test)] + pub(crate) fn set_raw_bitmap_member_for_test(&mut self, rgb: [u8; 3], member: bool) { + raw_bitmap24::set_member(&mut self.0[HEADER_LEN_V2..], rgb, member); + } + + #[cfg(test)] + pub(crate) fn resize_payload_for_test(&mut self, payload_len: usize) { + let mut bytes = core::mem::take(&mut self.0).into_vec(); + bytes.resize(HEADER_LEN_V2 + payload_len, 0); + self.0 = bytes.into_boxed_slice(); + } + + #[cfg(test)] + pub(crate) fn reseal_payload_for_test( + &mut self, + mut certificate: FamilyImageCertificateV2, + ) -> FamilyImageCertificateV2 { + let payload = &self.0[HEADER_LEN_V2..]; + certificate.payload_len = payload.len() as u64; + certificate.payload_digest = payload_digest(payload); + certificate.artifact_receipt = artifact_receipt(certificate); + let mut encoded = Vec::with_capacity(HEADER_LEN_V2 - MAGIC_V2.len()); + encode_certificate(&mut encoded, certificate); + self.0[MAGIC_V2.len()..HEADER_LEN_V2].copy_from_slice(&encoded); + certificate + } } #[cfg(test)] @@ -327,14 +376,29 @@ pub(crate) enum FixtureEnvelopeFieldV1 { } /// Executable storage after all envelope, digest and semantic checks. -#[derive(Debug)] pub(crate) struct AdmittedFamilyArtifactV2 { certificate: FamilyImageCertificateV2, - members: Box<[ColorSignal]>, + storage: ExecutableFamilyStorageV2, #[cfg(test)] drop_probe: Option, } +enum ExecutableFamilyStorageV2 { + RawBitmap24V1(raw_bitmap24::RawBitmap24V1), + #[cfg(test)] + FixtureMembersV1(Box<[ColorSignal]>), +} + +impl fmt::Debug for AdmittedFamilyArtifactV2 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("AdmittedFamilyArtifactV2") + .field("semantic_release", &self.semantic_release()) + .field("artifact_receipt", &self.artifact_receipt()) + .finish_non_exhaustive() + } +} + #[cfg(test)] #[derive(Debug)] struct ArtifactDropProbeV1(std::rc::Rc>); @@ -356,9 +420,13 @@ impl AdmittedFamilyArtifactV2 { } pub(crate) fn contains(&self, signal: ColorSignal) -> bool { - self.members - .binary_search_by_key(&signal_key(signal), |member| signal_key(*member)) - .is_ok() + match &self.storage { + ExecutableFamilyStorageV2::RawBitmap24V1(bitmap) => bitmap.contains(signal), + #[cfg(test)] + ExecutableFamilyStorageV2::FixtureMembersV1(members) => members + .binary_search_by_key(&signal_key(signal), |member| signal_key(*member)) + .is_ok(), + } } pub(crate) fn assess( @@ -387,6 +455,14 @@ impl AdmittedFamilyArtifactV2 { self.drop_probe = Some(ArtifactDropProbeV1(counter)); self } + + #[cfg(test)] + pub(crate) fn allocation_ptr_for_test(&self) -> Option<*const u8> { + match &self.storage { + ExecutableFamilyStorageV2::RawBitmap24V1(bitmap) => Some(bitmap.allocation_ptr()), + ExecutableFamilyStorageV2::FixtureMembersV1(_) => None, + } + } } /// Loader отказывается до decoder-а при любом transport/certificate mismatch. @@ -400,14 +476,25 @@ pub(crate) enum FamilyArtifactLoadErrorV1 { UnsupportedProofRelease, UnsupportedVerifierRelease, InvalidMemberCount, - ExactLengthMismatch { expected: usize, actual: usize }, + ExactLengthMismatch { + expected: usize, + actual: usize, + }, ForeignCertificate, PayloadDigestMismatch, ArtifactReceiptMismatch, SemanticReleaseMismatch, UnsupportedCodec, + CodecPayloadLengthMismatch { + codec_release: u8, + expected: u64, + actual: u64, + }, InvalidCodecPayload, - DecodedMemberCountMismatch { expected: u64, actual: u64 }, + MemberCountMismatch { + expected: u64, + actual: u64, + }, ImageDigestMismatch, ResourceExhausted, } @@ -447,12 +534,18 @@ impl FamilyArtifactLoaderV1 { expected: FamilyImageCertificateV2, encoded: EncodedFamilyArtifactV2, ) -> Result { - Self::load_with_codec( - expected, - encoded, - reject_unreleased_codec, - decode_unreachable_codec, - ) + let verified = Self::try_load_raw_bitmap24(expected, &encoded); + match verified { + Ok(certificate) => Ok(AdmittedFamilyArtifactV2 { + certificate, + storage: ExecutableFamilyStorageV2::RawBitmap24V1( + raw_bitmap24::RawBitmap24V1::from_verified(encoded), + ), + #[cfg(test)] + drop_probe: None, + }), + Err(cause) => Err(FamilyArtifactLoadFailureV1 { cause, encoded }), + } } #[cfg(test)] @@ -460,7 +553,7 @@ impl FamilyArtifactLoaderV1 { expected: FamilyImageCertificateV2, encoded: EncodedFamilyArtifactV2, ) -> Result { - Self::load_with_codec( + Self::load_fixture_with_codec( expected, encoded, preflight_fixture_payload, @@ -474,7 +567,7 @@ impl FamilyArtifactLoaderV1 { encoded: EncodedFamilyArtifactV2, decoder: impl FnOnce(u8, u64, &[u8]) -> Result, FamilyArtifactLoadErrorV1>, ) -> Result { - Self::load_with_codec(expected, encoded, preflight_fixture_payload, decoder) + Self::load_fixture_with_codec(expected, encoded, preflight_fixture_payload, decoder) } #[cfg(test)] @@ -484,27 +577,58 @@ impl FamilyArtifactLoaderV1 { preflight: impl FnOnce(u8, u64, u64) -> Result<(), FamilyArtifactLoadErrorV1>, decoder: impl FnOnce(u8, u64, &[u8]) -> Result, FamilyArtifactLoadErrorV1>, ) -> Result { - Self::load_with_codec(expected, encoded, preflight, decoder) + Self::load_fixture_with_codec(expected, encoded, preflight, decoder) } - fn load_with_codec( + #[cfg(test)] + fn load_fixture_with_codec( expected: FamilyImageCertificateV2, encoded: EncodedFamilyArtifactV2, preflight: impl FnOnce(u8, u64, u64) -> Result<(), FamilyArtifactLoadErrorV1>, decoder: impl FnOnce(u8, u64, &[u8]) -> Result, FamilyArtifactLoadErrorV1>, ) -> Result { - match Self::try_load_with_codec(expected, &encoded, preflight, decoder) { - Ok(admitted) => Ok(admitted), + match Self::try_load_fixture_with_codec(expected, &encoded, preflight, decoder) { + Ok(verified) => Ok(AdmittedFamilyArtifactV2 { + certificate: verified.certificate, + storage: ExecutableFamilyStorageV2::FixtureMembersV1(verified.decoded), + drop_probe: None, + }), Err(cause) => Err(FamilyArtifactLoadFailureV1 { cause, encoded }), } } - fn try_load_with_codec( + fn try_load_raw_bitmap24( + expected: FamilyImageCertificateV2, + encoded: &EncodedFamilyArtifactV2, + ) -> Result { + let parsed = Self::verify_bound_envelope(expected, encoded, |codec, _, payload_len| { + raw_bitmap24::preflight(codec, payload_len) + })?; + let payload = &encoded.0[HEADER_LEN_V2..]; + let image = raw_bitmap24::verify_image(payload, parsed.certificate.member_count)?; + if image != parsed.certificate.image_digest { + return Err(FamilyArtifactLoadErrorV1::ImageDigestMismatch); + } + Ok(parsed.certificate) + } + + #[cfg(test)] + fn try_load_fixture_with_codec( expected: FamilyImageCertificateV2, encoded: &EncodedFamilyArtifactV2, preflight: impl FnOnce(u8, u64, u64) -> Result<(), FamilyArtifactLoadErrorV1>, decoder: impl FnOnce(u8, u64, &[u8]) -> Result, FamilyArtifactLoadErrorV1>, - ) -> Result { + ) -> Result { + let parsed = Self::verify_bound_envelope(expected, encoded, preflight)?; + let payload = &encoded.0[HEADER_LEN_V2..]; + VerifiedFamilyArtifactEnvelopeV2::decode(parsed, payload, decoder) + } + + fn verify_bound_envelope( + expected: FamilyImageCertificateV2, + encoded: &EncodedFamilyArtifactV2, + preflight: impl FnOnce(u8, u64, u64) -> Result<(), FamilyArtifactLoadErrorV1>, + ) -> Result { let parsed = ParsedFamilyArtifactEnvelopeV2::parse(&encoded.0)?; if parsed.certificate != expected { return Err(FamilyArtifactLoadErrorV1::ForeignCertificate); @@ -529,13 +653,7 @@ impl FamilyArtifactLoaderV1 { if semantic != parsed.certificate.semantic_release { return Err(FamilyArtifactLoadErrorV1::SemanticReleaseMismatch); } - let verified = VerifiedFamilyArtifactEnvelopeV2::decode(parsed, payload, decoder)?; - Ok(AdmittedFamilyArtifactV2 { - certificate: verified.certificate, - members: verified.members, - #[cfg(test)] - drop_probe: None, - }) + Ok(parsed) } } @@ -561,10 +679,10 @@ impl ParsedFamilyArtifactEnvelopeV2 { if certificate.signal_ordinal != SIGNAL_ORDINAL_RGB_BIG_ENDIAN_V1 { return Err(FamilyArtifactLoadErrorV1::UnsupportedSignalOrdinal); } - if certificate.proof_release != PROOF_RELEASE_FIXTURE_EXACT_IMAGE_V1 { + if certificate.proof_release != PROOF_RELEASE_EXPECTED_EXACT_IMAGE_V1 { return Err(FamilyArtifactLoadErrorV1::UnsupportedProofRelease); } - if certificate.verifier_release != VERIFIER_RELEASE_FIXTURE_REPLAY_V1 { + if certificate.verifier_release != VERIFIER_RELEASE_EXPECTED_REPLAY_V1 { return Err(FamilyArtifactLoadErrorV1::UnsupportedVerifierRelease); } if certificate.member_count > MAX_SRGB8_MEMBER_COUNT_V1 { @@ -585,11 +703,13 @@ impl ParsedFamilyArtifactEnvelopeV2 { } } +#[cfg(test)] struct VerifiedFamilyArtifactEnvelopeV2 { certificate: FamilyImageCertificateV2, - members: Box<[ColorSignal]>, + decoded: Box<[ColorSignal]>, } +#[cfg(test)] impl VerifiedFamilyArtifactEnvelopeV2 { fn decode( parsed: ParsedFamilyArtifactEnvelopeV2, @@ -603,7 +723,7 @@ impl VerifiedFamilyArtifactEnvelopeV2 { )?; let actual_count = members.len() as u64; if actual_count != parsed.certificate.member_count { - return Err(FamilyArtifactLoadErrorV1::DecodedMemberCountMismatch { + return Err(FamilyArtifactLoadErrorV1::MemberCountMismatch { expected: parsed.certificate.member_count, actual: actual_count, }); @@ -617,7 +737,7 @@ impl VerifiedFamilyArtifactEnvelopeV2 { } Ok(Self { certificate: parsed.certificate, - members, + decoded: members, }) } } @@ -630,22 +750,6 @@ pub(crate) enum FixtureFamilyArtifactCodecV1 { ReversedMembersV1 = 0xF2, } -fn reject_unreleased_codec( - _codec: u8, - _member_count: u64, - _payload_len: u64, -) -> Result<(), FamilyArtifactLoadErrorV1> { - Err(FamilyArtifactLoadErrorV1::UnsupportedCodec) -} - -fn decode_unreachable_codec( - _codec: u8, - _member_count: u64, - _payload: &[u8], -) -> Result, FamilyArtifactLoadErrorV1> { - Err(FamilyArtifactLoadErrorV1::UnsupportedCodec) -} - #[cfg(test)] fn preflight_fixture_payload( codec: u8, @@ -775,8 +879,8 @@ pub(crate) fn encode_fixture_family_artifact_v2( codec_release: codec as u8, signal_domain: SIGNAL_DOMAIN_SRGB8_D65_V1, signal_ordinal: SIGNAL_ORDINAL_RGB_BIG_ENDIAN_V1, - proof_release: PROOF_RELEASE_FIXTURE_EXACT_IMAGE_V1, - verifier_release: VERIFIER_RELEASE_FIXTURE_REPLAY_V1, + proof_release: PROOF_RELEASE_EXPECTED_EXACT_IMAGE_V1, + verifier_release: VERIFIER_RELEASE_EXPECTED_REPLAY_V1, proof_artifact, verifier_identity, definition_digest: definition, @@ -840,8 +944,8 @@ fn fixture_verifier_identity() -> FamilyVerifierIdentityV2 { let mut hasher = Hasher::new(); hasher.update(FIXTURE_VERIFIER_IDENTITY_DOMAIN_V2); hasher.update(&[ - PROOF_RELEASE_FIXTURE_EXACT_IMAGE_V1, - VERIFIER_RELEASE_FIXTURE_REPLAY_V1, + PROOF_RELEASE_EXPECTED_EXACT_IMAGE_V1, + VERIFIER_RELEASE_EXPECTED_REPLAY_V1, ]); FamilyVerifierIdentityV2(*hasher.finalize().as_bytes()) } diff --git a/crates/labcolors-core/src/family_artifact/raw_bitmap24.rs b/crates/labcolors-core/src/family_artifact/raw_bitmap24.rs new file mode 100644 index 00000000..48782aa8 --- /dev/null +++ b/crates/labcolors-core/src/family_artifact/raw_bitmap24.rs @@ -0,0 +1,232 @@ +//! Exact executable bitmap полного IEC sRGB8 domain. + +use crate::Srgb8; +use crate::family::{ + CanonicalFamilyImageDigestV2, CanonicalFamilyImageStreamErrorV2, + canonical_family_image_digest_from_ordered_members_v2, +}; +#[cfg(test)] +use crate::family::{ + CanonicalFamilyImageErrorV2, FamilyDefinitionDigestV2, canonical_family_image_digest_v2, + semantic_family_release_id_v2, +}; +use crate::lcs_occurrence::{ColorSignal, OutputProfileId}; + +#[cfg(test)] +use super::{ + ENVELOPE_RELEASE_V2, MAGIC_V2, SIGNAL_DOMAIN_SRGB8_D65_V1, SIGNAL_ORDINAL_RGB_BIG_ENDIAN_V1, +}; +use super::{EncodedFamilyArtifactV2, FamilyArtifactLoadErrorV1, HEADER_LEN_V2}; + +pub(super) const CODEC_RELEASE_V1: u8 = 0x01; +// sRGB8 имеет 2^24 сигналов; exact membership требует один bit +// на сигнал, поэтому размер не является configurable budget. +pub(crate) const PAYLOAD_LEN_V1: usize = 1_usize << 21; +const _: () = assert!(PAYLOAD_LEN_V1 == 2_097_152); + +/// Исходная owned allocation одновременно является executable storage. +/// Header остаётся рядом с bitmap, чтобы admission не клонировал +/// и не переаллоцировал 2 MiB payload. +pub(super) struct RawBitmap24V1 { + encoded: EncodedFamilyArtifactV2, +} + +impl RawBitmap24V1 { + pub(super) fn from_verified(encoded: EncodedFamilyArtifactV2) -> Self { + debug_assert_eq!(encoded.0.len(), HEADER_LEN_V2 + PAYLOAD_LEN_V1); + Self { encoded } + } + + pub(super) fn contains(&self, signal: ColorSignal) -> bool { + let ordinal = signal_ordinal(signal.srgb8().bytes()); + let byte = self.payload()[ordinal >> 3]; + byte & bit_mask(ordinal) != 0 + } + + fn payload(&self) -> &[u8] { + &self.encoded.0[HEADER_LEN_V2..] + } + + #[cfg(test)] + pub(super) fn allocation_ptr(&self) -> *const u8 { + self.encoded.0.as_ptr() + } +} + +pub(super) fn preflight( + codec_release: u8, + payload_len: u64, +) -> Result<(), FamilyArtifactLoadErrorV1> { + if codec_release != CODEC_RELEASE_V1 { + return Err(FamilyArtifactLoadErrorV1::UnsupportedCodec); + } + if payload_len != PAYLOAD_LEN_V1 as u64 { + return Err(FamilyArtifactLoadErrorV1::CodecPayloadLengthMismatch { + codec_release, + expected: PAYLOAD_LEN_V1 as u64, + actual: payload_len, + }); + } + Ok(()) +} + +pub(super) fn verify_image( + payload: &[u8], + expected_member_count: u64, +) -> Result { + debug_assert_eq!(payload.len(), PAYLOAD_LEN_V1); + let actual_member_count = payload + .iter() + .copied() + .map(|byte| u64::from(byte.count_ones())) + .sum(); + if actual_member_count != expected_member_count { + return Err(FamilyArtifactLoadErrorV1::MemberCountMismatch { + expected: expected_member_count, + actual: actual_member_count, + }); + } + canonical_family_image_digest_from_ordered_members_v2( + OutputProfileId::Iec61966Srgb8D65V1, + expected_member_count, + RawBitmapMembersV1::new(payload), + ) + .map_err(|error| match error { + CanonicalFamilyImageStreamErrorV2::MemberCountMismatch { expected, actual } => { + FamilyArtifactLoadErrorV1::MemberCountMismatch { expected, actual } + } + CanonicalFamilyImageStreamErrorV2::NonCanonicalAdmittedImage => { + FamilyArtifactLoadErrorV1::InvalidCodecPayload + } + }) +} + +fn signal_ordinal([red, green, blue]: [u8; 3]) -> usize { + (usize::from(red) << 16) + (usize::from(green) << 8) + usize::from(blue) +} + +fn bit_mask(ordinal: usize) -> u8 { + // MSB0 делает ordinal 0 старшим bit первого byte; этот + // порядок сканируется как RGB big-endian без sort или decode. + 0x80_u8 >> (ordinal & 7) +} + +struct RawBitmapMembersV1<'a> { + bytes: core::iter::Enumerate>, + current_byte_index: usize, + remaining: u8, +} + +impl<'a> RawBitmapMembersV1<'a> { + fn new(payload: &'a [u8]) -> Self { + Self { + bytes: payload.iter().enumerate(), + current_byte_index: 0, + remaining: 0, + } + } +} + +impl Iterator for RawBitmapMembersV1<'_> { + type Item = ColorSignal; + + fn next(&mut self) -> Option { + loop { + if self.remaining == 0 { + let (byte_index, byte) = self.bytes.next()?; + self.current_byte_index = byte_index; + self.remaining = *byte; + if self.remaining == 0 { + continue; + } + } + let bit = self.remaining.leading_zeros() as usize; + self.remaining &= !(0x80_u8 >> bit); + let ordinal = (self.current_byte_index << 3) + bit; + let red = (ordinal >> 16) as u8; + let green = (ordinal >> 8) as u8; + let blue = ordinal as u8; + return Some(ColorSignal::from_srgb8(Srgb8::new([red, green, blue]))); + } + } +} + +#[cfg(test)] +pub(super) fn set_member(payload: &mut [u8], rgb: [u8; 3], member: bool) { + let ordinal = signal_ordinal(rgb); + let mask = bit_mask(ordinal); + if member { + payload[ordinal >> 3] |= mask; + } else { + payload[ordinal >> 3] &= !mask; + } +} + +#[cfg(test)] +pub(crate) fn encode_for_test( + definition: FamilyDefinitionDigestV2, + members: &[ColorSignal], +) -> Result< + (super::FamilyImageCertificateV2, EncodedFamilyArtifactV2), + super::FamilyArtifactBuildErrorV1, +> { + let mut canonical = Vec::new(); + canonical + .try_reserve_exact(members.len()) + .map_err(|_| super::FamilyArtifactBuildErrorV1::ResourceExhausted)?; + canonical.extend_from_slice(members); + canonical.sort_unstable_by_key(|member| member.srgb8().bytes()); + canonical.dedup_by_key(|member| member.srgb8().bytes()); + let member_count = canonical.len() as u64; + let image_digest = + canonical_family_image_digest_v2(OutputProfileId::Iec61966Srgb8D65V1, &canonical).map_err( + |CanonicalFamilyImageErrorV2::NonCanonicalAdmittedImage| { + super::FamilyArtifactBuildErrorV1::NonCanonicalFixture + }, + )?; + let semantic_release = semantic_family_release_id_v2(definition, image_digest, member_count); + let proof_artifact = + super::fixture_proof_artifact_id(definition, image_digest, semantic_release, member_count); + let verifier_identity = super::fixture_verifier_identity(); + let mut payload = Vec::new(); + payload + .try_reserve_exact(PAYLOAD_LEN_V1) + .map_err(|_| super::FamilyArtifactBuildErrorV1::ResourceExhausted)?; + payload.resize(PAYLOAD_LEN_V1, 0); + for member in canonical { + set_member(&mut payload, member.srgb8().bytes(), true); + } + let mut certificate = super::FamilyImageCertificateV2 { + envelope_release: ENVELOPE_RELEASE_V2, + codec_release: CODEC_RELEASE_V1, + signal_domain: SIGNAL_DOMAIN_SRGB8_D65_V1, + signal_ordinal: SIGNAL_ORDINAL_RGB_BIG_ENDIAN_V1, + proof_release: super::PROOF_RELEASE_EXPECTED_EXACT_IMAGE_V1, + verifier_release: super::VERIFIER_RELEASE_EXPECTED_REPLAY_V1, + proof_artifact, + verifier_identity, + definition_digest: definition, + image_digest, + semantic_release, + payload_digest: super::payload_digest(&payload), + member_count, + payload_len: PAYLOAD_LEN_V1 as u64, + artifact_receipt: super::FamilyArtifactReceiptIdV2::from_digest([0; 32]), + }; + certificate.artifact_receipt = super::artifact_receipt(certificate); + let total_len = HEADER_LEN_V2 + .checked_add(PAYLOAD_LEN_V1) + .ok_or(super::FamilyArtifactBuildErrorV1::ResourceExhausted)?; + let mut encoded = Vec::new(); + encoded + .try_reserve_exact(total_len) + .map_err(|_| super::FamilyArtifactBuildErrorV1::ResourceExhausted)?; + encoded.extend_from_slice(MAGIC_V2); + super::encode_certificate(&mut encoded, certificate); + encoded.extend_from_slice(&payload); + debug_assert_eq!(encoded.len(), total_len); + Ok(( + certificate, + EncodedFamilyArtifactV2(encoded.into_boxed_slice()), + )) +} diff --git a/crates/labcolors-core/src/family_artifact_tests.rs b/crates/labcolors-core/src/family_artifact_tests.rs index 7e16b079..54692af4 100644 --- a/crates/labcolors-core/src/family_artifact_tests.rs +++ b/crates/labcolors-core/src/family_artifact_tests.rs @@ -1,4 +1,4 @@ -//! Hostile-контракт transport artifact семейства до первого production codec. +//! Hostile-контракт transport artifact и exact RawBitmap24 codec. use proptest::prelude::*; @@ -12,7 +12,8 @@ use crate::family_artifact::{ FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS, FamilyArtifactBindErrorV2, FamilyArtifactBundleV2, FamilyArtifactContractErrorV2, FamilyArtifactLoadErrorV1, FamilyArtifactLoaderV1, FamilyImageCertificateV2, FixtureEnvelopeFieldV1, FixtureFamilyArtifactCodecV1, - encode_fixture_family_artifact_v2, + RAW_BITMAP24_PAYLOAD_LEN_V1, encode_fixture_family_artifact_v2, + encode_raw_bitmap24_family_artifact_v2_for_test, }; use crate::lcs_occurrence::ColorSignal; use crate::lcs_occurrence::OutputProfileId; @@ -48,6 +49,108 @@ fn owned_transport_round_trips_without_copy_or_private_constructor() { assert_eq!(&*returned, &[1, 2, 3, 4]); } +#[test] +fn raw_bitmap_uses_rgb_big_endian_ordinal_and_msb_zero_at_every_boundary() { + let members = signals(&[ + [0, 0, 0], + [0, 0, 7], + [0, 0, 8], + [0, 0, 255], + [0, 1, 0], + [1, 0, 0], + [127, 255, 255], + [128, 0, 0], + [255, 255, 255], + ]); + let (certificate, encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + assert_eq!(encoded.payload_byte_for_test(0), 0x81); + assert_eq!(encoded.payload_byte_for_test(1), 0x80); + assert_eq!(encoded.payload_byte_for_test(31), 0x01); + assert_eq!(encoded.payload_byte_for_test(32), 0x80); + assert_eq!(encoded.payload_byte_for_test(8_192), 0x80); + assert_eq!(encoded.payload_byte_for_test(1_048_575), 0x01); + assert_eq!(encoded.payload_byte_for_test(1_048_576), 0x80); + assert_eq!( + encoded.payload_byte_for_test(RAW_BITMAP24_PAYLOAD_LEN_V1 - 1), + 0x01 + ); + let admitted = FamilyArtifactLoaderV1::load(certificate, encoded).unwrap(); + + for member in members { + assert!( + admitted.contains(member), + "missing boundary member {member:?}" + ); + } + for nonmember in signals(&[ + [0, 0, 1], + [0, 0, 6], + [0, 0, 9], + [0, 1, 1], + [1, 0, 1], + [127, 255, 254], + [128, 0, 1], + [255, 255, 254], + ]) { + assert!( + !admitted.contains(nonmember), + "false boundary member {nonmember:?}" + ); + } +} + +#[test] +fn raw_bitmap_and_slice_oracle_share_the_independent_canonical_image_golden() { + let members = signals(&[[0, 0, 1], [0, 0, 2], [0, 0, 255]]); + let expected = [ + 0x3c, 0xe1, 0x50, 0x04, 0xfc, 0x39, 0x7b, 0xad, 0x7c, 0x95, 0x5f, 0x28, 0x90, 0xcb, 0x9c, + 0x99, 0x01, 0x86, 0x0b, 0x93, 0x5a, 0x24, 0xc2, 0xb5, 0xb8, 0x56, 0xec, 0xb6, 0x60, 0xe5, + 0xcf, 0x20, + ]; + let slice = + canonical_family_image_digest_v2(OutputProfileId::Iec61966Srgb8D65V1, &members).unwrap(); + let (raw, encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + + // Golden получен независимым Python hashlib над явным V2 preimage. + assert_eq!(slice.as_bytes(), &expected); + assert_eq!(raw.image_digest().as_bytes(), &expected); + FamilyArtifactLoaderV1::load(raw, encoded).unwrap(); +} + +#[test] +fn raw_and_fixture_codecs_preserve_semantics_but_not_transport_receipt() { + let members = signals(&[[0, 0, 1], [0, 0, 2], [0, 0, 255]]); + let (raw_certificate, raw_encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + let (fixture_certificate, fixture_encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + + assert_eq!( + raw_certificate.semantic_release(), + fixture_certificate.semantic_release() + ); + assert_eq!( + raw_certificate.image_digest(), + fixture_certificate.image_digest() + ); + assert_ne!( + raw_certificate.artifact_receipt(), + fixture_certificate.artifact_receipt() + ); + let raw = FamilyArtifactLoaderV1::load(raw_certificate, raw_encoded).unwrap(); + let fixture = load_fixture(fixture_certificate, fixture_encoded).unwrap(); + for member in members { + assert!(raw.contains(member)); + assert!(fixture.contains(member)); + } +} + #[test] fn canonical_image_digest_rejects_permutations_and_duplicates() { let first = ColorSignal::from_srgb8(Srgb8::new([0, 0, 1])); @@ -182,6 +285,85 @@ fn payload_corruption_is_rejected_before_decoder_dispatch() { assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); } +#[test] +fn raw_payload_flip_is_rejected_before_bitmap_scan() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, mut encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + encoded.flip_first_payload_bit_for_test(); + + assert_eq!( + FamilyArtifactLoaderV1::load(certificate, encoded) + .unwrap_err() + .cause(), + FamilyArtifactLoadErrorV1::PayloadDigestMismatch, + ); +} + +#[test] +fn coherent_wrong_raw_length_is_typed_and_precedes_payload_hashing() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, mut encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + encoded.resize_payload_for_test(RAW_BITMAP24_PAYLOAD_LEN_V1 - 1); + let certificate = encoded.reseal_payload_for_test(certificate); + FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS.with(|calls| calls.set(0)); + + assert_eq!( + FamilyArtifactLoaderV1::load(certificate, encoded) + .unwrap_err() + .cause(), + FamilyArtifactLoadErrorV1::CodecPayloadLengthMismatch { + codec_release: 0x01, + expected: RAW_BITMAP24_PAYLOAD_LEN_V1 as u64, + actual: (RAW_BITMAP24_PAYLOAD_LEN_V1 - 1) as u64, + }, + ); + assert_eq!( + FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS.with(core::cell::Cell::get), + 0 + ); +} + +#[test] +fn resealed_raw_missing_or_extra_bit_is_a_member_count_mismatch() { + let cases = [ + ([0, 0, 1], false, 2_u64, 1_u64), + ([0, 0, 3], true, 2_u64, 3_u64), + ]; + for (rgb, member, expected, actual) in cases { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, mut encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + encoded.set_raw_bitmap_member_for_test(rgb, member); + let certificate = encoded.reseal_payload_for_test(certificate); + + assert_eq!( + FamilyArtifactLoaderV1::load(certificate, encoded) + .unwrap_err() + .cause(), + FamilyArtifactLoadErrorV1::MemberCountMismatch { expected, actual }, + ); + } +} + +#[test] +fn resealed_same_count_raw_substitution_is_an_image_mismatch() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, mut encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + encoded.set_raw_bitmap_member_for_test([0, 0, 1], false); + encoded.set_raw_bitmap_member_for_test([0, 0, 3], true); + let certificate = encoded.reseal_payload_for_test(certificate); + + assert_eq!( + FamilyArtifactLoaderV1::load(certificate, encoded) + .unwrap_err() + .cause(), + FamilyArtifactLoadErrorV1::ImageDigestMismatch, + ); +} + #[test] fn envelope_discriminants_are_rejected_before_certificate_or_decoder_admission() { let members = signals(&[[0, 0, 1], [0, 0, 2]]); @@ -365,7 +547,7 @@ fn full_srgb8_set_cardinality_reaches_codec_admission() { assert!(decoder_reached.get()); assert_eq!( failure.cause(), - FamilyArtifactLoadErrorV1::DecodedMemberCountMismatch { + FamilyArtifactLoadErrorV1::MemberCountMismatch { expected: 1_u64 << 24, actual: 0, }, @@ -381,26 +563,29 @@ fn axis_membership_matches_the_full_srgb8_cube_oracle() { ColorSignal::from_srgb8(Srgb8::new([value; 3])) }) .collect::>(); - let (certificate, encoded) = encode_fixture_family_artifact_v2( - definition(), - &members, - FixtureFamilyArtifactCodecV1::CanonicalMembersV1, - ) - .unwrap(); - let admitted = load_fixture(certificate, encoded).unwrap(); + let (certificate, encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + let admitted = FamilyArtifactLoaderV1::load(certificate, encoded).unwrap(); + let mut visited = 0_u64; + let mut observed_members = 0_u64; for red in 0_u16..=255 { for green in 0_u16..=255 { for blue in 0_u16..=255 { let bytes = [red as u8, green as u8, blue as u8]; + visited += 1; + let actual = admitted.contains(ColorSignal::from_srgb8(Srgb8::new(bytes))); + observed_members += u64::from(actual); assert_eq!( - admitted.contains(ColorSignal::from_srgb8(Srgb8::new(bytes))), + actual, bytes[0] == bytes[1] && bytes[1] == bytes[2], "full-domain disagreement at {bytes:?}", ); } } } + assert_eq!(visited, 1_u64 << 24); + assert_eq!(observed_members, 256); } #[test] @@ -422,7 +607,7 @@ fn central_loader_rejects_a_decoder_that_lies_about_member_count() { assert_eq!( failure.cause(), - FamilyArtifactLoadErrorV1::DecodedMemberCountMismatch { + FamilyArtifactLoadErrorV1::MemberCountMismatch { expected: 2, actual: 1, }, @@ -452,6 +637,30 @@ fn truncation_and_extension_fail_exact_length_before_decoder_dispatch() { } } +#[test] +fn raw_truncation_and_extension_return_exact_length_mismatch_before_hashing() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + + for malformed in [ + encoded.clone().truncate_one_byte_for_test(), + encoded.extend_one_byte_for_test(), + ] { + FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS.with(|calls| calls.set(0)); + assert!(matches!( + FamilyArtifactLoaderV1::load(certificate, malformed) + .unwrap_err() + .cause(), + FamilyArtifactLoadErrorV1::ExactLengthMismatch { .. }, + )); + assert_eq!( + FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS.with(core::cell::Cell::get), + 0 + ); + } +} + #[test] fn a_structurally_valid_foreign_certificate_is_not_a_generic_digest_error() { let members = signals(&[[0, 0, 1], [0, 0, 2]]); @@ -492,6 +701,69 @@ fn admitted_storage_does_not_borrow_transport_bytes() { assert!(admitted.contains(member)); } +#[test] +fn raw_admission_moves_the_original_allocation_into_executable_storage() { + let members = signals(&[[0, 0, 1], [0, 0, 2], [255, 255, 255]]); + let (certificate, encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + let original = encoded.allocation_ptr_for_test(); + + let (admitted, events) = crate::test_support::measured_allocator_events(|| { + FamilyArtifactLoaderV1::load(certificate, encoded).unwrap() + }); + + assert_eq!(admitted.allocation_ptr_for_test(), Some(original)); + assert_eq!(events, crate::test_support::AllocatorEvents::default()); + let debug = format!("{admitted:?}"); + assert!(debug.starts_with("AdmittedFamilyArtifactV2 { semantic_release: ")); + assert!(debug.contains(", artifact_receipt: ") && debug.ends_with(", .. }")); + assert!( + !debug.contains("LCFAM2"), + "Debug must not expose transport bytes" + ); +} + +#[test] +fn raw_contains_and_assess_are_allocator_free_hot_lookups() { + let members = signals(&[[0, 0, 1], [0, 0, 2], [255, 255, 255]]); + let (certificate, encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition(), &members).unwrap(); + let admitted = FamilyArtifactLoaderV1::load(certificate, encoded).unwrap(); + let hit = ColorSignal::from_srgb8(Srgb8::new([255, 255, 255])); + let miss = ColorSignal::from_srgb8(Srgb8::new([255, 255, 254])); + + let ((contains_hit, contains_miss, (_, pass), (_, violation)), events) = + crate::test_support::measured_allocator_events(|| { + ( + admitted.contains(hit), + admitted.contains(miss), + admitted.assess(hit), + admitted.assess(miss), + ) + }); + + assert!(contains_hit); + assert!(!contains_miss); + assert!(matches!(pass, crate::constraints::HardDecision::Pass(_))); + assert!(matches!( + violation, + crate::constraints::HardDecision::Violation(_) + )); + assert_eq!(events, crate::test_support::AllocatorEvents::default()); +} + +#[test] +fn production_family_storage_has_no_decoded_member_box_or_binary_search_path() { + let source = include_str!("family_artifact.rs"); + assert!( + !source.contains("members: Box<[ColorSignal]>") + && !source.contains("self.members\n .binary_search_by_key"), + "production admission must execute the owned bitmap instead of a second decoded set", + ); + let raw = include_str!("family_artifact/raw_bitmap24.rs"); + assert!(!raw.contains("Box<[ColorSignal]>") && !raw.contains("binary_search")); +} + #[test] fn every_certificate_identity_field_is_bound_before_decode() { let members = signals(&[[0, 0, 1], [0, 0, 2]]); diff --git a/crates/labcolors-core/src/program_family_tests.rs b/crates/labcolors-core/src/program_family_tests.rs index 1173a0e2..66c5e9b7 100644 --- a/crates/labcolors-core/src/program_family_tests.rs +++ b/crates/labcolors-core/src/program_family_tests.rs @@ -5,6 +5,7 @@ use crate::family::FamilyDefinitionDigestV2; use crate::family_artifact::{ AdmittedFamilyArtifactV2, FamilyArtifactBundleV2, FamilyArtifactLoaderV1, FixtureFamilyArtifactCodecV1, encode_fixture_family_artifact_v2, + encode_raw_bitmap24_family_artifact_v2_for_test, }; use crate::lcs_occurrence::ColorSignal; use crate::program_boundary_tests::CommitProgramUpdateForTest as _; @@ -32,7 +33,19 @@ struct LoadedFamilyFixtureV2 { } fn family(values: &[[u8; 3]]) -> LoadedFamilyFixtureV2 { - family_with_codec(values, FixtureFamilyArtifactCodecV1::CanonicalMembersV1) + let members = values + .iter() + .copied() + .map(Srgb8::new) + .map(ColorSignal::from_srgb8) + .collect::>(); + let definition = + FamilyDefinitionDigestV2::from_fixture_bytes_v2(b"program-family-tests/declared-set"); + let (certificate, encoded) = + encode_raw_bitmap24_family_artifact_v2_for_test(definition, &members).unwrap(); + let semantic = program::FamilySemanticReleaseV2::from_core(certificate.semantic_release()); + let artifact = FamilyArtifactLoaderV1::load(certificate, encoded).unwrap(); + LoadedFamilyFixtureV2 { semantic, artifact } } fn family_with_codec(