1use core::fmt;
31
32use crate::bytes::{Cursor, OutOfBounds};
33use crate::Rgb6;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Slab {
38 pub ztop: u8,
40 pub vis: u8,
45 pub colors: Vec<u8>,
48}
49
50#[derive(Debug, Clone)]
53pub struct Kvx {
54 pub xsiz: u32,
55 pub ysiz: u32,
56 pub zsiz: u32,
57 pub xpivot: u32,
60 pub ypivot: u32,
61 pub zpivot: u32,
62 pub xoffset: Vec<u32>,
65 pub xyoffset: Vec<Vec<u16>>,
68 pub columns: Vec<Vec<Vec<Slab>>>,
72 pub palette: [Rgb6; 256],
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum ParseError {
79 TooSmall { got: usize },
82 Truncated { at: usize, need: usize },
85 NonMonotonicOffsets { x: u32, y: u32 },
88 SlabOverrun { x: u32, y: u32 },
91}
92
93impl fmt::Display for ParseError {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match *self {
96 Self::TooSmall { got } => write!(
97 f,
98 "kvx file too small ({got} bytes; need at least 28 byte header + 768 byte palette)"
99 ),
100 Self::Truncated { at, need } => {
101 write!(f, "kvx truncated: need {need} bytes at offset {at}")
102 }
103 Self::NonMonotonicOffsets { x, y } => write!(
104 f,
105 "kvx column (x={x}, y={y}): xyoffset table is non-monotonic"
106 ),
107 Self::SlabOverrun { x, y } => write!(
108 f,
109 "kvx column (x={x}, y={y}): slab declared zleng overruns column byte budget"
110 ),
111 }
112 }
113}
114
115impl std::error::Error for ParseError {}
116
117impl From<OutOfBounds> for ParseError {
118 fn from(e: OutOfBounds) -> Self {
119 Self::Truncated {
120 at: e.at,
121 need: e.need,
122 }
123 }
124}
125
126const HEADER_LEN: usize = 28;
127const PALETTE_LEN: usize = 768;
128
129pub fn parse(bytes: &[u8]) -> Result<Kvx, ParseError> {
138 if bytes.len() < HEADER_LEN + PALETTE_LEN {
139 return Err(ParseError::TooSmall { got: bytes.len() });
140 }
141
142 let mut cur = Cursor::new(bytes);
143 let _numbytes = cur.read_u32()?;
144 let xsiz = cur.read_u32()?;
145 let ysiz = cur.read_u32()?;
146 let zsiz = cur.read_u32()?;
147 let xpivot = cur.read_u32()?;
148 let ypivot = cur.read_u32()?;
149 let zpivot = cur.read_u32()?;
150
151 let xoff_len = xsiz as usize + 1;
152 let mut xoffset = Vec::with_capacity(xoff_len);
153 for _ in 0..xoff_len {
154 xoffset.push(cur.read_u32()?);
155 }
156
157 let yoff_len = ysiz as usize + 1;
158 let mut xyoffset = Vec::with_capacity(xsiz as usize);
159 for _ in 0..xsiz {
160 let mut row = Vec::with_capacity(yoff_len);
161 for _ in 0..yoff_len {
162 row.push(cur.read_u16()?);
163 }
164 xyoffset.push(row);
165 }
166
167 let slab_region_start = cur.pos;
169 let slab_region_end = bytes
170 .len()
171 .checked_sub(PALETTE_LEN)
172 .ok_or(ParseError::TooSmall { got: bytes.len() })?;
173
174 let mut columns = Vec::with_capacity(xsiz as usize);
175 let mut slabs_cur = Cursor::new(&bytes[..slab_region_end]);
176 slabs_cur.pos = slab_region_start;
177
178 for x in 0..xsiz {
179 let mut col = Vec::with_capacity(ysiz as usize);
180 for y in 0..ysiz {
181 let lo = u32::from(xyoffset[x as usize][y as usize]);
182 let hi = u32::from(xyoffset[x as usize][y as usize + 1]);
183 if hi < lo {
184 return Err(ParseError::NonMonotonicOffsets { x, y });
185 }
186 let nbytes = (hi - lo) as usize;
187 let mut budget = nbytes;
188 let mut slabs = Vec::new();
189 while budget > 0 {
190 if budget < 3 {
191 return Err(ParseError::SlabOverrun { x, y });
192 }
193 let ztop = slabs_cur.read_u8()?;
194 let zleng = slabs_cur.read_u8()? as usize;
195 let vis = slabs_cur.read_u8()?;
196 budget -= 3;
197 if zleng > budget {
198 return Err(ParseError::SlabOverrun { x, y });
199 }
200 let mut colors = vec![0u8; zleng];
201 for c in &mut colors {
202 *c = slabs_cur.read_u8()?;
203 }
204 budget -= zleng;
205 slabs.push(Slab { ztop, vis, colors });
206 }
207 col.push(slabs);
208 }
209 columns.push(col);
210 }
211
212 let mut palette = [Rgb6 { r: 0, g: 0, b: 0 }; 256];
214 let pal = &bytes[bytes.len() - PALETTE_LEN..];
215 for (i, e) in palette.iter_mut().enumerate() {
216 e.r = pal[i * 3];
217 e.g = pal[i * 3 + 1];
218 e.b = pal[i * 3 + 2];
219 }
220
221 Ok(Kvx {
222 xsiz,
223 ysiz,
224 zsiz,
225 xpivot,
226 ypivot,
227 zpivot,
228 xoffset,
229 xyoffset,
230 columns,
231 palette,
232 })
233}
234
235#[must_use]
246pub fn serialize(kvx: &Kvx) -> Vec<u8> {
247 let slab_bytes_total: usize = kvx
249 .columns
250 .iter()
251 .flatten()
252 .flatten()
253 .map(|s| 3 + s.colors.len())
254 .sum();
255
256 let offset_table_bytes =
257 kvx.xoffset.len() * 4 + kvx.xyoffset.iter().map(|row| row.len() * 2).sum::<usize>();
258 let numbytes = HEADER_LEN - 4 + offset_table_bytes + slab_bytes_total;
259 let numbytes_u32 = u32::try_from(numbytes).expect("kvx file >= 4 GiB is not representable");
260
261 let mut out =
262 Vec::with_capacity(HEADER_LEN + offset_table_bytes + slab_bytes_total + PALETTE_LEN);
263
264 out.extend_from_slice(&numbytes_u32.to_le_bytes());
265 out.extend_from_slice(&kvx.xsiz.to_le_bytes());
266 out.extend_from_slice(&kvx.ysiz.to_le_bytes());
267 out.extend_from_slice(&kvx.zsiz.to_le_bytes());
268 out.extend_from_slice(&kvx.xpivot.to_le_bytes());
269 out.extend_from_slice(&kvx.ypivot.to_le_bytes());
270 out.extend_from_slice(&kvx.zpivot.to_le_bytes());
271 for v in &kvx.xoffset {
272 out.extend_from_slice(&v.to_le_bytes());
273 }
274 for row in &kvx.xyoffset {
275 for v in row {
276 out.extend_from_slice(&v.to_le_bytes());
277 }
278 }
279 for col in &kvx.columns {
280 for slabs in col {
281 for s in slabs {
282 let zleng = u8::try_from(s.colors.len())
286 .expect("kvx slab zleng must fit in u8 (file format limit)");
287 out.push(s.ztop);
288 out.push(zleng);
289 out.push(s.vis);
290 out.extend_from_slice(&s.colors);
291 }
292 }
293 }
294 for e in &kvx.palette {
295 out.push(e.r);
296 out.push(e.g);
297 out.push(e.b);
298 }
299
300 out
301}
302
303#[cfg(test)]
306mod tests {
307 use super::*;
308
309 const COCO_KVX: &[u8] = include_bytes!("../../../assets/coco.kvx");
312
313 #[test]
314 fn parse_coco_header() {
315 let kvx = parse(COCO_KVX).expect("parse coco.kvx");
316 assert_eq!(kvx.xsiz, 9);
317 assert_eq!(kvx.ysiz, 11);
318 assert_eq!(kvx.zsiz, 9);
319 assert_eq!(kvx.xpivot, 0x200);
320 assert_eq!(kvx.ypivot, 0x300);
321 assert_eq!(kvx.zpivot, 0x900);
322 assert_eq!(kvx.columns.len(), 9);
323 assert_eq!(kvx.columns[0].len(), 11);
324 }
325
326 #[test]
327 fn coco_palette_widening() {
328 let kvx = parse(COCO_KVX).expect("parse coco.kvx");
329 let p0 = kvx.palette[0];
331 assert_eq!((p0.r, p0.g, p0.b), (0x3f, 0x19, 0x19));
332 let want = 0x8000_0000u32 | (0x3fu32 << 18) | (0x19u32 << 10) | (0x19u32 << 2);
335 assert_eq!(p0.to_voxlap_argb(), want);
336 }
337
338 #[test]
339 fn coco_roundtrips_byte_equal() {
340 let kvx = parse(COCO_KVX).expect("parse coco.kvx");
341 let out = serialize(&kvx);
342 assert_eq!(out.len(), COCO_KVX.len(), "length differs");
343 assert_eq!(out.as_slice(), COCO_KVX, "byte content differs");
344 }
345
346 #[test]
347 fn parse_truncated_fails() {
348 let r = parse(&[0u8; 16]);
349 assert!(matches!(r, Err(ParseError::TooSmall { .. })));
350 }
351}