use crate::adapter::json::{
self, JNode, JsonMatch, JsonModel, K_ARRAY, K_NULL, K_NUMBER, K_OBJECT, K_STRING,
};
use crate::error::{Error, ErrorClass, Result};
use crate::limits::Limits;
pub const MODEL_VERSION: u8 = 1;
pub const MAX_MODEL_NODES: u32 = 1 << 24;
pub const MAX_MODEL_ANCHORS: u32 = 1 << 24;
pub const ABSENT: u32 = u32::MAX;
pub const C_CODE: u8 = 0;
pub const C_MARKDOWN: u8 = 1;
pub const C_RAW: u8 = 2;
pub const C_UNKNOWN: u8 = 3;
pub const CELL_TYPE_NAMES: [&str; 3] = ["code", "markdown", "raw"];
pub const fn cell_type_name(class: u8) -> &'static str {
match class {
C_CODE => "code",
C_MARKDOWN => "markdown",
C_RAW => "raw",
C_UNKNOWN => "unknown",
_ => "unknown",
}
}
pub fn cell_class_of(name: &str) -> u8 {
match CELL_TYPE_NAMES.iter().position(|&n| n == name) {
Some(i) => i as u8,
None => C_UNKNOWN,
}
}
pub const O_STREAM: u8 = 0;
pub const O_EXECUTE_RESULT: u8 = 1;
pub const O_DISPLAY_DATA: u8 = 2;
pub const O_ERROR: u8 = 3;
pub const O_UNKNOWN: u8 = 4;
pub const OUTPUT_TYPE_NAMES: [&str; 4] = ["stream", "execute_result", "display_data", "error"];
pub const fn output_type_name(class: u8) -> &'static str {
match class {
O_STREAM => "stream",
O_EXECUTE_RESULT => "execute_result",
O_DISPLAY_DATA => "display_data",
O_ERROR => "error",
O_UNKNOWN => "unknown",
_ => "unknown",
}
}
pub fn output_class_of(name: &str) -> u8 {
match OUTPUT_TYPE_NAMES.iter().position(|&n| n == name) {
Some(i) => i as u8,
None => O_UNKNOWN,
}
}
pub const SRC_MISSING: u8 = 0;
pub const SRC_STRING: u8 = 1;
pub const SRC_LINES: u8 = 2;
pub const fn source_form_name(form: u8) -> &'static str {
match form {
SRC_MISSING => "missing",
SRC_STRING => "string",
SRC_LINES => "lines",
_ => "unknown",
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CellAnchor {
pub node: u32,
pub class: u8,
pub cell_type_node: u32,
pub source_node: u32,
pub source_form: u8,
pub execution_count_node: u32,
pub metadata_node: u32,
pub attachments_node: u32,
pub outputs: Vec<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NotebookModel {
pub root: u32,
pub nbformat: u32,
pub nbformat_minor: u32,
pub nbformat_node: u32,
pub nbformat_minor_node: u32,
pub metadata_node: u32,
pub cells: Vec<CellAnchor>,
pub json: JsonModel,
}
impl NotebookModel {
pub fn node(&self, index: u32) -> Option<&JNode> {
self.json.node(index)
}
pub fn cell_count(&self) -> u32 {
self.cells.len() as u32
}
pub fn output_count(&self) -> u32 {
self.cells.iter().map(|c| c.outputs.len() as u32).sum()
}
pub fn cell(&self, index: u32) -> Option<&CellAnchor> {
self.cells.get(index as usize)
}
pub fn encode(&self) -> Vec<u8> {
let json_bytes = self.json.encode();
let anchors: usize = self.cells.iter().map(|c| 1 + c.outputs.len()).sum();
let mut out = Vec::with_capacity(64 + json_bytes.len() + anchors * 4);
out.extend_from_slice(b"NBKM");
out.push(MODEL_VERSION);
out.extend_from_slice(&[0, 0, 0]); out.extend_from_slice(&self.nbformat.to_le_bytes());
out.extend_from_slice(&self.nbformat_minor.to_le_bytes());
out.extend_from_slice(&self.doc_len().to_le_bytes());
out.extend_from_slice(&self.root.to_le_bytes());
out.extend_from_slice(&self.nbformat_node.to_le_bytes());
out.extend_from_slice(&self.nbformat_minor_node.to_le_bytes());
out.extend_from_slice(&self.metadata_node.to_le_bytes());
out.extend_from_slice(&(self.cells.len() as u32).to_le_bytes());
for c in &self.cells {
out.extend_from_slice(&c.node.to_le_bytes());
out.push(c.class);
out.push(c.source_form);
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&c.cell_type_node.to_le_bytes());
out.extend_from_slice(&c.source_node.to_le_bytes());
out.extend_from_slice(&c.execution_count_node.to_le_bytes());
out.extend_from_slice(&c.metadata_node.to_le_bytes());
out.extend_from_slice(&c.attachments_node.to_le_bytes());
out.extend_from_slice(&(c.outputs.len() as u32).to_le_bytes());
for o in &c.outputs {
out.extend_from_slice(&o.to_le_bytes());
}
}
out.extend_from_slice(&(json_bytes.len() as u32).to_le_bytes());
out.extend_from_slice(&json_bytes);
out
}
pub fn decode(bytes: &[u8]) -> Result<NotebookModel> {
let mut r = Reader::new(bytes);
if r.bytes(4)? != b"NBKM" {
return Err(corrupt("bad model magic"));
}
if r.u8()? != MODEL_VERSION {
return Err(corrupt("unsupported model version"));
}
if r.bytes(3)? != [0, 0, 0] {
return Err(corrupt("unknown model flags"));
}
let nbformat = r.u32()?;
let nbformat_minor = r.u32()?;
let doc_len = r.u64()?;
let root = r.u32()?;
let nbformat_node = r.u32()?;
let nbformat_minor_node = r.u32()?;
let metadata_node = r.u32()?;
let cn = r.u32()?;
if cn > MAX_MODEL_ANCHORS {
return Err(corrupt("cell count is implausible"));
}
let mut cells = Vec::with_capacity(cn as usize);
for _ in 0..cn {
let node = r.u32()?;
let class = r.u8()?;
if class > C_UNKNOWN {
return Err(corrupt("unknown cell class"));
}
let source_form = r.u8()?;
if source_form > SRC_LINES {
return Err(corrupt("unknown source form"));
}
if r.bytes(2)? != [0, 0] {
return Err(corrupt("unknown cell flags"));
}
let cell_type_node = r.u32()?;
let source_node = r.u32()?;
let execution_count_node = r.u32()?;
let cell_metadata = r.u32()?;
let attachments_node = r.u32()?;
let on = r.u32()?;
if on > MAX_MODEL_ANCHORS {
return Err(corrupt("output count is implausible"));
}
let mut outputs = Vec::with_capacity(on as usize);
for _ in 0..on {
outputs.push(r.u32()?);
}
cells.push(CellAnchor {
node,
class,
cell_type_node,
source_node,
source_form,
execution_count_node,
metadata_node: cell_metadata,
attachments_node,
outputs,
});
}
let json_len = r.u32()?;
let json_bytes = r.bytes(json_len as usize)?;
if !r.at_end() {
return Err(corrupt("model has trailing bytes"));
}
let json = JsonModel::decode(json_bytes)?;
if json.doc_len != doc_len {
return Err(corrupt("model doc_len disagrees with the JSON model"));
}
let nnodes = json.nodes.len() as u32;
for idx in [root, nbformat_node] {
if idx >= nnodes {
return Err(corrupt("model anchor is out of range"));
}
}
for idx in [nbformat_minor_node, metadata_node] {
if idx != ABSENT && idx >= nnodes {
return Err(corrupt("model optional anchor is out of range"));
}
}
for c in &cells {
if c.node >= nnodes || c.cell_type_node >= nnodes {
return Err(corrupt("cell anchor is out of range"));
}
for idx in [
c.source_node,
c.execution_count_node,
c.metadata_node,
c.attachments_node,
] {
if idx != ABSENT && idx >= nnodes {
return Err(corrupt("cell optional anchor is out of range"));
}
}
if c.source_node == ABSENT && c.source_form != SRC_MISSING {
return Err(corrupt("cell source form disagrees with its anchor"));
}
for &o in &c.outputs {
if o >= nnodes {
return Err(corrupt("output index is out of range"));
}
}
}
Ok(NotebookModel {
root,
nbformat,
nbformat_minor,
nbformat_node,
nbformat_minor_node,
metadata_node,
cells,
json,
})
}
pub fn doc_len(&self) -> u64 {
self.json.doc_len
}
}
pub type NotebookMatch = JsonMatch;
pub fn detect(source: &[u8], limits: Limits) -> bool {
if source.len() as u64 > limits.max_notebook_document_bytes {
return false;
}
if !json::detect(source, limits) {
return false;
}
parse(source, limits, false).is_ok()
}
pub fn parse(source: &[u8], limits: Limits, build: bool) -> Result<NotebookModel> {
if source.len() as u64 > limits.max_notebook_document_bytes {
return Err(Error::resource_limit(format!(
"notebook source is {} bytes, above the {}-byte document cap",
source.len(),
limits.max_notebook_document_bytes
)));
}
let json = json::parse(source, limits, true).map_err(|e| match e.class() {
ErrorClass::ResourceLimit => e,
_ => invalid_notebook(format!(
"notebook source is not a single JSON value: {}",
e.message()
)),
})?;
if json.nodes.len() as u64 > limits.max_notebook_nodes as u64 {
return Err(Error::resource_limit(format!(
"notebook document exceeds the {}-node cap",
limits.max_notebook_nodes
)));
}
if json.max_depth as u64 > limits.max_notebook_depth as u64 {
return Err(Error::resource_limit(format!(
"notebook nesting exceeds the {}-level cap",
limits.max_notebook_depth
)));
}
let a = analyze(&json, source, limits)?;
Ok(NotebookModel {
root: json.root,
nbformat: a.nbformat,
nbformat_minor: a.nbformat_minor,
nbformat_node: a.nbformat_node,
nbformat_minor_node: a.nbformat_minor_node,
metadata_node: a.metadata_node,
cells: if build { a.cells } else { Vec::new() },
json,
})
}
pub fn build_notebook_model(source: &[u8], limits: Limits) -> Result<Vec<u8>> {
Ok(parse(source, limits, true)?.encode())
}
pub fn classify(source: &[u8], limits: Limits) -> Result<u32> {
Ok(parse(source, limits, false)?.nbformat)
}
pub fn object_member(
model: &NotebookModel,
source: &[u8],
obj: u32,
key: &str,
) -> Result<Option<u32>> {
let node = model
.node(obj)
.ok_or_else(|| corrupt("object index is out of range"))?;
if node.kind != K_OBJECT {
return Err(corrupt("node is not a JSON object"));
}
let mut i = 0usize;
while i + 1 < node.children.len() {
let k = node.children[i];
let v = node.children[i + 1];
i += 2;
let kn = model
.node(k)
.ok_or_else(|| corrupt("object key index is out of range"))?;
if kn.kind == K_STRING && json::decode_string(source, kn)? == key {
return Ok(Some(v));
}
}
Ok(None)
}
pub fn object_members(
model: &NotebookModel,
source: &[u8],
obj: u32,
) -> Result<Vec<(String, u32)>> {
let node = model
.node(obj)
.ok_or_else(|| corrupt("object index is out of range"))?;
if node.kind != K_OBJECT {
return Err(corrupt("node is not a JSON object"));
}
let mut out = Vec::with_capacity(node.children.len() / 2);
let mut i = 0usize;
while i + 1 < node.children.len() {
let k = node.children[i];
let v = node.children[i + 1];
i += 2;
let kn = model
.node(k)
.ok_or_else(|| corrupt("object key index is out of range"))?;
let key = json::decode_string(source, kn)?;
out.push((key, v));
}
Ok(out)
}
pub fn member_string_value(
model: &NotebookModel,
source: &[u8],
obj: u32,
key: &str,
) -> Result<Option<String>> {
match object_member(model, source, obj, key)? {
Some(v) => {
let n = model
.node(v)
.ok_or_else(|| corrupt("member value index is out of range"))?;
if n.kind == K_STRING {
Ok(Some(json::decode_string(source, n)?))
} else {
Ok(None)
}
}
None => Ok(None),
}
}
pub fn member_keys(
model: &NotebookModel,
source: &[u8],
obj: u32,
key: &str,
) -> Result<Option<Vec<String>>> {
let Some(v) = object_member(model, source, obj, key)? else {
return Ok(None);
};
let n = model
.node(v)
.ok_or_else(|| corrupt("member value index is out of range"))?;
if n.kind != K_OBJECT {
return Ok(None);
}
let mut keys = Vec::with_capacity(n.children.len() / 2);
let mut i = 0usize;
while i + 1 < n.children.len() {
let k = n.children[i];
i += 2;
let kn = model
.node(k)
.ok_or_else(|| corrupt("member key index is out of range"))?;
keys.push(json::decode_string(source, kn)?);
}
Ok(Some(keys))
}
pub fn cell_type_of(model: &NotebookModel, source: &[u8], cell: u32) -> Result<Option<String>> {
member_string_value(model, source, cell, "cell_type")
}
pub fn output_type_of(model: &NotebookModel, source: &[u8], output: u32) -> Result<Option<String>> {
member_string_value(model, source, output, "output_type")
}
pub fn token_bytes<'a>(source: &'a [u8], node: &JNode) -> Result<&'a [u8]> {
json::token_bytes(source, node)
}
pub fn decode_string(source: &[u8], node: &JNode) -> Result<String> {
json::decode_string(source, node)
}
pub fn node_span(model: &NotebookModel, index: u32) -> Result<(u64, u64)> {
let n = model
.node(index)
.ok_or_else(|| corrupt("node index is out of range"))?;
Ok((n.start, n.end))
}
pub fn find(
model: &NotebookModel,
source: &[u8],
pattern: &str,
limits: Limits,
) -> Result<Vec<NotebookMatch>> {
json::find(&model.json, source, pattern, limits)
}
pub fn canonical_text(model: &NotebookModel, source: &[u8]) -> Result<String> {
json::canonical_text(&model.json, source)
}
struct Analyzed {
nbformat: u32,
nbformat_minor: u32,
nbformat_node: u32,
nbformat_minor_node: u32,
metadata_node: u32,
cells: Vec<CellAnchor>,
}
fn analyze(json: &JsonModel, source: &[u8], limits: Limits) -> Result<Analyzed> {
let root = json.root;
let root_node = json
.node(root)
.ok_or_else(|| corrupt("root index is out of range"))?;
if root_node.kind != K_OBJECT {
return Err(invalid_notebook(
"notebook root must be a JSON object with an integer \"nbformat\" and an array \"cells\"",
));
}
let nbformat_node = member_node(json, source, root, "nbformat")?
.ok_or_else(|| invalid_notebook("notebook root has no \"nbformat\" member"))?;
let nn = json
.node(nbformat_node)
.ok_or_else(|| corrupt("nbformat node index is out of range"))?;
if nn.kind != K_NUMBER {
return Err(invalid_notebook("notebook \"nbformat\" must be an integer"));
}
let nbformat = parse_nonneg_integer(token_text(source, nn)?.as_str()).ok_or_else(|| {
invalid_notebook("notebook \"nbformat\" must be a plain non-negative integer literal")
})?;
if nbformat == 0 {
return Err(invalid_notebook("notebook \"nbformat\" must be at least 1"));
}
let nbformat_minor_node = member_node(json, source, root, "nbformat_minor")?;
let nbformat_minor = match nbformat_minor_node {
Some(n) => {
let node = json
.node(n)
.ok_or_else(|| corrupt("nbformat_minor node index is out of range"))?;
if node.kind != K_NUMBER {
return Err(invalid_notebook(
"notebook \"nbformat_minor\" must be an integer",
));
}
parse_nonneg_integer(token_text(source, node)?.as_str()).ok_or_else(|| {
invalid_notebook(
"notebook \"nbformat_minor\" must be a plain non-negative integer literal",
)
})?
}
None => 0,
};
let metadata_node = member_node(json, source, root, "metadata")?;
if let Some(m) = metadata_node {
let node = json
.node(m)
.ok_or_else(|| corrupt("metadata node index is out of range"))?;
if node.kind != K_OBJECT {
return Err(invalid_notebook(
"notebook root \"metadata\" must be an object",
));
}
}
let cells_node = member_node(json, source, root, "cells")?
.ok_or_else(|| invalid_notebook("notebook root has no \"cells\" member"))?;
let cn = json
.node(cells_node)
.ok_or_else(|| corrupt("cells node index is out of range"))?;
if cn.kind != K_ARRAY {
return Err(invalid_notebook("notebook \"cells\" must be an array"));
}
let mut cells: Vec<CellAnchor> = Vec::with_capacity(cn.children.len().min(4096));
let mut total_outputs: u64 = 0;
let mut total_source_bytes: u64 = 0;
for &cell in &cn.children {
if cells.len() as u64 >= limits.max_notebook_cells as u64 {
return Err(Error::resource_limit(format!(
"notebook exceeds the {}-cell cap",
limits.max_notebook_cells
)));
}
let node = json
.node(cell)
.ok_or_else(|| corrupt("cell index is out of range"))?;
if node.kind != K_OBJECT {
return Err(invalid_notebook("notebook cell must be a JSON object"));
}
let cell_type_node = member_node(json, source, cell, "cell_type")?
.ok_or_else(|| invalid_notebook("notebook cell has no \"cell_type\" member"))?;
let ctn = json
.node(cell_type_node)
.ok_or_else(|| corrupt("cell_type node index is out of range"))?;
if ctn.kind != K_STRING {
return Err(invalid_notebook(
"notebook cell \"cell_type\" must be a string",
));
}
let cell_type = json::decode_string(source, ctn)?;
let class = cell_class_of(&cell_type);
let source_node = member_node(json, source, cell, "source")?;
let mut source_form = SRC_MISSING;
if let Some(s) = source_node {
let sn = json
.node(s)
.ok_or_else(|| corrupt("source node index is out of range"))?;
match sn.kind {
K_STRING => source_form = SRC_STRING,
K_ARRAY => {
for &el in &sn.children {
let en = json
.node(el)
.ok_or_else(|| corrupt("source element index is out of range"))?;
if en.kind != K_STRING {
return Err(invalid_notebook(
"notebook cell \"source\" array elements must be strings",
));
}
}
source_form = SRC_LINES;
}
_ => {
return Err(invalid_notebook(
"notebook cell \"source\" must be a string or an array of strings",
));
}
}
total_source_bytes = total_source_bytes.saturating_add(sn.end.saturating_sub(sn.start));
if total_source_bytes > limits.max_notebook_source_bytes {
return Err(Error::resource_limit(format!(
"notebook source exceeds the {}-byte cap",
limits.max_notebook_source_bytes
)));
}
}
let execution_count_node = member_node(json, source, cell, "execution_count")?;
if let Some(e) = execution_count_node {
let en = json
.node(e)
.ok_or_else(|| corrupt("execution_count node index is out of range"))?;
if en.kind != K_NUMBER && en.kind != K_NULL {
return Err(invalid_notebook(
"notebook cell \"execution_count\" must be an integer or null",
));
}
}
let cell_metadata = member_node(json, source, cell, "metadata")?;
if let Some(m) = cell_metadata {
let node = json
.node(m)
.ok_or_else(|| corrupt("cell metadata node index is out of range"))?;
if node.kind != K_OBJECT {
return Err(invalid_notebook(
"notebook cell \"metadata\" must be an object",
));
}
}
let attachments_node = member_node(json, source, cell, "attachments")?;
if let Some(a) = attachments_node {
let node = json
.node(a)
.ok_or_else(|| corrupt("attachments node index is out of range"))?;
if node.kind != K_OBJECT {
return Err(invalid_notebook(
"notebook cell \"attachments\" must be an object",
));
}
}
let mut outputs: Vec<u32> = Vec::new();
if let Some(o) = member_node(json, source, cell, "outputs")? {
let on = json
.node(o)
.ok_or_else(|| corrupt("outputs node index is out of range"))?;
if on.kind != K_ARRAY {
return Err(invalid_notebook(
"notebook cell \"outputs\" must be an array",
));
}
for &out in &on.children {
total_outputs = total_outputs.saturating_add(1);
if total_outputs > limits.max_notebook_outputs as u64 {
return Err(Error::resource_limit(format!(
"notebook exceeds the {}-output cap",
limits.max_notebook_outputs
)));
}
let un = json
.node(out)
.ok_or_else(|| corrupt("output index is out of range"))?;
if un.kind != K_OBJECT {
return Err(invalid_notebook("notebook output must be a JSON object"));
}
let ot = member_node(json, source, out, "output_type")?.ok_or_else(|| {
invalid_notebook("notebook output has no \"output_type\" member")
})?;
let otn = json
.node(ot)
.ok_or_else(|| corrupt("output_type node index is out of range"))?;
if otn.kind != K_STRING {
return Err(invalid_notebook(
"notebook output \"output_type\" must be a string",
));
}
outputs.push(out);
}
}
cells.push(CellAnchor {
node: cell,
class,
cell_type_node,
source_node: source_node.unwrap_or(ABSENT),
source_form,
execution_count_node: execution_count_node.unwrap_or(ABSENT),
metadata_node: cell_metadata.unwrap_or(ABSENT),
attachments_node: attachments_node.unwrap_or(ABSENT),
outputs,
});
}
Ok(Analyzed {
nbformat,
nbformat_minor,
nbformat_node,
nbformat_minor_node: nbformat_minor_node.unwrap_or(ABSENT),
metadata_node: metadata_node.unwrap_or(ABSENT),
cells,
})
}
fn member_node(json: &JsonModel, source: &[u8], obj: u32, key: &str) -> Result<Option<u32>> {
let node = json
.node(obj)
.ok_or_else(|| corrupt("object index is out of range"))?;
if node.kind != K_OBJECT {
return Err(corrupt("node is not a JSON object"));
}
let mut i = 0usize;
while i + 1 < node.children.len() {
let k = node.children[i];
let v = node.children[i + 1];
i += 2;
let kn = json
.node(k)
.ok_or_else(|| corrupt("object key index is out of range"))?;
if kn.kind == K_STRING && json::decode_string(source, kn)? == key {
return Ok(Some(v));
}
}
Ok(None)
}
fn token_text(source: &[u8], node: &JNode) -> Result<String> {
Ok(String::from_utf8_lossy(json::token_bytes(source, node)?).into_owned())
}
fn parse_nonneg_integer(tok: &str) -> Option<u32> {
if tok.is_empty() {
return None;
}
let mut v: u64 = 0;
for b in tok.bytes() {
if !b.is_ascii_digit() {
return None;
}
v = v.checked_mul(10)?.checked_add((b - b'0') as u64)?;
if v > u32::MAX as u64 {
return None;
}
}
Some(v as u32)
}
fn corrupt(msg: &str) -> Error {
Error::invalid_model(format!("corrupt notebook model: {msg}"))
}
fn invalid_notebook(msg: impl Into<String>) -> Error {
Error::invalid_notebook_structure(msg)
}
struct Reader<'a> {
b: &'a [u8],
at: usize,
}
impl<'a> Reader<'a> {
fn new(b: &'a [u8]) -> Self {
Reader { b, at: 0 }
}
fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self
.at
.checked_add(n)
.ok_or_else(|| corrupt("reader overflow"))?;
let s = self
.b
.get(self.at..end)
.ok_or_else(|| corrupt("reader underrun"))?;
self.at = end;
Ok(s)
}
fn u8(&mut self) -> Result<u8> {
Ok(self.bytes(1)?[0])
}
fn u32(&mut self) -> Result<u32> {
let b = self.bytes(4)?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn u64(&mut self) -> Result<u64> {
let b = self.bytes(8)?;
Ok(u64::from_le_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
fn at_end(&self) -> bool {
self.at == self.b.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
const NOTEBOOK: &[u8] = br##"{
"cells": [
{"cell_type": "markdown", "metadata": {}, "source": ["# Title\n", "text\n"]},
{"cell_type": "code", "execution_count": 1, "metadata": {},
"outputs": [
{"output_type": "stream", "name": "stdout", "text": ["hi\n"]},
{"output_type": "execute_result", "execution_count": 1,
"data": {"text/plain": "2"}, "metadata": {}}
],
"source": "print(1 + 1)\n"},
{"cell_type": "raw", "metadata": {}, "source": "raw bytes"}
],
"metadata": {"kernelspec": {"name": "python3"}, "language_info": {"name": "python"}},
"nbformat": 4,
"nbformat_minor": 5
}"##;
fn model(src: &[u8]) -> NotebookModel {
parse(src, Limits::DEFAULT, true).unwrap()
}
#[test]
fn detects_notebook_and_rejects_json_and_junk() {
assert!(detect(NOTEBOOK, Limits::DEFAULT));
assert!(detect(br#"{"nbformat":4,"cells":[]}"#, Limits::DEFAULT));
assert!(detect(
br#"{"nbformat":3,"nbformat_minor":0,"cells":[{"cell_type":"code","source":""}]}"#,
Limits::DEFAULT
));
assert!(!detect(br#"{"a":1}"#, Limits::DEFAULT));
assert!(!detect(
br#"{"cells":[{"cell_type":"code"}]}"#,
Limits::DEFAULT
));
assert!(!detect(
br#"{"nbformat":4,"cells":[1,2,3]}"#,
Limits::DEFAULT
));
assert!(!detect(br#"{"nbformat":"4","cells":[]}"#, Limits::DEFAULT));
assert!(!detect(br#"{"nbformat":4.0,"cells":[]}"#, Limits::DEFAULT));
assert!(!detect(
br#"{"nbformat":4,"cells":[{"not_cell_type":1}]}"#,
Limits::DEFAULT
));
assert!(!detect(b"[1,2,3]", Limits::DEFAULT));
assert!(!detect(b"the quick brown fox", Limits::DEFAULT));
assert!(!detect(b"", Limits::DEFAULT));
}
#[test]
fn cell_types_and_source_representation_are_preserved() {
let m = model(NOTEBOOK);
assert_eq!(m.nbformat, 4);
assert_eq!(m.nbformat_minor, 5);
assert_eq!(m.cell_count(), 3);
assert_eq!(m.cells[0].class, C_MARKDOWN);
assert_eq!(m.cells[1].class, C_CODE);
assert_eq!(m.cells[2].class, C_RAW);
assert_eq!(m.cells[0].source_form, SRC_LINES);
assert_eq!(m.cells[1].source_form, SRC_STRING);
assert_eq!(m.cells[2].source_form, SRC_STRING);
let ct = m.node(m.cells[1].cell_type_node).unwrap();
assert_eq!(token_bytes(NOTEBOOK, ct).unwrap(), b"\"code\"");
let src_lines = m.node(m.cells[0].source_node).unwrap();
assert_eq!(src_lines.kind, K_ARRAY);
assert_eq!(src_lines.children.len(), 2);
let src_str = m.node(m.cells[1].source_node).unwrap();
assert_eq!(src_str.kind, K_STRING);
}
#[test]
fn outputs_preserve_type_and_fields() {
let m = model(NOTEBOOK);
let code = &m.cells[1];
assert_eq!(code.outputs.len(), 2);
let o0 = code.outputs[0];
assert_eq!(output_type_of(&m, NOTEBOOK, o0).unwrap().unwrap(), "stream");
assert_eq!(
member_string_value(&m, NOTEBOOK, o0, "name")
.unwrap()
.unwrap(),
"stdout"
);
let text = object_member(&m, NOTEBOOK, o0, "text").unwrap().unwrap();
assert_eq!(m.node(text).unwrap().kind, K_ARRAY);
let o1 = code.outputs[1];
assert_eq!(
output_type_of(&m, NOTEBOOK, o1).unwrap().unwrap(),
"execute_result"
);
let data_keys = member_keys(&m, NOTEBOOK, o1, "data").unwrap().unwrap();
assert_eq!(data_keys, vec!["text/plain"]);
}
#[test]
fn attachments_and_metadata_order_are_preserved() {
let src = br#"{"nbformat":4,"nbformat_minor":5,"metadata":{"a":1,"a":2},
"cells":[{"cell_type":"markdown","source":"x",
"attachments":{"img.png":{"image/png":"AAAA"},"b.txt":{"text/plain":"bb"}}}]}"#;
let m = model(src);
let cell = m.cells[0].node;
let keys = member_keys(&m, src, cell, "attachments").unwrap().unwrap();
assert_eq!(keys, vec!["img.png", "b.txt"]);
let members = object_members(&m, src, m.metadata_node).unwrap();
let mk: Vec<&str> = members.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(mk, vec!["a", "a"]);
}
#[test]
fn model_roundtrips_and_rejects_corruption() {
let m = model(NOTEBOOK);
let bytes = m.encode();
assert_eq!(NotebookModel::decode(&bytes).unwrap(), m);
for cut in 0..bytes.len() {
let _ = NotebookModel::decode(&bytes[..cut]);
}
let mut bad = bytes.clone();
bad[0] ^= 0xFF;
assert!(NotebookModel::decode(&bad).is_err());
}
#[test]
fn malformed_and_inconsistent_decline_typed() {
for src in [
&b"{\"nbformat\":4}"[..],
&b"{\"nbformat\":4,\"cells\":[1]}"[..],
&b"{\"nbformat\":4,\"cells\":[{\"cell_type\":5}]}"[..],
&b"{\"nbformat\":4,\"cells\":[{\"cell_type\":\"code\",\"source\":5}]}"[..],
&b"{\"nbformat\":4,\"cells\":[{\"cell_type\":\"code\",\"outputs\":[1]}]}"[..],
&b"[]"[..],
] {
let e = parse(src, Limits::DEFAULT, true).unwrap_err();
assert_eq!(
e.class(),
ErrorClass::InvalidNotebookStructure,
"src={src:?}"
);
}
let e = parse(b"{", Limits::DEFAULT, true).unwrap_err();
assert_eq!(e.class(), ErrorClass::InvalidNotebookStructure);
}
#[test]
fn caps_decline_typed() {
let tight = Limits {
max_notebook_cells: 1,
..Limits::DEFAULT
};
assert!(!detect(NOTEBOOK, tight));
let e = parse(NOTEBOOK, tight, true).unwrap_err();
assert_eq!(e.class(), ErrorClass::ResourceLimit);
let tight_bytes = Limits {
max_notebook_document_bytes: 4,
..Limits::DEFAULT
};
assert!(!detect(NOTEBOOK, tight_bytes));
let mut big = Vec::new();
big.extend_from_slice(b"{\"nbformat\":4,\"cells\":[{\"cell_type\":\"code\",\"outputs\":[");
for i in 0..64 {
if i > 0 {
big.push(b',');
}
big.extend_from_slice(
b"{\"output_type\":\"stream\",\"name\":\"stdout\",\"text\":\"x\"}",
);
}
big.extend_from_slice(b"]}]}");
let tight_out = Limits {
max_notebook_outputs: 8,
..Limits::DEFAULT
};
assert!(parse(&big, Limits::DEFAULT, true).is_ok());
let e = parse(&big, tight_out, true).unwrap_err();
assert_eq!(e.class(), ErrorClass::ResourceLimit);
}
#[test]
fn find_reuses_json_vocabulary() {
let m = model(NOTEBOOK);
let ms = find(&m, NOTEBOOK, "output_type", Limits::DEFAULT).unwrap();
assert_eq!(ms.len(), 2);
assert!(
ms.iter()
.all(|m| m.role == crate::adapter::json::MatchRole::Key)
);
}
#[test]
fn random_bytes_never_panic() {
let mut x: u64 = 0x0B0C_2024_1234_5678;
for _ in 0..512 {
let mut buf = vec![0u8; 256];
for b in buf.iter_mut() {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*b = (x & 0xFF) as u8;
}
let _ = detect(&buf, Limits::STRICT);
if let Ok(m) = parse(&buf, Limits::STRICT, true) {
let _ = m.encode();
let _ = canonical_text(&m, &buf);
let _ = find(&m, &buf, "a", Limits::STRICT);
for c in &m.cells {
let _ = cell_type_of(&m, &buf, c.node);
for &o in &c.outputs {
let _ = output_type_of(&m, &buf, o);
}
}
}
}
}
}