sculblog 0.1.8

project xanadu revivalism
Documentation
use crate::{dtob_ffi, html::Autolink, html::Macrolink, maps};
use std::process::Command;
use std::io::Write;
use tempfile::NamedTempFile;
use std::fs;

fn get_env_bin(env_var: &str, default: &str) -> String {
    std::env::var(env_var).unwrap_or_else(|_| default.to_string())
}

/// Flatten refs into a TOB bitstream for ccid: trit-encode each value, concatenate.
/// No field names, no control words, no nesting.
fn flatten_refs_for_ccid(macrolinks: &[Macrolink], autolinks: &[Autolink]) -> Vec<u8> {
    let mut blob = Vec::new();
    // macrolinks: just ref_start, ref_end as u32 big-endian, trit-encoded
    for m in macrolinks {
        blob.extend_from_slice(&xanadoc::trit_encode(&(m.ref_start as u32).to_be_bytes()));
        blob.extend_from_slice(&xanadoc::trit_encode(&(m.ref_end as u32).to_be_bytes()));
    }
    // autolinks: def_start, def_end, ref_start, ref_end
    for a in autolinks {
        blob.extend_from_slice(&xanadoc::trit_encode(&(a.def_start as u32).to_be_bytes()));
        blob.extend_from_slice(&xanadoc::trit_encode(&(a.def_end as u32).to_be_bytes()));
        blob.extend_from_slice(&xanadoc::trit_encode(&(a.ref_start as u32).to_be_bytes()));
        blob.extend_from_slice(&xanadoc::trit_encode(&(a.ref_end as u32).to_be_bytes()));
    }
    blob
}

pub fn generate_xvuid() -> [u8; 32] {
    let mut arr = [0u8; 32];
    if let Ok(mut f) = fs::File::open("/dev/urandom") {
        use std::io::Read;
        let _ = f.read_exact(&mut arr);
    }
    arr
}

pub fn stamp_ots(content: &[u8]) -> Option<Vec<u8>> {
    let ots = get_env_bin("OTS_BIN", "/usr/local/bin/ots");
    let mut tmp = NamedTempFile::new().ok()?;
    tmp.write_all(content).ok()?;
    
    let ots_path = format!("{}.ots", tmp.path().display());
    let out = Command::new(&ots)
        .arg("stamp")
        .arg(tmp.path())
        .output()
        .ok()?;
        
    if out.status.success() && Path::new(&ots_path).exists() {
        let pf = fs::read(&ots_path).ok()?;
        let _ = fs::remove_file(&ots_path);
        return Some(pf);
    }
    None
}

use std::path::Path;



/// Writes the `.dif` and immediately rebuilds map `.dtob` files and signals memcache (same as `cache-maps`).
///



pub fn write_xanadoc_dif(
    repo_path: &Path,
    maps_path: &Path,
    _category: &str,
    _file_name: &str,
    xvuid: [u8; 32],
    title: &str,
    word_count: usize,
    content_body: &str,
    macrolinks: Vec<Macrolink>,
    autolinks: Vec<Autolink>,
    doc_timestamp: u64,
    debug: bool,
    rebuild_maps: bool,
) -> Result<(), String> {

    let content_bytes = content_body.as_bytes();
    if debug { eprintln!("[write_xanadoc_dif] content_bytes len={}", content_bytes.len()); }

    let cid = xanadoc::compute_cid(content_bytes);
    if debug { eprintln!("[write_xanadoc_dif] CID={}", hex::encode(cid)); }

    let sha256 = xanadoc::sha256(content_bytes);
    if debug { eprintln!("[write_xanadoc_dif] SHA256={}", hex::encode(sha256)); }

    let ots_proof = stamp_ots(content_bytes);
    if debug { eprintln!("[write_xanadoc_dif] ots_proof present={}", ots_proof.is_some()); }

    unsafe {
        let meta = dtob_ffi::ffi_kvset_new();
        if debug { eprintln!("[write_xanadoc_dif] allocated meta kvset"); }

        let xvuid_val = dtob_ffi::ffi_raw_new(xvuid.as_ptr(), 32);
        dtob_ffi::ffi_kvset_put(meta, b"xvuid\0".as_ptr() as *const i8, xvuid_val);
        if debug { eprintln!("[write_xanadoc_dif] xvuid={}", hex::encode(xvuid)); }

        let t_val = dtob_ffi::ffi_uint_new(doc_timestamp);
        dtob_ffi::ffi_kvset_put(meta, b"timestamp\0".as_ptr() as *const i8, t_val);
        if debug { eprintln!("[write_xanadoc_dif] timestamp={}", doc_timestamp); }

        if let Some(pf) = &ots_proof {
            let p_val = dtob_ffi::ffi_raw_new(pf.as_ptr(), pf.len());
            dtob_ffi::ffi_kvset_put(meta, b"ots_proof\0".as_ptr() as *const i8, p_val);
            if debug { eprintln!("[write_xanadoc_dif] ots_proof bytes={}", pf.len()); }
        }

        let wc = dtob_ffi::ffi_uint_new(word_count as u64);
        dtob_ffi::ffi_kvset_put(meta, b"word_count\0".as_ptr() as *const i8, wc);
        if debug { eprintln!("[write_xanadoc_dif] word_count={}", word_count); }

        let t_str = title.as_bytes();
        let tit = dtob_ffi::ffi_raw_new(t_str.as_ptr(), t_str.len());
        dtob_ffi::ffi_kvset_put(meta, b"title\0".as_ptr() as *const i8, tit);
        if debug { eprintln!("[write_xanadoc_dif] title={:?}", title); }

        // Build refs kvset for the dif entry (still needed for storage)
        let refs = dtob_ffi::ffi_kvset_new();
        let p_macros = dtob_ffi::ffi_array_new();
        for _m in &macrolinks {
            let dummy = dtob_ffi::ffi_array_new();
            dtob_ffi::ffi_array_push(p_macros, dummy);
        }
        let p_autos = dtob_ffi::ffi_array_new();
        for a in &autolinks {
            let auto_entry = dtob_ffi::ffi_array_new();
            dtob_ffi::ffi_array_push(auto_entry, dtob_ffi::ffi_uint_new(a.def_start as u64));
            dtob_ffi::ffi_array_push(auto_entry, dtob_ffi::ffi_uint_new(a.def_end as u64));
            dtob_ffi::ffi_array_push(auto_entry, dtob_ffi::ffi_uint_new(a.ref_start as u64));
            dtob_ffi::ffi_array_push(auto_entry, dtob_ffi::ffi_uint_new(a.ref_end as u64));
            dtob_ffi::ffi_array_push(p_autos, auto_entry);
        }
        dtob_ffi::ffi_kvset_put(refs, b"paleomacrolinks\0".as_ptr() as *const i8, p_macros);
        dtob_ffi::ffi_kvset_put(refs, b"autolinks\0".as_ptr() as *const i8, p_autos);

        // ccid = bromberg_hash(flat_tob_refs + content)
        let flat_refs = flatten_refs_for_ccid(&macrolinks, &autolinks);
        if debug { eprintln!("[write_xanadoc_dif] CCID flat_refs len={}, content len={}", flat_refs.len(), content_bytes.len()); }

        let ccid = xanadoc::compute_ccid(&flat_refs, content_bytes);
        if debug { eprintln!("[write_xanadoc_dif] CCID={}", hex::encode(ccid)); }

        let dif_path = repo_path.join(format!("{}.dif", hex::encode(xvuid)));
        if debug { eprintln!("[write_xanadoc_dif] dif_path={}", dif_path.display()); }

        let ops = dtob_ffi::ffi_dif_ops_new();

        // ** Construct .xanadoc memory buffer **
        let header_kv = dtob_ffi::ffi_kvset_new();
        // 1. magic
        dtob_ffi::ffi_kvset_put(header_kv, b"magic\0".as_ptr() as *const i8, dtob_ffi::ffi_raw_new(b"08042026".as_ptr(), 8)); // Valid TOB format requires standard magic for inner string
        // 2. ids
        let ids = dtob_ffi::ffi_xanadoc_ids_new(ccid.as_ptr(), cid.as_ptr(), sha256.as_ptr());
        dtob_ffi::ffi_kvset_put(header_kv, b"ids\0".as_ptr() as *const i8, ids);
        // 3. references
        dtob_ffi::ffi_kvset_put(header_kv, b"references\0".as_ptr() as *const i8, refs);
        // 4. neolinks
        dtob_ffi::ffi_kvset_put(header_kv, b"neolinks\0".as_ptr() as *const i8, dtob_ffi::ffi_kvset_new());
        // 5. non-essential metadata
        dtob_ffi::ffi_kvset_put(header_kv, b"non-essential metadata\0".as_ptr() as *const i8, meta);

        let mut header_len = 0;
        let enc_ptr = dtob_ffi::ffi_encode_xanadoc_header(header_kv, &mut header_len);
        let mut xanadoc_bytes = Vec::new();
        if !enc_ptr.is_null() && header_len > 8 {
            let s = std::slice::from_raw_parts(enc_ptr, header_len);
            // Prefix the physical xanadoc file magic override:
            xanadoc_bytes.extend_from_slice(b"13032026");
            xanadoc_bytes.extend_from_slice(&s[8..]);
            libc::free(enc_ptr as *mut libc::c_void);
        } else {
            panic!("Failed to encode `.xanadoc` header block natively.");
        }
        // Append raw content exactly as the xanadoc struct mandates
        xanadoc_bytes.push(0x0A);
        xanadoc_bytes.extend_from_slice(content_bytes);
        // ** Construction Complete **

        let (old_content, n) = if let Some(built) = crate::serve::build_content(&dif_path, None, debug) {
             (Some(built.current_content), built.entry_count)
        } else {
             (None, 0)
        };
        let has_prev_content = old_content.is_some();
        if debug { eprintln!("[write_xanadoc_dif] has_prev_content={} entry_count={}", has_prev_content, n); }

        if n % crate::SNAPSHOT_INTERVAL == 0 || old_content.is_none() {
            if debug { eprintln!("[write_xanadoc_dif] snapshot interval hit (n={}) — writing full content block", n); }
            dtob_ffi::ffi_dif_ops_push_add(ops, xanadoc_bytes.as_ptr(), xanadoc_bytes.len());
        } else {
            if debug { eprintln!("[write_xanadoc_dif] delta mode — diffing against old content ({} bytes)", old_content.as_ref().unwrap().len()); }
            let old_bytes = old_content.as_ref().unwrap();
            dtob_ffi::ffi_diff_build(
                old_bytes.as_ptr(), old_bytes.len(),
                xanadoc_bytes.as_ptr(), xanadoc_bytes.len(),
                ops
            );
        }

        let entry = dtob_ffi::ffi_build_dif_entry(
            ops
        );
        if debug { eprintln!("[write_xanadoc_dif] dif entry built, entry={:?}", entry); }

        if has_prev_content {
            if debug { eprintln!("[write_xanadoc_dif] appending to existing dif file"); }

            let cpath = std::ffi::CString::new(dif_path.to_str().unwrap()).unwrap();
            
            // Temporarily redirect stderr to /dev/null to silence the harmless "stream ended with unclosed open"
            // error printed by the C parser when it hits EOF reading the truncated verification buffer.
            let null_fd = libc::open(b"/dev/null\0".as_ptr() as *const libc::c_char, libc::O_WRONLY);
            let old_stderr = libc::dup(libc::STDERR_FILENO);
            if null_fd >= 0 && old_stderr >= 0 {
                libc::dup2(null_fd, libc::STDERR_FILENO);
            }
            
            let valid = dtob_ffi::ffi_verify_file_types(cpath.as_ptr(), 1);
            
            if null_fd >= 0 && old_stderr >= 0 {
                libc::dup2(old_stderr, libc::STDERR_FILENO);
                libc::close(null_fd);
                libc::close(old_stderr);
            }
            
            if debug { eprintln!("[write_xanadoc_dif] ffi_verify_file_types result={}", valid); }
            if valid == 0 {
                panic!("DIF Serialization Error: The custom types generated by Sculblog structurally diverge from the physical Types Header built into the file! Commit safely aborted to prevent data corruption.");
            }

            let mut final_len = 0;
            let inner_enc = dtob_ffi::ffi_encode_chunk(entry, &mut final_len);
            if debug { eprintln!("[write_xanadoc_dif] ffi_encode_chunk len={}", final_len); }

            if inner_enc.is_null() {
                panic!("DIF Serialization Error: The dynamically generated AST nodes strictly failed the compiler schema layout requirements! Commit aborted.");
            }

            let mut file_obj = fs::OpenOptions::new().read(true).write(true).open(&dif_path).unwrap();
            let flen = file_obj.metadata().unwrap().len();
            if debug { eprintln!("[write_xanadoc_dif] existing file len={}, trimming trailing 2 bytes", flen); }
            if flen >= 2 {
                file_obj.set_len(flen - 2).unwrap();
            }

            use std::io::{Seek, SeekFrom, Write};
            file_obj.seek(SeekFrom::End(0)).unwrap();

            let mut buf = Vec::new();
            if final_len > 0 {
                buf.extend_from_slice(std::slice::from_raw_parts(inner_enc, final_len));
            }

            buf.push(0xC0);
            buf.push(0x01);
            if debug { eprintln!("[write_xanadoc_dif] writing {} bytes (chunk + 0xC001 terminator)", buf.len()); }

            file_obj.write_all(&buf).unwrap();

            libc::free(inner_enc as *mut libc::c_void);
            dtob_ffi::ffi_dtob_free(entry);
        } else {
            if debug { eprintln!("[write_xanadoc_dif] no prior file — writing fresh dif"); }

            let root = dtob_ffi::ffi_array_new();
            dtob_ffi::ffi_array_push(root, entry);

            let mut out_len = 0;
            let enc = dtob_ffi::ffi_encode_dif(root, &mut out_len);
            if debug { eprintln!("[write_xanadoc_dif] fresh dif encoded len={}", out_len); }

            if !enc.is_null() {
                let s = std::slice::from_raw_parts(enc, out_len);
                let _ = fs::write(&dif_path, s);
                if debug { eprintln!("[write_xanadoc_dif] wrote {} bytes to {}", out_len, dif_path.display()); }
                libc::free(enc as *mut libc::c_void);
            }
            dtob_ffi::ffi_dtob_free(root);
        }
    }

    if rebuild_maps {
        if debug { eprintln!("[write_xanadoc_dif] rebuilding memcache maps"); }
        maps::rebuild_memcache_maps(&repo_path, maps_path);
    }
    if debug { eprintln!("[write_xanadoc_dif] done"); }

    Ok(())
}