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 number of bytes in a complete metadata JSON document.
20///
21/// This bound applies to both the complete input accepted during decoding and
22/// the complete output produced during encoding. Directional JSON profiles
23/// apply it through their respective input and output byte budgets.
24pub const DEFAULT_MAX_JSON_BYTES: usize = 1_048_576;
25
26/// Default maximum number of entries accepted in a metadata map.
27///
28/// This domain limit is separate from the generic JSON map-entry budget.
29pub const DEFAULT_MAX_METADATA_ENTRIES: usize = 4_096;
30
31/// Default maximum number of fields accepted in a metadata schema.
32///
33/// This domain limit applies to schema fields independently of generic JSON
34/// traversal limits.
35pub const DEFAULT_MAX_SCHEMA_FIELDS: usize = 4_096;
36
37/// Default maximum UTF-8 byte length of one metadata or schema key.
38///
39/// The limit counts encoded bytes, so a multi-byte character consumes more
40/// than one byte.
41pub const DEFAULT_MAX_KEY_BYTES: usize = 256;
42
43/// Domain-specific metadata limits composed with a shared JSON profile.
44///
45/// `MetadataLimits` deliberately keeps metadata-entry, schema-field, and key
46/// bounds separate from generic JSON map/key accounting. This preserves the
47/// protocol's domain limits while the directional JSON limit types handle
48/// document traversal, payload lengths, and complete input/output bytes.
49///
50/// # Examples
51///
52/// ```
53/// use qubit_metadata::MetadataLimits;
54///
55/// # fn main() -> Result<(), serde_json::Error> {
56/// let limits = MetadataLimits::builder().max_key_bytes(128).build()?;
57/// assert_eq!(limits.max_key_bytes(), 128);
58/// # Ok(())
59/// # }
60/// ```
61#[must_use]
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub struct MetadataLimits {
64 /// JSON budgets applied while decoding an untrusted document.
65 json_decode: JsonDecodeLimits,
66 /// JSON budgets applied while encoding a document.
67 json_encode: JsonEncodeLimits,
68 /// Maximum number of entries accepted in one metadata map.
69 max_metadata_entries: usize,
70 /// Maximum number of fields accepted in one metadata schema.
71 max_schema_fields: usize,
72 /// Maximum UTF-8 byte length accepted for a metadata or schema key.
73 max_key_bytes: usize,
74}
75
76impl MetadataLimits {
77 /// Creates a builder with the default JSON profile and domain limits.
78 ///
79 /// # Returns
80 ///
81 /// A builder initialized with the protocol defaults.
82 #[inline(always)]
83 pub fn builder() -> MetadataLimitsBuilder {
84 MetadataLimitsBuilder::default()
85 }
86
87 /// Creates immutable limits from the values held by `builder` without
88 /// validating the domain hard caps.
89 ///
90 /// # Parameters
91 ///
92 /// * `builder` - The builder whose JSON profiles and domain limits are
93 /// copied into the resulting value.
94 ///
95 /// # Returns
96 ///
97 /// A limits value containing the builder's configured values.
98 #[inline]
99 pub(crate) fn from_builder(builder: MetadataLimitsBuilder) -> Self {
100 Self {
101 json_decode: builder.json_decode,
102 json_encode: builder.json_encode,
103 max_metadata_entries: builder.max_metadata_entries,
104 max_schema_fields: builder.max_schema_fields,
105 max_key_bytes: builder.max_key_bytes,
106 }
107 }
108
109 /// Returns the JSON budgets used to decode an untrusted document.
110 ///
111 /// The returned value is copied and can be used to inspect the profile
112 /// without changing this limits value.
113 ///
114 /// # Returns
115 ///
116 /// The decoding profile stored in this value.
117 #[inline(always)]
118 #[must_use]
119 pub const fn json_decode(&self) -> JsonDecodeLimits {
120 self.json_decode
121 }
122
123 /// Returns the JSON budgets used to encode a document.
124 ///
125 /// The returned value is copied and can be used to inspect the profile
126 /// without changing this limits value.
127 ///
128 /// # Returns
129 ///
130 /// The encoding profile stored in this value.
131 #[inline(always)]
132 #[must_use]
133 pub const fn json_encode(&self) -> JsonEncodeLimits {
134 self.json_encode
135 }
136
137 /// Returns the maximum number of entries accepted in a metadata map.
138 ///
139 /// This domain-specific bound is independent of generic JSON map
140 /// accounting.
141 ///
142 /// # Returns
143 ///
144 /// The metadata-entry limit stored in this value.
145 #[inline(always)]
146 #[must_use]
147 pub const fn max_metadata_entries(&self) -> usize {
148 self.max_metadata_entries
149 }
150
151 /// Returns the maximum number of fields accepted in a metadata schema.
152 ///
153 /// This domain-specific bound is independent of generic JSON traversal
154 /// limits.
155 ///
156 /// # Returns
157 ///
158 /// The schema-field limit stored in this value.
159 #[inline(always)]
160 #[must_use]
161 pub const fn max_schema_fields(&self) -> usize {
162 self.max_schema_fields
163 }
164
165 /// Returns the maximum UTF-8 byte length accepted for one key.
166 ///
167 /// The byte count applies to metadata and schema keys and is distinct from
168 /// the generic JSON key budget.
169 ///
170 /// # Returns
171 ///
172 /// The key-byte limit stored in this value.
173 #[inline(always)]
174 #[must_use]
175 pub const fn max_key_bytes(&self) -> usize {
176 self.max_key_bytes
177 }
178
179 /// Validates receiver-controlled domain limits against protocol hard caps.
180 ///
181 /// # Errors
182 ///
183 /// Returns a JSON error when the metadata-entry, schema-field, or key-byte
184 /// limit exceeds its corresponding protocol maximum.
185 pub fn validate(&self) -> Result<(), serde_json::Error> {
186 if self.max_metadata_entries > DEFAULT_MAX_METADATA_ENTRIES {
187 return Err(serde_json::Error::custom(format!(
188 "metadata entries limit {} exceeds {}",
189 self.max_metadata_entries, DEFAULT_MAX_METADATA_ENTRIES,
190 )));
191 }
192 if self.max_schema_fields > DEFAULT_MAX_SCHEMA_FIELDS {
193 return Err(serde_json::Error::custom(format!(
194 "schema fields limit {} exceeds {}",
195 self.max_schema_fields, DEFAULT_MAX_SCHEMA_FIELDS,
196 )));
197 }
198 if self.max_key_bytes > DEFAULT_MAX_KEY_BYTES {
199 return Err(serde_json::Error::custom(format!(
200 "metadata key limit {} exceeds {}",
201 self.max_key_bytes, DEFAULT_MAX_KEY_BYTES,
202 )));
203 }
204 Ok(())
205 }
206}
207
208impl Default for MetadataLimits {
209 /// Creates the protocol's default metadata and JSON limits.
210 ///
211 /// The defaults satisfy the domain hard caps, so construction cannot fail.
212 ///
213 /// # Returns
214 ///
215 /// A limits value containing the standard decoding, encoding, and domain
216 /// profiles.
217 fn default() -> Self {
218 Self::builder()
219 .build()
220 .expect("default metadata limits satisfy protocol hard caps")
221 }
222}
223
224/// Creates the default direction-independent metadata JSON value profile.
225///
226/// The profile bounds nesting, nodes, sequence items, map entries, keys,
227/// strings, numbers, and payload size. Complete input and output byte limits
228/// are supplied separately by the directional profiles.
229///
230/// # Returns
231///
232/// A value profile configured with the protocol defaults.
233pub fn default_json_value_limits() -> JsonValueLimits {
234 JsonValueLimits::<JsonResource, usize>::builder()
235 .max_depth(64_usize)
236 .max_nodes(100_000_usize)
237 .max_sequence_items(4_096_usize)
238 .max_map_entries(json_quantity(DEFAULT_MAX_METADATA_ENTRIES))
239 .max_key_bytes(256 * 1024_usize)
240 .max_string_bytes(256 * 1024_usize)
241 .max_number_bytes(4_096_usize)
242 .max_payload_bytes(json_quantity(DEFAULT_MAX_JSON_BYTES))
243 .build()
244}
245
246/// Creates the default metadata JSON decoding profile.
247///
248/// The profile applies the protocol's complete-input byte limit together with
249/// the shared metadata JSON value limits.
250///
251/// # Returns
252///
253/// A decoding profile configured with the protocol defaults.
254pub fn default_json_decode_limits() -> JsonDecodeLimits {
255 JsonDecodeLimits::builder()
256 .input_bytes_limit(ResourceLimit::new(
257 JsonResource::InputBytes,
258 json_quantity(DEFAULT_MAX_JSON_BYTES),
259 ))
260 .value_limits(default_json_value_limits())
261 .build()
262}
263
264/// Creates the default metadata JSON encoding profile.
265///
266/// The profile applies the protocol's complete-output byte limit together
267/// with the shared metadata JSON value limits.
268///
269/// # Returns
270///
271/// An encoding profile configured with the protocol defaults.
272pub fn default_json_encode_limits() -> JsonEncodeLimits {
273 JsonEncodeLimits::builder()
274 .output_bytes_limit(ResourceLimit::new(
275 JsonResource::OutputBytes,
276 json_quantity(DEFAULT_MAX_JSON_BYTES),
277 ))
278 .value_limits(default_json_value_limits())
279 .build()
280}
281
282/// Converts protocol JSON limits to the native quantity used by JSON parsing.
283///
284/// The protocol and parser currently represent these quantities with the same
285/// unsigned integer type, so conversion preserves the input exactly.
286///
287/// # Parameters
288///
289/// * `value` - A protocol limit expressed as a `usize`.
290///
291/// # Returns
292///
293/// The unchanged quantity in the parser's native representation.
294fn json_quantity(value: usize) -> usize {
295 value
296}