sculblog 0.1.9

project xanadu revivalism
Documentation
use crate::maps::{self, MapsCache, HashLookup};
use crate::serve;
use std::fs;
use std::path::Path;

/// Remove the entire xvuid: delete the .dif file (and md/ counterpart), then rebuild maps.
pub fn remove_xvuid(repo_path: &Path, maps_path: &Path, xvuid: &str) {
    let dif_path = repo_path.join(format!("{}.dif", xvuid));
    if !dif_path.exists() {
        eprintln!("Error: No .dif file found for xvuid {}", xvuid);
        std::process::exit(1);
    }

    if let Err(e) = fs::remove_file(&dif_path) {
        eprintln!("Error removing {}: {}", dif_path.display(), e);
        std::process::exit(1);
    }
    eprintln!("Removed {}", dif_path.display());

    let md_dif_path = repo_path.join("md").join(format!("{}.dif", xvuid));
    if md_dif_path.exists() {
        if let Err(e) = fs::remove_file(&md_dif_path) {
            eprintln!("Warning: could not remove md counterpart {}: {}", md_dif_path.display(), e);
        } else {
            eprintln!("Removed {}", md_dif_path.display());
        }
    }

    maps::rebuild_memcache_maps(repo_path, maps_path);
}

/// Remove the latest entry from a dif file identified via cid or ccid.
/// Only removes if the hash corresponds to the latest (last) entry on that xvuid.
pub fn remove_by_hash(repo_path: &Path, maps_path: &Path, map_name: &str, hash: &str) {
    let cache = MapsCache::load(maps_path);

    let (xvuid, index) = match cache.lookup_hash(map_name, hash) {
        HashLookup::Found { xvuid, index } => (xvuid, index),
        HashLookup::Missing => {
            eprintln!("Error: hash {} not found in {}", hash, map_name);
            std::process::exit(1);
        }
    };

    let (latest_index, _) = match cache.lookup_latest(&xvuid) {
        Some(pair) => pair,
        None => {
            eprintln!("Error: could not look up latest entry for xvuid {}", xvuid);
            std::process::exit(1);
        }
    };

    if index != latest_index {
        eprintln!(
            "Error: hash {} is at index {} but the latest entry is at index {}. Only the latest entry can be removed.",
            hash, index, latest_index
        );
        std::process::exit(1);
    }

    if index == 0 {
        eprintln!("This is the only entry — removing entire xvuid {}", xvuid);
        remove_xvuid(repo_path, maps_path, &xvuid);
        return;
    }

    truncate_last_entry_at(&repo_path.join(format!("{}.dif", xvuid)));

    let md_dif_path = repo_path.join("md").join(format!("{}.dif", &xvuid));
    if md_dif_path.exists() {
        truncate_last_entry_at(&md_dif_path);
    }

    maps::rebuild_memcache_maps(repo_path, maps_path);
    eprintln!("Removed latest entry (index {}) from xvuid {}", index, xvuid);
}

fn truncate_last_entry_at(dif_path: &Path) {
    if !dif_path.exists() {
        return;
    }

    let file = match fs::File::open(dif_path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error: could not open {}: {}", dif_path.display(), e);
            std::process::exit(1);
        }
    };
    let file_len = file.metadata().unwrap().len();

    let prefix = match serve::read_dif_prefix(&file, file_len) {
        Some(p) => p,
        None => {
            eprintln!("Error: could not read dif prefix from {}", dif_path.display());
            std::process::exit(1);
        }
    };

    let spans = match serve::scan_entry_spans_from_offset(&file, file_len, prefix.len() as u64, None) {
        Some(s) => s,
        None => {
            eprintln!("Error: could not parse entry spans from {}", dif_path.display());
            std::process::exit(1);
        }
    };

    drop(file); // release read handle before writing

    if spans.len() < 2 {
        eprintln!("Error: dif file has fewer than 2 entries, use xvuid removal instead");
        std::process::exit(1);
    }

    let last_span = spans.last().unwrap();
    let truncate_to = last_span.offset;

    // Truncate to just before the last entry
    let file = fs::OpenOptions::new()
        .write(true)
        .open(dif_path)
        .unwrap_or_else(|e| {
            eprintln!("Error opening {} for truncation: {}", dif_path.display(), e);
            std::process::exit(1);
        });

    file.set_len(truncate_to).unwrap_or_else(|e| {
        eprintln!("Error truncating {}: {}", dif_path.display(), e);
        std::process::exit(1);
    });

    drop(file);

    // Re-add the root array close terminator
    let mut file = fs::OpenOptions::new()
        .append(true)
        .open(dif_path)
        .unwrap();
    use std::io::Write;
    file.write_all(&[0xC0, 0x01]).unwrap();

    eprintln!("Truncated last entry from {}", dif_path.display());
}