avrocadabra 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/Cargo.lock +923 -0
  4. data/Cargo.toml +8 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +131 -0
  7. data/docs/avro_turf.md +61 -0
  8. data/docs/licenses/crates/quad-rand-0.2.3/DECLARED-LICENSE.txt +32 -0
  9. data/docs/licenses/gcc-16.2.0/COPYING +340 -0
  10. data/docs/licenses/gcc-16.2.0/COPYING.RUNTIME +73 -0
  11. data/docs/licenses/mingw-w64-14.0.0/AUTHORS +73 -0
  12. data/docs/licenses/mingw-w64-14.0.0/COPYING +43 -0
  13. data/docs/licenses/rust-1.99.0/COMPILER-BUILTINS.txt +275 -0
  14. data/docs/licenses/rust-1.99.0/COPYRIGHT-library.html +29357 -0
  15. data/docs/licenses/rust-1.99.0/LIBM.txt +258 -0
  16. data/docs/licenses/rust-1.99.0/LLVM-LIBUNWIND.txt +311 -0
  17. data/docs/licenses/rust-1.99.0/RUST-LICENSE-APACHE.txt +176 -0
  18. data/docs/licenses/rust-1.99.0/RUST-LICENSE-MIT.txt +25 -0
  19. data/docs/licenses/rust-1.99.0/STDLIB-BACKTRACE-MIT.txt +25 -0
  20. data/docs/licenses/rust-1.99.0/STDLIB-PORTABLE-SIMD-MIT.txt +19 -0
  21. data/docs/licenses/rust-1.99.0/STDLIB-STDARCH-MIT.txt +25 -0
  22. data/docs/licenses/rust-1.99.0/licenses/Apache-2.0.txt +73 -0
  23. data/docs/licenses/rust-1.99.0/licenses/BSD-2-Clause.txt +9 -0
  24. data/docs/licenses/rust-1.99.0/licenses/CC-BY-SA-4.0.txt +427 -0
  25. data/docs/licenses/rust-1.99.0/licenses/GCC-exception-3.1.txt +30 -0
  26. data/docs/licenses/rust-1.99.0/licenses/GPL-2.0-only.txt +133 -0
  27. data/docs/licenses/rust-1.99.0/licenses/GPL-3.0-or-later.txt +202 -0
  28. data/docs/licenses/rust-1.99.0/licenses/ISC.txt +7 -0
  29. data/docs/licenses/rust-1.99.0/licenses/LLVM-exception.txt +15 -0
  30. data/docs/licenses/rust-1.99.0/licenses/MIT.txt +9 -0
  31. data/docs/licenses/rust-1.99.0/licenses/NCSA.txt +28 -0
  32. data/docs/licenses/rust-1.99.0/licenses/OFL-1.1.txt +43 -0
  33. data/docs/licenses/rust-1.99.0/licenses/Unicode-3.0.txt +39 -0
  34. data/docs/releasing.md +71 -0
  35. data/docs/third_party.md +5922 -0
  36. data/ext/avrocadabra/Cargo.toml +29 -0
  37. data/ext/avrocadabra/build.rs +4 -0
  38. data/ext/avrocadabra/extconf.rb +9 -0
  39. data/ext/avrocadabra/src/big_decimal.rs +107 -0
  40. data/ext/avrocadabra/src/callback.rs +106 -0
  41. data/ext/avrocadabra/src/convert.rs +617 -0
  42. data/ext/avrocadabra/src/decode_value.rs +473 -0
  43. data/ext/avrocadabra/src/guard.rs +680 -0
  44. data/ext/avrocadabra/src/lib.rs +670 -0
  45. data/ext/avrocadabra/src/mapping.rs +63 -0
  46. data/ext/avrocadabra/src/memory.rs +194 -0
  47. data/ext/avrocadabra/src/prepare.rs +819 -0
  48. data/ext/avrocadabra/src/resolution.rs +1932 -0
  49. data/ext/avrocadabra/src/schema_state.rs +60 -0
  50. data/ext/avrocadabra/src/validation.rs +174 -0
  51. data/ext/avrocadabra/src/wire.rs +269 -0
  52. data/lib/avrocadabra/avro_turf/cache.rb +28 -0
  53. data/lib/avrocadabra/avro_turf/codec.rb +69 -0
  54. data/lib/avrocadabra/avro_turf/datum_reader.rb +17 -0
  55. data/lib/avrocadabra/avro_turf/datum_writer.rb +14 -0
  56. data/lib/avrocadabra/avro_turf/mapping.rb +48 -0
  57. data/lib/avrocadabra/avro_turf/messaging.rb +12 -0
  58. data/lib/avrocadabra/avro_turf/ractor_support.rb +154 -0
  59. data/lib/avrocadabra/avro_turf/routing.rb +15 -0
  60. data/lib/avrocadabra/avro_turf/schema_state.rb +70 -0
  61. data/lib/avrocadabra/avro_turf/validation.rb +33 -0
  62. data/lib/avrocadabra/avro_turf.rb +25 -0
  63. data/lib/avrocadabra/logical.rb +71 -0
  64. data/lib/avrocadabra/schema.rb +48 -0
  65. data/lib/avrocadabra/version.rb +5 -0
  66. data/lib/avrocadabra.rb +34 -0
  67. metadata +183 -0
@@ -0,0 +1,819 @@
1
+ use crate::guard::Limits;
2
+ use apache_avro::{
3
+ Schema,
4
+ schema::{Name, ResolvedSchema},
5
+ };
6
+ use serde_json::{Map, Value as Json, json};
7
+ use std::collections::{HashMap, HashSet};
8
+
9
+ const MAX_BYTES: usize = 1024 * 1024;
10
+ const MAX_NODES: usize = 65_536;
11
+ const MAX_JSON_DEPTH: usize = 64;
12
+ type Fields = HashMap<String, HashMap<String, FieldMetadata>>;
13
+
14
+ struct FieldMetadata {
15
+ default: Option<Json>,
16
+ aliases: Vec<String>,
17
+ }
18
+
19
+ pub fn schemas(json: &str, references: &[String], limits: Limits) -> Result<Vec<Schema>, String> {
20
+ crate::validation::initialize()?;
21
+ if limits.max_depth > 128 {
22
+ return Err("schema max_depth exceeds 128".into());
23
+ }
24
+ if references.len() >= MAX_NODES {
25
+ return Err("schema exceeds node limit".into());
26
+ }
27
+ references
28
+ .iter()
29
+ .try_fold(json.len(), |total, value| total.checked_add(value.len()))
30
+ .filter(|&total| total <= MAX_BYTES)
31
+ .ok_or("schemas exceed 1 MiB")?;
32
+ let mut nodes = 0;
33
+ let mut inputs = Vec::with_capacity(references.len() + 1);
34
+ for input in references.iter().map(String::as_str).chain([json]) {
35
+ let tree: Json =
36
+ serde_json::from_str(input).map_err(|error| format!("invalid schema JSON: {error}"))?;
37
+ check_json(&tree, 0, &mut nodes)?;
38
+ inputs.push(tree);
39
+ }
40
+ let mut normalizer = Normalizer {
41
+ names: HashSet::new(),
42
+ reserved: HashSet::new(),
43
+ fields: HashMap::new(),
44
+ errors: HashSet::new(),
45
+ name_bytes: 0,
46
+ limits,
47
+ };
48
+ let root_index = inputs.len() - 1;
49
+ for (index, input) in inputs.iter_mut().enumerate() {
50
+ if index != root_index && !is_named(input) {
51
+ return Err("references must contain named record, enum, or fixed schemas".into());
52
+ }
53
+ normalizer.schema(input, None)?;
54
+ }
55
+ let mut definitions = HashMap::new();
56
+ for input in &inputs {
57
+ index_definitions(input, &mut definitions);
58
+ }
59
+ let mut expander = Expander {
60
+ definitions,
61
+ defined: HashSet::new(),
62
+ depth: limits.max_depth.min(64),
63
+ };
64
+ let mut dependencies: Vec<_> = inputs[..root_index].iter().collect();
65
+ dependencies.sort_by_key(|schema| schema.get("name").and_then(Json::as_str));
66
+ let mut fields = Vec::with_capacity(inputs.len());
67
+ let mut nodes = 0;
68
+ for (index, input) in dependencies
69
+ .into_iter()
70
+ .chain([&inputs[root_index]])
71
+ .enumerate()
72
+ {
73
+ let expanded = expander.schema(input, 0)?;
74
+ check_json(&expanded, 0, &mut nodes)?;
75
+ fields.push(json!({"name": format!("schema_{index}"), "type": expanded}));
76
+ }
77
+ let mut suffix = 0;
78
+ let bundle_name = loop {
79
+ let name = format!("__AvrocadabraSchemaBundle{suffix}");
80
+ if !normalizer.reserved.contains(&name) {
81
+ break name;
82
+ }
83
+ suffix += 1;
84
+ };
85
+ let bundle = json!({"type":"record", "name":bundle_name, "namespace":"", "fields":fields});
86
+ let Schema::Record(bundle) = Schema::parse(&bundle).map_err(|error| error.to_string())? else {
87
+ return Err("invalid internal schema bundle".into());
88
+ };
89
+ let mut schemas: Vec<_> = bundle
90
+ .fields
91
+ .into_iter()
92
+ .map(|field| field.schema)
93
+ .collect();
94
+ for schema in &mut schemas {
95
+ restore_metadata(schema, &mut normalizer.fields, &mut normalizer.errors)?;
96
+ }
97
+ if !normalizer.fields.is_empty() || !normalizer.errors.is_empty() {
98
+ return Err("schema metadata could not be associated with its records".into());
99
+ }
100
+ let resolved = ResolvedSchema::new_with_schemata(schemas.iter().collect())
101
+ .map_err(|error| error.to_string())?;
102
+ crate::resolution::validate_defaults(&schemas, resolved.get_names(), limits)?;
103
+ Ok(schemas)
104
+ }
105
+
106
+ fn check_json(value: &Json, depth: usize, nodes: &mut usize) -> Result<(), String> {
107
+ *nodes += 1;
108
+ if depth > MAX_JSON_DEPTH || *nodes > MAX_NODES {
109
+ return Err("schema exceeds nesting or node limit".into());
110
+ }
111
+ match value {
112
+ Json::Array(values) => {
113
+ for value in values {
114
+ check_json(value, depth + 1, nodes)?;
115
+ }
116
+ }
117
+ Json::Object(values) => {
118
+ for value in values.values() {
119
+ check_json(value, depth + 1, nodes)?;
120
+ }
121
+ }
122
+ _ => {}
123
+ }
124
+ Ok(())
125
+ }
126
+
127
+ fn primitive(name: &str) -> bool {
128
+ matches!(
129
+ name,
130
+ "null" | "boolean" | "int" | "long" | "float" | "double" | "bytes" | "string"
131
+ )
132
+ }
133
+
134
+ fn is_named(schema: &Json) -> bool {
135
+ matches!(
136
+ schema.get("type").and_then(Json::as_str),
137
+ Some("record" | "error" | "enum" | "fixed")
138
+ )
139
+ }
140
+
141
+ struct Normalizer {
142
+ names: HashSet<String>,
143
+ reserved: HashSet<String>,
144
+ fields: Fields,
145
+ errors: HashSet<String>,
146
+ name_bytes: usize,
147
+ limits: Limits,
148
+ }
149
+
150
+ impl Normalizer {
151
+ fn count_name(&mut self, length: usize) -> Result<(), String> {
152
+ self.name_bytes = self
153
+ .name_bytes
154
+ .checked_add(length)
155
+ .filter(|&bytes| bytes <= MAX_BYTES)
156
+ .ok_or("normalized schema names exceed 1 MiB")?;
157
+ Ok(())
158
+ }
159
+
160
+ fn schema(&mut self, schema: &mut Json, namespace: Option<&str>) -> Result<(), String> {
161
+ match schema {
162
+ Json::String(name) if !primitive(name) => {
163
+ let full = Name::new_with_enclosing_namespace(name.as_str(), namespace)
164
+ .map_err(|error| error.to_string())?
165
+ .fullname(None);
166
+ self.count_name(full.len())?;
167
+ *name = full;
168
+ }
169
+ Json::String(_) => {}
170
+ Json::Array(branches) => {
171
+ if branches.is_empty() {
172
+ return Err("union must contain at least one branch".into());
173
+ }
174
+ for branch in branches {
175
+ self.schema(branch, namespace)?;
176
+ }
177
+ }
178
+ Json::Object(object) => {
179
+ let is_error = object.get("type").and_then(Json::as_str) == Some("error");
180
+ if is_error {
181
+ object.insert("type".into(), Json::String("record".into()));
182
+ }
183
+ self.logical(object)?;
184
+ let kind = object.get("type").and_then(Json::as_str).map(str::to_owned);
185
+ match kind.as_deref() {
186
+ Some("record" | "enum" | "fixed") => {
187
+ let name = object
188
+ .get("name")
189
+ .and_then(Json::as_str)
190
+ .ok_or("named schema requires a name")?;
191
+ let enclosing = match object.get("namespace") {
192
+ Some(Json::String(namespace)) => Some(namespace.as_str()),
193
+ Some(_) => return Err("schema namespace must be a string".into()),
194
+ None => namespace,
195
+ };
196
+ let name = Name::new_with_enclosing_namespace(name, enclosing)
197
+ .map_err(|error| error.to_string())?;
198
+ let fullname = name.fullname(None);
199
+ if is_error {
200
+ self.errors.insert(fullname.clone());
201
+ }
202
+ self.count_name(fullname.len())?;
203
+ if !self.names.insert(fullname.clone()) {
204
+ return Err(format!("duplicate named schema {fullname}"));
205
+ }
206
+ self.reserved.insert(fullname.clone());
207
+ if let Some(Json::Array(aliases)) = object.get("aliases") {
208
+ for alias in aliases {
209
+ if let Some(alias) = alias.as_str() {
210
+ let alias =
211
+ Name::new_with_enclosing_namespace(alias, name.namespace())
212
+ .map_err(|error| error.to_string())?;
213
+ let alias = alias.fullname(None);
214
+ self.count_name(alias.len())?;
215
+ self.reserved.insert(alias);
216
+ }
217
+ }
218
+ }
219
+ object.insert("name".into(), Json::String(fullname.clone()));
220
+ object.insert("namespace".into(), Json::String(String::new()));
221
+ if kind.as_deref() == Some("enum")
222
+ && object
223
+ .get("default")
224
+ .is_some_and(|value| !value.is_string())
225
+ {
226
+ return Err("enum default must be a symbol string".into());
227
+ }
228
+ if kind.as_deref() == Some("record") {
229
+ let fields = object
230
+ .get_mut("fields")
231
+ .and_then(Json::as_array_mut)
232
+ .ok_or("record fields must be an Array")?;
233
+ for field in fields {
234
+ let field = field
235
+ .as_object_mut()
236
+ .ok_or("record field must be an object")?;
237
+ let field_name = field
238
+ .get("name")
239
+ .and_then(Json::as_str)
240
+ .ok_or("record field requires a name")?
241
+ .to_owned();
242
+ let default = field.remove("default");
243
+ let aliases = field
244
+ .remove("aliases")
245
+ .and_then(|value| value.as_array().cloned())
246
+ .unwrap_or_default()
247
+ .into_iter()
248
+ .filter_map(|value| value.as_str().map(str::to_owned))
249
+ .collect::<Vec<_>>();
250
+ if default.is_some() || !aliases.is_empty() {
251
+ self.fields
252
+ .entry(fullname.clone())
253
+ .or_default()
254
+ .insert(field_name, FieldMetadata { default, aliases });
255
+ }
256
+ self.schema(
257
+ field
258
+ .get_mut("type")
259
+ .ok_or("record field requires a type")?,
260
+ name.namespace(),
261
+ )?;
262
+ }
263
+ }
264
+ }
265
+ Some("array") => self.schema(
266
+ object.get_mut("items").ok_or("array requires items")?,
267
+ namespace,
268
+ )?,
269
+ Some("map") => self.schema(
270
+ object.get_mut("values").ok_or("map requires values")?,
271
+ namespace,
272
+ )?,
273
+ Some(_) | None => self.schema(
274
+ object.get_mut("type").ok_or("schema requires a type")?,
275
+ namespace,
276
+ )?,
277
+ }
278
+ }
279
+ _ => return Err("schema must be a type name, object, or union Array".into()),
280
+ }
281
+ Ok(())
282
+ }
283
+
284
+ fn logical(&self, object: &mut Map<String, Json>) -> Result<(), String> {
285
+ let kind = object.get("type").and_then(Json::as_str);
286
+ let fixed_size = if kind == Some("fixed") {
287
+ let size = object
288
+ .get("size")
289
+ .and_then(Json::as_u64)
290
+ .ok_or("fixed size must be a nonnegative integer")?;
291
+ if size > self.limits.max_bytes as u64 {
292
+ return Err("fixed size exceeds max_bytes".into());
293
+ }
294
+ Some(size)
295
+ } else {
296
+ None
297
+ };
298
+ if object.get("logicalType").and_then(Json::as_str) != Some("decimal") {
299
+ return Ok(());
300
+ }
301
+ let precision = object.get("precision").and_then(Json::as_u64);
302
+ let scale = object.get("scale").map_or(Some(0), Json::as_u64);
303
+ let valid = matches!(kind, Some("bytes" | "fixed"))
304
+ && matches!((precision, scale), (Some(p), Some(s)) if p > 0 && s <= p
305
+ && fixed_size.is_none_or(|size| p <= fixed_decimal_capacity(size)));
306
+ if !valid {
307
+ object.remove("logicalType");
308
+ } else if precision.is_some_and(|value| value > 4096) {
309
+ return Err("decimal precision exceeds 4096 digit limit".into());
310
+ }
311
+ Ok(())
312
+ }
313
+ }
314
+
315
+ fn fixed_decimal_capacity(size: u64) -> u64 {
316
+ let bits = size.saturating_mul(8).saturating_sub(1);
317
+ // floor(bits * log10(2)); 28 decimal places keep the integer boundary exact
318
+ // throughout the 64 MiB fixed-size bound without exponentiating schema input.
319
+ (u128::from(bits) * 3_010_299_956_639_811_952_137_388_947 / 10_u128.pow(28)) as u64
320
+ }
321
+
322
+ fn index_definitions<'a>(schema: &'a Json, definitions: &mut HashMap<&'a str, &'a Json>) {
323
+ match schema {
324
+ Json::Array(branches) => {
325
+ for branch in branches {
326
+ index_definitions(branch, definitions);
327
+ }
328
+ }
329
+ Json::Object(object) => {
330
+ let kind = object.get("type").and_then(Json::as_str);
331
+ if matches!(kind, Some("record" | "enum" | "fixed"))
332
+ && let Some(name) = object.get("name").and_then(Json::as_str)
333
+ {
334
+ definitions.insert(name, schema);
335
+ }
336
+ match kind {
337
+ Some("record") => {
338
+ if let Some(Json::Array(fields)) = object.get("fields") {
339
+ for field in fields {
340
+ if let Some(schema) = field.get("type") {
341
+ index_definitions(schema, definitions);
342
+ }
343
+ }
344
+ }
345
+ }
346
+ Some("array") => {
347
+ if let Some(schema) = object.get("items") {
348
+ index_definitions(schema, definitions);
349
+ }
350
+ }
351
+ Some("map") => {
352
+ if let Some(schema) = object.get("values") {
353
+ index_definitions(schema, definitions);
354
+ }
355
+ }
356
+ _ => {
357
+ if let Some(schema) = object.get("type") {
358
+ index_definitions(schema, definitions);
359
+ }
360
+ }
361
+ }
362
+ }
363
+ _ => {}
364
+ }
365
+ }
366
+
367
+ struct Expander<'a> {
368
+ definitions: HashMap<&'a str, &'a Json>,
369
+ defined: HashSet<&'a str>,
370
+ depth: usize,
371
+ }
372
+
373
+ impl<'a> Expander<'a> {
374
+ fn schema(&mut self, schema: &'a Json, depth: usize) -> Result<Json, String> {
375
+ if depth > self.depth {
376
+ return Err("schema dependency expansion exceeds maximum depth".into());
377
+ }
378
+ match schema {
379
+ Json::String(name) => {
380
+ if primitive(name) || self.defined.contains(name.as_str()) {
381
+ return Ok(schema.clone());
382
+ }
383
+ let definition = self
384
+ .definitions
385
+ .get(name.as_str())
386
+ .copied()
387
+ .ok_or_else(|| format!("unresolved named schema {name}"))?;
388
+ self.schema(definition, depth)
389
+ }
390
+ Json::Array(branches) => branches
391
+ .iter()
392
+ .map(|schema| self.schema(schema, depth + 1))
393
+ .collect::<Result<Vec<_>, _>>()
394
+ .map(Json::Array),
395
+ Json::Object(object) => {
396
+ let kind = object.get("type").and_then(Json::as_str);
397
+ if matches!(kind, Some("record" | "enum" | "fixed")) {
398
+ let name = object
399
+ .get("name")
400
+ .and_then(Json::as_str)
401
+ .ok_or("named schema requires name")?;
402
+ if !self.defined.insert(name) {
403
+ return Ok(Json::String(name.into()));
404
+ }
405
+ }
406
+ let child_key = match kind {
407
+ Some("record") => "fields",
408
+ Some("array") => "items",
409
+ Some("map") => "values",
410
+ Some("enum" | "fixed") => return Ok(schema.clone()),
411
+ _ => "type",
412
+ };
413
+ let mut output: Map<_, _> = object
414
+ .iter()
415
+ .filter(|(key, _)| key.as_str() != child_key)
416
+ .map(|(key, value)| (key.clone(), value.clone()))
417
+ .collect();
418
+ let child = object
419
+ .get(child_key)
420
+ .ok_or_else(|| format!("schema requires {child_key}"))?;
421
+ let expanded = if child_key == "fields" {
422
+ let fields = child.as_array().ok_or("record requires fields")?;
423
+ let mut output = Vec::with_capacity(fields.len());
424
+ for field in fields {
425
+ let field = field.as_object().ok_or("record field requires object")?;
426
+ let mut output_field: Map<_, _> = field
427
+ .iter()
428
+ .filter(|(key, _)| key.as_str() != "type")
429
+ .map(|(key, value)| (key.clone(), value.clone()))
430
+ .collect();
431
+ let schema = field.get("type").ok_or("record field requires type")?;
432
+ output_field.insert("type".into(), self.schema(schema, depth + 1)?);
433
+ output.push(Json::Object(output_field));
434
+ }
435
+ Json::Array(output)
436
+ } else {
437
+ self.schema(child, depth + usize::from(child_key != "type"))?
438
+ };
439
+ output.insert(child_key.into(), expanded);
440
+ Ok(Json::Object(output))
441
+ }
442
+ _ => Err("invalid schema".into()),
443
+ }
444
+ }
445
+ }
446
+
447
+ fn restore_metadata(
448
+ schema: &mut Schema,
449
+ fields: &mut Fields,
450
+ errors: &mut HashSet<String>,
451
+ ) -> Result<(), String> {
452
+ match schema {
453
+ Schema::Record(record) => {
454
+ if errors.remove(record.name.as_ref()) {
455
+ record
456
+ .attributes
457
+ .insert("type".into(), Json::String("error".into()));
458
+ }
459
+ let mut metadata = fields.remove(record.name.as_ref()).unwrap_or_default();
460
+ for field in &mut record.fields {
461
+ if let Some(metadata) = metadata.remove(&field.name) {
462
+ field.default = metadata.default;
463
+ field.aliases = metadata.aliases;
464
+ }
465
+ restore_metadata(&mut field.schema, fields, errors)?;
466
+ }
467
+ if !metadata.is_empty() {
468
+ return Err("schema metadata references missing record fields".into());
469
+ }
470
+ }
471
+ Schema::Array(array) => restore_metadata(&mut array.items, fields, errors)?,
472
+ Schema::Map(map) => restore_metadata(&mut map.types, fields, errors)?,
473
+ Schema::Union(union) => {
474
+ // UnionSchema has no mutable variant access.
475
+ let mut variants = union.variants().to_vec();
476
+ for schema in &mut variants {
477
+ restore_metadata(schema, fields, errors)?;
478
+ }
479
+ *union = apache_avro::schema::UnionSchema::new(variants)
480
+ .map_err(|error| error.to_string())?;
481
+ }
482
+ _ => {}
483
+ }
484
+ Ok(())
485
+ }
486
+
487
+ #[cfg(test)]
488
+ mod tests {
489
+ use super::*;
490
+ use apache_avro::{reader::datum::GenericDatumReader, types::Value};
491
+ use num_bigint::BigInt;
492
+
493
+ fn limits() -> Limits {
494
+ Limits {
495
+ max_depth: 64,
496
+ max_bytes: 1_000_000,
497
+ max_items: 10_000,
498
+ }
499
+ }
500
+
501
+ fn read(root: &str, references: &[String], bytes: &[u8]) -> Value {
502
+ let schemas = schemas(root, references, limits()).unwrap();
503
+ let resolved = ResolvedSchema::new_with_schemata(schemas.iter().collect()).unwrap();
504
+ let root = schemas.last().unwrap();
505
+ crate::guard::prefix(root, resolved.get_names(), bytes, limits()).unwrap();
506
+ GenericDatumReader::builder(root)
507
+ .resolved_writer_schemata(resolved)
508
+ .build()
509
+ .unwrap()
510
+ .read_value(&mut &bytes[..])
511
+ .unwrap()
512
+ }
513
+
514
+ #[test]
515
+ fn primitives_records_and_recursive_roots_keep_the_requested_root_last() {
516
+ assert_eq!(read(r#""int""#, &[], &[2]), Value::Int(1));
517
+ assert_eq!(
518
+ read(
519
+ r#"{"type":"record","name":"R","fields":[{"name":"next","type":["null","R"]}]}"#,
520
+ &[],
521
+ &[0]
522
+ ),
523
+ Value::Record(vec![(
524
+ "next".into(),
525
+ Value::Union(0, Box::new(Value::Null))
526
+ )])
527
+ );
528
+ }
529
+
530
+ #[test]
531
+ fn forward_and_reverse_dependency_order_are_equivalent() {
532
+ let a = r#"{"type":"record","name":"A","fields":[{"name":"b","type":"B"}]}"#.to_owned();
533
+ let b = r#"{"type":"record","name":"B","fields":[{"name":"v","type":"int"}]}"#.to_owned();
534
+ let forward = read(r#""A""#, &[a.clone(), b.clone()], &[14]);
535
+ assert_eq!(forward, read(r#""A""#, &[b, a], &[14]));
536
+ assert_eq!(
537
+ forward,
538
+ Value::Record(vec![(
539
+ "b".into(),
540
+ Value::Record(vec![("v".into(), Value::Int(7))])
541
+ )])
542
+ );
543
+ }
544
+
545
+ #[test]
546
+ fn mutual_dependencies_are_defined_before_their_references_are_resolved() {
547
+ let a = r#"{"type":"record","name":"A","fields":[{"name":"b","type":["null","B"]}]}"#
548
+ .to_owned();
549
+ let b = r#"{"type":"record","name":"B","fields":[{"name":"a","type":["null","A"]}]}"#
550
+ .to_owned();
551
+ assert_eq!(
552
+ read(r#""A""#, &[a.clone(), b.clone()], &[0]),
553
+ read(r#""A""#, &[b, a], &[0])
554
+ );
555
+ }
556
+
557
+ #[test]
558
+ fn root_can_participate_in_a_dependency_cycle() {
559
+ let root = r#"{"type":"record","name":"Root","fields":[{"name":"child","type":"Child"}]}"#;
560
+ let child =
561
+ r#"{"type":"record","name":"Child","fields":[{"name":"root","type":["null","Root"]}]}"#
562
+ .into();
563
+ assert_eq!(
564
+ read(root, &[child], &[0]),
565
+ Value::Record(vec![(
566
+ "child".into(),
567
+ Value::Record(vec![(
568
+ "root".into(),
569
+ Value::Union(0, Box::new(Value::Null))
570
+ )])
571
+ )])
572
+ );
573
+ }
574
+
575
+ #[test]
576
+ fn namespaces_survive_dependency_embedding() {
577
+ let root = r#"{"type":"record","name":"outer.Root","fields":[{"name":"other","type":"other.Value"}]}"#;
578
+ let dependency = r#"{"type":"record","name":"other.Value","fields":[
579
+ {"name":"f","type":{"type":"fixed","name":"F","size":1}}, {"name":"again","type":"F"}]}"#.into();
580
+ assert!(matches!(
581
+ read(root, &[dependency], &[1, 2]),
582
+ Value::Record(_)
583
+ ));
584
+ }
585
+
586
+ #[test]
587
+ fn defaults_and_metadata_are_not_mistaken_for_logical_schemas() {
588
+ let schema = r#"{"type":"record","name":"R","metadata":{"logicalType":"big-decimal"},"fields":[
589
+ {"name":"v","type":{"type":"map","values":"string"},"default":{"logicalType":"big-decimal"}},
590
+ {"name":"b","type":"bytes","default":"\u00ff"},
591
+ {"name":"u","type":["null","int"],"default":7}]}"#;
592
+ let parsed = schemas(schema, &[], limits()).unwrap();
593
+ let Schema::Record(root) = parsed.last().unwrap() else {
594
+ panic!("record expected");
595
+ };
596
+ assert_eq!(
597
+ root.fields[0].default,
598
+ Some(json!({"logicalType":"big-decimal"}))
599
+ );
600
+ assert_eq!(root.fields[1].default, Some(json!("ÿ")));
601
+ assert_eq!(root.fields[2].default, Some(json!(7)));
602
+ }
603
+
604
+ #[test]
605
+ fn defaults_inside_union_record_definitions_are_restored() {
606
+ let schema = r#"["null",{"type":"record","name":"R","fields":[{"name":"v","type":"int","default":1}]}]"#;
607
+ let parsed = schemas(schema, &[], limits()).unwrap();
608
+ let Schema::Union(union) = parsed.last().unwrap() else {
609
+ panic!("union expected");
610
+ };
611
+ let Schema::Record(record) = &union.variants()[1] else {
612
+ panic!("record expected");
613
+ };
614
+ assert_eq!(record.fields[0].default, Some(json!(1)));
615
+ }
616
+
617
+ #[test]
618
+ fn invalid_defaults_and_empty_unions_fail_during_preparation() {
619
+ for schema in [
620
+ "[]",
621
+ r#"{"type":"record","name":"R","fields":[{"name":"v","type":"int","default":1.5}]}"#,
622
+ r#"{"type":"record","name":"R","fields":[{"name":"v","type":"boolean","default":"false"}]}"#,
623
+ r#"{"type":"record","name":"R","fields":[{"name":"v","type":["null","int"],"default":"no"}]}"#,
624
+ r#"{"type":"enum","name":"E","symbols":["A"],"default":1}"#,
625
+ r#"{"type":"record","name":"R","fields":[null]}"#,
626
+ ] {
627
+ assert!(schemas(schema, &[], limits()).is_err(), "{schema}");
628
+ }
629
+ }
630
+
631
+ #[test]
632
+ fn invalid_decimal_annotations_fall_back_to_the_physical_schema() {
633
+ for schema in [
634
+ r#"{"type":"bytes","logicalType":"decimal"}"#,
635
+ r#"{"type":"bytes","logicalType":"decimal","precision":0}"#,
636
+ r#"{"type":"bytes","logicalType":"decimal","precision":-1}"#,
637
+ r#"{"type":"bytes","logicalType":"decimal","precision":2.5}"#,
638
+ r#"{"type":"bytes","logicalType":"decimal","precision":"2"}"#,
639
+ r#"{"type":"bytes","logicalType":"decimal","precision":2,"scale":-1}"#,
640
+ r#"{"type":"bytes","logicalType":"decimal","precision":2,"scale":null}"#,
641
+ r#"{"type":"bytes","logicalType":"decimal","precision":2,"scale":3}"#,
642
+ r#"{"type":"bytes","logicalType":"decimal","precision":4097,"scale":4098}"#,
643
+ ] {
644
+ assert_eq!(schemas(schema, &[], limits()).unwrap(), vec![Schema::Bytes]);
645
+ }
646
+ for schema in [
647
+ r#"{"type":"fixed","name":"D","size":1,"logicalType":"decimal","precision":3}"#,
648
+ r#"{"type":"fixed","name":"D","size":1,"logicalType":"decimal","precision":18446744073709551615}"#,
649
+ ] {
650
+ assert!(matches!(
651
+ schemas(schema, &[], limits()).unwrap()[0],
652
+ Schema::Fixed(_)
653
+ ));
654
+ }
655
+ assert_eq!(
656
+ schemas(
657
+ r#"{"type":"string","logicalType":"decimal","precision":2}"#,
658
+ &[],
659
+ limits()
660
+ )
661
+ .unwrap(),
662
+ vec![Schema::String]
663
+ );
664
+ }
665
+
666
+ #[test]
667
+ fn valid_decimal_annotations_still_obey_resource_limits() {
668
+ for schema in [
669
+ r#"{"type":"bytes","logicalType":"decimal","precision":4097}"#,
670
+ r#"{"type":"fixed","name":"D","size":2048,"logicalType":"decimal","precision":4097}"#,
671
+ ] {
672
+ assert!(schemas(schema, &[], limits()).unwrap_err().contains("4096"));
673
+ }
674
+ assert!(
675
+ schemas(
676
+ r#"{"type":"fixed","name":"D","size":1,"logicalType":"decimal","precision":2}"#,
677
+ &[],
678
+ limits()
679
+ )
680
+ .is_ok()
681
+ );
682
+ }
683
+
684
+ #[test]
685
+ fn fixed_decimal_capacity_matches_integer_boundaries() {
686
+ assert_eq!(fixed_decimal_capacity(0), 0);
687
+ for size in [1, 2, 4, 16, 128, 512, 1701, 1702, 4096] {
688
+ let capacity = fixed_decimal_capacity(size) as u32;
689
+ let bits = size * 8 - 1;
690
+ assert!(BigInt::from(10_u8).pow(capacity).bits() <= bits);
691
+ assert!(BigInt::from(10_u8).pow(capacity + 1).bits() > bits);
692
+ }
693
+ }
694
+
695
+ #[test]
696
+ fn big_decimal_uses_value_scale_and_ignores_schema_precision_metadata() {
697
+ for schema in [
698
+ r#"{"type":"bytes","logicalType":"big-decimal"}"#,
699
+ r#"{"type":"bytes","logicalType":"big-decimal","precision":0,"scale":-5000}"#,
700
+ ] {
701
+ assert_eq!(
702
+ schemas(schema, &[], limits()).unwrap(),
703
+ vec![Schema::BigDecimal]
704
+ );
705
+ }
706
+ assert_eq!(
707
+ schemas(
708
+ r#"{"type":"string","logicalType":"big-decimal"}"#,
709
+ &[],
710
+ limits()
711
+ )
712
+ .unwrap(),
713
+ vec![Schema::String]
714
+ );
715
+ }
716
+
717
+ #[test]
718
+ fn big_decimal_defaults_validate_inner_framing_and_share_the_byte_budget() {
719
+ let schema = |default: Json| {
720
+ json!({"type":"record","name":"R","fields":[
721
+ {"name":"v","type":{"type":"bytes","logicalType":"big-decimal"},"default":default}
722
+ ]})
723
+ .to_string()
724
+ };
725
+ for scale in [0, 5000, -5000, i64::MIN, i64::MAX] {
726
+ let decimal = apache_avro::BigDecimal::new((-129).into(), scale);
727
+ let bytes = crate::big_decimal::encode(&decimal).unwrap();
728
+ let default: String = bytes.into_iter().map(char::from).collect();
729
+ assert!(schemas(&schema(json!(default)), &[], limits()).is_ok());
730
+ }
731
+ for default in [
732
+ json!(7),
733
+ json!(""),
734
+ json!("\u{0}\u{0}"),
735
+ json!("\u{2}\u{7}"),
736
+ json!("\u{2}\u{7}\u{4}\u{0}"),
737
+ json!("Ā"),
738
+ ] {
739
+ let error = schemas(&schema(default), &[], limits()).unwrap_err();
740
+ assert!(error.starts_with("$.v:"), "{error}");
741
+ }
742
+ let schema = json!({"type":"record","name":"R","fields":[
743
+ {"name":"a","type":{"type":"bytes","logicalType":"big-decimal"},"default":"\u{2}\u{7}\u{4}"},
744
+ {"name":"b","type":{"type":"bytes","logicalType":"big-decimal"},"default":"\u{2}\u{7}\u{4}"}
745
+ ]}).to_string();
746
+ let mut limits = limits();
747
+ limits.max_bytes = 5;
748
+ assert!(
749
+ schemas(&schema, &[], limits)
750
+ .unwrap_err()
751
+ .contains("byte count")
752
+ );
753
+ }
754
+
755
+ #[test]
756
+ fn long_dependency_chains_fail_without_entering_apache_parser() {
757
+ let references: Vec<_> = (0..512).map(|index| json!({"type":"record","name":format!("R{index:04}"),
758
+ "fields":[{"name":"next","type":if index == 511 {"null".into()} else {format!("R{:04}", index + 1)}}]}).to_string()).collect();
759
+ let error = schemas(r#""R0000""#, &references, limits()).unwrap_err();
760
+ assert!(error.contains("depth") || error.contains("nesting"));
761
+ let mut reverse = references;
762
+ reverse.reverse();
763
+ assert!(schemas(r#""R0000""#, &reverse, limits()).is_err());
764
+ }
765
+
766
+ #[test]
767
+ fn implicit_default_expansion_is_bounded_before_any_apache_default_validation() {
768
+ let mut references = vec![r#"{"type":"record","name":"Tree00","fields":[]}"#.into()];
769
+ for level in 1..24 {
770
+ references.push(
771
+ json!({"type":"record","name":format!("Tree{level:02}"),"fields":[
772
+ {"name":"left","type":format!("Tree{:02}",level-1),"default":{}},
773
+ {"name":"right","type":format!("Tree{:02}",level-1),"default":{}}]})
774
+ .to_string(),
775
+ );
776
+ }
777
+ let error = schemas(r#""Tree23""#, &references, limits()).unwrap_err();
778
+ assert!(error.contains("item count"));
779
+ }
780
+
781
+ #[test]
782
+ fn unknown_duplicate_and_non_named_references_fail_cleanly() {
783
+ assert!(schemas(r#""Missing""#, &[], limits()).is_err());
784
+ assert!(schemas(r#""int""#, &[r#""int""#.into()], limits()).is_err());
785
+ let definition = r#"{"type":"record","name":"R","fields":[]}"#.to_owned();
786
+ assert!(schemas(r#""R""#, &[definition.clone(), definition], limits()).is_err());
787
+ }
788
+
789
+ #[test]
790
+ fn namespace_expansion_cannot_multiply_schema_memory() {
791
+ let namespace = "N".repeat(100_000);
792
+ let fields: Vec<_> = (0..32)
793
+ .map(|index| json!({"name":format!("v{index}"),"type":"R"}))
794
+ .collect();
795
+ let json =
796
+ json!({"type":"record","name":"R","namespace":namespace,"fields":fields}).to_string();
797
+ assert!(json.len() < MAX_BYTES);
798
+ let error = schemas(&json, &[], limits()).unwrap_err();
799
+ assert!(error.contains("normalized schema names"));
800
+ }
801
+
802
+ #[test]
803
+ fn zero_size_fixed_and_object_form_primitives_are_supported() {
804
+ assert_eq!(
805
+ read(r#"{"type":"fixed","name":"Empty","size":0}"#, &[], &[]),
806
+ Value::Fixed(0, vec![])
807
+ );
808
+ let mut limits = limits();
809
+ limits.max_depth = 1;
810
+ assert!(
811
+ schemas(
812
+ r#"{"type":"record","name":"R","fields":[{"name":"v","type":{"type":"int"}}]}"#,
813
+ &[],
814
+ limits
815
+ )
816
+ .is_ok()
817
+ );
818
+ }
819
+ }