Skip to content

Commit b108975

Browse files
committed
Fix edge cases and add property tests
1 parent 5cbaf09 commit b108975

11 files changed

Lines changed: 690 additions & 76 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ Run benchmarks locally:
189189
MIX_ENV=bench mix run bench/torque_bench.exs
190190
```
191191

192+
## Limitations
193+
194+
- **Nesting depth**: JSON documents nested deeper than 512 levels return `{:error, :nesting_too_deep}` from `decode/1`, `get/2`, and `encode/1` rather than crashing the VM. Real-world documents are never this deep; the limit exists to prevent stack overflow in the NIF.
195+
- **Numeric string keys**: Object keys that are pure integers (e.g. `"0"`, `"42"`) cannot be addressed via JSON Pointer because the pointer walker treats numeric path segments as array indices. Use `decode/1` if you need to access such keys.
196+
192197
## License
193198

194199
MIT

lib/torque.ex

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ defmodule Torque do
5454
{:ok, "example.com"} = Torque.get(doc, "/site/domain")
5555
{:error, :no_such_field} = Torque.get(doc, "/missing")
5656
"""
57-
@spec get(reference(), binary()) :: {:ok, term()} | {:error, :no_such_field}
57+
@spec get(reference(), binary()) ::
58+
{:ok, term()} | {:error, :no_such_field | :nesting_too_deep}
5859
def get(doc, path) when is_reference(doc) and is_binary(path) do
5960
Torque.Native.get(doc, path)
6061
end
@@ -75,6 +76,7 @@ defmodule Torque do
7576
case Torque.Native.get(doc, path) do
7677
{:ok, value} -> value
7778
{:error, :no_such_field} -> default
79+
{:error, reason} -> raise ArgumentError, "get error: #{reason}"
7880
end
7981
end
8082

@@ -93,7 +95,8 @@ defmodule Torque do
9395
[{:ok, 1}, {:ok, 2}, {:error, :no_such_field}] =
9496
Torque.get_many(doc, ["/a", "/b", "/c"])
9597
"""
96-
@spec get_many(reference(), [binary()]) :: [{:ok, term()} | {:error, :no_such_field}]
98+
@spec get_many(reference(), [binary()]) ::
99+
[{:ok, term()} | {:error, :no_such_field | :nesting_too_deep}]
97100
def get_many(doc, paths) when is_reference(doc) and is_list(paths) do
98101
Torque.Native.get_many(doc, paths)
99102
end
@@ -141,7 +144,7 @@ defmodule Torque do
141144
142145
Automatically uses a dirty CPU scheduler for inputs larger than 10 KB.
143146
"""
144-
@spec decode(binary()) :: {:ok, term()} | {:error, binary()}
147+
@spec decode(binary()) :: {:ok, term()} | {:error, binary() | :nesting_too_deep}
145148
def decode(json) when is_binary(json) and byte_size(json) > @timeslice_bytes do
146149
Torque.Native.decode_dirty(json)
147150
end
@@ -176,7 +179,7 @@ defmodule Torque do
176179
* Other atoms (encoded as JSON strings)
177180
* `{keyword_list}` tuples (jiffy-style proplist objects)
178181
"""
179-
@spec encode(term()) :: {:ok, binary()} | {:error, binary()}
182+
@spec encode(term()) :: {:ok, binary()} | {:error, binary() | :nesting_too_deep}
180183
def encode(term) do
181184
Torque.Native.encode(term)
182185
end

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ defmodule Torque.MixProject do
2929
{:ex_doc, "~> 0.35", only: :dev, runtime: false},
3030
{:dialyxir, "~> 1.4", only: :dev, runtime: false},
3131
{:jason, "~> 1.4"},
32+
{:stream_data, "~> 1.1", only: :test},
3233
{:simdjsone, "~> 0.5.0", only: :bench},
3334
{:jiffy, "~> 1.1", only: :bench},
3435
{:benchee, "~> 1.3", only: :bench}

mix.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@
1616
"rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"},
1717
"simdjsone": {:hex, :simdjsone, "0.5.0", "31f0d90f14005c6d984b479ccdbd3cd871536f6e1e61b05760a9682b511e4dd2", [:rebar3], [], "hexpm", "5d4addaa8095f6ace8ca2a4ecf76965e63bc5b25b863dc4b0a15c7de6c3f6856"},
1818
"statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"},
19+
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
1920
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
2021
}

native/torque_nif/src/atoms.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ rustler::atoms! {
22
ok,
33
error,
44
no_such_field,
5+
nesting_too_deep,
56
nil,
67
// atoms for fast identity comparison in encoder
78
r#true = "true",

native/torque_nif/src/decoder.rs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
use crate::atoms;
22
use crate::nif_util::make_tuple2;
3-
use crate::types::value_to_term;
3+
use crate::types::{value_to_term, MAX_DEPTH};
44
use crate::ParsedDocument;
55
use rustler::sys::{enif_make_list_from_array, ERL_NIF_TERM};
66
use rustler::{Binary, Encoder, Env, ListIterator, ResourceArc, Term};
77
use sonic_rs::{JsonContainerTrait, JsonValueTrait};
88

99
const GET_MANY_STACK: usize = 64;
1010

11+
/// Returns the last value for `key` in an object, matching the last-value-wins
12+
/// behaviour of `value_to_term` / `build_map_dedup` for duplicate keys.
13+
#[inline]
14+
fn object_get_last<'v>(value: &'v sonic_rs::Value, key: &str) -> Option<&'v sonic_rs::Value> {
15+
value
16+
.as_object()?
17+
.iter()
18+
.rfind(|(k, _)| *k == key)
19+
.map(|(_, v)| v)
20+
}
21+
1122
#[inline]
1223
fn pointer_lookup<'v>(value: &'v sonic_rs::Value, path: &str) -> Option<&'v sonic_rs::Value> {
1324
let bytes = path.as_bytes();
@@ -32,9 +43,9 @@ fn pointer_lookup<'v>(value: &'v sonic_rs::Value, path: &str) -> Option<&'v soni
3243
}
3344
if segment.contains('~') {
3445
let unescaped = segment.replace("~1", "/").replace("~0", "~");
35-
current = current.get(&unescaped as &str)?;
46+
current = object_get_last(current, &unescaped)?;
3647
} else {
37-
current = current.get(segment)?;
48+
current = object_get_last(current, segment)?;
3849
}
3950
}
4051
Some(current)
@@ -76,8 +87,12 @@ fn get<'a>(env: Env<'a>, doc: ResourceArc<ParsedDocument>, path: &str) -> Term<'
7687
let ok_raw = atoms::ok().as_c_arg();
7788
let err_raw = atoms::error().as_c_arg();
7889
let nsf_raw = atoms::no_such_field().as_c_arg();
90+
let ntd_raw = atoms::nesting_too_deep().as_c_arg();
7991
match pointer_lookup(&doc.value, path) {
80-
Some(value) => make_tuple2(env, ok_raw, value_to_term(env, value).as_c_arg()),
92+
Some(value) => match value_to_term(env, value, MAX_DEPTH) {
93+
Some(term) => make_tuple2(env, ok_raw, term.as_c_arg()),
94+
None => make_tuple2(env, err_raw, ntd_raw),
95+
},
8196
None => make_tuple2(env, err_raw, nsf_raw),
8297
}
8398
}
@@ -90,9 +105,13 @@ fn get_one_result(
90105
ok_raw: ERL_NIF_TERM,
91106
err_raw: ERL_NIF_TERM,
92107
nsf_raw: ERL_NIF_TERM,
108+
ntd_raw: ERL_NIF_TERM,
93109
) -> ERL_NIF_TERM {
94110
match pointer_lookup(&doc.value, path) {
95-
Some(value) => make_tuple2(env, ok_raw, value_to_term(env, value).as_c_arg()).as_c_arg(),
111+
Some(value) => match value_to_term(env, value, MAX_DEPTH) {
112+
Some(term) => make_tuple2(env, ok_raw, term.as_c_arg()).as_c_arg(),
113+
None => make_tuple2(env, err_raw, ntd_raw).as_c_arg(),
114+
},
96115
None => make_tuple2(env, err_raw, nsf_raw).as_c_arg(),
97116
}
98117
}
@@ -106,6 +125,7 @@ fn get_many<'a>(
106125
let ok_raw = atoms::ok().as_c_arg();
107126
let err_raw = atoms::error().as_c_arg();
108127
let nsf_raw = atoms::no_such_field().as_c_arg();
128+
let ntd_raw = atoms::nesting_too_deep().as_c_arg();
109129

110130
// Collect into stack array when possible
111131
let mut stack: [ERL_NIF_TERM; GET_MANY_STACK] = [0; GET_MANY_STACK];
@@ -132,7 +152,7 @@ fn get_many<'a>(
132152
}
133153
};
134154

135-
let r = get_one_result(env, &doc, path, ok_raw, err_raw, nsf_raw);
155+
let r = get_one_result(env, &doc, path, ok_raw, err_raw, nsf_raw, ntd_raw);
136156
if count < GET_MANY_STACK && heap.is_none() {
137157
stack[count] = r;
138158
} else {
@@ -177,11 +197,14 @@ fn array_length<'a>(env: Env<'a>, doc: ResourceArc<ParsedDocument>, path: &str)
177197

178198
fn do_decode<'a>(env: Env<'a>, bytes: &[u8]) -> Term<'a> {
179199
match sonic_rs::from_slice::<sonic_rs::Value>(bytes) {
180-
Ok(value) => make_tuple2(
181-
env,
182-
atoms::ok().as_c_arg(),
183-
value_to_term(env, &value).as_c_arg(),
184-
),
200+
Ok(value) => match value_to_term(env, &value, MAX_DEPTH) {
201+
Some(term) => make_tuple2(env, atoms::ok().as_c_arg(), term.as_c_arg()),
202+
None => make_tuple2(
203+
env,
204+
atoms::error().as_c_arg(),
205+
atoms::nesting_too_deep().as_c_arg(),
206+
),
207+
},
185208
Err(e) => make_tuple2(
186209
env,
187210
atoms::error().as_c_arg(),
@@ -232,7 +255,10 @@ fn get_many_nil<'a>(
232255
};
233256

234257
let r = match pointer_lookup(&doc.value, path) {
235-
Some(value) => value_to_term(env, value).as_c_arg(),
258+
Some(value) => match value_to_term(env, value, MAX_DEPTH) {
259+
Some(term) => term.as_c_arg(),
260+
None => nil_raw,
261+
},
236262
None => nil_raw,
237263
};
238264
if count < GET_MANY_STACK && heap.is_none() {

native/torque_nif/src/encoder.rs

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
use crate::atoms;
22
use crate::nif_util::make_tuple2;
3+
use crate::types::MAX_DEPTH;
34
use rustler::sys::{
45
c_int, c_uint, enif_get_atom, enif_get_atom_length, enif_get_double, enif_get_int64,
5-
enif_get_list_cell, enif_get_tuple, enif_inspect_binary, ErlNifBinary, ErlNifCharEncoding,
6-
ErlNifEnv, ERL_NIF_TERM,
6+
enif_get_list_cell, enif_get_tuple, enif_get_uint64, enif_inspect_binary, ErlNifBinary,
7+
ErlNifCharEncoding, ErlNifEnv, ERL_NIF_TERM,
78
};
89
use rustler::{Encoder, Env, MapIterator, NewBinary, Term, TermType};
910
use std::mem::MaybeUninit;
1011

1112
#[derive(Debug)]
1213
enum EncodeError {
1314
BadArg,
15+
DepthExceeded,
1416
}
1517

1618
/// Read an atom's name into a stack buffer without heap allocation.
@@ -48,14 +50,19 @@ unsafe fn atom_to_stack_buf(
4850
fn encode<'a>(env: Env<'a>, term: Term<'a>) -> Term<'a> {
4951
let mut buf: Vec<u8> = Vec::with_capacity(2048);
5052
let env_raw = env.as_c_arg();
51-
match encode_term(env, env_raw, term, &mut buf) {
53+
match encode_term(env, env_raw, term, &mut buf, MAX_DEPTH) {
5254
Ok(()) => {
5355
let mut binary = NewBinary::new(env, buf.len());
5456
binary.as_mut_slice().copy_from_slice(&buf);
5557
let bin_term: Term = binary.into();
5658
make_tuple2(env, atoms::ok().as_c_arg(), bin_term.as_c_arg())
5759
}
58-
Err(_) => make_tuple2(
60+
Err(EncodeError::DepthExceeded) => make_tuple2(
61+
env,
62+
atoms::error().as_c_arg(),
63+
atoms::nesting_too_deep().as_c_arg(),
64+
),
65+
Err(EncodeError::BadArg) => make_tuple2(
5966
env,
6067
atoms::error().as_c_arg(),
6168
"encode error".encode(env).as_c_arg(),
@@ -69,14 +76,17 @@ fn encode<'a>(env: Env<'a>, term: Term<'a>) -> Term<'a> {
6976
fn encode_iodata<'a>(env: Env<'a>, term: Term<'a>) -> Term<'a> {
7077
let mut buf: Vec<u8> = Vec::with_capacity(2048);
7178
let env_raw = env.as_c_arg();
72-
match encode_term(env, env_raw, term, &mut buf) {
79+
match encode_term(env, env_raw, term, &mut buf, MAX_DEPTH) {
7380
Ok(()) => {
7481
let mut binary = NewBinary::new(env, buf.len());
7582
binary.as_mut_slice().copy_from_slice(&buf);
7683
binary.into()
7784
}
78-
Err(_) => unsafe {
79-
let reason = "encode error".encode(env).as_c_arg();
85+
Err(e) => unsafe {
86+
let reason = match e {
87+
EncodeError::DepthExceeded => atoms::nesting_too_deep().as_c_arg(),
88+
EncodeError::BadArg => "encode error".encode(env).as_c_arg(),
89+
};
8090
Term::new(env, rustler::sys::enif_raise_exception(env_raw, reason))
8191
},
8292
}
@@ -88,15 +98,16 @@ fn encode_term(
8898
env_raw: *mut ErlNifEnv,
8999
term: Term,
90100
buf: &mut Vec<u8>,
101+
depth: u32,
91102
) -> Result<(), EncodeError> {
92103
match term.get_type() {
93-
TermType::Map => encode_map(env, env_raw, term, buf),
94-
TermType::List => encode_list(env, env_raw, term, buf),
104+
TermType::Map => encode_map(env, env_raw, term, buf, depth),
105+
TermType::List => encode_list(env, env_raw, term, buf, depth),
95106
TermType::Binary => encode_binary(env_raw, term, buf),
96107
TermType::Integer => encode_integer(env_raw, term, buf),
97108
TermType::Float => encode_float(env_raw, term, buf),
98109
TermType::Atom => encode_atom(env_raw, term, buf),
99-
TermType::Tuple => encode_tuple(env, env_raw, term, buf),
110+
TermType::Tuple => encode_tuple(env, env_raw, term, buf, depth),
100111
_ => Err(EncodeError::BadArg),
101112
}
102113
}
@@ -106,7 +117,11 @@ fn encode_map(
106117
env_raw: *mut ErlNifEnv,
107118
term: Term,
108119
buf: &mut Vec<u8>,
120+
depth: u32,
109121
) -> Result<(), EncodeError> {
122+
if depth == 0 {
123+
return Err(EncodeError::DepthExceeded);
124+
}
110125
let iter = MapIterator::new(term).ok_or(EncodeError::BadArg)?;
111126
buf.push(b'{');
112127
let mut first = true;
@@ -117,7 +132,7 @@ fn encode_map(
117132
first = false;
118133
encode_map_key(env_raw, key, buf)?;
119134
buf.push(b':');
120-
encode_term(env, env_raw, value, buf)?;
135+
encode_term(env, env_raw, value, buf, depth - 1)?;
121136
}
122137
buf.push(b'}');
123138
Ok(())
@@ -158,7 +173,11 @@ fn encode_list(
158173
env_raw: *mut ErlNifEnv,
159174
term: Term,
160175
buf: &mut Vec<u8>,
176+
depth: u32,
161177
) -> Result<(), EncodeError> {
178+
if depth == 0 {
179+
return Err(EncodeError::DepthExceeded);
180+
}
162181
buf.push(b'[');
163182
let mut first = true;
164183
let mut current = term.as_c_arg();
@@ -170,7 +189,7 @@ fn encode_list(
170189
}
171190
first = false;
172191
let item = unsafe { Term::new(env, head) };
173-
encode_term(env, env_raw, item, buf)?;
192+
encode_term(env, env_raw, item, buf, depth - 1)?;
174193
current = tail;
175194
}
176195
buf.push(b']');
@@ -204,12 +223,19 @@ fn encode_integer(
204223
buf: &mut Vec<u8>,
205224
) -> Result<(), EncodeError> {
206225
let mut n: i64 = 0;
207-
if unsafe { enif_get_int64(env_raw, term.as_c_arg(), &mut n) } == 0 {
208-
return Err(EncodeError::BadArg);
226+
if unsafe { enif_get_int64(env_raw, term.as_c_arg(), &mut n) } != 0 {
227+
let mut itoa_buf = itoa::Buffer::new();
228+
buf.extend_from_slice(itoa_buf.format(n).as_bytes());
229+
return Ok(());
209230
}
210-
let mut itoa_buf = itoa::Buffer::new();
211-
buf.extend_from_slice(itoa_buf.format(n).as_bytes());
212-
Ok(())
231+
// Fallback for u64 range (i64::MAX + 1 ..= u64::MAX)
232+
let mut u: u64 = 0;
233+
if unsafe { enif_get_uint64(env_raw, term.as_c_arg(), &mut u) } != 0 {
234+
let mut itoa_buf = itoa::Buffer::new();
235+
buf.extend_from_slice(itoa_buf.format(u).as_bytes());
236+
return Ok(());
237+
}
238+
Err(EncodeError::BadArg)
213239
}
214240

215241
#[inline]
@@ -218,6 +244,10 @@ fn encode_float(env_raw: *mut ErlNifEnv, term: Term, buf: &mut Vec<u8>) -> Resul
218244
if unsafe { enif_get_double(env_raw, term.as_c_arg(), &mut n) } == 0 {
219245
return Err(EncodeError::BadArg);
220246
}
247+
// ryu panics on non-finite floats; JSON has no representation for them
248+
if !n.is_finite() {
249+
return Err(EncodeError::BadArg);
250+
}
221251
let mut ryu_buf = ryu::Buffer::new();
222252
buf.extend_from_slice(ryu_buf.format(n).as_bytes());
223253
Ok(())
@@ -264,12 +294,13 @@ fn encode_tuple(
264294
env_raw: *mut ErlNifEnv,
265295
term: Term,
266296
buf: &mut Vec<u8>,
297+
depth: u32,
267298
) -> Result<(), EncodeError> {
268299
let elements = unsafe { get_tuple_raw(env_raw, term)? };
269300
if elements.len() == 1 {
270301
let inner = unsafe { Term::new(env, elements[0]) };
271302
if inner.get_type() == TermType::List {
272-
return encode_proplist(env, env_raw, inner, buf);
303+
return encode_proplist(env, env_raw, inner, buf, depth);
273304
}
274305
}
275306
Err(EncodeError::BadArg)
@@ -280,7 +311,11 @@ fn encode_proplist(
280311
env_raw: *mut ErlNifEnv,
281312
term: Term,
282313
buf: &mut Vec<u8>,
314+
depth: u32,
283315
) -> Result<(), EncodeError> {
316+
if depth == 0 {
317+
return Err(EncodeError::DepthExceeded);
318+
}
284319
buf.push(b'{');
285320
let mut first = true;
286321
let mut current = term.as_c_arg();
@@ -302,7 +337,7 @@ fn encode_proplist(
302337
let val = unsafe { Term::new(env, pair[1]) };
303338
encode_map_key(env_raw, key, buf)?;
304339
buf.push(b':');
305-
encode_term(env, env_raw, val, buf)?;
340+
encode_term(env, env_raw, val, buf, depth - 1)?;
306341
current = tail;
307342
}
308343
buf.push(b'}');

0 commit comments

Comments
 (0)