use crate::store::StoreError;
use serde::{Deserialize, Serialize};
use std::io::{BufWriter, Write};
use std::path::Path;
use tempfile::NamedTempFile;
pub(crate) const VISIBILITY_RANGES_MAGIC: &[u8; 6] = b"FBATVR";
pub(crate) const VISIBILITY_RANGES_VERSION: u16 = 1;
pub(crate) const VISIBILITY_RANGES_FILENAME: &str = "visibility_ranges.fbv";
#[derive(Serialize, Deserialize)]
struct VisibilityRangesData {
ranges: Vec<VisibilityRangeEntry>,
}
#[derive(Serialize, Deserialize)]
struct VisibilityRangeEntry {
start: u64,
end: u64,
}
fn normalize_ranges(ranges: &[(u64, u64)]) -> Result<Vec<(u64, u64)>, StoreError> {
let mut normalized: Vec<(u64, u64)> = Vec::with_capacity(ranges.len());
for &(start, end) in ranges {
if start >= end {
return Err(StoreError::RangeMalformed { start, end });
}
normalized.push((start, end));
}
normalized.sort_by_key(|(start, _)| *start);
let mut merged: Vec<(u64, u64)> = Vec::with_capacity(normalized.len());
for (start, end) in normalized {
if let Some((_, merged_end)) = merged.last_mut() {
if start <= *merged_end {
*merged_end = (*merged_end).max(end);
continue;
}
}
merged.push((start, end));
}
Ok(merged)
}
pub(crate) fn write_cancelled_ranges(
data_dir: &Path,
ranges: &[(u64, u64)],
) -> Result<(), StoreError> {
let final_path = data_dir.join(VISIBILITY_RANGES_FILENAME);
crate::store::platform::fs::reject_symlink_leaf(&final_path, "visibility-ranges metadata")?;
let normalized = normalize_ranges(ranges)?;
if normalized.is_empty() {
match std::fs::remove_file(&final_path) {
Ok(()) => crate::store::platform::sync::sync_parent_dir(&final_path)?,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(StoreError::Io(error)),
}
return Ok(());
}
let body = rmp_serde::to_vec_named(&VisibilityRangesData {
ranges: normalized
.into_iter()
.map(|(start, end)| VisibilityRangeEntry { start, end })
.collect(),
})
.map_err(|error| StoreError::Serialization(Box::new(error)))?;
let crc = crc32fast::hash(&body);
let tmp = NamedTempFile::new_in(data_dir)?;
{
let file = tmp.as_file();
let mut writer = BufWriter::new(file);
writer.write_all(VISIBILITY_RANGES_MAGIC)?;
writer.write_all(&VISIBILITY_RANGES_VERSION.to_le_bytes())?;
writer.write_all(&crc.to_le_bytes())?;
writer.write_all(&body)?;
writer.flush()?;
}
crate::store::platform::sync::sync_file_all_io(tmp.as_file()).map_err(StoreError::Io)?;
let admission = crate::store::platform::sync::admit_current_parent_dir_sync()?;
crate::store::platform::sync::persist_temp_with_parent_sync(tmp, &final_path, admission)
.map_err(StoreError::Io)?;
Ok(())
}
pub(crate) fn load_cancelled_ranges(
data_dir: &Path,
) -> Result<Option<Vec<(u64, u64)>>, StoreError> {
let path = data_dir.join(VISIBILITY_RANGES_FILENAME);
let raw = match std::fs::read(&path) {
Ok(raw) => raw,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => {
return Err(corrupt_ranges(
&path,
format!("failed to read visibility-ranges metadata: {error}"),
));
}
};
const HEADER_LEN: usize = 6 + 2 + 4;
if raw.len() < HEADER_LEN {
return Err(corrupt_ranges(
&path,
"visibility-ranges file too short".to_string(),
));
}
if &raw[..6] != VISIBILITY_RANGES_MAGIC {
return Err(corrupt_ranges(
&path,
"visibility-ranges file has wrong magic".to_string(),
));
}
let version = u16::from_le_bytes([raw[6], raw[7]]);
if version != VISIBILITY_RANGES_VERSION {
return Err(corrupt_ranges(
&path,
format!("unsupported visibility-ranges version: {version}"),
));
}
let stored_crc = u32::from_le_bytes([raw[8], raw[9], raw[10], raw[11]]);
let body = &raw[HEADER_LEN..];
let actual_crc = crc32fast::hash(body);
if stored_crc != actual_crc {
return Err(corrupt_ranges(
&path,
"visibility-ranges CRC mismatch".to_string(),
));
}
let data: VisibilityRangesData = match rmp_serde::from_slice(body) {
Ok(data) => data,
Err(error) => {
return Err(corrupt_ranges(
&path,
format!("visibility-ranges deserialisation failed: {error}"),
));
}
};
let raw_ranges: Vec<(u64, u64)> = data
.ranges
.into_iter()
.map(|entry| (entry.start, entry.end))
.collect();
match normalize_ranges(&raw_ranges) {
Ok(normalized) => Ok(Some(normalized)),
Err(err) => Err(corrupt_ranges(
&path,
format!("visibility-ranges file contained malformed entries: {err}"),
)),
}
}
fn corrupt_ranges(path: &Path, reason: String) -> StoreError {
tracing::warn!(
target: "batpak::visibility",
path = %path.display(),
reason = %reason,
"visibility-ranges metadata unreadable; failing closed"
);
StoreError::HiddenRangesCorrupt {
path: path.to_path_buf(),
reason,
}
}