Skip to main content

qubit_metadata/
metadata_limits.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Default JSON and metadata-domain limits for metadata wire documents.
9
10use qubit_budget::ResourceLimit;
11use qubit_budget::json::JsonDecodeLimits;
12use qubit_budget::json::JsonEncodeLimits;
13use qubit_budget::json::JsonResource;
14use qubit_budget::json::JsonValueLimits;
15use serde::de::Error as _;
16
17use crate::metadata_limits_builder::MetadataLimitsBuilder;
18
19/// Default maximum complete metadata JSON input or output length.
20pub const DEFAULT_MAX_JSON_BYTES: usize = 1_048_576;
21
22/// Default maximum metadata map entries accepted by the JSON profile.
23pub const DEFAULT_MAX_METADATA_ENTRIES: usize = 4_096;
24
25/// Default maximum schema fields accepted by the JSON profile.
26pub const DEFAULT_MAX_SCHEMA_FIELDS: usize = 4_096;
27
28/// Default maximum UTF-8 bytes in one metadata key.
29pub const DEFAULT_MAX_KEY_BYTES: usize = 256;
30
31/// Domain-specific metadata limits composed with a shared JSON profile.
32///
33/// `MetadataLimits` deliberately keeps metadata-entry, schema-field, and key
34/// bounds separate from generic JSON map/key accounting. This preserves the
35/// protocol's domain limits while the directional JSON limit types handle
36/// document traversal, payload lengths, and complete input/output bytes.
37///
38/// # Examples
39///
40/// ```
41/// use qubit_metadata::MetadataLimits;
42///
43/// # fn main() -> Result<(), serde_json::Error> {
44/// let limits = MetadataLimits::builder().max_key_bytes(128).build()?;
45/// assert_eq!(limits.max_key_bytes(), 128);
46/// # Ok(())
47/// # }
48/// ```
49#[must_use]
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub struct MetadataLimits {
52    /// JSON budgets applied while decoding an untrusted document.
53    json_decode: JsonDecodeLimits,
54    /// JSON budgets applied while encoding a document.
55    json_encode: JsonEncodeLimits,
56    /// Maximum number of entries accepted in one metadata map.
57    max_metadata_entries: usize,
58    /// Maximum number of fields accepted in one metadata schema.
59    max_schema_fields: usize,
60    /// Maximum UTF-8 byte length accepted for a metadata or schema key.
61    max_key_bytes: usize,
62}
63
64impl MetadataLimits {
65    /// Creates a builder with the default JSON profile and domain limits.
66    #[inline(always)]
67    #[must_use = "the builder must be configured or used to build metadata limits"]
68    pub fn builder() -> MetadataLimitsBuilder {
69        MetadataLimitsBuilder::default()
70    }
71
72    /// Constructs domain limits that intentionally bypass
73    /// [`MetadataLimitsBuilder::build`] validation for wire-boundary
74    /// regression tests.
75    #[cfg(debug_assertions)]
76    #[doc(hidden)]
77    pub fn debug_only_invalid_domain_limits(
78        max_metadata_entries: usize,
79        max_schema_fields: usize,
80        max_key_bytes: usize,
81    ) -> Self {
82        MetadataLimits::from_builder(
83            MetadataLimitsBuilder::default()
84                .max_metadata_entries(max_metadata_entries)
85                .max_schema_fields(max_schema_fields)
86                .max_key_bytes(max_key_bytes),
87        )
88    }
89
90    /// Creates immutable limits from the values held by `builder`.
91    #[inline]
92    pub(crate) fn from_builder(builder: MetadataLimitsBuilder) -> Self {
93        Self {
94            json_decode: builder.json_decode,
95            json_encode: builder.json_encode,
96            max_metadata_entries: builder.max_metadata_entries,
97            max_schema_fields: builder.max_schema_fields,
98            max_key_bytes: builder.max_key_bytes,
99        }
100    }
101
102    /// Returns the JSON decoding profile.
103    #[inline(always)]
104    #[must_use]
105    pub const fn json_decode(&self) -> JsonDecodeLimits {
106        self.json_decode
107    }
108
109    /// Returns the JSON encoding profile.
110    #[inline(always)]
111    #[must_use]
112    pub const fn json_encode(&self) -> JsonEncodeLimits {
113        self.json_encode
114    }
115
116    /// Returns the metadata-entry domain limit.
117    #[inline(always)]
118    #[must_use]
119    pub const fn max_metadata_entries(&self) -> usize {
120        self.max_metadata_entries
121    }
122
123    /// Returns the schema-field domain limit.
124    #[inline(always)]
125    #[must_use]
126    pub const fn max_schema_fields(&self) -> usize {
127        self.max_schema_fields
128    }
129
130    /// Returns the metadata/schema key-byte domain limit.
131    #[inline(always)]
132    #[must_use]
133    pub const fn max_key_bytes(&self) -> usize {
134        self.max_key_bytes
135    }
136
137    /// Validates receiver-controlled domain limits against protocol hard caps.
138    pub fn validate(&self) -> Result<(), serde_json::Error> {
139        if self.max_metadata_entries > DEFAULT_MAX_METADATA_ENTRIES {
140            return Err(serde_json::Error::custom(format!(
141                "metadata entries limit {} exceeds {}",
142                self.max_metadata_entries, DEFAULT_MAX_METADATA_ENTRIES,
143            )));
144        }
145        if self.max_schema_fields > DEFAULT_MAX_SCHEMA_FIELDS {
146            return Err(serde_json::Error::custom(format!(
147                "schema fields limit {} exceeds {}",
148                self.max_schema_fields, DEFAULT_MAX_SCHEMA_FIELDS,
149            )));
150        }
151        if self.max_key_bytes > DEFAULT_MAX_KEY_BYTES {
152            return Err(serde_json::Error::custom(format!(
153                "metadata key limit {} exceeds {}",
154                self.max_key_bytes, DEFAULT_MAX_KEY_BYTES,
155            )));
156        }
157        Ok(())
158    }
159}
160
161impl Default for MetadataLimits {
162    fn default() -> Self {
163        Self::builder()
164            .build()
165            .expect("default metadata limits satisfy protocol hard caps")
166    }
167}
168
169/// Creates the default direction-independent metadata JSON value profile.
170pub fn default_json_value_limits() -> JsonValueLimits {
171    JsonValueLimits::<JsonResource, usize>::builder()
172        .max_depth(64_usize)
173        .max_nodes(100_000_usize)
174        .max_sequence_items(4_096_usize)
175        .max_map_entries(json_quantity(DEFAULT_MAX_METADATA_ENTRIES))
176        .max_key_bytes(256 * 1024_usize)
177        .max_string_bytes(256 * 1024_usize)
178        .max_number_bytes(4_096_usize)
179        .max_payload_bytes(json_quantity(DEFAULT_MAX_JSON_BYTES))
180        .build()
181}
182
183/// Creates the default metadata JSON decoding profile.
184pub fn default_json_decode_limits() -> JsonDecodeLimits {
185    JsonDecodeLimits::builder()
186        .input_bytes_limit(ResourceLimit::new(
187            JsonResource::InputBytes,
188            json_quantity(DEFAULT_MAX_JSON_BYTES),
189        ))
190        .value_limits(default_json_value_limits())
191        .build()
192}
193
194/// Creates the default metadata JSON encoding profile.
195pub fn default_json_encode_limits() -> JsonEncodeLimits {
196    JsonEncodeLimits::builder()
197        .output_bytes_limit(ResourceLimit::new(
198            JsonResource::OutputBytes,
199            json_quantity(DEFAULT_MAX_JSON_BYTES),
200        ))
201        .value_limits(default_json_value_limits())
202        .build()
203}
204
205/// Converts protocol JSON limits to the native quantity used by JSON parsing.
206fn json_quantity(value: usize) -> usize {
207    value
208}