use serde_json::{json, Map, Value};
use crate::file::FileError;
use crate::ingest::BuildStats;
pub mod front_end {
pub const CLI: &str = "cli";
pub const PYTHON: &str = "python";
pub const R: &str = "r";
pub const JAVA: &str = "java";
pub const WASM: &str = "wasm";
}
#[derive(Debug, Clone, Default)]
pub struct Options {
pub command: String,
pub format: Option<String>,
pub no_pyramid: bool,
pub text_index: bool,
pub label_index: bool,
pub pyramid_algo: Option<String>,
pub permutations: Option<u8>,
}
#[derive(Debug, Clone, Default)]
pub struct Input {
pub name: String,
pub format: String,
pub bytes: Option<u64>,
pub blake3: Option<String>,
}
impl Input {
pub fn from_bytes(name: &str, format: &str, data: &[u8]) -> Self {
Input {
name: name.to_string(),
format: format.to_string(),
bytes: Some(data.len() as u64),
blake3: Some(blake3::hash(data).to_hex().to_string()),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Record {
pub front_end: String,
pub front_end_version: String,
pub built_at: Option<String>,
pub options: Options,
pub inputs: Vec<Input>,
pub statements_read: Option<u64>,
}
pub fn reproducible() -> bool {
#[cfg(not(target_arch = "wasm32"))]
{
std::env::var("RETE_BUILD_RECORD").is_ok_and(|v| v.trim() == "reproducible")
}
#[cfg(target_arch = "wasm32")]
{
false
}
}
pub fn writes_legacy() -> bool {
#[cfg(not(target_arch = "wasm32"))]
{
std::env::var("RETE_WRITE_FORMAT").is_ok_and(|v| v.trim() == "5")
}
#[cfg(target_arch = "wasm32")]
{
false
}
}
pub fn timestamp() -> Option<String> {
#[cfg(not(target_arch = "wasm32"))]
{
let epoch = std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|v| v.parse::<i64>().ok());
if reproducible() && epoch.is_none() {
return None;
}
let secs = epoch.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
});
Some(rfc3339(secs))
}
#[cfg(target_arch = "wasm32")]
{
None
}
}
pub fn rfc3339(secs: i64) -> String {
let days = secs.div_euclid(86_400);
let rem = secs.rem_euclid(86_400);
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!(
"{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
rem / 3600,
rem % 3600 / 60,
rem % 60
)
}
pub fn engine() -> Value {
json!({
"name": "rete-core",
"version": env!("CARGO_PKG_VERSION"),
"commit": env!("RETE_GIT_COMMIT"),
"target": env!("RETE_TARGET"),
"profile": env!("RETE_PROFILE"),
"features": crate::build_trace::enabled_features(),
})
}
pub fn to_json(r: &Record, stats: Option<&BuildStats>) -> Vec<u8> {
let mut o = Map::new();
o.insert("schema".into(), json!(1));
if let Some(t) = &r.built_at {
o.insert("built_at".into(), json!(t));
}
o.insert(
"builder".into(),
json!(format!("rete-{} {}", r.front_end, r.front_end_version)),
);
o.insert("front_end".into(), json!(r.front_end));
o.insert("engine".into(), engine());
#[cfg(not(target_arch = "wasm32"))]
let host = json!({"os": std::env::consts::OS, "arch": std::env::consts::ARCH});
#[cfg(target_arch = "wasm32")]
let host = json!({"os": "browser", "arch": "wasm32"});
o.insert("host".into(), host);
let mut p = Map::new();
p.insert("command".into(), json!(r.options.command));
if let Some(f) = &r.options.format {
p.insert("format".into(), json!(f));
}
for (k, v) in [
("no_pyramid", r.options.no_pyramid),
("text_index", r.options.text_index),
(
"label_index",
r.options.label_index || stats.is_some_and(|s| s.label_index),
),
] {
if v {
p.insert(k.into(), json!(true));
}
}
if let Some(a) = &r.options.pyramid_algo {
p.insert("pyramid_algo".into(), json!(a));
}
p.insert(
"codec".into(),
json!(if crate::file::writer_codec() == crate::CODEC_ZSTD {
"zstd"
} else {
"none"
}),
);
p.insert(
"permutations".into(),
json!(r.options.permutations.unwrap_or(6)),
);
p.insert("named_graphs".into(), json!("quad"));
let w = crate::write_opts::write_options();
p.insert(
"write".into(),
json!({
"tile_kib": w.tile_budget / 1024,
"dict_chunk_kib": w.dict_chunk_budget / 1024,
"zstd_level": w.zstd_level,
"zstd_frame": match w.frame {
crate::write_opts::FrameShape::Sized => "sized",
crate::write_opts::FrameShape::Stream => "stream",
},
}),
);
o.insert("params".into(), Value::Object(p));
if !r.inputs.is_empty() {
let inputs: Vec<Value> = r
.inputs
.iter()
.map(|i| {
let mut m = Map::new();
m.insert("name".into(), json!(i.name));
m.insert("format".into(), json!(i.format));
if let Some(b) = i.bytes {
m.insert("bytes".into(), json!(b));
}
if let Some(h) = &i.blake3 {
m.insert("blake3".into(), json!(h));
}
Value::Object(m)
})
.collect();
o.insert("inputs".into(), Value::Array(inputs));
}
if let Some(s) = stats {
let unique = s.statements as u64;
let default = s.default_triples as u64;
let read = r.statements_read.unwrap_or(unique);
o.insert(
"counts".into(),
json!({
"statements_read": read,
"statements_unique": unique,
"duplicates_dropped": read.saturating_sub(unique),
"default_graph_triples": default,
"named_graphs": s.named_graphs as u64,
"named_graph_quads": unique.saturating_sub(default),
"terms": s.terms as u64,
}),
);
}
serde_json::to_vec(&Value::Object(o)).unwrap_or_default()
}
pub fn finish_in_ram(image: Vec<u8>, record: Option<&[u8]>) -> Result<Vec<u8>, FileError> {
let record = record.filter(|r| !r.is_empty());
if writes_legacy() {
return match record {
Some(r) => crate::file::attach_build_info(&image, r),
None => Ok(image),
};
}
if crate::v06::open::is_v06(&image) {
return match record {
Some(r) => crate::file::attach_build_info(&image, r),
None => Ok(image),
};
}
let src = crate::reader::SliceReader::new(&image);
let mut parts = match crate::v06::parts::from_v05(&src) {
Ok(p) => p,
Err(FileError::Container(m)) if m.contains("kind 5") => {
let requad = reencode_per_graph(&image)?;
return finish_in_ram(requad, record);
}
Err(e) => return Err(e),
};
if let Some(r) = record {
parts.build = Some(r.to_vec());
}
let mut out = std::io::Cursor::new(Vec::with_capacity(image.len()));
crate::v06::write::assemble(
&parts,
&src,
&mut out,
crate::v06::write::WriteOpts::default(),
)?;
Ok(out.into_inner())
}
pub fn reencode_per_graph(image: &[u8]) -> Result<Vec<u8>, FileError> {
let rete = crate::Rete::open(image)?;
let card = rete.metadata().map(<[u8]>::to_vec);
let mut quads: Vec<crate::format::RawQuad> = rete
.dump(None)
.into_iter()
.map(|(s, p, o)| (s, p, o, None))
.collect();
let named: Vec<String> = rete.graph_names().iter().map(|g| g.to_string()).collect();
for g in &named {
for (s, p, o) in rete.dump(Some(g)) {
quads.push((s, p, o, Some(g.clone())));
}
}
let scope = rete.text_index_scope();
let mut opts = crate::ingest::AssembleOptions::sectioned();
opts.graph_layout = crate::GraphLayout::Quad;
opts.with_pyramid = rete.header().pyramid_meta_len > 0;
opts.with_text_index = rete.has_text_index();
opts.text_index_scope = scope.as_ref();
opts.label_index = rete.has_label_index();
opts.perms = rete.header().perms;
let (out, _) =
crate::ingest::assemble_dataset_opts(quads, &opts, |_, _| card.clone().unwrap_or_default());
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfc3339_known_dates() {
assert_eq!(rfc3339(0), "1970-01-01T00:00:00Z");
assert_eq!(rfc3339(1_700_000_000), "2023-11-14T22:13:20Z");
assert_eq!(rfc3339(1_785_801_600), "2026-08-04T00:00:00Z");
}
#[test]
fn the_record_names_its_front_end_and_the_engine() {
let r = Record {
front_end: front_end::PYTHON.into(),
front_end_version: "9.9.9".into(),
built_at: Some("2026-01-02T03:04:05Z".into()),
options: Options {
command: "build".into(),
format: Some("nt".into()),
..Default::default()
},
inputs: vec![Input::from_bytes("string", "nt", b"<a> <b> <c> .\n")],
statements_read: None,
};
let v: Value = serde_json::from_slice(&to_json(&r, None)).unwrap();
assert_eq!(v["front_end"], "python");
assert_eq!(v["builder"], "rete-python 9.9.9");
assert_eq!(v["engine"]["name"], "rete-core");
assert_eq!(v["engine"]["version"], env!("CARGO_PKG_VERSION"));
assert_eq!(v["params"]["command"], "build");
assert_eq!(v["inputs"][0]["bytes"], 14);
assert_eq!(v["schema"], 1);
}
#[test]
fn a_binding_style_record_says_the_file_has_a_label_index() {
let quads: Vec<crate::format::RawQuad> = vec![(
"<http://ex/a>".into(),
"<http://www.w3.org/2000/01/rdf-schema#label>".into(),
"\"A\"".into(),
None,
)];
let (image, stats) = crate::ingest::assemble_dataset_quad(quads, &[]);
assert!(crate::Rete::open(&image).unwrap().has_label_index());
let r = Record {
front_end: front_end::PYTHON.into(),
front_end_version: "9.9.9".into(),
built_at: None,
options: Options {
command: "build".into(),
..Default::default()
},
inputs: vec![],
statements_read: Some(1),
};
let v: Value = serde_json::from_slice(&to_json(&r, Some(&stats))).unwrap();
assert_eq!(v["params"]["label_index"], true);
let v: Value = serde_json::from_slice(&to_json(&r, None)).unwrap();
assert!(v["params"].get("label_index").is_none());
}
}