1use std::fs::{File, OpenOptions};
2use std::io::{BufReader, BufWriter, Read, Write};
3use std::path::Path;
4
5const MAGIC: u32 = 0x5A4D4150; const VERSION: u32 = 1;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct PersistedZone {
10 pub column_index: u32,
11 pub block_id: u32,
12 pub min_value: String,
13 pub max_value: String,
14 pub null_count: u64,
15 pub row_count: u64,
16}
17
18#[derive(Debug)]
19pub enum ZoneMapPersistError {
20 Io(std::io::Error),
21 BadMagic { found: u32 },
22 BadVersion { found: u32 },
23 Truncated,
24 InvalidUtf8(std::string::FromUtf8Error),
25}
26
27impl From<std::io::Error> for ZoneMapPersistError {
28 fn from(e: std::io::Error) -> Self {
29 Self::Io(e)
30 }
31}
32
33impl From<std::string::FromUtf8Error> for ZoneMapPersistError {
34 fn from(e: std::string::FromUtf8Error) -> Self {
35 Self::InvalidUtf8(e)
36 }
37}
38
39impl std::fmt::Display for ZoneMapPersistError {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::Io(e) => write!(f, "zone-map i/o: {e}"),
43 Self::BadMagic { found } => {
44 write!(f, "zone-map bad magic: expected {MAGIC:#x}, got {found:#x}")
45 }
46 Self::BadVersion { found } => {
47 write!(f, "zone-map version {found} not supported (max {VERSION})")
48 }
49 Self::Truncated => write!(f, "zone-map file ended unexpectedly"),
50 Self::InvalidUtf8(e) => write!(f, "zone-map utf8 error: {e}"),
51 }
52 }
53}
54
55impl std::error::Error for ZoneMapPersistError {}
56
57pub fn write_zone_map_sidecar(
58 path: &Path,
59 column_count: u32,
60 zones: &[PersistedZone],
61) -> Result<(), ZoneMapPersistError> {
62 let tmp_path = path.with_extension("zonemap.tmp");
63 {
64 let file = OpenOptions::new()
65 .write(true)
66 .create(true)
67 .truncate(true)
68 .open(&tmp_path)?;
69 let mut w = BufWriter::new(file);
70 w.write_all(&MAGIC.to_le_bytes())?;
71 w.write_all(&VERSION.to_le_bytes())?;
72 w.write_all(&column_count.to_le_bytes())?;
73 w.write_all(&(zones.len() as u32).to_le_bytes())?;
74 for z in zones {
75 w.write_all(&z.column_index.to_le_bytes())?;
76 w.write_all(&z.block_id.to_le_bytes())?;
77 write_str(&mut w, &z.min_value)?;
78 write_str(&mut w, &z.max_value)?;
79 w.write_all(&z.null_count.to_le_bytes())?;
80 w.write_all(&z.row_count.to_le_bytes())?;
81 }
82 w.flush()?;
83 }
84 std::fs::rename(&tmp_path, path)?;
85 Ok(())
86}
87
88pub fn read_zone_map_sidecar(
89 path: &Path,
90) -> Result<(u32, Vec<PersistedZone>), ZoneMapPersistError> {
91 let file = File::open(path)?;
92 let mut r = BufReader::new(file);
93 let magic = read_u32(&mut r)?;
94 if magic != MAGIC {
95 return Err(ZoneMapPersistError::BadMagic { found: magic });
96 }
97 let version = read_u32(&mut r)?;
98 if version != VERSION {
99 return Err(ZoneMapPersistError::BadVersion { found: version });
100 }
101 let column_count = read_u32(&mut r)?;
102 let zone_count = read_u32(&mut r)?;
103 let mut zones = Vec::with_capacity(zone_count as usize);
104 for _ in 0..zone_count {
105 let column_index = read_u32(&mut r)?;
106 let block_id = read_u32(&mut r)?;
107 let min_value = read_str(&mut r)?;
108 let max_value = read_str(&mut r)?;
109 let null_count = read_u64(&mut r)?;
110 let row_count = read_u64(&mut r)?;
111 zones.push(PersistedZone {
112 column_index,
113 block_id,
114 min_value,
115 max_value,
116 null_count,
117 row_count,
118 });
119 }
120 Ok((column_count, zones))
121}
122
123fn write_str<W: Write>(w: &mut W, s: &str) -> Result<(), ZoneMapPersistError> {
124 let bytes = s.as_bytes();
125 w.write_all(&(bytes.len() as u32).to_le_bytes())?;
126 w.write_all(bytes)?;
127 Ok(())
128}
129
130fn read_u32<R: Read>(r: &mut R) -> Result<u32, ZoneMapPersistError> {
131 let mut buf = [0u8; 4];
132 r.read_exact(&mut buf)
133 .map_err(|_| ZoneMapPersistError::Truncated)?;
134 Ok(u32::from_le_bytes(buf))
135}
136
137fn read_u64<R: Read>(r: &mut R) -> Result<u64, ZoneMapPersistError> {
138 let mut buf = [0u8; 8];
139 r.read_exact(&mut buf)
140 .map_err(|_| ZoneMapPersistError::Truncated)?;
141 Ok(u64::from_le_bytes(buf))
142}
143
144fn read_str<R: Read>(r: &mut R) -> Result<String, ZoneMapPersistError> {
145 let len = read_u32(r)?;
146 let mut buf = vec![0u8; len as usize];
147 r.read_exact(&mut buf)
148 .map_err(|_| ZoneMapPersistError::Truncated)?;
149 Ok(String::from_utf8(buf)?)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use std::time::{SystemTime, UNIX_EPOCH};
156
157 fn temp_path() -> std::path::PathBuf {
158 let suffix = SystemTime::now()
159 .duration_since(UNIX_EPOCH)
160 .expect("clock")
161 .as_nanos();
162 std::env::temp_dir().join(format!("reddb-file-zone-map-{suffix}.zonemap"))
163 }
164
165 #[test]
166 fn zone_map_sidecar_round_trips() {
167 let path = temp_path();
168 let zones = vec![PersistedZone {
169 column_index: 2,
170 block_id: 9,
171 min_value: "a".into(),
172 max_value: "z".into(),
173 null_count: 1,
174 row_count: 50,
175 }];
176
177 write_zone_map_sidecar(&path, 4, &zones).expect("write");
178 let (column_count, decoded) = read_zone_map_sidecar(&path).expect("read");
179 assert_eq!(column_count, 4);
180 assert_eq!(decoded, zones);
181
182 let _ = std::fs::remove_file(path);
183 }
184}