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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/Cargo.lock +923 -0
- data/Cargo.toml +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +131 -0
- data/docs/avro_turf.md +61 -0
- data/docs/licenses/crates/quad-rand-0.2.3/DECLARED-LICENSE.txt +32 -0
- data/docs/licenses/gcc-16.2.0/COPYING +340 -0
- data/docs/licenses/gcc-16.2.0/COPYING.RUNTIME +73 -0
- data/docs/licenses/mingw-w64-14.0.0/AUTHORS +73 -0
- data/docs/licenses/mingw-w64-14.0.0/COPYING +43 -0
- data/docs/licenses/rust-1.99.0/COMPILER-BUILTINS.txt +275 -0
- data/docs/licenses/rust-1.99.0/COPYRIGHT-library.html +29357 -0
- data/docs/licenses/rust-1.99.0/LIBM.txt +258 -0
- data/docs/licenses/rust-1.99.0/LLVM-LIBUNWIND.txt +311 -0
- data/docs/licenses/rust-1.99.0/RUST-LICENSE-APACHE.txt +176 -0
- data/docs/licenses/rust-1.99.0/RUST-LICENSE-MIT.txt +25 -0
- data/docs/licenses/rust-1.99.0/STDLIB-BACKTRACE-MIT.txt +25 -0
- data/docs/licenses/rust-1.99.0/STDLIB-PORTABLE-SIMD-MIT.txt +19 -0
- data/docs/licenses/rust-1.99.0/STDLIB-STDARCH-MIT.txt +25 -0
- data/docs/licenses/rust-1.99.0/licenses/Apache-2.0.txt +73 -0
- data/docs/licenses/rust-1.99.0/licenses/BSD-2-Clause.txt +9 -0
- data/docs/licenses/rust-1.99.0/licenses/CC-BY-SA-4.0.txt +427 -0
- data/docs/licenses/rust-1.99.0/licenses/GCC-exception-3.1.txt +30 -0
- data/docs/licenses/rust-1.99.0/licenses/GPL-2.0-only.txt +133 -0
- data/docs/licenses/rust-1.99.0/licenses/GPL-3.0-or-later.txt +202 -0
- data/docs/licenses/rust-1.99.0/licenses/ISC.txt +7 -0
- data/docs/licenses/rust-1.99.0/licenses/LLVM-exception.txt +15 -0
- data/docs/licenses/rust-1.99.0/licenses/MIT.txt +9 -0
- data/docs/licenses/rust-1.99.0/licenses/NCSA.txt +28 -0
- data/docs/licenses/rust-1.99.0/licenses/OFL-1.1.txt +43 -0
- data/docs/licenses/rust-1.99.0/licenses/Unicode-3.0.txt +39 -0
- data/docs/releasing.md +71 -0
- data/docs/third_party.md +5922 -0
- data/ext/avrocadabra/Cargo.toml +29 -0
- data/ext/avrocadabra/build.rs +4 -0
- data/ext/avrocadabra/extconf.rb +9 -0
- data/ext/avrocadabra/src/big_decimal.rs +107 -0
- data/ext/avrocadabra/src/callback.rs +106 -0
- data/ext/avrocadabra/src/convert.rs +617 -0
- data/ext/avrocadabra/src/decode_value.rs +473 -0
- data/ext/avrocadabra/src/guard.rs +680 -0
- data/ext/avrocadabra/src/lib.rs +670 -0
- data/ext/avrocadabra/src/mapping.rs +63 -0
- data/ext/avrocadabra/src/memory.rs +194 -0
- data/ext/avrocadabra/src/prepare.rs +819 -0
- data/ext/avrocadabra/src/resolution.rs +1932 -0
- data/ext/avrocadabra/src/schema_state.rs +60 -0
- data/ext/avrocadabra/src/validation.rs +174 -0
- data/ext/avrocadabra/src/wire.rs +269 -0
- data/lib/avrocadabra/avro_turf/cache.rb +28 -0
- data/lib/avrocadabra/avro_turf/codec.rb +69 -0
- data/lib/avrocadabra/avro_turf/datum_reader.rb +17 -0
- data/lib/avrocadabra/avro_turf/datum_writer.rb +14 -0
- data/lib/avrocadabra/avro_turf/mapping.rb +48 -0
- data/lib/avrocadabra/avro_turf/messaging.rb +12 -0
- data/lib/avrocadabra/avro_turf/ractor_support.rb +154 -0
- data/lib/avrocadabra/avro_turf/routing.rb +15 -0
- data/lib/avrocadabra/avro_turf/schema_state.rb +70 -0
- data/lib/avrocadabra/avro_turf/validation.rb +33 -0
- data/lib/avrocadabra/avro_turf.rb +25 -0
- data/lib/avrocadabra/logical.rb +71 -0
- data/lib/avrocadabra/schema.rb +48 -0
- data/lib/avrocadabra/version.rb +5 -0
- data/lib/avrocadabra.rb +34 -0
- metadata +183 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
use apache_avro::{
|
|
2
|
+
Schema,
|
|
3
|
+
schema::{InnerDecimalSchema, NamesRef, UuidSchema},
|
|
4
|
+
};
|
|
5
|
+
use num_bigint::BigInt;
|
|
6
|
+
use num_traits::Signed;
|
|
7
|
+
use std::fmt::Display;
|
|
8
|
+
|
|
9
|
+
#[derive(Clone, Copy)]
|
|
10
|
+
pub struct Limits {
|
|
11
|
+
pub max_depth: usize,
|
|
12
|
+
pub max_bytes: usize,
|
|
13
|
+
pub max_items: usize,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl Limits {
|
|
17
|
+
pub fn intersect(self, other: Self) -> Self {
|
|
18
|
+
Self {
|
|
19
|
+
max_depth: self.max_depth.min(other.max_depth),
|
|
20
|
+
max_bytes: self.max_bytes.min(other.max_bytes),
|
|
21
|
+
max_items: self.max_items.min(other.max_items),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[cfg(test)]
|
|
27
|
+
pub fn prefix(
|
|
28
|
+
schema: &Schema,
|
|
29
|
+
names: &NamesRef<'_>,
|
|
30
|
+
bytes: &[u8],
|
|
31
|
+
limits: Limits,
|
|
32
|
+
) -> Result<usize, String> {
|
|
33
|
+
inspect(schema, names, bytes, limits).map(|frame| frame.consumed)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub struct Frame {
|
|
37
|
+
pub consumed: usize,
|
|
38
|
+
pub unions: Vec<u32>,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub fn inspect(
|
|
42
|
+
schema: &Schema,
|
|
43
|
+
names: &NamesRef<'_>,
|
|
44
|
+
bytes: &[u8],
|
|
45
|
+
mut limits: Limits,
|
|
46
|
+
) -> Result<Frame, String> {
|
|
47
|
+
// A caller-selected limit must not turn recursive schemas into a native stack overflow.
|
|
48
|
+
limits.max_depth = limits.max_depth.min(256);
|
|
49
|
+
let mut scanner = Scanner {
|
|
50
|
+
bytes,
|
|
51
|
+
position: 0,
|
|
52
|
+
end: bytes.len().min(limits.max_bytes),
|
|
53
|
+
remaining: limits.max_items,
|
|
54
|
+
remaining_bytes: limits.max_bytes,
|
|
55
|
+
limits,
|
|
56
|
+
names,
|
|
57
|
+
path: "$".into(),
|
|
58
|
+
unions: Vec::new(),
|
|
59
|
+
};
|
|
60
|
+
scanner.value(schema, 0)?;
|
|
61
|
+
Ok(Frame {
|
|
62
|
+
consumed: scanner.position,
|
|
63
|
+
unions: scanner.unions,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
struct Scanner<'data, 'names, 'schema> {
|
|
68
|
+
bytes: &'data [u8],
|
|
69
|
+
position: usize,
|
|
70
|
+
end: usize,
|
|
71
|
+
remaining: usize,
|
|
72
|
+
remaining_bytes: usize,
|
|
73
|
+
limits: Limits,
|
|
74
|
+
names: &'names NamesRef<'schema>,
|
|
75
|
+
path: String,
|
|
76
|
+
unions: Vec<u32>,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
impl<'data, 'schema> Scanner<'data, '_, 'schema> {
|
|
80
|
+
fn error(&self, message: impl Display) -> String {
|
|
81
|
+
format!("{} at byte {}: {message}", self.path, self.position)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn allocation(&mut self, bytes: usize) -> Result<(), String> {
|
|
85
|
+
self.remaining_bytes = self
|
|
86
|
+
.remaining_bytes
|
|
87
|
+
.checked_sub(bytes)
|
|
88
|
+
.ok_or_else(|| self.error("decoded values exceed maximum byte count"))?;
|
|
89
|
+
Ok(())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn take(&mut self, length: usize) -> Result<&'data [u8], String> {
|
|
93
|
+
if length > self.limits.max_bytes.saturating_sub(self.position) {
|
|
94
|
+
return Err(self.error("datum exceeds max_bytes"));
|
|
95
|
+
}
|
|
96
|
+
let end = self
|
|
97
|
+
.position
|
|
98
|
+
.checked_add(length)
|
|
99
|
+
.filter(|&end| end <= self.end)
|
|
100
|
+
.ok_or_else(|| self.error("truncated datum or collection block"))?;
|
|
101
|
+
let bytes = &self.bytes[self.position..end];
|
|
102
|
+
self.position = end;
|
|
103
|
+
Ok(bytes)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn long(&mut self) -> Result<i64, String> {
|
|
107
|
+
let mut value = 0_u64;
|
|
108
|
+
for shift in (0..70).step_by(7) {
|
|
109
|
+
let byte = self.take(1)?[0];
|
|
110
|
+
if shift == 63 && byte > 1 {
|
|
111
|
+
return Err(self.error("Avro integer varint overflows 64 bits"));
|
|
112
|
+
}
|
|
113
|
+
value |= u64::from(byte & 0x7f) << shift;
|
|
114
|
+
if byte & 0x80 == 0 {
|
|
115
|
+
return Ok(((value >> 1) as i64) ^ -((value & 1) as i64));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
Err(self.error("unterminated Avro integer varint"))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn int(&mut self) -> Result<i32, String> {
|
|
122
|
+
i32::try_from(self.long()?)
|
|
123
|
+
.map_err(|_| self.error("Avro int is outside signed 32-bit range"))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn length(&mut self) -> Result<usize, String> {
|
|
127
|
+
let length = self.long()?;
|
|
128
|
+
usize::try_from(length).map_err(|_| self.error("negative or overflowing byte length"))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn bytes(&mut self) -> Result<&'data [u8], String> {
|
|
132
|
+
let length = self.length()?;
|
|
133
|
+
self.allocation(length)?;
|
|
134
|
+
self.take(length)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn string(&mut self) -> Result<&'data str, String> {
|
|
138
|
+
std::str::from_utf8(self.bytes()?).map_err(|_| self.error("Avro string is not valid UTF-8"))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn value(&mut self, mut schema: &'schema Schema, depth: usize) -> Result<(), String> {
|
|
142
|
+
if depth > self.limits.max_depth {
|
|
143
|
+
return Err(self.error("datum exceeds maximum depth"));
|
|
144
|
+
}
|
|
145
|
+
if self.remaining == 0 {
|
|
146
|
+
return Err(self.error("datum exceeds maximum item count"));
|
|
147
|
+
}
|
|
148
|
+
self.remaining -= 1;
|
|
149
|
+
let mut hops = 0;
|
|
150
|
+
while let Schema::Ref { name } = schema {
|
|
151
|
+
if hops > self.names.len() {
|
|
152
|
+
return Err(self.error(format_args!("cyclic unresolved schema reference {name}")));
|
|
153
|
+
}
|
|
154
|
+
schema =
|
|
155
|
+
self.names.get(name).copied().ok_or_else(|| {
|
|
156
|
+
self.error(format_args!("unresolved schema reference {name}"))
|
|
157
|
+
})?;
|
|
158
|
+
hops += 1;
|
|
159
|
+
}
|
|
160
|
+
match schema {
|
|
161
|
+
Schema::Null => {}
|
|
162
|
+
Schema::Boolean => {
|
|
163
|
+
if self.take(1)?[0] > 1 {
|
|
164
|
+
return Err(self.error("Avro boolean must be 0 or 1"));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
Schema::Int | Schema::Date => {
|
|
168
|
+
self.int()?;
|
|
169
|
+
}
|
|
170
|
+
Schema::Long
|
|
171
|
+
| Schema::TimestampMillis
|
|
172
|
+
| Schema::TimestampMicros
|
|
173
|
+
| Schema::TimestampNanos
|
|
174
|
+
| Schema::LocalTimestampMillis
|
|
175
|
+
| Schema::LocalTimestampMicros
|
|
176
|
+
| Schema::LocalTimestampNanos => {
|
|
177
|
+
self.long()?;
|
|
178
|
+
}
|
|
179
|
+
Schema::TimeMillis => {
|
|
180
|
+
if !(0..86_400_000).contains(&self.int()?) {
|
|
181
|
+
return Err(self.error("time-millis is outside one day"));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
Schema::TimeMicros => {
|
|
185
|
+
if !(0..86_400_000_000).contains(&self.long()?) {
|
|
186
|
+
return Err(self.error("time-micros is outside one day"));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
Schema::Float => {
|
|
190
|
+
self.take(4)?;
|
|
191
|
+
}
|
|
192
|
+
Schema::Double => {
|
|
193
|
+
self.take(8)?;
|
|
194
|
+
}
|
|
195
|
+
Schema::Bytes => {
|
|
196
|
+
self.bytes()?;
|
|
197
|
+
}
|
|
198
|
+
Schema::String => {
|
|
199
|
+
self.string()?;
|
|
200
|
+
}
|
|
201
|
+
Schema::Fixed(fixed) => {
|
|
202
|
+
self.allocation(fixed.size)?;
|
|
203
|
+
self.take(fixed.size)?;
|
|
204
|
+
}
|
|
205
|
+
Schema::Duration(fixed) => {
|
|
206
|
+
if fixed.size != 12 {
|
|
207
|
+
return Err(self.error("duration requires fixed size 12"));
|
|
208
|
+
}
|
|
209
|
+
self.take(12)?;
|
|
210
|
+
}
|
|
211
|
+
Schema::Uuid(UuidSchema::String) => {
|
|
212
|
+
uuid::Uuid::parse_str(self.string()?)
|
|
213
|
+
.map_err(|error| self.error(format_args!("invalid UUID: {error}")))?;
|
|
214
|
+
}
|
|
215
|
+
Schema::Uuid(UuidSchema::Bytes) => {
|
|
216
|
+
uuid::Uuid::from_slice(self.bytes()?)
|
|
217
|
+
.map_err(|error| self.error(format_args!("invalid UUID: {error}")))?;
|
|
218
|
+
}
|
|
219
|
+
Schema::Uuid(UuidSchema::Fixed(fixed)) => {
|
|
220
|
+
if fixed.size != 16 {
|
|
221
|
+
return Err(self.error("UUID requires fixed size 16"));
|
|
222
|
+
}
|
|
223
|
+
self.allocation(16)?;
|
|
224
|
+
self.take(16)?;
|
|
225
|
+
}
|
|
226
|
+
Schema::Decimal(decimal) => {
|
|
227
|
+
let bytes = match &decimal.inner {
|
|
228
|
+
InnerDecimalSchema::Bytes => self.bytes()?,
|
|
229
|
+
InnerDecimalSchema::Fixed(fixed) => {
|
|
230
|
+
self.allocation(fixed.size)?;
|
|
231
|
+
self.take(fixed.size)?
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
decimal_precision(bytes, decimal.precision).map_err(|error| self.error(error))?;
|
|
235
|
+
}
|
|
236
|
+
Schema::BigDecimal => {
|
|
237
|
+
crate::big_decimal::validate(self.bytes()?).map_err(|error| self.error(error))?;
|
|
238
|
+
}
|
|
239
|
+
Schema::Enum(enumeration) => {
|
|
240
|
+
let index = self.int()?;
|
|
241
|
+
if index < 0 || index as usize >= enumeration.symbols.len() {
|
|
242
|
+
return Err(self.error("invalid enum symbol index"));
|
|
243
|
+
}
|
|
244
|
+
self.allocation(enumeration.symbols[index as usize].len())?;
|
|
245
|
+
}
|
|
246
|
+
Schema::Union(union) => {
|
|
247
|
+
let index = self.long()?;
|
|
248
|
+
let branch = usize::try_from(index)
|
|
249
|
+
.ok()
|
|
250
|
+
.and_then(|index| union.variants().get(index))
|
|
251
|
+
.ok_or_else(|| self.error("invalid union branch index"))?;
|
|
252
|
+
self.unions.push(index as u32);
|
|
253
|
+
self.value(branch, depth)?;
|
|
254
|
+
}
|
|
255
|
+
Schema::Record(record) => {
|
|
256
|
+
if record.fields.len() > self.remaining {
|
|
257
|
+
return Err(self.error("record exceeds maximum item count"));
|
|
258
|
+
}
|
|
259
|
+
for field in &record.fields {
|
|
260
|
+
self.allocation(field.name.len())?;
|
|
261
|
+
let path_length = self.path.len();
|
|
262
|
+
self.path.push('.');
|
|
263
|
+
self.path.push_str(&field.name);
|
|
264
|
+
self.value(&field.schema, depth + 1)?;
|
|
265
|
+
self.path.truncate(path_length);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
Schema::Array(array) => self.collection(&array.items, false, depth)?,
|
|
269
|
+
Schema::Map(map) => self.collection(&map.types, true, depth)?,
|
|
270
|
+
Schema::Ref { .. } => return Err(self.error("unresolved schema reference")),
|
|
271
|
+
}
|
|
272
|
+
Ok(())
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fn collection(
|
|
276
|
+
&mut self,
|
|
277
|
+
item_schema: &'schema Schema,
|
|
278
|
+
is_map: bool,
|
|
279
|
+
depth: usize,
|
|
280
|
+
) -> Result<(), String> {
|
|
281
|
+
let mut index = 0_usize;
|
|
282
|
+
loop {
|
|
283
|
+
let signed_count = self.long()?;
|
|
284
|
+
if signed_count == 0 {
|
|
285
|
+
return Ok(());
|
|
286
|
+
}
|
|
287
|
+
let count = usize::try_from(signed_count.unsigned_abs())
|
|
288
|
+
.ok()
|
|
289
|
+
.filter(|&count| count <= self.remaining)
|
|
290
|
+
.ok_or_else(|| self.error("collection exceeds maximum item count"))?;
|
|
291
|
+
let enclosing_end = self.end;
|
|
292
|
+
let block_end = if signed_count < 0 {
|
|
293
|
+
let size = self.length()?;
|
|
294
|
+
let end = self
|
|
295
|
+
.position
|
|
296
|
+
.checked_add(size)
|
|
297
|
+
.filter(|&end| end <= enclosing_end)
|
|
298
|
+
.ok_or_else(|| self.error("truncated collection block"))?;
|
|
299
|
+
self.end = end;
|
|
300
|
+
Some(end)
|
|
301
|
+
} else {
|
|
302
|
+
None
|
|
303
|
+
};
|
|
304
|
+
for _ in 0..count {
|
|
305
|
+
let path_length = self.path.len();
|
|
306
|
+
crate::push_index(&mut self.path, index);
|
|
307
|
+
if is_map {
|
|
308
|
+
self.path.push_str(".key");
|
|
309
|
+
self.string()?;
|
|
310
|
+
self.path.truncate(self.path.len() - 4);
|
|
311
|
+
}
|
|
312
|
+
self.value(item_schema, depth + 1)?;
|
|
313
|
+
self.path.truncate(path_length);
|
|
314
|
+
index += 1;
|
|
315
|
+
}
|
|
316
|
+
if block_end.is_some_and(|end| self.position != end) {
|
|
317
|
+
return Err(self.error("collection block byte size does not match its contents"));
|
|
318
|
+
}
|
|
319
|
+
self.end = enclosing_end;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
pub(crate) fn decimal_precision(bytes: &[u8], precision: usize) -> Result<(), &'static str> {
|
|
325
|
+
if bytes.is_empty() {
|
|
326
|
+
return Err("decimal requires at least one signed byte");
|
|
327
|
+
}
|
|
328
|
+
if precision == 0 {
|
|
329
|
+
return Err("decimal requires positive precision");
|
|
330
|
+
}
|
|
331
|
+
if bytes.len().saturating_mul(3) <= precision {
|
|
332
|
+
return Ok(());
|
|
333
|
+
}
|
|
334
|
+
let value = BigInt::from_signed_bytes_be(bytes);
|
|
335
|
+
if value.bits() <= precision.saturating_mul(3) as u64 {
|
|
336
|
+
return Ok(());
|
|
337
|
+
}
|
|
338
|
+
if value.bits() > precision.saturating_mul(4) as u64 {
|
|
339
|
+
return Err("decimal exceeds schema precision");
|
|
340
|
+
}
|
|
341
|
+
let precision = u32::try_from(precision).map_err(|_| "decimal precision is too large")?;
|
|
342
|
+
if value.abs() >= BigInt::from(10_u8).pow(precision) {
|
|
343
|
+
return Err("decimal exceeds schema precision");
|
|
344
|
+
}
|
|
345
|
+
Ok(())
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#[cfg(test)]
|
|
349
|
+
mod tests {
|
|
350
|
+
use super::*;
|
|
351
|
+
use apache_avro::schema::ResolvedSchema;
|
|
352
|
+
|
|
353
|
+
fn limits() -> Limits {
|
|
354
|
+
crate::validation::initialize().unwrap();
|
|
355
|
+
Limits {
|
|
356
|
+
max_depth: 64,
|
|
357
|
+
max_items: 1_000,
|
|
358
|
+
max_bytes: 1_000_000,
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
fn valid(schema: &Schema, bytes: &[u8]) -> Result<usize, String> {
|
|
363
|
+
crate::validation::initialize().unwrap();
|
|
364
|
+
let resolved = ResolvedSchema::new(schema).unwrap();
|
|
365
|
+
prefix(schema, resolved.get_names(), bytes, limits())
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn schema(json: &str) -> Schema {
|
|
369
|
+
crate::validation::initialize().unwrap();
|
|
370
|
+
Schema::parse_str(json).unwrap()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fn long(value: i64) -> Vec<u8> {
|
|
374
|
+
let mut value = ((value as u64) << 1) ^ ((value >> 63) as u64);
|
|
375
|
+
let mut bytes = Vec::new();
|
|
376
|
+
while value >= 0x80 {
|
|
377
|
+
bytes.push((value as u8) | 0x80);
|
|
378
|
+
value >>= 7;
|
|
379
|
+
}
|
|
380
|
+
bytes.push(value as u8);
|
|
381
|
+
bytes
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn bytes(value: &[u8]) -> Vec<u8> {
|
|
385
|
+
let mut encoded = long(value.len() as i64);
|
|
386
|
+
encoded.extend_from_slice(value);
|
|
387
|
+
encoded
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#[test]
|
|
391
|
+
fn exactly_one_datum_including_zero_byte_values() {
|
|
392
|
+
assert_eq!(valid(&Schema::Null, &[]).unwrap(), 0);
|
|
393
|
+
assert_eq!(valid(&Schema::Null, &[0]).unwrap(), 0);
|
|
394
|
+
assert_eq!(valid(&Schema::Int, &[2]).unwrap(), 1);
|
|
395
|
+
assert_eq!(valid(&Schema::Int, &[2, 0]).unwrap(), 1);
|
|
396
|
+
assert!(valid(&schema(r#"{"type":"record","name":"R","fields":[]}"#), &[]).is_ok());
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[test]
|
|
400
|
+
fn booleans_strings_and_unions_cannot_decode_truncated_as_null() {
|
|
401
|
+
for candidate in [Schema::Boolean, Schema::String, schema(r#"["null","int"]"#)] {
|
|
402
|
+
assert!(valid(&candidate, &[]).unwrap_err().contains("truncated"));
|
|
403
|
+
}
|
|
404
|
+
assert!(valid(&Schema::Boolean, &[0]).is_ok());
|
|
405
|
+
assert!(valid(&Schema::Boolean, &[1]).is_ok());
|
|
406
|
+
assert!(valid(&Schema::Boolean, &[2]).is_err());
|
|
407
|
+
assert!(valid(&Schema::String, &[0]).is_ok());
|
|
408
|
+
assert!(valid(&Schema::String, &[2]).is_err());
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#[test]
|
|
412
|
+
fn integer_boundaries_and_overflowing_varints() {
|
|
413
|
+
for value in [i64::MIN, -1, 0, 1, i64::MAX] {
|
|
414
|
+
assert!(valid(&Schema::Long, &long(value)).is_ok());
|
|
415
|
+
}
|
|
416
|
+
for value in [i64::from(i32::MIN), 0, i64::from(i32::MAX)] {
|
|
417
|
+
assert!(valid(&Schema::Int, &long(value)).is_ok());
|
|
418
|
+
}
|
|
419
|
+
for value in [i64::from(i32::MIN) - 1, i64::from(i32::MAX) + 1] {
|
|
420
|
+
assert!(valid(&Schema::Int, &long(value)).is_err());
|
|
421
|
+
assert!(valid(&Schema::Date, &long(value)).is_err());
|
|
422
|
+
}
|
|
423
|
+
assert!(valid(&Schema::Long, &[0x80]).is_err());
|
|
424
|
+
assert!(valid(&Schema::Long, &[0x80; 10]).is_err());
|
|
425
|
+
let mut overflowing = vec![0xff; 9];
|
|
426
|
+
overflowing.push(2);
|
|
427
|
+
assert!(
|
|
428
|
+
valid(&Schema::Long, &overflowing)
|
|
429
|
+
.unwrap_err()
|
|
430
|
+
.contains("overflows")
|
|
431
|
+
);
|
|
432
|
+
overflowing.push(0);
|
|
433
|
+
assert!(valid(&Schema::Long, &overflowing).is_err());
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
#[test]
|
|
437
|
+
fn byte_lengths_and_utf8_are_validated_before_allocation() {
|
|
438
|
+
assert!(valid(&Schema::Bytes, &long(-1)).is_err());
|
|
439
|
+
assert!(valid(&Schema::Bytes, &long(i64::MAX)).is_err());
|
|
440
|
+
assert!(valid(&Schema::String, &bytes("Zażółć".as_bytes())).is_ok());
|
|
441
|
+
assert!(
|
|
442
|
+
valid(&Schema::String, &bytes(&[0xff]))
|
|
443
|
+
.unwrap_err()
|
|
444
|
+
.contains("UTF-8")
|
|
445
|
+
);
|
|
446
|
+
assert!(valid(&Schema::Bytes, &bytes(&[0xff])).is_ok());
|
|
447
|
+
let mut limits = limits();
|
|
448
|
+
limits.max_bytes = 2;
|
|
449
|
+
assert!(prefix(&Schema::Bytes, &NamesRef::new(), &bytes(&[1, 2]), limits).is_err());
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
#[test]
|
|
453
|
+
fn enum_union_and_fixed_indices_and_sizes() {
|
|
454
|
+
let enumeration = schema(r#"{"type":"enum","name":"E","symbols":["A","B"]}"#);
|
|
455
|
+
assert!(valid(&enumeration, &[2]).is_ok());
|
|
456
|
+
assert!(valid(&enumeration, &[4]).is_err());
|
|
457
|
+
assert!(valid(&enumeration, &[1]).is_err());
|
|
458
|
+
let union = schema(r#"["null","int"]"#);
|
|
459
|
+
assert!(valid(&union, &[0]).is_ok());
|
|
460
|
+
assert!(valid(&union, &[2, 0]).is_ok());
|
|
461
|
+
for bytes in [vec![4], vec![1], vec![2]] {
|
|
462
|
+
assert!(valid(&union, &bytes).is_err());
|
|
463
|
+
}
|
|
464
|
+
let fixed = schema(r#"{"type":"fixed","name":"F","size":2}"#);
|
|
465
|
+
assert!(valid(&fixed, &[0xff, 0]).is_ok());
|
|
466
|
+
assert!(valid(&fixed, &[0xff]).is_err());
|
|
467
|
+
assert_eq!(valid(&fixed, &[0xff, 0, 0]).unwrap(), 2);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
#[test]
|
|
471
|
+
fn collection_blocks_require_exact_declared_sizes() {
|
|
472
|
+
let array = schema(r#"{"type":"array","items":"int"}"#);
|
|
473
|
+
assert!(valid(&array, &[3, 4, 2, 4, 0]).is_ok());
|
|
474
|
+
assert!(valid(&array, &[3, 2, 2, 4, 0]).is_err());
|
|
475
|
+
assert!(valid(&array, &[3, 6, 2, 4, 0]).is_err());
|
|
476
|
+
assert!(valid(&array, &[3, 1, 2, 4, 0]).is_err());
|
|
477
|
+
assert!(valid(&array, &[3, 100, 2, 4, 0]).is_err());
|
|
478
|
+
assert!(valid(&array, &[3, 4, 2, 4]).is_err());
|
|
479
|
+
assert!(valid(&array, &[2, 2, 2, 4, 0]).is_ok());
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#[test]
|
|
483
|
+
fn nested_blocks_cannot_read_past_parent_block() {
|
|
484
|
+
let array = schema(r#"{"type":"array","items":{"type":"array","items":"int"}}"#);
|
|
485
|
+
assert!(valid(&array, &[1, 6, 2, 2, 0, 0]).is_ok());
|
|
486
|
+
assert!(valid(&array, &[1, 4, 2, 2, 0, 0]).is_err());
|
|
487
|
+
assert!(valid(&array, &[1, 6, 1, 4, 2, 0]).is_err());
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#[test]
|
|
491
|
+
fn null_items_and_empty_records_cannot_bypass_allocation_budget() {
|
|
492
|
+
for array in [
|
|
493
|
+
schema(r#"{"type":"array","items":"null"}"#),
|
|
494
|
+
schema(r#"{"type":"array","items":{"type":"record","name":"R","fields":[]}}"#),
|
|
495
|
+
] {
|
|
496
|
+
let resolved = ResolvedSchema::new(&array).unwrap();
|
|
497
|
+
let mut limits = limits();
|
|
498
|
+
limits.max_items = 4;
|
|
499
|
+
assert!(prefix(&array, resolved.get_names(), &[6, 0], limits).is_ok());
|
|
500
|
+
assert!(prefix(&array, resolved.get_names(), &[8, 0], limits).is_err());
|
|
501
|
+
assert!(prefix(&array, resolved.get_names(), &[4, 4, 0], limits).is_err());
|
|
502
|
+
assert!(prefix(&array, resolved.get_names(), &[3, 0, 2, 0], limits).is_ok());
|
|
503
|
+
assert!(prefix(&array, resolved.get_names(), &[3, 0, 4, 0], limits).is_err());
|
|
504
|
+
let mut bomb = long(i64::MIN);
|
|
505
|
+
bomb.extend_from_slice(&[0, 0]);
|
|
506
|
+
assert!(valid(&array, &bomb).is_err());
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
#[test]
|
|
511
|
+
fn repeated_field_names_and_enum_symbols_cannot_amplify_tiny_input() {
|
|
512
|
+
let records = schema(
|
|
513
|
+
r#"{"type":"array","items":{"type":"record","name":"R","fields":[
|
|
514
|
+
{"name":"large_field_name","type":"null"}]}}"#,
|
|
515
|
+
);
|
|
516
|
+
let enums = schema(
|
|
517
|
+
r#"{"type":"array","items":{"type":"enum","name":"E","symbols":["LARGE_SYMBOL"]}}"#,
|
|
518
|
+
);
|
|
519
|
+
let mut limits = limits();
|
|
520
|
+
limits.max_bytes = 20;
|
|
521
|
+
for (schema, bytes) in [(records, vec![4, 0]), (enums, vec![4, 0, 0, 0])] {
|
|
522
|
+
let resolved = ResolvedSchema::new(&schema).unwrap();
|
|
523
|
+
assert!(
|
|
524
|
+
prefix(&schema, resolved.get_names(), &bytes, limits)
|
|
525
|
+
.unwrap_err()
|
|
526
|
+
.contains("byte count")
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#[test]
|
|
532
|
+
fn map_keys_are_utf8_and_zero_length_keys_are_bounded_by_entry_count() {
|
|
533
|
+
let map = schema(r#"{"type":"map","values":"null"}"#);
|
|
534
|
+
assert!(valid(&map, &[2, 0, 0]).is_ok());
|
|
535
|
+
assert!(valid(&map, &[2, 2, 0xff, 0]).unwrap_err().contains(".key"));
|
|
536
|
+
assert!(valid(&map, &[1, 2, 0, 0]).is_ok());
|
|
537
|
+
assert!(valid(&map, &[1, 0, 0, 0]).is_err());
|
|
538
|
+
let mut limits = limits();
|
|
539
|
+
limits.max_items = 2;
|
|
540
|
+
assert!(prefix(&map, &NamesRef::new(), &[4, 0, 0, 0], limits).is_err());
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
#[test]
|
|
544
|
+
fn recursion_and_schema_references_have_a_depth_bound() {
|
|
545
|
+
let recursive = schema(
|
|
546
|
+
r#"{"type":"record","name":"n.R","fields":[
|
|
547
|
+
{"name":"next","type":["null","n.R"]}]}"#,
|
|
548
|
+
);
|
|
549
|
+
let resolved = ResolvedSchema::new(&recursive).unwrap();
|
|
550
|
+
let mut limits = limits();
|
|
551
|
+
limits.max_depth = 4;
|
|
552
|
+
assert!(prefix(&recursive, resolved.get_names(), &[2, 0], limits).is_ok());
|
|
553
|
+
assert!(prefix(&recursive, resolved.get_names(), &[2, 2, 2, 0], limits).is_ok());
|
|
554
|
+
let error = prefix(&recursive, resolved.get_names(), &[2, 2, 2, 2, 0], limits).unwrap_err();
|
|
555
|
+
assert!(error.contains("$.next.next.next.next.next"));
|
|
556
|
+
assert!(error.contains("depth"));
|
|
557
|
+
let impossible =
|
|
558
|
+
schema(r#"{"type":"record","name":"R","fields":[{"name":"r","type":"R"}]}"#);
|
|
559
|
+
assert!(valid(&impossible, &[]).unwrap_err().contains("depth"));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
#[test]
|
|
563
|
+
fn named_dependencies_resolve_and_ref_hops_do_not_add_depth() {
|
|
564
|
+
let dependency = schema(r#"{"type":"fixed","name":"example.F","size":1}"#);
|
|
565
|
+
let reference = Schema::Ref {
|
|
566
|
+
name: apache_avro::schema::Name::new("example.F").unwrap(),
|
|
567
|
+
};
|
|
568
|
+
let names = ResolvedSchema::new(&dependency).unwrap();
|
|
569
|
+
let mut limits = limits();
|
|
570
|
+
limits.max_depth = 0;
|
|
571
|
+
assert!(prefix(&reference, names.get_names(), &[1], limits).is_ok());
|
|
572
|
+
assert!(prefix(&reference, &NamesRef::new(), &[1], limits).is_err());
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
#[test]
|
|
576
|
+
fn temporal_logical_values_obey_their_physical_limits() {
|
|
577
|
+
assert!(valid(&Schema::Date, &long(i64::from(i32::MIN))).is_ok());
|
|
578
|
+
for (schema, end) in [
|
|
579
|
+
(Schema::TimeMillis, 86_400_000_i64),
|
|
580
|
+
(Schema::TimeMicros, 86_400_000_000),
|
|
581
|
+
] {
|
|
582
|
+
assert!(valid(&schema, &long(0)).is_ok());
|
|
583
|
+
assert!(valid(&schema, &long(end - 1)).is_ok());
|
|
584
|
+
assert!(valid(&schema, &long(-1)).is_err());
|
|
585
|
+
assert!(valid(&schema, &long(end)).is_err());
|
|
586
|
+
}
|
|
587
|
+
for schema in [
|
|
588
|
+
Schema::TimestampMillis,
|
|
589
|
+
Schema::TimestampMicros,
|
|
590
|
+
Schema::TimestampNanos,
|
|
591
|
+
Schema::LocalTimestampMillis,
|
|
592
|
+
Schema::LocalTimestampMicros,
|
|
593
|
+
Schema::LocalTimestampNanos,
|
|
594
|
+
] {
|
|
595
|
+
assert!(valid(&schema, &long(i64::MIN)).is_ok());
|
|
596
|
+
assert!(valid(&schema, &long(i64::MAX)).is_ok());
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#[test]
|
|
601
|
+
fn decimal_precision_is_checked_for_bytes_and_fixed() {
|
|
602
|
+
let decimal = schema(r#"{"type":"bytes","logicalType":"decimal","precision":2,"scale":1}"#);
|
|
603
|
+
for value in [-99_i64, 0, 99] {
|
|
604
|
+
assert!(valid(&decimal, &bytes(&BigInt::from(value).to_signed_bytes_be())).is_ok());
|
|
605
|
+
}
|
|
606
|
+
for value in [-100_i64, 100] {
|
|
607
|
+
assert!(valid(&decimal, &bytes(&BigInt::from(value).to_signed_bytes_be())).is_err());
|
|
608
|
+
}
|
|
609
|
+
assert!(valid(&decimal, &[0]).is_err());
|
|
610
|
+
assert!(valid(&decimal, &bytes(&[0, 0, 99])).is_ok());
|
|
611
|
+
let fixed = schema(
|
|
612
|
+
r#"{"type":"fixed","name":"D","size":2,"logicalType":"decimal","precision":3,"scale":2}"#,
|
|
613
|
+
);
|
|
614
|
+
assert!(valid(&fixed, &[3, 231]).is_ok());
|
|
615
|
+
assert!(valid(&fixed, &[3, 232]).is_err());
|
|
616
|
+
assert!(valid(&Schema::BigDecimal, &[]).is_err());
|
|
617
|
+
assert!(valid(&Schema::BigDecimal, &bytes(&[2, 123, 4])).is_ok());
|
|
618
|
+
assert!(valid(&Schema::BigDecimal, &bytes(&[2, 123, 4, 0])).is_err());
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
#[test]
|
|
622
|
+
fn uuid_and_duration_underlying_representations_are_validated() {
|
|
623
|
+
let uuid = schema(r#"{"type":"string","logicalType":"uuid"}"#);
|
|
624
|
+
assert!(valid(&uuid, &bytes(b"550e8400-e29b-41d4-a716-446655440000")).is_ok());
|
|
625
|
+
assert!(valid(&uuid, &bytes(b"not a UUID")).is_err());
|
|
626
|
+
assert!(valid(&Schema::Uuid(UuidSchema::Bytes), &bytes(&[0; 16])).is_ok());
|
|
627
|
+
assert!(valid(&Schema::Uuid(UuidSchema::Bytes), &bytes(&[0; 15])).is_err());
|
|
628
|
+
let uuid = schema(r#"{"type":"fixed","name":"U","size":16,"logicalType":"uuid"}"#);
|
|
629
|
+
assert!(valid(&uuid, &[0; 16]).is_ok());
|
|
630
|
+
assert!(valid(&uuid, &[0; 15]).is_err());
|
|
631
|
+
let duration = schema(r#"{"type":"fixed","name":"D","size":12,"logicalType":"duration"}"#);
|
|
632
|
+
assert!(valid(&duration, &[255; 12]).is_ok());
|
|
633
|
+
assert!(valid(&duration, &[255; 11]).is_err());
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#[test]
|
|
637
|
+
fn seeded_malformed_input_never_passes_a_datum_that_apache_cannot_read() {
|
|
638
|
+
use apache_avro::reader::datum::GenericDatumReader;
|
|
639
|
+
|
|
640
|
+
let schemas = [
|
|
641
|
+
Schema::Null,
|
|
642
|
+
Schema::Boolean,
|
|
643
|
+
Schema::Int,
|
|
644
|
+
Schema::Long,
|
|
645
|
+
Schema::Float,
|
|
646
|
+
Schema::Double,
|
|
647
|
+
Schema::String,
|
|
648
|
+
Schema::Bytes,
|
|
649
|
+
schema(r#"["null","boolean","long","string"]"#),
|
|
650
|
+
schema(r#"{"type":"array","items":["null","int"]}"#),
|
|
651
|
+
schema(r#"{"type":"map","values":"null"}"#),
|
|
652
|
+
schema(
|
|
653
|
+
r#"{"type":"record","name":"R","fields":[{"name":"v","type":"int"},{"name":"next","type":["null","R"]}]}"#,
|
|
654
|
+
),
|
|
655
|
+
];
|
|
656
|
+
let mut random = 0x7f9b_0a4c_823d_65e1_u64;
|
|
657
|
+
for schema in schemas {
|
|
658
|
+
let resolved = ResolvedSchema::new(&schema).unwrap();
|
|
659
|
+
let reader = GenericDatumReader::builder(&schema).build().unwrap();
|
|
660
|
+
for sample in 0..4_096 {
|
|
661
|
+
let mut input = [0_u8; 32];
|
|
662
|
+
for byte in &mut input {
|
|
663
|
+
random ^= random << 13;
|
|
664
|
+
random ^= random >> 7;
|
|
665
|
+
random ^= random << 17;
|
|
666
|
+
*byte = random as u8;
|
|
667
|
+
}
|
|
668
|
+
let input = &input[..sample % 33];
|
|
669
|
+
if let Ok(consumed) = prefix(&schema, resolved.get_names(), input, limits()) {
|
|
670
|
+
let mut remaining = &input[..consumed];
|
|
671
|
+
assert!(
|
|
672
|
+
reader.read_value(&mut remaining).is_ok(),
|
|
673
|
+
"{schema}: {input:?}"
|
|
674
|
+
);
|
|
675
|
+
assert!(remaining.is_empty(), "{schema}: {input:?}");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|