1use anyhow::{Context, Result};
2use serde::Deserialize;
3use std::fs;
4use std::io::{Read, Seek, SeekFrom};
5use std::path::Path;
6
7#[derive(Debug, Deserialize)]
8struct MotionJsonMetadata {
9 #[serde(rename = "sensorArrangment", default)]
10 sensor_arrangement: Option<String>,
11 #[serde(rename = "sensorOrientation", default)]
12 sensor_orientation: Option<i64>,
13 #[serde(rename = "forwardMatrix1", default)]
14 forward_matrix1: Option<Vec<f64>>,
15 #[serde(rename = "forwardMatrix2", default)]
16 forward_matrix2: Option<Vec<f64>>,
17 #[serde(rename = "colorMatrix1", default)]
18 color_matrix1: Option<Vec<f64>>,
19 #[serde(rename = "colorMatrix2", default)]
20 color_matrix2: Option<Vec<f64>>,
21 #[serde(rename = "calibrationMatrix1", default)]
22 calibration_matrix1: Option<Vec<f64>>,
23 #[serde(rename = "calibrationMatrix2", default)]
24 calibration_matrix2: Option<Vec<f64>>,
25 #[serde(rename = "whiteLevel", default)]
26 white_level: Option<f64>,
27 #[serde(rename = "blackLevel", default)]
28 black_level: Option<Vec<f64>>,
29 #[serde(rename = "baselineExposure", default)]
30 baseline_exposure: Option<f64>,
31 #[serde(rename = "apertures", default)]
32 apertures: Option<Vec<f64>>,
33 #[serde(rename = "focalLengths", default)]
34 focal_lengths: Option<Vec<f64>>,
35 #[serde(rename = "uniqueCameraModel", default)]
36 unique_camera_model: Option<String>,
37 #[serde(rename = "numSegments", default)]
38 num_segments: Option<i64>,
39 #[serde(rename = "extraData", default)]
40 extra_data: Option<ExtraData>,
41 #[serde(rename = "deviceSpecificProfile", default)]
42 device_specific_profile: Option<DeviceProfile>,
43 #[serde(rename = "colorIlluminant1", default)]
44 color_illuminant1: Option<String>,
45 #[serde(rename = "colorIlluminant2", default)]
46 color_illuminant2: Option<String>,
47 #[serde(rename = "lensShadingMap", default)]
48 lens_shading_map: Option<Vec<Vec<f64>>>,
49 #[serde(rename = "lensShadingMapWidth", default)]
50 lens_shading_map_width: Option<i64>,
51 #[serde(rename = "lensShadingMapHeight", default)]
52 lens_shading_map_height: Option<i64>,
53}
54
55#[derive(Debug, Deserialize)]
56struct ExtraData {
57 #[serde(rename = "recordingType", default)]
58 recording_type: Option<String>,
59 #[serde(rename = "audioSampleRate", default)]
60 audio_sample_rate: Option<i64>,
61 #[serde(rename = "audioChannels", default)]
62 audio_channels: Option<i64>,
63 #[serde(rename = "useAccurateTimestamp", default)]
64 use_accurate_timestamp: Option<bool>,
65 #[serde(rename = "metadata", default)]
66 metadata: Option<BuildMetadata>,
67}
68
69#[derive(Debug, Deserialize)]
70struct BuildMetadata {
71 #[serde(rename = "build.model", default)]
72 build_model: Option<String>,
73 #[serde(rename = "build.manufacturer", default)]
74 build_manufacturer: Option<String>,
75 #[serde(rename = "version.major", default)]
76 version_major: Option<String>,
77 #[serde(rename = "version.build", default)]
78 version_build: Option<String>,
79}
80
81#[derive(Debug, Deserialize)]
82struct DeviceProfile {
83 #[serde(rename = "cameraId", default)]
84 camera_id: Option<String>,
85 #[serde(rename = "deviceModel", default)]
86 device_model: Option<String>,
87}
88
89const INDEX_MAGIC: u32 = 0x8A905612;
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum BayerPattern {
94 RGGB,
95 GRBG,
96 GBRG,
97 BGGR,
98 QuadBayerRGGB,
99 QuadBayerGRBG,
100 QuadBayerGBRG,
101 QuadBayerBGGR,
102}
103
104impl BayerPattern {
105 pub fn from_u8(value: u8) -> Self {
106 match value {
107 0 => BayerPattern::RGGB,
108 1 => BayerPattern::GRBG,
109 2 => BayerPattern::GBRG,
110 3 => BayerPattern::BGGR,
111 4 => BayerPattern::QuadBayerRGGB,
112 5 => BayerPattern::QuadBayerGRBG,
113 6 => BayerPattern::QuadBayerGBRG,
114 7 => BayerPattern::QuadBayerBGGR,
115 _ => BayerPattern::RGGB,
116 }
117 }
118
119 pub fn to_u8(&self) -> u8 {
120 match self {
121 BayerPattern::RGGB => 0,
122 BayerPattern::GRBG => 1,
123 BayerPattern::GBRG => 2,
124 BayerPattern::BGGR => 3,
125 BayerPattern::QuadBayerRGGB => 4,
126 BayerPattern::QuadBayerGRBG => 5,
127 BayerPattern::QuadBayerGBRG => 6,
128 BayerPattern::QuadBayerBGGR => 7,
129 }
130 }
131
132 pub fn name(&self) -> &'static str {
133 match self {
134 BayerPattern::RGGB => "RGGB",
135 BayerPattern::GRBG => "GRBG",
136 BayerPattern::GBRG => "GBRG",
137 BayerPattern::BGGR => "BGGR",
138 BayerPattern::QuadBayerRGGB => "QuadBayer RGGB",
139 BayerPattern::QuadBayerGRBG => "QuadBayer GRBG",
140 BayerPattern::QuadBayerGBRG => "QuadBayer GBRG",
141 BayerPattern::QuadBayerBGGR => "QuadBayer BGGR",
142 }
143 }
144
145 pub fn to_dcraw_filters(&self) -> u32 {
148 match self {
149 BayerPattern::RGGB => 0x94949494,
150 BayerPattern::BGGR => 0x16161616,
151 BayerPattern::GRBG => 0x61616161,
152 BayerPattern::GBRG => 0x49494949,
153 _ => 0x94949494, }
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct CameraMetadata {
161 pub sensor_make: Option<String>,
162 pub sensor_model: Option<String>,
163 pub camera_model: Option<String>,
164 pub lens_model: Option<String>,
165 pub focal_length: Option<f64>,
166 pub aperture: Option<f64>,
167 pub iso: Option<u32>,
168 pub exposure_time: Option<f64>,
169 pub white_balance: Option<f64>,
170 pub capture_date: Option<String>,
171 pub color_matrix: Option<[f64; 9]>,
172 pub color_matrix2: Option<[f64; 9]>,
173 pub forward_matrix1: Option<[f64; 9]>,
174 pub forward_matrix2: Option<[f64; 9]>,
175 pub calibration_matrix1: Option<[f64; 9]>,
176 pub calibration_matrix2: Option<[f64; 9]>,
177 pub calibration_illuminant1: Option<i32>,
178 pub calibration_illuminant2: Option<i32>,
179 pub calibration_illuminant: Option<String>,
180 pub wb_multipliers: Option<[f32; 3]>,
181}
182
183impl Default for CameraMetadata {
184 fn default() -> Self {
185 CameraMetadata {
186 sensor_make: None,
187 sensor_model: None,
188 camera_model: None,
189 lens_model: None,
190 focal_length: None,
191 aperture: None,
192 iso: None,
193 exposure_time: None,
194 white_balance: None,
195 capture_date: None,
196 color_matrix: None,
197 color_matrix2: None,
198 forward_matrix1: None,
199 forward_matrix2: None,
200 calibration_matrix1: None,
201 calibration_matrix2: None,
202 calibration_illuminant1: None,
203 calibration_illuminant2: None,
204 calibration_illuminant: None,
205 wb_multipliers: None,
206 }
207 }
208}
209
210#[derive(Debug, Clone)]
212pub struct McrawFileInfo {
213 pub path: String,
214 pub size: u64,
215 pub format_version: u32,
216 pub frame_count: u32,
217 pub width: u16,
218 pub height: u16,
219 pub fps: f64,
220 pub has_audio: bool,
221 pub audio_sample_rate: u32,
222 pub audio_channels: u16,
223 pub bit_depth: u16,
224 pub bayer_pattern: BayerPattern,
225 pub camera_metadata: CameraMetadata,
226 pub frame_offsets: Vec<u64>,
227 pub audio_offset: Option<u64>,
228 pub audio_length: Option<u64>,
229 pub sensor_width: u16,
230 pub sensor_height: u16,
231 pub active_offset_x: u16,
232 pub active_offset_y: u16,
233 pub active_width: u16,
234 pub active_height: u16,
235 pub white_level: f64,
236 pub black_level: f64,
237 pub black_level_per_channel: [f64; 4],
238 pub black_level_count: i32,
239 pub lens_shading_map: Option<crate::decoder::LensShadingMap>,
240 pub dynamic_black_level: Option<[f32; 4]>,
241 pub dynamic_white_level: Option<f32>,
242 pub first_timestamp: Option<i64>,
244}
245
246struct FirstFrameMeta {
248 width: u16,
249 height: u16,
250 wb_gains: Option<[f32; 3]>,
252}
253
254fn read_first_frame_meta(file: &mut fs::File, frame0_offset: u64) -> Option<FirstFrameMeta> {
256 file.seek(SeekFrom::Start(frame0_offset)).ok()?;
257 let mut hdr = [0u8; 8];
258 file.read_exact(&mut hdr).ok()?;
259 let buf_type = u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
260 let buf_size = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
261 if buf_type != 2 {
262 return None;
263 }
264 file.seek(SeekFrom::Current(buf_size as i64)).ok()?;
265 file.read_exact(&mut hdr).ok()?;
266 let meta_type = u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
267 let meta_size = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
268 if meta_type != 3 {
269 return None;
270 }
271 let mut json_buf = vec![0u8; meta_size as usize];
272 file.read_exact(&mut json_buf).ok()?;
273 let json: serde_json::Value = serde_json::from_slice(&json_buf).ok()?;
274 let w = json.get("width")?.as_u64()? as u16;
275 let h = json.get("height")?.as_u64()? as u16;
276 if w == 0 || h == 0 {
277 return None;
278 }
279 let wb_gains = json.get("asShotNeutral").and_then(|v| v.as_array()).and_then(|arr| {
280 if arr.len() >= 3 {
281 let r = arr[0].as_f64()?;
282 let g = arr[1].as_f64()?;
283 let b = arr[2].as_f64()?;
284 if r > 1e-6 && g > 1e-6 && b > 1e-6 {
285 Some([(g / r) as f32, 1.0, (g / b) as f32])
286 } else {
287 None
288 }
289 } else {
290 None
291 }
292 });
293 tracing::debug!("read_first_frame_meta: w={} h={} wb_gains={:?}", w, h, wb_gains);
294 Some(FirstFrameMeta { width: w, height: h, wb_gains })
295}
296
297impl McrawFileInfo {
298 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
299 let path = path.as_ref();
300 tracing::debug!("McrawFileInfo::from_path: {:?}", path);
301 let file_meta = fs::metadata(&path)
302 .with_context(|| format!("Failed to read metadata for {:?}", path))?;
303 let file_size = file_meta.len();
304
305 let mut file = std::fs::File::open(path)
306 .with_context(|| format!("Failed to open {:?}", path))?;
307
308 let mut magic_buf = [0u8; 16];
309 file.read_exact(&mut magic_buf)
310 .with_context(|| format!("Failed to read header from {:?}", path))?;
311
312 let info = if magic_buf.starts_with(b"MOTION ") {
313 let json_len = u32::from_le_bytes([
314 magic_buf[12], magic_buf[13], magic_buf[14], magic_buf[15],
315 ]) as usize;
316
317 let mut json_buf = vec![0u8; json_len];
318 file.read_exact(&mut json_buf)
319 .with_context(|| format!("Failed to read MOTION JSON block from {:?}", path))?;
320
321 let mut data = Vec::with_capacity(16 + json_len);
322 data.extend_from_slice(&magic_buf);
323 data.extend_from_slice(&json_buf);
324
325 let mut info = parse_motion_header(&data, path)?;
326
327 if file_size >= 24 {
329 let mut end_buf = [0u8; 24];
330 file.seek(SeekFrom::End(-24))
331 .with_context(|| format!("Failed to seek to BufferIndex in {:?}", path))?;
332 file.read_exact(&mut end_buf)
333 .with_context(|| format!("Failed to read BufferIndex from {:?}", path))?;
334
335 let idx_magic = u32::from_le_bytes([end_buf[8], end_buf[9], end_buf[10], end_buf[11]]);
336 if idx_magic == INDEX_MAGIC {
337 let num_offsets = u32::from_le_bytes([end_buf[12], end_buf[13], end_buf[14], end_buf[15]]);
338 let idx_data_offset = i64::from_le_bytes([
339 end_buf[16], end_buf[17], end_buf[18], end_buf[19],
340 end_buf[20], end_buf[21], end_buf[22], end_buf[23],
341 ]) as u64;
342
343 info.frame_count = num_offsets;
344
345 if num_offsets > 0 && idx_data_offset + (num_offsets as u64 * 16) <= file_size {
346 let mut offset_buf = vec![0u8; num_offsets as usize * 16];
347 file.seek(SeekFrom::Start(idx_data_offset))
348 .with_context(|| format!("Failed to seek to offset data in {:?}", path))?;
349 file.read_exact(&mut offset_buf)
350 .with_context(|| format!("Failed to read offset data from {:?}", path))?;
351
352 let mut first_frame_offset: u64 = 0;
353 let mut timestamps = Vec::with_capacity(num_offsets as usize);
354 for i in 0..num_offsets as usize {
355 let off = i64::from_le_bytes([
356 offset_buf[i*16], offset_buf[i*16+1], offset_buf[i*16+2], offset_buf[i*16+3],
357 offset_buf[i*16+4], offset_buf[i*16+5], offset_buf[i*16+6], offset_buf[i*16+7],
358 ]);
359 let ts = i64::from_le_bytes([
360 offset_buf[i*16+8], offset_buf[i*16+9], offset_buf[i*16+10], offset_buf[i*16+11],
361 offset_buf[i*16+12], offset_buf[i*16+13], offset_buf[i*16+14], offset_buf[i*16+15],
362 ]);
363 if i == 0 { first_frame_offset = off as u64; }
364 timestamps.push(ts);
365 }
366
367 timestamps.sort();
369 info.first_timestamp = Some(timestamps[0]);
370 if num_offsets >= 2 {
371 let duration_ns = timestamps[num_offsets as usize - 1] - timestamps[0];
372 if duration_ns > 0 {
373 info.fps = (num_offsets as f64 - 1.0) / (duration_ns as f64 / 1_000_000_000.0);
374 }
375 }
376
377 if first_frame_offset > 0 {
379 if let Some(meta) = read_first_frame_meta(&mut file, first_frame_offset) {
380 info.width = meta.width;
381 info.height = meta.height;
382 if let Some(wb) = meta.wb_gains {
383 info.camera_metadata.wb_multipliers = Some(wb);
384 }
385 } else {
386 tracing::debug!("from_path: read_first_frame_meta returned None for offset={}", first_frame_offset);
387 }
388 }
389 }
390 }
391 }
392
393 info
394 } else if &magic_buf[..5] == b"MCRAW" {
395 let mut rest_header = [0u8; 20];
396 file.read_exact(&mut rest_header)
397 .with_context(|| format!("Failed to read legacy header from {:?}", path))?;
398
399 let mut data = Vec::with_capacity(36);
400 data.extend_from_slice(&magic_buf);
401 data.extend_from_slice(&rest_header);
402
403 if file_size > 36 {
404 let mut block_len_buf = [0u8; 4];
405 file.read_exact(&mut block_len_buf)
406 .with_context(|| format!("Failed to read TLV block length from {:?}", path))?;
407 let block_length = u32::from_be_bytes(block_len_buf) as usize;
408
409 let mut tlv_buf = vec![0u8; block_length];
410 file.read_exact(&mut tlv_buf)
411 .with_context(|| format!("Failed to read TLV block from {:?}", path))?;
412
413 data.extend_from_slice(&block_len_buf);
414 data.extend_from_slice(&tlv_buf);
415 }
416
417 parse_header(&data, path)?
418 } else {
419 anyhow::bail!(
420 "Invalid MCRAW magic header in {:?}: expected 'MCRAW' or 'MOTION ', got {:?}",
421 path,
422 &magic_buf[..7]
423 );
424 };
425
426 Ok(McrawFileInfo {
427 path: path.to_string_lossy().into_owned(),
428 size: file_size,
429 ..info
430 })
431 }
432
433 pub fn is_metadata_complete(&self) -> bool {
435 self.width > 0 && self.height > 0 && self.frame_count > 0
436 && self.first_timestamp.is_some()
437 && self.camera_metadata.wb_multipliers.is_some()
438 }
439
440 pub fn enhance_from_decoder(&mut self, decoder: &crate::decoder::Decoder) {
441 if self.camera_metadata.color_matrix.is_none() {
443 if let Ok(container_meta) = decoder.container_metadata() {
444 if container_meta.white_level > 0.0 {
445 self.white_level = container_meta.white_level;
446 tracing::debug!("white_level from container: {}", self.white_level);
447 }
448 if container_meta.black_level_count > 0 {
449 self.black_level = container_meta.black_level[0];
450 self.black_level_per_channel = container_meta.black_level;
451 self.black_level_count = container_meta.black_level_count;
452 tracing::debug!("black_level from container: {} ({} ch)", self.black_level, self.black_level_count);
453 }
454 self.lens_shading_map = container_meta.lens_shading_map.clone();
455
456 let as_f64 = |v: &[f32; 9]| -> [f64; 9] {
457 let mut r = [0.0; 9];
458 for (i, &x) in v.iter().enumerate() { r[i] = x as f64; }
459 r
460 };
461
462 self.camera_metadata.color_matrix = Some(as_f64(&container_meta.color_matrix1));
463 let non_zero = |m: &[f32; 9]| m.iter().any(|&x| x != 0.0);
464
465 if non_zero(&container_meta.color_matrix2) {
466 self.camera_metadata.color_matrix2 = Some(as_f64(&container_meta.color_matrix2));
467 }
468 if non_zero(&container_meta.forward_matrix1) {
469 self.camera_metadata.forward_matrix1 = Some(as_f64(&container_meta.forward_matrix1));
470 }
471 if non_zero(&container_meta.forward_matrix2) {
472 self.camera_metadata.forward_matrix2 = Some(as_f64(&container_meta.forward_matrix2));
473 }
474 if non_zero(&container_meta.calibration_matrix1) {
475 self.camera_metadata.calibration_matrix1 = Some(as_f64(&container_meta.calibration_matrix1));
476 }
477 if non_zero(&container_meta.calibration_matrix2) {
478 self.camera_metadata.calibration_matrix2 = Some(as_f64(&container_meta.calibration_matrix2));
479 }
480 if container_meta.has_calibration_illuminants {
481 self.camera_metadata.calibration_illuminant1 = Some(container_meta.calibration_illuminant1);
482 self.camera_metadata.calibration_illuminant2 = Some(container_meta.calibration_illuminant2);
483 tracing::debug!("calibration_illuminants: illum1={}, illum2={}",
484 container_meta.calibration_illuminant1, container_meta.calibration_illuminant2);
485 }
486 }
487 }
488 if let Ok(timestamps) = decoder.timestamps() {
490 if !timestamps.is_empty() {
491 self.frame_count = timestamps.len() as u32;
492 if timestamps.len() >= 2 {
493 let duration_ns = timestamps[timestamps.len() - 1] - timestamps[0];
494 if duration_ns > 0 {
495 let duration_in_seconds = duration_ns as f64 / 1_000_000_000.0;
496 self.fps = (self.frame_count.saturating_sub(1)) as f64 / duration_in_seconds;
497 }
498 }
499 tracing::debug!("enhanced from timestamps: {} frames, {:.2} fps", self.frame_count, self.fps);
500 }
501 if let Ok(first_frame_meta) = decoder.load_frame_metadata(timestamps[0]) {
502 if self.width == 0 || self.height == 0 {
503 self.width = first_frame_meta.width as u16;
504 self.height = first_frame_meta.height as u16;
505 tracing::debug!("enhanced dimensions: {}x{}", first_frame_meta.width, first_frame_meta.height);
506 }
507 let n = first_frame_meta.as_shot_neutral;
508 if self.camera_metadata.wb_multipliers.is_none()
509 && n[0] > 1e-6 && n[1] > 1e-6 && n[2] > 1e-6
510 {
511 let r_gain = n[1] / n[0];
512 let b_gain = n[1] / n[2];
513 self.camera_metadata.wb_multipliers = Some([r_gain, 1.0, b_gain]);
514 tracing::debug!("wb_multipliers: R={:.3} G={:.3} B={:.3}", r_gain, 1.0, b_gain);
515 }
516 if first_frame_meta.dynamic_black_level.is_some() {
517 self.dynamic_black_level = first_frame_meta.dynamic_black_level;
518 tracing::debug!("dynamic_black_level from first frame: {:?}", self.dynamic_black_level);
519 }
520 if first_frame_meta.dynamic_white_level.is_some() {
521 self.dynamic_white_level = first_frame_meta.dynamic_white_level;
522 tracing::debug!("dynamic_white_level from first frame: {:?}", self.dynamic_white_level);
523 }
524 }
525 }
526 }
527
528 pub fn enhance_with_decoder(&mut self) {
529 if self.camera_metadata.color_matrix.is_some() {
530 tracing::debug!("enhance_with_decoder: metadata already populated, skipping decoder");
531 return;
532 }
533 let path = self.path.clone();
534 tracing::debug!("enhance_with_decoder: {}", path);
535 let decoder_result = crate::decoder::Decoder::new(&path);
536 let decoder = match decoder_result {
537 Ok(d) => d,
538 Err(e) => {
539 tracing::warn!("failed to open decoder for {}: {}", path, e);
540 return;
541 }
542 };
543 self.enhance_from_decoder(&decoder);
544 }
545
546 pub fn format_name(&self) -> &'static str {
547 match self.format_version {
548 1 => "MotionCam v1 (Legacy)",
549 2 => "MotionCam v2",
550 3 => "MotionCam v3",
551 _ => "Unknown format",
552 }
553 }
554
555 pub fn duration_seconds(&self) -> f64 {
556 self.frame_count as f64 / self.fps
557 }
558
559 pub fn resolution_label(&self) -> &'static str {
560 match (self.width, self.height) {
561 (1920, 1080) => "1080p",
562 (2560, 1440) => "1440p",
563 (3840, 2160) => "4K",
564 (4096, 2160) => "4K DCI",
565 _ => "Custom",
566 }
567 }
568}
569
570fn parse_motion_header(data: &[u8], path: &Path) -> Result<McrawFileInfo> {
571 if data.len() < 17 {
572 anyhow::bail!("File {:?} is too small for MOTION header", path);
573 }
574
575 let format_version = data[7] as u32;
576 tracing::debug!("parse_motion_header: version={} json_len={}", format_version, u32::from_le_bytes([data[12], data[13], data[14], data[15]]));
577 let json_len = u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
578 let json_start = 16;
579 let json_end = json_start + json_len;
580
581 if json_end > data.len() {
582 anyhow::bail!("JSON metadata extends beyond file data");
583 }
584
585 let json_str = std::str::from_utf8(&data[json_start..json_end])
586 .with_context(|| "Invalid UTF-8 in MOTION JSON metadata")?;
587
588 let json: MotionJsonMetadata = serde_json::from_str(json_str)
589 .with_context(|| "Failed to parse MOTION JSON metadata")?;
590
591 let bayer_pattern = match json.sensor_arrangement.as_deref() {
592 Some("rggb") | Some("standard") => BayerPattern::RGGB,
593 Some("grbg") => BayerPattern::GRBG,
594 Some("gbrg") => BayerPattern::GBRG,
595 Some("bggr") => BayerPattern::BGGR,
596 _ => BayerPattern::RGGB,
597 };
598
599 let extra_data = json.extra_data;
600 let device_profile = json.device_specific_profile;
601
602 let build_model = extra_data
603 .as_ref()
604 .and_then(|e| e.metadata.as_ref())
605 .and_then(|m| m.build_model.clone());
606
607 let camera_model: Option<String> = device_profile.as_ref()
608 .and_then(|p| p.device_model.clone())
609 .filter(|s| !s.is_empty())
610 .or_else(|| json.unique_camera_model.filter(|s| !s.is_empty()))
611 .or_else(|| build_model.filter(|s| !s.is_empty()));
612
613 let sensor_make = extra_data
614 .as_ref()
615 .and_then(|e| e.metadata.as_ref())
616 .and_then(|m| m.build_manufacturer.clone())
617 .unwrap_or_default();
618
619 let aperture = json.apertures.and_then(|mut a| a.pop());
620 let focal_length = json.focal_lengths.and_then(|mut a| a.pop());
621 let audio_sample_rate = extra_data.as_ref()
622 .and_then(|e| e.audio_sample_rate)
623 .unwrap_or(0) as u32;
624 let audio_channels = extra_data.as_ref()
625 .and_then(|e| e.audio_channels)
626 .unwrap_or(0) as u16;
627 let has_audio = audio_channels > 0;
628
629 let color_matrix = json.color_matrix1.clone().or(json.forward_matrix1.clone())
630 .and_then(|m| {
631 if m.len() == 9 {
632 Some(m.try_into().ok()?)
633 } else {
634 None
635 }
636 });
637
638 let color_matrix2 = json.color_matrix2.clone().and_then(|m| {
639 if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
640 });
641 let forward_matrix1 = json.forward_matrix1.clone().and_then(|m| {
642 if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
643 });
644 let forward_matrix2 = json.forward_matrix2.clone().and_then(|m| {
645 if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
646 });
647 let calibration_matrix1 = json.calibration_matrix1.clone().and_then(|m| {
648 if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
649 });
650 let calibration_matrix2 = json.calibration_matrix2.clone().and_then(|m| {
651 if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
652 });
653
654 let bit_depth = json.white_level
655 .map(detect_bit_depth_from_white_level)
656 .unwrap_or(12);
657
658 let frame_count: u32 = 0;
659 let width: u16 = 0;
660 let height: u16 = 0;
661 let fps: f64 = 0.0;
662
663 let (black_level, black_level_per_channel, black_level_count) = json.black_level
664 .as_ref()
665 .map(|levels| {
666 let count = levels.len() as i32;
667 let avg = if levels.is_empty() { 0.0 } else { levels.iter().sum::<f64>() / levels.len() as f64 };
668 let mut per_ch = [avg; 4];
669 for (i, &v) in levels.iter().enumerate().take(4) {
670 per_ch[i] = v;
671 }
672 (avg, per_ch, count)
673 })
674 .unwrap_or((0.0, [0.0; 4], 0));
675
676 let white_level = json.white_level.unwrap_or(16383.0);
677
678 let wb_multipliers: Option<[f32; 3]> = None;
682
683
684 let json_illuminant_to_const = |s: &str| -> Option<i32> {
686 match s.trim().to_lowercase().as_str() {
687 "d50" | "horizon" | "cool_white" => Some(23),
688 "d55" => Some(22),
689 "d65" | "daylight" | "fine_weather" | "cloudy" => Some(21),
690 "d75" | "shade" => Some(24),
691 "standardlighta" | "standard_a" | "tungsten" | "incandescent" | "warm_white" | "iso_studio_tungsten" => Some(17),
692 "fluorescent" | "tl84" => Some(12),
693 "flash" | "standardlightb" => Some(4),
694 _ => None,
695 }
696 };
697
698 let calibration_illuminant1 = json.color_illuminant1.as_deref().and_then(json_illuminant_to_const);
699 let calibration_illuminant2 = json.color_illuminant2.as_deref().and_then(json_illuminant_to_const);
700
701 let lens_shading_map = json.lens_shading_map.as_ref().and_then(|channels| {
702 let width = json.lens_shading_map_width? as u32;
703 let height = json.lens_shading_map_height? as u32;
704 if channels.len() < 4 { return None; }
705 let f32_channels: Vec<Vec<f32>> = channels.iter().take(4).map(|ch| ch.iter().map(|&v| v as f32).collect()).collect();
706 if f32_channels.len() < 4 { return None; }
707 Some(crate::decoder::LensShadingMap { channels: f32_channels, width, height })
708 });
709
710 Ok(McrawFileInfo {
711 path: path.to_string_lossy().into_owned(),
712 size: data.len() as u64,
713 format_version,
714 frame_count,
715 width,
716 height,
717 fps,
718 has_audio,
719 audio_sample_rate,
720 audio_channels,
721 bit_depth,
722 bayer_pattern,
723 camera_metadata: CameraMetadata {
724 sensor_make: if sensor_make.is_empty() { None } else { Some(sensor_make) },
725 sensor_model: None,
726 camera_model,
727 lens_model: None,
728 focal_length,
729 aperture,
730 iso: None,
731 exposure_time: None,
732 white_balance: None,
733 capture_date: None,
734 color_matrix,
735 color_matrix2,
736 forward_matrix1,
737 forward_matrix2,
738 calibration_matrix1,
739 calibration_matrix2,
740 calibration_illuminant1,
741 calibration_illuminant2,
742 calibration_illuminant: None,
743 wb_multipliers,
744 },
745 frame_offsets: Vec::new(),
746 audio_offset: None,
747 audio_length: None,
748 sensor_width: 0,
749 sensor_height: 0,
750 active_offset_x: 0,
751 active_offset_y: 0,
752 active_width: 0,
753 active_height: 0,
754 white_level,
755 black_level,
756 black_level_per_channel,
757 black_level_count,
758 lens_shading_map,
759 dynamic_black_level: None,
760 dynamic_white_level: None,
761 first_timestamp: None,
762 })
763}
764
765fn parse_header(data: &[u8], path: &Path) -> Result<McrawFileInfo> {
766 if data.len() < 17 {
767 anyhow::bail!("File {:?} is too small to be a valid file (need at least 17 bytes, got {})", path, data.len());
768 }
769
770 if data.starts_with(b"MOTION ") {
772 tracing::debug!("detected MOTION format header");
773 return parse_motion_header(data, path);
774 }
775
776 if data.len() < 36 {
778 anyhow::bail!("File {:?} is too small to be a valid MCRAW file (need at least 36 bytes, got {})", path, data.len());
779 }
780
781 let magic = &data[0..5];
782 if magic != b"MCRAW" {
783 anyhow::bail!(
784 "Invalid MCRAW magic header in {:?}: expected 'MCRAW', found {:?}",
785 path,
786 magic
787 );
788 }
789 tracing::debug!("detected MCRAW legacy format header");
790
791 let format_version = u32::from_be_bytes([data[5], data[6], data[7], data[8]]);
792 let frame_count = u32::from_be_bytes([data[9], data[10], data[11], data[12]]);
793 let width = u16::from_be_bytes([data[13], data[14]]);
794 let height = u16::from_be_bytes([data[15], data[16]]);
795 let fps = f64::from_be_bytes([
796 data[17], data[18], data[19], data[20], data[21], data[22], data[23], data[24],
797 ]);
798 let has_audio = data[25] != 0;
799
800 let audio_sample_rate = if has_audio && data.len() >= 30 {
801 u32::from_be_bytes([data[26], data[27], data[28], data[29]])
802 } else {
803 0
804 };
805
806 let audio_channels = if data.len() >= 32 {
807 u16::from_be_bytes([data[30], data[31]])
808 } else {
809 0
810 };
811
812 let bit_depth = if data.len() >= 34 {
813 u16::from_be_bytes([data[32], data[33]])
814 } else {
815 0
816 };
817
818 let bayer_pattern_id = if data.len() >= 35 {
819 data[34]
820 } else {
821 0
822 };
823 let bayer_pattern = BayerPattern::from_u8(bayer_pattern_id);
824
825 let mut offset = 36;
826 let mut camera_metadata = CameraMetadata::default();
827 let mut frame_offsets = Vec::new();
828 let mut audio_offset: Option<u64> = None;
829 let mut audio_length: Option<u64> = None;
830 let mut sensor_width: u16 = 0;
831 let mut sensor_height: u16 = 0;
832 let mut active_offset_x: u16 = 0;
833 let mut active_offset_y: u16 = 0;
834 let mut active_width: u16 = 0;
835 let mut active_height: u16 = 0;
836 let mut _color_matrix: Option<[f64; 9]> = None;
837 let mut _calibration_illuminant: Option<String> = None;
838
839 if offset < data.len() {
840 let block_length = read_u32_be(&data, offset) as usize;
841 offset += 4;
842 let block_end = offset + block_length;
843
844 while offset < block_end && offset < data.len() {
845 let tag = data[offset];
846 offset += 1;
847
848 match tag {
849 0x01 => {
850 if let Ok(s) = parse_string(&data, &mut offset) {
851 camera_metadata.sensor_make = Some(s);
852 }
853 }
854 0x02 => {
855 if let Ok(s) = parse_string(&data, &mut offset) {
856 camera_metadata.sensor_model = Some(s);
857 }
858 }
859 0x03 => {
860 if let Ok(s) = parse_string(&data, &mut offset) {
861 camera_metadata.camera_model = Some(s);
862 }
863 }
864 0x04 => {
865 if let Ok(s) = parse_string(&data, &mut offset) {
866 camera_metadata.lens_model = Some(s);
867 }
868 }
869 0x05 => {
870 if let Ok(v) = parse_f64(&data, &mut offset) {
871 camera_metadata.focal_length = Some(v);
872 }
873 }
874 0x06 => {
875 if let Ok(v) = parse_f64(&data, &mut offset) {
876 camera_metadata.aperture = Some(v);
877 }
878 }
879 0x07 => {
880 if let Ok(v) = parse_u32_be(&data, &mut offset) {
881 camera_metadata.iso = Some(v);
882 }
883 }
884 0x08 => {
885 if let Ok(v) = parse_f64(&data, &mut offset) {
886 camera_metadata.exposure_time = Some(v);
887 }
888 }
889 0x09 => {
890 if let Ok(v) = parse_f64(&data, &mut offset) {
891 camera_metadata.white_balance = Some(v);
892 }
893 }
894 0x0A => {
895 if let Ok(s) = parse_string(&data, &mut offset) {
896 camera_metadata.capture_date = Some(s);
897 }
898 }
899 0x0B => {
900 let matrix = parse_f64_array(&data, &mut offset, 9);
901 if matrix.len() == 9 {
902 let arr: [f64; 9] = matrix.try_into().ok().unwrap_or([0.0; 9]);
903 _color_matrix = Some(arr);
904 }
905 }
906 0x0C => {
907 if let Ok(s) = parse_string(&data, &mut offset) {
908 _calibration_illuminant = Some(s);
909 }
910 }
911 0x10 => {
912 let count = parse_u32_be(&data, &mut offset);
913 if let Ok(n) = count {
914 let mut offsets = Vec::with_capacity(n as usize);
915 for _ in 0..n {
916 if offset + 8 <= data.len() {
917 let val = u64::from_be_bytes([
918 data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
919 data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
920 ]);
921 offsets.push(val);
922 offset += 8;
923 } else {
924 break;
925 }
926 }
927 frame_offsets = offsets;
928 }
929 }
930 0x11 => {
931 if has_audio && offset + 8 <= data.len() {
932 audio_offset = Some(u64::from_be_bytes([
933 data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
934 data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
935 ]));
936 offset += 8;
937 }
938 }
939 0x12 => {
940 if has_audio && offset + 8 <= data.len() {
941 audio_length = Some(u64::from_be_bytes([
942 data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
943 data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
944 ]));
945 offset += 8;
946 }
947 }
948 0x13 => {
949 if offset + 2 <= data.len() {
950 sensor_width = u16::from_be_bytes([data[offset], data[offset + 1]]);
951 offset += 2;
952 }
953 }
954 0x14 => {
955 if offset + 2 <= data.len() {
956 sensor_height = u16::from_be_bytes([data[offset], data[offset + 1]]);
957 offset += 2;
958 }
959 }
960 0x15 => {
961 if offset + 2 <= data.len() {
962 active_offset_x = u16::from_be_bytes([data[offset], data[offset + 1]]);
963 offset += 2;
964 }
965 }
966 0x16 => {
967 if offset + 2 <= data.len() {
968 active_offset_y = u16::from_be_bytes([data[offset], data[offset + 1]]);
969 offset += 2;
970 }
971 }
972 0x17 => {
973 if offset + 2 <= data.len() {
974 active_width = u16::from_be_bytes([data[offset], data[offset + 1]]);
975 offset += 2;
976 }
977 }
978 0x18 => {
979 if offset + 2 <= data.len() {
980 active_height = u16::from_be_bytes([data[offset], data[offset + 1]]);
981 offset += 2;
982 }
983 }
984 _ => {
985 offset += 1;
986 }
987 }
988 }
989 }
990
991 Ok(McrawFileInfo {
992 path: path.to_string_lossy().into_owned(),
993 size: data.len() as u64,
994 format_version,
995 frame_count,
996 width,
997 height,
998 fps,
999 has_audio,
1000 audio_sample_rate,
1001 audio_channels,
1002 bit_depth,
1003 bayer_pattern,
1004 camera_metadata,
1005 frame_offsets,
1006 audio_offset,
1007 audio_length,
1008 sensor_width,
1009 sensor_height,
1010 active_offset_x,
1011 active_offset_y,
1012 active_width,
1013 active_height,
1014 white_level: 16383.0,
1015 black_level: 0.0,
1016 black_level_per_channel: [0.0; 4],
1017 black_level_count: 0,
1018 lens_shading_map: None,
1019 dynamic_black_level: None,
1020 dynamic_white_level: None,
1021 first_timestamp: None,
1022 })
1023}
1024
1025fn read_u32_be(data: &[u8], offset: usize) -> u32 {
1027 u32::from_be_bytes([
1028 data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
1029 ])
1030}
1031
1032fn parse_u32_be(data: &[u8], offset: &mut usize) -> Result<u32> {
1033 if *offset + 4 > data.len() {
1034 return Err(anyhow::anyhow!("Unexpected end of data"));
1035 }
1036 let val = u32::from_be_bytes([
1037 data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
1038 ]);
1039 *offset += 4;
1040 Ok(val)
1041}
1042
1043fn parse_f64(data: &[u8], offset: &mut usize) -> Result<f64> {
1044 if *offset + 8 > data.len() {
1045 return Err(anyhow::anyhow!("Unexpected end of data"));
1046 }
1047 let val = f64::from_be_bytes([
1048 data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
1049 data[*offset + 4], data[*offset + 5], data[*offset + 6], data[*offset + 7],
1050 ]);
1051 *offset += 8;
1052 Ok(val)
1053}
1054
1055fn parse_f64_array(data: &[u8], offset: &mut usize, len: usize) -> Vec<f64> {
1056 let mut result = Vec::with_capacity(len);
1057 for _ in 0..len {
1058 if let Ok(v) = parse_f64(data, offset) {
1059 result.push(v);
1060 } else {
1061 break;
1062 }
1063 }
1064 result
1065}
1066
1067fn parse_string(data: &[u8], offset: &mut usize) -> Result<String> {
1068 if *offset + 4 > data.len() {
1069 return Err(anyhow::anyhow!("Unexpected end of data"));
1070 }
1071 let str_len = u32::from_be_bytes([
1072 data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
1073 ]) as usize;
1074 *offset += 4;
1075 if *offset + str_len > data.len() {
1076 return Err(anyhow::anyhow!("String extends beyond data"));
1077 }
1078 let s = std::str::from_utf8(&data[*offset..*offset + str_len])
1079 .map_err(|e| anyhow::anyhow!("Invalid UTF-8 string: {}", e))?;
1080 *offset += str_len;
1081 Ok(s.to_string())
1082}
1083
1084pub fn detect_bit_depth_from_white_level(white_level: f64) -> u16 {
1085 if white_level <= 1024.0 {
1086 10
1087 } else if white_level <= 4096.0 {
1088 12
1089 } else if white_level <= 16384.0 {
1090 14
1091 } else if white_level <= 65536.0 {
1092 16
1093 } else {
1094 12
1095 }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100 use super::*;
1101
1102 #[test]
1103 fn test_bayer_pattern_from_u8() {
1104 assert_eq!(BayerPattern::from_u8(0), BayerPattern::RGGB);
1105 assert_eq!(BayerPattern::from_u8(1), BayerPattern::GRBG);
1106 assert_eq!(BayerPattern::from_u8(2), BayerPattern::GBRG);
1107 assert_eq!(BayerPattern::from_u8(3), BayerPattern::BGGR);
1108 assert_eq!(BayerPattern::from_u8(4), BayerPattern::QuadBayerRGGB);
1109 assert_eq!(BayerPattern::from_u8(5), BayerPattern::QuadBayerGRBG);
1110 assert_eq!(BayerPattern::from_u8(6), BayerPattern::QuadBayerGBRG);
1111 assert_eq!(BayerPattern::from_u8(7), BayerPattern::QuadBayerBGGR);
1112 assert_eq!(BayerPattern::from_u8(99), BayerPattern::RGGB);
1113 }
1114
1115 #[test]
1116 fn test_bayer_pattern_to_u8() {
1117 assert_eq!(BayerPattern::RGGB.to_u8(), 0);
1118 assert_eq!(BayerPattern::GRBG.to_u8(), 1);
1119 assert_eq!(BayerPattern::GBRG.to_u8(), 2);
1120 assert_eq!(BayerPattern::BGGR.to_u8(), 3);
1121 assert_eq!(BayerPattern::QuadBayerRGGB.to_u8(), 4);
1122 assert_eq!(BayerPattern::QuadBayerGRBG.to_u8(), 5);
1123 assert_eq!(BayerPattern::QuadBayerGBRG.to_u8(), 6);
1124 assert_eq!(BayerPattern::QuadBayerBGGR.to_u8(), 7);
1125 }
1126
1127 #[test]
1128 fn test_detect_bit_depth_from_white_level() {
1129 assert_eq!(detect_bit_depth_from_white_level(1023.0), 10);
1130 assert_eq!(detect_bit_depth_from_white_level(1024.0), 10);
1131 assert_eq!(detect_bit_depth_from_white_level(1025.0), 12);
1132 assert_eq!(detect_bit_depth_from_white_level(4095.0), 12);
1133 assert_eq!(detect_bit_depth_from_white_level(4096.0), 12);
1134 assert_eq!(detect_bit_depth_from_white_level(4097.0), 14);
1135 assert_eq!(detect_bit_depth_from_white_level(16383.0), 14);
1136 assert_eq!(detect_bit_depth_from_white_level(16384.0), 14);
1137 assert_eq!(detect_bit_depth_from_white_level(16385.0), 16);
1138 assert_eq!(detect_bit_depth_from_white_level(65535.0), 16);
1139 assert_eq!(detect_bit_depth_from_white_level(65536.0), 16);
1140 assert_eq!(detect_bit_depth_from_white_level(65537.0), 12);
1141 assert_eq!(detect_bit_depth_from_white_level(0.0), 10);
1142 }
1143
1144 #[test]
1145 fn test_parse_header_minimal() {
1146 let mut data = vec![0u8; 36];
1147 data[0..5].copy_from_slice(b"MCRAW");
1148 data[5..9].copy_from_slice(&2u32.to_be_bytes());
1149 data[9..13].copy_from_slice(&10u32.to_be_bytes());
1150 data[13..15].copy_from_slice(&(1920u16).to_be_bytes());
1151 data[15..17].copy_from_slice(&(1080u16).to_be_bytes());
1152 data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
1153 data[25] = 0;
1154
1155 let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
1156 assert_eq!(info.format_version, 2);
1157 assert_eq!(info.frame_count, 10);
1158 assert_eq!(info.width, 1920);
1159 assert_eq!(info.height, 1080);
1160 assert!((info.fps - 30.0).abs() < 0.001);
1161 assert!(!info.has_audio);
1162 }
1163
1164 #[test]
1165 fn test_duration_seconds() {
1166 let mut data = vec![0u8; 36];
1167 data[0..5].copy_from_slice(b"MCRAW");
1168 data[9..13].copy_from_slice(&600u32.to_be_bytes());
1169 data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
1170 let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
1171 assert!((info.duration_seconds() - 20.0).abs() < 0.001);
1172 }
1173
1174 fn make_test_info(w: u16, h: u16) -> McrawFileInfo {
1175 McrawFileInfo {
1176 path: String::new(),
1177 size: 0,
1178 format_version: 2,
1179 frame_count: 0,
1180 width: w,
1181 height: h,
1182 fps: 30.0,
1183 has_audio: false,
1184 audio_sample_rate: 0,
1185 audio_channels: 0,
1186 bit_depth: 0,
1187 bayer_pattern: BayerPattern::RGGB,
1188 camera_metadata: CameraMetadata::default(),
1189 frame_offsets: Vec::new(),
1190 audio_offset: None,
1191 audio_length: None,
1192 sensor_width: 0,
1193 sensor_height: 0,
1194 active_offset_x: 0,
1195 active_offset_y: 0,
1196 active_width: 0,
1197 active_height: 0,
1198 white_level: 16383.0,
1199 black_level: 0.0,
1200 black_level_per_channel: [0.0; 4],
1201 black_level_count: 0,
1202 lens_shading_map: None,
1203 dynamic_black_level: None,
1204 dynamic_white_level: None,
1205 first_timestamp: None,
1206 }
1207 }
1208
1209 #[test]
1210 fn test_resolution_label() {
1211 assert_eq!(make_test_info(1920, 1080).resolution_label(), "1080p");
1212 assert_eq!(make_test_info(2560, 1440).resolution_label(), "1440p");
1213 assert_eq!(make_test_info(3840, 2160).resolution_label(), "4K");
1214 assert_eq!(make_test_info(4096, 2160).resolution_label(), "4K DCI");
1215 assert_eq!(make_test_info(1280, 720).resolution_label(), "Custom");
1216 }
1217
1218 #[test]
1219 fn test_parse_header_with_string_metadata() {
1220 let mut data = vec![0u8; 64];
1221 data[0..5].copy_from_slice(b"MCRAW");
1222 data[5] = 2;
1223 data[9..13].copy_from_slice(&1u32.to_be_bytes());
1224 data[13..15].copy_from_slice(&(1920u16).to_be_bytes());
1225 data[15..17].copy_from_slice(&(1080u16).to_be_bytes());
1226 data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
1227 data[25] = 0;
1228
1229 let camera_model = "TestCamera";
1230 let block_offset = 36;
1231 let str_len = camera_model.len() as u32;
1232 let block_len = 1 + 4 + str_len as u32; data[block_offset..block_offset + 4].copy_from_slice(&(block_len as u32).to_be_bytes());
1234 data[block_offset + 4] = 0x03;
1235 data[block_offset + 5..block_offset + 9].copy_from_slice(&str_len.to_be_bytes());
1236 data[block_offset + 9..block_offset + 9 + camera_model.len()]
1237 .copy_from_slice(camera_model.as_bytes());
1238
1239 let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
1240 assert_eq!(info.camera_metadata.camera_model, Some("TestCamera".to_string()));
1241 }
1242
1243 #[test]
1244 fn test_parse_header_invalid_magic() {
1245 let data = vec![b'X'; 36];
1246 let result = parse_header(&data, std::path::Path::new("test.mcraw"));
1247 assert!(result.is_err());
1248 }
1249
1250 #[test]
1251 fn test_parse_header_too_small() {
1252 let data = vec![0u8; 10];
1253 let result = parse_header(&data, std::path::Path::new("test.mcraw"));
1254 assert!(result.is_err());
1255 }
1256}