1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! SPARQL-Update fuzzing property tests (khive bb54cb41).
//!
//! These tests generate random SPARQL-like strings and verify the parser
//! either succeeds or returns an error — never panics. They also verify
//! the size cap: inputs exceeding `SPARQL_UPDATE_MAX_BYTES` are rejected
//! before reaching the spargebra parser.
use solid_pod_rs::ldp::{apply_sparql_patch, Graph, SPARQL_UPDATE_MAX_BYTES};
use solid_pod_rs::PodError;
// ---------------------------------------------------------------------------
// Size-cap enforcement
// ---------------------------------------------------------------------------
#[test]
fn sparql_update_rejects_oversized_input() {
// Build a string that exceeds the 1 MiB limit.
let oversized = "x".repeat(SPARQL_UPDATE_MAX_BYTES + 1);
let result = apply_sparql_patch(Graph::new(), &oversized);
assert!(result.is_err());
match result.unwrap_err() {
PodError::BadRequest(msg) => {
assert!(
msg.contains("exceeds"),
"error message should mention the limit: {msg}"
);
}
other => panic!("expected BadRequest, got: {other:?}"),
}
}
#[test]
fn sparql_update_accepts_input_at_exact_limit() {
// An input at exactly the limit should not be rejected by the size
// guard (it may still fail to parse, but that is an Unsupported
// error, not a BadRequest about size).
let at_limit = "x".repeat(SPARQL_UPDATE_MAX_BYTES);
let result = apply_sparql_patch(Graph::new(), &at_limit);
// The parser will reject this as invalid SPARQL, but the size guard
// should not trigger.
match result {
Err(PodError::BadRequest(msg)) => {
assert!(
!msg.contains("exceeds"),
"at-limit input should not trigger size guard: {msg}"
);
}
_ => {} // Either Ok or some other parse error — both acceptable.
}
}
// ---------------------------------------------------------------------------
// Property: random bytes never cause a panic
// ---------------------------------------------------------------------------
#[test]
fn sparql_random_ascii_does_not_panic() {
// Deterministic PRNG using a simple xorshift approach.
let mut state: u64 = 0xDEAD_BEEF_CAFE_BABE;
for round in 0..500 {
// Simple xorshift-like PRNG.
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state = state.wrapping_add(round);
let len = (state as usize % 512) + 1;
let input: String = (0..len)
.map(|i| {
let byte = ((state.wrapping_mul(i as u64 + 1)) % 128) as u8;
// Ensure printable ASCII or common whitespace.
if byte >= 32 && byte < 127 {
byte as char
} else {
' '
}
})
.collect();
let _ = apply_sparql_patch(Graph::new(), &input);
}
}
#[test]
fn sparql_structured_variants_do_not_panic() {
// Feed structured SPARQL-like strings that exercise parser edge
// cases: unclosed braces, mixed keywords, nested constructs.
let cases = [
"",
" ",
"INSERT",
"INSERT DATA",
"INSERT DATA {",
"INSERT DATA { }",
"INSERT DATA { <> <> <> }",
"INSERT DATA { <> <> <> . }",
"DELETE DATA { <> <> <> . }",
"DELETE WHERE { ?s ?p ?o . }",
"INSERT DATA { <http://s> <http://p> \"\" . }",
"INSERT DATA { <http://s> <http://p> \"value\"@en . }",
"INSERT DATA { <http://s> <http://p> \"42\"^^<http://www.w3.org/2001/XMLSchema#integer> . }",
"DELETE DATA { <http://s> <http://p> <http://o> . } ; INSERT DATA { <http://s> <http://p> <http://o2> . }",
"PREFIX : <http://example.org/> INSERT DATA { :s :p :o . }",
// Malformed — should produce errors, not panics.
"INSERT DATA {{ <> <> <> . }}",
"DELETE",
"SELECT * WHERE { ?s ?p ?o }",
"CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }",
"DROP ALL",
"LOAD <http://example.org/data>",
"CREATE GRAPH <http://example.org/g>",
// Deeply nested
"INSERT DATA { <http://s> <http://p> \"\\\"escaped\\\"\" . }",
// Unicode
"INSERT DATA { <http://s> <http://p> \"\u{1F600}\" . }",
// Very long IRI
&format!(
"INSERT DATA {{ <http://example.org/{}> <http://p> <http://o> . }}",
"a".repeat(8000)
),
// Repeated operations
&"INSERT DATA { <http://s> <http://p> <http://o> . } ; ".repeat(100),
];
for (i, input) in cases.iter().enumerate() {
let result = apply_sparql_patch(Graph::new(), input);
// Must not panic — Ok or Err are both acceptable.
match &result {
Ok(outcome) => {
// Sanity: if the parse succeeded, the graph should be
// consistent (insert count is usize, always >= 0, but
// verify the graph length matches).
assert!(
outcome.graph.len() <= outcome.inserted,
"case {i}: graph larger than insert count"
);
}
Err(_) => {
// Expected for malformed input.
}
}
}
}
#[test]
fn sparql_empty_input_returns_zero_mutations() {
// An empty SPARQL-Update document is syntactically valid (no
// operations). The parser should accept it and return zero changes.
let result = apply_sparql_patch(Graph::new(), "");
match result {
Ok(outcome) => {
assert_eq!(outcome.inserted, 0);
assert_eq!(outcome.deleted, 0);
assert!(outcome.graph.is_empty());
}
Err(_) => {
// Some parsers consider empty input an error — acceptable.
}
}
}
#[test]
fn sparql_null_bytes_do_not_panic() {
let inputs = [
"\0",
"INSERT DATA { \0 }",
"INSERT\0DATA { <http://s> <http://p> <http://o> . }",
"\0\0\0\0\0",
];
for input in &inputs {
let _ = apply_sparql_patch(Graph::new(), input);
}
}