sculblog 0.1.9

project xanadu revivalism
Documentation
use crate::dtob_ffi;
use crate::decode_dtob_file;
use crate::serve;
use std::fs;
use std::path::{Path, PathBuf};

pub enum HashLookup {
    Found { xvuid: String, index: usize },
    Missing,
}

fn nul_terminated_key(key: &str) -> String {
    let mut out = key.to_owned();
    out.push('\0');
    out
}

fn map_file_path(maps_path: &Path, map_name: &str) -> PathBuf {
    maps_path.join(format!("{}.dtob", map_name))
}

/// All 5 compiled memcache maps loaded into memory at once.
/// Avoids repeated disk reads when multiple lookups happen in a single invocation.
pub struct MapsCache {
    cid_to_xvuid: Option<*mut dtob_ffi::DtobValue>,
    ccid_to_xvuid: Option<*mut dtob_ffi::DtobValue>,
    xvuid_to_snapshot_offset: Option<*mut dtob_ffi::DtobValue>,
    xvuid_to_latest: Option<*mut dtob_ffi::DtobValue>,
    xvuid_to_meta: Option<*mut dtob_ffi::DtobValue>,
}

impl MapsCache {
    pub fn load(maps_path: &Path) -> Self {
        let load = |name: &str| decode_dtob_file(&map_file_path(maps_path, name));
        Self {
            cid_to_xvuid:              load("cid_to_xvuid"),
            ccid_to_xvuid:             load("ccid_to_xvuid"),
            xvuid_to_snapshot_offset:  load("xvuid_to_snapshot_offset"),
            xvuid_to_latest:           load("xvuid_to_latest"),
            xvuid_to_meta:             load("xvuid_to_meta"),
        }
    }

    pub fn lookup_hash(&self, map_name: &str, hash: &str) -> HashLookup {
        let root = match map_name {
            "cid_to_xvuid"  => self.cid_to_xvuid,
            "ccid_to_xvuid" => self.ccid_to_xvuid,
            _ => return HashLookup::Missing,
        };
        let Some(root) = root else { return HashLookup::Missing; };
        let key = nul_terminated_key(hash);
        unsafe {
            let hits = dtob_ffi::ffi_kvset_get(root, key.as_ptr() as *const i8);
            if hits.is_null() { return HashLookup::Missing; }
            let count = dtob_ffi::ffi_dtob_array_len(hits);
            if count == 0 { return HashLookup::Missing; }
            let prefer_max = map_name == "cid_to_xvuid";
            let mut best_xvuid: Option<String> = None;
            let mut best_index = 0usize;
            for i in 0..count {
                let tuple = dtob_ffi::ffi_dtob_array_get(hits, i);
                if tuple.is_null() { continue; }
                let mut dummy = 0;
                let xr_raw = dtob_ffi::ffi_dtob_get_raw(dtob_ffi::ffi_dtob_array_get(tuple, 0), &mut dummy);
                let idx = dtob_ffi::ffi_dtob_get_u64(dtob_ffi::ffi_dtob_array_get(tuple, 1)) as usize;
                if xr_raw.is_null() || dummy != 32 { continue; }
                let xvuid = hex::encode(std::slice::from_raw_parts(xr_raw, 32));
                match &best_xvuid {
                    None => { best_xvuid = Some(xvuid); best_index = idx; }
                    Some(_) => {
                        let better = if prefer_max { idx > best_index } else { idx < best_index };
                        if better { best_xvuid = Some(xvuid); best_index = idx; }
                    }
                }
            }
            match best_xvuid {
                Some(xvuid) => HashLookup::Found { xvuid, index: best_index },
                None => HashLookup::Missing,
            }
        }
    }

    pub fn lookup_snapshot_offset(&self, xvuid: &str, snapshot_index: usize) -> Option<u64> {
        let root = self.xvuid_to_snapshot_offset?;
        let composite = nul_terminated_key(&format!("{}:{}", xvuid, snapshot_index));
        unsafe {
            let v = dtob_ffi::ffi_kvset_get(root, composite.as_ptr() as *const i8);
            if v.is_null() { None } else { Some(dtob_ffi::ffi_dtob_get_u64(v)) }
        }
    }

    pub fn lookup_latest(&self, xvuid: &str) -> Option<(usize, u64)> {
        let root = self.xvuid_to_latest?;
        let key = nul_terminated_key(xvuid);
        unsafe {
            let tuple = dtob_ffi::ffi_kvset_get(root, key.as_ptr() as *const i8);
            if tuple.is_null() { return None; }
            let latest_index = dtob_ffi::ffi_dtob_get_u64(dtob_ffi::ffi_dtob_array_get(tuple, 0)) as usize;
            let snapshot_offset = dtob_ffi::ffi_dtob_get_u64(dtob_ffi::ffi_dtob_array_get(tuple, 1));
            Some((latest_index, snapshot_offset))
        }
    }

    pub fn lookup_title(&self, xvuid: &str) -> Option<String> {
        self.lookup_meta_str(xvuid, 0)
    }

    fn lookup_meta_raw(&self, xvuid: &str, field_index: usize) -> Option<Vec<u8>> {
        let root = self.xvuid_to_meta?;
        let key = nul_terminated_key(xvuid);
        unsafe {
            let v = dtob_ffi::ffi_kvset_get(root, key.as_ptr() as *const i8);
            if v.is_null() { return None; }
            let v_field = dtob_ffi::ffi_dtob_array_get(v, field_index);
            if v_field.is_null() { return None; }
            let mut len = 0;
            let raw = dtob_ffi::ffi_dtob_get_raw(v_field, &mut len);
            if raw.is_null() || len == 0 { return None; }
            Some(std::slice::from_raw_parts(raw, len).to_vec())
        }
    }

    pub fn lookup_meta_hex(&self, xvuid: &str, field_index: usize) -> Option<String> {
        self.lookup_meta_raw(xvuid, field_index).map(|b| hex::encode(b))
    }

    fn lookup_meta_str(&self, xvuid: &str, field_index: usize) -> Option<String> {
        self.lookup_meta_raw(xvuid, field_index)
            .map(|b| String::from_utf8_lossy(&b).into_owned())
    }

    pub fn meta_root(&self) -> Option<*mut dtob_ffi::DtobValue> {
        self.xvuid_to_meta
    }
}

impl Drop for MapsCache {
    fn drop(&mut self) {
        unsafe {
            for root in [self.cid_to_xvuid, self.ccid_to_xvuid, self.xvuid_to_snapshot_offset, self.xvuid_to_latest, self.xvuid_to_meta] {
                if let Some(r) = root { dtob_ffi::ffi_dtob_free(r); }
            }
        }
    }
}



pub fn rebuild_memcache_maps(repo_path: &Path, maps_path: &Path) {
    let cid_to_xvuid = unsafe { dtob_ffi::ffi_kvset_new() };
    let ccid_to_xvuid = unsafe { dtob_ffi::ffi_kvset_new() };
    let xvuid_to_snapshot_offset = unsafe { dtob_ffi::ffi_kvset_new() };
    let xvuid_to_latest = unsafe { dtob_ffi::ffi_kvset_new() };
    let xvuid_to_meta = unsafe { dtob_ffi::ffi_kvset_new() };

    let mut count = 0;

    if let Ok(entries) = fs::read_dir(repo_path) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|s| s.to_str()) != Some("dif") {
                continue;
            }

            let file_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
            let Ok(xvuid_bytes) = hex::decode(file_name) else {
                continue;
            };
            if xvuid_bytes.len() != 32 {
                continue;
            }

            let xvuid_hex = hex::encode(&xvuid_bytes);
            let snapshot_offsets = serve::snapshot_offsets(&path).unwrap_or_default();

            for (snapshot_index, byte_offset) in &snapshot_offsets {
                let key = nul_terminated_key(&format!("{}:{}", xvuid_hex, snapshot_index));
                unsafe {
                    dtob_ffi::ffi_kvset_put(
                        xvuid_to_snapshot_offset,
                        key.as_ptr() as *const i8,
                        dtob_ffi::ffi_uint_new(*byte_offset),
                    );
                }
            }

            // Determine total entry count by scanning spans
            let entry_count = serve::entry_count(&path).unwrap_or(0);
            if entry_count == 0 { continue; }

            let mut date_created = 0u64;

            for i in 0..entry_count {
                let snapshot_start = (i / crate::SNAPSHOT_INTERVAL) * crate::SNAPSHOT_INTERVAL;
                let snapshot_offset = snapshot_offsets
                    .iter()
                    .find(|(idx, _)| *idx == snapshot_start)
                    .map(|(_, off)| *off);

                let built = match serve::build_content_from_snapshot_offset(
                    &path, Some(i), snapshot_offset, false,
                ) {
                    Some(b) => b,
                    None => continue,
                };

                let xdc_bytes = &built.current_content;

                // Extract ids (ccid, cid) from xanadoc header
                let ids = match xanadoc::decode_ids(xdc_bytes) {
                    Some(ids) => ids,
                    None => {
                        unsafe { dtob_ffi::ffi_dtob_free(built.target_root); }
                        continue;
                    }
                };

                let ccid_key = nul_terminated_key(&hex::encode(ids.ccid));
                let cid_key = nul_terminated_key(&hex::encode(ids.cid));

                unsafe {
                    // ccid -> (xvuid, index)
                    let entry_arr = dtob_ffi::ffi_array_new();
                    dtob_ffi::ffi_array_push(entry_arr, dtob_ffi::ffi_raw_new(xvuid_bytes.as_ptr(), 32));
                    dtob_ffi::ffi_array_push(entry_arr, dtob_ffi::ffi_uint_new(i as u64));
                    let existing_ccid = dtob_ffi::ffi_kvset_get(ccid_to_xvuid, ccid_key.as_ptr() as *const i8);
                    if !existing_ccid.is_null() {
                        dtob_ffi::ffi_array_push(existing_ccid, entry_arr);
                    } else {
                        let wrap_arr = dtob_ffi::ffi_array_new();
                        dtob_ffi::ffi_array_push(wrap_arr, entry_arr);
                        dtob_ffi::ffi_kvset_put(ccid_to_xvuid, ccid_key.as_ptr() as *const i8, wrap_arr);
                    }

                    // cid -> (xvuid, index)
                    let entry_arr2 = dtob_ffi::ffi_array_new();
                    dtob_ffi::ffi_array_push(entry_arr2, dtob_ffi::ffi_raw_new(xvuid_bytes.as_ptr(), 32));
                    dtob_ffi::ffi_array_push(entry_arr2, dtob_ffi::ffi_uint_new(i as u64));
                    let existing_cid = dtob_ffi::ffi_kvset_get(cid_to_xvuid, cid_key.as_ptr() as *const i8);
                    if !existing_cid.is_null() {
                        dtob_ffi::ffi_array_push(existing_cid, entry_arr2);
                    } else {
                        let wrap_arr2 = dtob_ffi::ffi_array_new();
                        dtob_ffi::ffi_array_push(wrap_arr2, entry_arr2);
                        dtob_ffi::ffi_kvset_put(cid_to_xvuid, cid_key.as_ptr() as *const i8, wrap_arr2);
                    }

                    // Extract metadata from first and last entries
                    if i == 0 {
                        if let Some(meta) = xanadoc::decode_metadata(xdc_bytes) {
                            date_created = meta.timestamp.unwrap_or(0);
                        }
                    }

                    if i == entry_count - 1 {
                        let snapshot_start = ((entry_count - 1) / crate::SNAPSHOT_INTERVAL) * crate::SNAPSHOT_INTERVAL;
                        let snap_off = snapshot_offsets
                            .iter()
                            .find(|(idx, _)| *idx == snapshot_start)
                            .map(|(_, byte_offset)| *byte_offset)
                            .unwrap_or(0);

                        let latest_tuple = dtob_ffi::ffi_array_new();
                        dtob_ffi::ffi_array_push(latest_tuple, dtob_ffi::ffi_uint_new((entry_count - 1) as u64));
                        dtob_ffi::ffi_array_push(latest_tuple, dtob_ffi::ffi_uint_new(snap_off));
                        dtob_ffi::ffi_kvset_put(
                            xvuid_to_latest,
                            nul_terminated_key(&xvuid_hex).as_ptr() as *const i8,
                            latest_tuple,
                        );

                        let meta = xanadoc::decode_metadata(xdc_bytes);
                        let date_edited = meta.as_ref().and_then(|m| m.timestamp).unwrap_or(date_created);
                        let title = meta.as_ref()
                            .and_then(|m| m.title.as_deref())
                            .unwrap_or("Untitled");

                        let meta_arr = dtob_ffi::ffi_array_new();
                        // 0: title
                        dtob_ffi::ffi_array_push(meta_arr, dtob_ffi::ffi_raw_new(title.as_ptr(), title.len()));
                        // 1: cid
                        dtob_ffi::ffi_array_push(meta_arr, dtob_ffi::ffi_raw_new(ids.cid.as_ptr(), 32));
                        // 2: ccid
                        dtob_ffi::ffi_array_push(meta_arr, dtob_ffi::ffi_raw_new(ids.ccid.as_ptr(), 32));
                        // 3: date_created
                        dtob_ffi::ffi_array_push(meta_arr, dtob_ffi::ffi_uint_new(date_created));
                        // 4: date_edited
                        dtob_ffi::ffi_array_push(meta_arr, dtob_ffi::ffi_uint_new(date_edited));

                        dtob_ffi::ffi_kvset_put(
                            xvuid_to_meta,
                            nul_terminated_key(&xvuid_hex).as_ptr() as *const i8,
                            meta_arr,
                        );
                    }

                    dtob_ffi::ffi_dtob_free(built.target_root);
                }
            }

            count += 1;
        }
    }

    let _ = fs::create_dir_all(maps_path);

    unsafe {
        let dump_map = |m_ptr: *mut dtob_ffi::DtobValue, name: &str| {
            let mut out_len = 0;
            let enc = dtob_ffi::ffi_encode_dif(m_ptr, &mut out_len);
            if !enc.is_null() && out_len > 0 {
                let s = std::slice::from_raw_parts(enc, out_len);
                let _ = fs::write(map_file_path(maps_path, name), s);
                libc::free(enc as *mut libc::c_void);
            }
            dtob_ffi::ffi_dtob_free(m_ptr);

            let pid_path = format!("/tmp/dtob-{}.pid", name);
            if let Ok(pid_str) = fs::read_to_string(&pid_path) {
                if let Ok(pid) = pid_str.trim().parse::<i32>() {
                    libc::kill(pid, libc::SIGHUP);
                    println!("Notified memcache daemon for {} (PID {})", name, pid);
                }
            }
        };

        dump_map(cid_to_xvuid, "cid_to_xvuid");
        dump_map(ccid_to_xvuid, "ccid_to_xvuid");
        dump_map(xvuid_to_snapshot_offset, "xvuid_to_snapshot_offset");
        dump_map(xvuid_to_latest, "xvuid_to_latest");
        dump_map(xvuid_to_meta, "xvuid_to_meta");
    }

    println!("Rebuilt memcache maps for {} xvuids. Saved correctly to {:?}", count, maps_path);
}