Skip to main content

appcore_transfer/
descriptor.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: descriptor.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/10/06 00:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/10/06 00:00:00 by dnettoRaw
8//      ###########      S: 0.1.0-beta.1
9// =============================================================================
10
11//! Versioned immutable object identity and hard resource bounds.
12use crate::{TransferError as E, TransferResult};
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use std::io::Read;
16/// Admission bounds, validated before network or persistent work.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct TransferLimits {
19    /// Maximum complete object bytes, at most 256 MiB.
20    pub max_object_bytes: u64,
21    /// Maximum block bytes, at most 1 MiB.
22    pub max_block_bytes: u32,
23    /// Maximum hashes/blocks per object, at most 4096.
24    pub max_blocks: usize,
25    /// Maximum store files, including metadata and lock, at most 65536.
26    pub max_files: usize,
27    /// Store bytes including complete objects, partial blocks, staging and metadata.
28    pub max_store_bytes: u64,
29}
30impl Default for TransferLimits {
31    fn default() -> Self {
32        Self {
33            max_object_bytes: 64 * 1024 * 1024,
34            max_block_bytes: 64 * 1024,
35            max_blocks: 4096,
36            max_files: 16384,
37            max_store_bytes: 256 * 1024 * 1024,
38        }
39    }
40}
41impl TransferLimits {
42    /// Rejects zero and policies outside this beta's hard limits.
43    pub fn validate(&self) -> TransferResult<()> {
44        if self.max_object_bytes == 0
45            || self.max_object_bytes > 256 * 1024 * 1024
46            || self.max_block_bytes == 0
47            || self.max_block_bytes > 1024 * 1024
48            || self.max_blocks == 0
49            || self.max_blocks > 4096
50            || self.max_files < 4
51            || self.max_files > 65536
52            || self.max_store_bytes == 0
53        {
54            return Err(E::InvalidContract);
55        }
56        Ok(())
57    }
58}
59/// Complete object plus exact ordered block hashes. A digest is not authenticity.
60/// Deployment must authenticate this descriptor before admitting a transfer.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(try_from = "DescriptorWire")]
63pub struct ObjectDescriptor {
64    format_version: u16,
65    object_hash: String,
66    total_bytes: u64,
67    block_bytes: u32,
68    block_hashes: Vec<String>,
69}
70#[derive(Deserialize)]
71#[serde(deny_unknown_fields)]
72pub(crate) struct DescriptorWire {
73    format_version: u16,
74    object_hash: String,
75    total_bytes: u64,
76    block_bytes: u32,
77    #[serde(deserialize_with = "deserialize_hashes")]
78    block_hashes: Vec<String>,
79}
80impl TryFrom<DescriptorWire> for ObjectDescriptor {
81    type Error = E;
82    fn try_from(wire: DescriptorWire) -> TransferResult<Self> {
83        let value = Self {
84            format_version: wire.format_version,
85            object_hash: wire.object_hash,
86            total_bytes: wire.total_bytes,
87            block_bytes: wire.block_bytes,
88            block_hashes: wire.block_hashes,
89        };
90        value.validate(TransferLimits {
91            max_object_bytes: 256 * 1024 * 1024,
92            max_block_bytes: 1024 * 1024,
93            ..TransferLimits::default()
94        })?;
95        Ok(value)
96    }
97}
98impl ObjectDescriptor {
99    /// Constructs a version-one descriptor with strict lowercase SHA-256 identities.
100    pub fn new(
101        object_hash: String,
102        total_bytes: u64,
103        block_bytes: u32,
104        block_hashes: Vec<String>,
105        limits: TransferLimits,
106    ) -> TransferResult<Self> {
107        let value = Self {
108            format_version: 1,
109            object_hash,
110            total_bytes,
111            block_bytes,
112            block_hashes,
113        };
114        value.validate(limits)?;
115        Ok(value)
116    }
117    /// Reads an exact bounded object incrementally; no complete-object allocation.
118    /// Callers own source I/O deadlines and authenticity of the resulting descriptor.
119    pub fn from_reader(
120        mut reader: impl Read,
121        total_bytes: u64,
122        block_bytes: u32,
123        limits: TransferLimits,
124    ) -> TransferResult<Self> {
125        limits.validate()?;
126        if total_bytes == 0
127            || total_bytes > limits.max_object_bytes
128            || block_bytes == 0
129            || block_bytes > limits.max_block_bytes
130        {
131            return Err(E::Limit);
132        }
133        let blocks = ((total_bytes - 1) / u64::from(block_bytes) + 1) as usize;
134        if blocks > limits.max_blocks {
135            return Err(E::Limit);
136        }
137        let mut full = Sha256::new();
138        let mut hashes = Vec::with_capacity(blocks);
139        let mut remaining = total_bytes;
140        let mut buffer = vec![0; block_bytes as usize];
141        while remaining > 0 {
142            let length = remaining.min(u64::from(block_bytes)) as usize;
143            reader
144                .read_exact(&mut buffer[..length])
145                .map_err(|_| E::Source)?;
146            full.update(&buffer[..length]);
147            hashes.push(hash(&buffer[..length]));
148            remaining -= length as u64;
149        }
150        if reader.read(&mut [0]).map_err(|_| E::Source)? != 0 {
151            return Err(E::Integrity);
152        }
153        Self::new(
154            hex(&full.finalize()),
155            total_bytes,
156            block_bytes,
157            hashes,
158            limits,
159        )
160    }
161    /// Validates version, arithmetic and all resource/identity bounds.
162    pub fn validate(&self, limits: TransferLimits) -> TransferResult<()> {
163        limits.validate()?;
164        if self.format_version != 1 {
165            return Err(E::UnsupportedVersion);
166        }
167        if self.total_bytes == 0
168            || self.total_bytes > limits.max_object_bytes
169            || self.block_bytes == 0
170            || self.block_bytes > limits.max_block_bytes
171        {
172            return Err(E::Limit);
173        }
174        let expected = ((self.total_bytes - 1) / u64::from(self.block_bytes) + 1) as usize;
175        if expected > limits.max_blocks || self.block_hashes.len() != expected {
176            return Err(E::Limit);
177        }
178        if !valid_hash(&self.object_hash) || self.block_hashes.iter().any(|h| !valid_hash(h)) {
179            return Err(E::InvalidContract);
180        }
181        Ok(())
182    }
183    /// Strict bounded JSON; unknown fields/versions fail at this boundary.
184    pub fn from_json(bytes: &[u8], limits: TransferLimits) -> TransferResult<Self> {
185        if bytes.len() > 1024 * 1024 {
186            return Err(E::Limit);
187        }
188        let wire: DescriptorWire = serde_json::from_slice(bytes).map_err(|_| E::InvalidContract)?;
189        let value = Self::try_from(wire)?;
190        value.validate(limits)?;
191        Ok(value)
192    }
193    /// Deterministic descriptor serialization, bounded by the validated hash inventory.
194    pub fn to_json(&self) -> TransferResult<Vec<u8>> {
195        serde_json::to_vec(self).map_err(|_| E::InvalidContract)
196    }
197    /// Full object SHA-256 identity.
198    pub fn object_hash(&self) -> &str {
199        &self.object_hash
200    }
201    /// Exact complete object bytes.
202    pub fn total_bytes(&self) -> u64 {
203        self.total_bytes
204    }
205    /// Maximum block length; the last block may be shorter.
206    pub fn block_bytes(&self) -> u32 {
207        self.block_bytes
208    }
209    /// Number of complete/incomplete blocks.
210    pub fn block_count(&self) -> usize {
211        self.block_hashes.len()
212    }
213    /// Exact length for a valid block index.
214    pub fn block_len(&self, index: usize) -> TransferResult<usize> {
215        if index >= self.block_count() {
216            return Err(E::InvalidContract);
217        }
218        Ok(
219            (self.total_bytes - index as u64 * u64::from(self.block_bytes))
220                .min(u64::from(self.block_bytes)) as usize,
221        )
222    }
223    /// Offset for a validated block index.
224    pub fn block_offset(&self, index: usize) -> TransferResult<u64> {
225        self.block_len(index)?;
226        Ok(index as u64 * u64::from(self.block_bytes))
227    }
228    /// Verifies length and digest against the trusted inventory.
229    pub fn verify_block(&self, index: usize, bytes: &[u8]) -> TransferResult<()> {
230        if bytes.len() != self.block_len(index)? || hash(bytes) != self.block_hashes[index] {
231            return Err(E::Integrity);
232        }
233        Ok(())
234    }
235}
236pub(crate) fn hash(bytes: &[u8]) -> String {
237    hex(&Sha256::digest(bytes))
238}
239pub(crate) fn valid_hash(value: &str) -> bool {
240    value.len() == 64
241        && value
242            .bytes()
243            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
244}
245
246pub(crate) fn hex(bytes: &[u8]) -> String {
247    bytes.iter().map(|b| format!("{b:02x}")).collect()
248}
249
250fn deserialize_hashes<'de, D: serde::Deserializer<'de>>(
251    deserializer: D,
252) -> Result<Vec<String>, D::Error> {
253    struct Hashes;
254    impl<'de> serde::de::Visitor<'de> for Hashes {
255        type Value = Vec<String>;
256        fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257            f.write_str("at most 4096 SHA-256 hashes")
258        }
259        fn visit_seq<A: serde::de::SeqAccess<'de>>(
260            self,
261            mut seq: A,
262        ) -> Result<Self::Value, A::Error> {
263            let mut hashes = Vec::new();
264            while let Some(value) = seq.next_element::<String>()? {
265                if hashes.len() >= 4096 || !valid_hash(&value) {
266                    return Err(serde::de::Error::custom("invalid block hash inventory"));
267                }
268                hashes.push(value);
269            }
270            Ok(hashes)
271        }
272    }
273    deserializer.deserialize_seq(Hashes)
274}