@knolo/cli 6.2.0 → 6.2.2

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.
@@ -0,0 +1,486 @@
1
+ //! Research JSON canonical form.
2
+ //! Object keys sort by UTF-16 code units. Arrays keep their order.
3
+ //! Solidus, U+2028, and U+2029 stay literal, matching `JSON.stringify`.
4
+
5
+ use std::cmp::Ordering;
6
+
7
+ #[derive(Debug, Clone, PartialEq)]
8
+ pub enum Json {
9
+ Null,
10
+ Bool(bool),
11
+ Int(i64),
12
+ String(String),
13
+ Array(Vec<Json>),
14
+ Object(Vec<(String, Json)>),
15
+ }
16
+
17
+ impl Json {
18
+ pub fn object(pairs: Vec<(&str, Json)>) -> Json {
19
+ Json::Object(
20
+ pairs
21
+ .into_iter()
22
+ .map(|(key, value)| (key.to_string(), value))
23
+ .collect(),
24
+ )
25
+ }
26
+
27
+ pub fn get(&self, key: &str) -> Option<&Json> {
28
+ match self {
29
+ Json::Object(pairs) => pairs
30
+ .iter()
31
+ .find(|(name, _)| name == key)
32
+ .map(|(_, value)| value),
33
+ _ => None,
34
+ }
35
+ }
36
+
37
+ pub fn as_object(&self) -> Option<&[(String, Json)]> {
38
+ match self {
39
+ Json::Object(pairs) => Some(pairs),
40
+ _ => None,
41
+ }
42
+ }
43
+
44
+ pub fn as_str(&self) -> Option<&str> {
45
+ match self {
46
+ Json::String(value) => Some(value),
47
+ _ => None,
48
+ }
49
+ }
50
+
51
+ pub fn as_int(&self) -> Option<i64> {
52
+ match self {
53
+ Json::Int(value) => Some(*value),
54
+ _ => None,
55
+ }
56
+ }
57
+ }
58
+
59
+ pub fn parse_json(input: &str) -> Result<Json, String> {
60
+ let mut parser = Parser { input, index: 0 };
61
+ parser.skip();
62
+ let value = parser.value()?;
63
+ parser.skip();
64
+ if parser.index != parser.input.len() {
65
+ return Err("trailing JSON".into());
66
+ }
67
+ Ok(value)
68
+ }
69
+
70
+ pub fn canonicalize(value: &Json) -> String {
71
+ let mut out = String::new();
72
+ write_canonical(value, &mut out);
73
+ out
74
+ }
75
+
76
+ pub fn stringify(value: &Json) -> String {
77
+ let mut out = String::new();
78
+ write_stringify(value, &mut out);
79
+ out
80
+ }
81
+
82
+ pub fn digest_json(value: &Json) -> String {
83
+ let raw = crate::sha256(canonicalize(value).as_bytes());
84
+ let mut hex = String::with_capacity(64);
85
+ for byte in raw {
86
+ hex.push_str(&format!("{byte:02x}"));
87
+ }
88
+ format!("sha256-{hex}")
89
+ }
90
+
91
+ pub fn utf16_cmp(left: &str, right: &str) -> Ordering {
92
+ left.encode_utf16().cmp(right.encode_utf16())
93
+ }
94
+
95
+ struct Parser<'a> {
96
+ input: &'a str,
97
+ index: usize,
98
+ }
99
+
100
+ impl<'a> Parser<'a> {
101
+ fn skip(&mut self) {
102
+ while let Some(byte) = self.input.as_bytes().get(self.index).copied() {
103
+ if byte == b' ' || byte == b'\n' || byte == b'\r' || byte == b'\t' {
104
+ self.index += 1;
105
+ } else {
106
+ break;
107
+ }
108
+ }
109
+ }
110
+
111
+ fn value(&mut self) -> Result<Json, String> {
112
+ self.skip();
113
+ let byte = *self
114
+ .input
115
+ .as_bytes()
116
+ .get(self.index)
117
+ .ok_or_else(|| "truncated JSON".to_string())?;
118
+ match byte {
119
+ b'n' => self.literal("null", Json::Null),
120
+ b't' => self.literal("true", Json::Bool(true)),
121
+ b'f' => self.literal("false", Json::Bool(false)),
122
+ b'"' => Ok(Json::String(self.string()?)),
123
+ b'[' => self.array(),
124
+ b'{' => self.object(),
125
+ b'-' | b'0'..=b'9' => self.number(),
126
+ _ => Err("invalid JSON value".into()),
127
+ }
128
+ }
129
+
130
+ fn literal(&mut self, text: &str, value: Json) -> Result<Json, String> {
131
+ if self.input[self.index..].starts_with(text) {
132
+ self.index += text.len();
133
+ Ok(value)
134
+ } else {
135
+ Err(format!("invalid JSON literal {text}"))
136
+ }
137
+ }
138
+
139
+ fn array(&mut self) -> Result<Json, String> {
140
+ self.index += 1;
141
+ let mut items = Vec::new();
142
+ self.skip();
143
+ if self.eat(b']') {
144
+ return Ok(Json::Array(items));
145
+ }
146
+ loop {
147
+ items.push(self.value()?);
148
+ self.skip();
149
+ if self.eat(b']') {
150
+ return Ok(Json::Array(items));
151
+ }
152
+ if !self.eat(b',') {
153
+ return Err("invalid JSON array".into());
154
+ }
155
+ }
156
+ }
157
+
158
+ fn object(&mut self) -> Result<Json, String> {
159
+ self.index += 1;
160
+ let mut pairs = Vec::new();
161
+ self.skip();
162
+ if self.eat(b'}') {
163
+ return Ok(Json::Object(pairs));
164
+ }
165
+ loop {
166
+ self.skip();
167
+ if !self.eat(b'"') {
168
+ return Err("JSON object key must be a string".into());
169
+ }
170
+ let key = self.string_body()?;
171
+ self.skip();
172
+ if !self.eat(b':') {
173
+ return Err("JSON object missing colon".into());
174
+ }
175
+ let value = self.value()?;
176
+ if pairs.iter().any(|(name, _): &(String, Json)| name == &key) {
177
+ return Err("duplicate JSON key".into());
178
+ }
179
+ pairs.push((key, value));
180
+ self.skip();
181
+ if self.eat(b'}') {
182
+ return Ok(Json::Object(pairs));
183
+ }
184
+ if !self.eat(b',') {
185
+ return Err("invalid JSON object".into());
186
+ }
187
+ }
188
+ }
189
+
190
+ fn number(&mut self) -> Result<Json, String> {
191
+ let start = self.index;
192
+ if self.eat(b'-') {}
193
+ if self.eat(b'0') {
194
+ if self
195
+ .input
196
+ .as_bytes()
197
+ .get(self.index)
198
+ .is_some_and(|byte| byte.is_ascii_digit())
199
+ {
200
+ return Err("invalid JSON number".into());
201
+ }
202
+ } else if self
203
+ .input
204
+ .as_bytes()
205
+ .get(self.index)
206
+ .is_some_and(|byte| byte.is_ascii_digit())
207
+ {
208
+ while self
209
+ .input
210
+ .as_bytes()
211
+ .get(self.index)
212
+ .is_some_and(|byte| byte.is_ascii_digit())
213
+ {
214
+ self.index += 1;
215
+ }
216
+ } else {
217
+ return Err("invalid JSON number".into());
218
+ }
219
+ let mut fractional = false;
220
+ if self.eat(b'.') {
221
+ fractional = true;
222
+ if !self
223
+ .input
224
+ .as_bytes()
225
+ .get(self.index)
226
+ .is_some_and(|byte| byte.is_ascii_digit())
227
+ {
228
+ return Err("invalid JSON number".into());
229
+ }
230
+ while self
231
+ .input
232
+ .as_bytes()
233
+ .get(self.index)
234
+ .is_some_and(|byte| byte.is_ascii_digit())
235
+ {
236
+ self.index += 1;
237
+ }
238
+ }
239
+ if self
240
+ .input
241
+ .as_bytes()
242
+ .get(self.index)
243
+ .is_some_and(|byte| *byte == b'e' || *byte == b'E')
244
+ {
245
+ fractional = true;
246
+ self.index += 1;
247
+ if self
248
+ .input
249
+ .as_bytes()
250
+ .get(self.index)
251
+ .is_some_and(|byte| *byte == b'+' || *byte == b'-')
252
+ {
253
+ self.index += 1;
254
+ }
255
+ if !self
256
+ .input
257
+ .as_bytes()
258
+ .get(self.index)
259
+ .is_some_and(|byte| byte.is_ascii_digit())
260
+ {
261
+ return Err("invalid JSON number".into());
262
+ }
263
+ while self
264
+ .input
265
+ .as_bytes()
266
+ .get(self.index)
267
+ .is_some_and(|byte| byte.is_ascii_digit())
268
+ {
269
+ self.index += 1;
270
+ }
271
+ }
272
+ let token = &self.input[start..self.index];
273
+ if !fractional {
274
+ let parsed = token
275
+ .parse::<i64>()
276
+ .map_err(|_| format!("JSON integer out of range: {token}"))?;
277
+ return Ok(Json::Int(parsed));
278
+ }
279
+ let number: f64 = token
280
+ .parse()
281
+ .map_err(|_| format!("invalid JSON number: {token}"))?;
282
+ if number.fract() == 0.0 && number >= i64::MIN as f64 && number <= i64::MAX as f64 {
283
+ let whole = number as i64;
284
+ if whole as f64 == number && (whole as f64).abs() <= 9007199254740991.0 {
285
+ return Ok(Json::Int(whole));
286
+ }
287
+ }
288
+ Err("KAR JSON numbers must be safe integers".into())
289
+ }
290
+
291
+ fn string(&mut self) -> Result<String, String> {
292
+ if !self.eat(b'"') {
293
+ return Err("JSON string expected".into());
294
+ }
295
+ self.string_body()
296
+ }
297
+
298
+ fn string_body(&mut self) -> Result<String, String> {
299
+ let mut out = String::new();
300
+ loop {
301
+ let byte = *self
302
+ .input
303
+ .as_bytes()
304
+ .get(self.index)
305
+ .ok_or_else(|| "unterminated JSON string".to_string())?;
306
+ if byte == b'"' {
307
+ self.index += 1;
308
+ return Ok(out);
309
+ }
310
+ if byte == b'\\' {
311
+ self.index += 1;
312
+ let escape = *self
313
+ .input
314
+ .as_bytes()
315
+ .get(self.index)
316
+ .ok_or_else(|| "truncated JSON escape".to_string())?;
317
+ self.index += 1;
318
+ match escape {
319
+ b'"' => out.push('"'),
320
+ b'\\' => out.push('\\'),
321
+ b'/' => out.push('/'),
322
+ b'b' => out.push('\u{0008}'),
323
+ b'f' => out.push('\u{000c}'),
324
+ b'n' => out.push('\n'),
325
+ b'r' => out.push('\r'),
326
+ b't' => out.push('\t'),
327
+ b'u' => out.push(self.unicode_escape()?),
328
+ _ => return Err("invalid JSON string escape".into()),
329
+ }
330
+ continue;
331
+ }
332
+ if byte < 0x20 {
333
+ return Err("raw control character in JSON string".into());
334
+ }
335
+ let ch = self.input[self.index..]
336
+ .chars()
337
+ .next()
338
+ .ok_or_else(|| "invalid UTF-8 in JSON".to_string())?;
339
+ self.index += ch.len_utf8();
340
+ out.push(ch);
341
+ }
342
+ }
343
+
344
+ fn unicode_escape(&mut self) -> Result<char, String> {
345
+ let unit = self.hex4()?;
346
+ if (0xD800..=0xDBFF).contains(&unit) {
347
+ if self.input[self.index..].starts_with("\\u") {
348
+ self.index += 2;
349
+ let second = self.hex4()?;
350
+ if (0xDC00..=0xDFFF).contains(&second) {
351
+ let point =
352
+ 0x10000 + (((unit as u32 - 0xD800) << 10) | (second as u32 - 0xDC00));
353
+ return char::from_u32(point)
354
+ .ok_or_else(|| "invalid JSON surrogate pair".to_string());
355
+ }
356
+ }
357
+ return Err("lone JSON surrogate".into());
358
+ }
359
+ if (0xDC00..=0xDFFF).contains(&unit) {
360
+ return Err("lone JSON surrogate".into());
361
+ }
362
+ char::from_u32(unit as u32).ok_or_else(|| "invalid JSON unicode escape".to_string())
363
+ }
364
+
365
+ fn hex4(&mut self) -> Result<u16, String> {
366
+ let end = self.index + 4;
367
+ let text = self
368
+ .input
369
+ .get(self.index..end)
370
+ .ok_or_else(|| "truncated JSON unicode escape".to_string())?;
371
+ let value =
372
+ u16::from_str_radix(text, 16).map_err(|_| "invalid JSON unicode escape".to_string())?;
373
+ self.index = end;
374
+ Ok(value)
375
+ }
376
+
377
+ fn eat(&mut self, byte: u8) -> bool {
378
+ if self.input.as_bytes().get(self.index) == Some(&byte) {
379
+ self.index += 1;
380
+ true
381
+ } else {
382
+ false
383
+ }
384
+ }
385
+ }
386
+
387
+ fn write_canonical(value: &Json, out: &mut String) {
388
+ match value {
389
+ Json::Array(items) => {
390
+ out.push('[');
391
+ for (index, item) in items.iter().enumerate() {
392
+ if index > 0 {
393
+ out.push(',');
394
+ }
395
+ write_canonical(item, out);
396
+ }
397
+ out.push(']');
398
+ }
399
+ Json::Object(pairs) => {
400
+ let mut keys: Vec<&(String, Json)> = pairs.iter().collect();
401
+ keys.sort_by(|left, right| utf16_cmp(&left.0, &right.0));
402
+ out.push('{');
403
+ for (index, (key, child)) in keys.iter().enumerate() {
404
+ if index > 0 {
405
+ out.push(',');
406
+ }
407
+ write_string(key, out);
408
+ out.push(':');
409
+ write_canonical(child, out);
410
+ }
411
+ out.push('}');
412
+ }
413
+ other => write_scalar(other, out),
414
+ }
415
+ }
416
+
417
+ fn write_stringify(value: &Json, out: &mut String) {
418
+ match value {
419
+ Json::Array(items) => {
420
+ out.push('[');
421
+ for (index, item) in items.iter().enumerate() {
422
+ if index > 0 {
423
+ out.push(',');
424
+ }
425
+ write_stringify(item, out);
426
+ }
427
+ out.push(']');
428
+ }
429
+ Json::Object(pairs) => {
430
+ out.push('{');
431
+ for (index, (key, child)) in pairs.iter().enumerate() {
432
+ if index > 0 {
433
+ out.push(',');
434
+ }
435
+ write_string(key, out);
436
+ out.push(':');
437
+ write_stringify(child, out);
438
+ }
439
+ out.push('}');
440
+ }
441
+ other => write_scalar(other, out),
442
+ }
443
+ }
444
+
445
+ fn write_scalar(value: &Json, out: &mut String) {
446
+ match value {
447
+ Json::Null => out.push_str("null"),
448
+ Json::Bool(true) => out.push_str("true"),
449
+ Json::Bool(false) => out.push_str("false"),
450
+ Json::Int(value) => out.push_str(&value.to_string()),
451
+ Json::String(value) => write_string(value, out),
452
+ Json::Array(_) | Json::Object(_) => write_canonical(value, out),
453
+ }
454
+ }
455
+
456
+ fn write_string(value: &str, out: &mut String) {
457
+ out.push('"');
458
+ for ch in value.chars() {
459
+ match ch {
460
+ '"' => out.push_str("\\\""),
461
+ '\\' => out.push_str("\\\\"),
462
+ '\u{0008}' => out.push_str("\\b"),
463
+ '\u{000c}' => out.push_str("\\f"),
464
+ '\n' => out.push_str("\\n"),
465
+ '\r' => out.push_str("\\r"),
466
+ '\t' => out.push_str("\\t"),
467
+ other if (other as u32) < 0x20 => {
468
+ out.push_str(&format!("\\u{:04x}", other as u32));
469
+ }
470
+ other => out.push(other),
471
+ }
472
+ }
473
+ out.push('"');
474
+ }
475
+
476
+ #[cfg(test)]
477
+ mod tests {
478
+ use super::*;
479
+
480
+ #[test]
481
+ fn line_separators_stay_literal() {
482
+ let value = Json::String("\u{2028}\u{2029}".into());
483
+ assert_eq!(canonicalize(&value), "\"\u{2028}\u{2029}\"");
484
+ assert_eq!(stringify(&value), "\"\u{2028}\u{2029}\"");
485
+ }
486
+ }
@@ -0,0 +1,14 @@
1
+ //! Native evaluate and verify for public KAR V5 conformance vectors.
2
+ //! Authoring, coverage reports, Cortex, and composition stay in TypeScript.
3
+
4
+ mod closure;
5
+ mod cover;
6
+ mod evaluate;
7
+ mod json;
8
+ mod model;
9
+ mod session;
10
+
11
+ pub use session::{
12
+ create_kar_dossier_v5, evaluate_kar_v5, render_kar_dossier, verify_kar_dossier_v5,
13
+ verify_kar_v5, KarDossier, KarEvaluation, KarRunError, KarVerification,
14
+ };