use crate::{DecodeContext, Error, Result};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum DataUnit {
Payload,
Strip(u32),
Tile(u32),
Plane(u32),
}
impl DataUnit {
pub fn index(&self) -> Option<u32> {
match *self {
DataUnit::Payload => None,
DataUnit::Strip(i) | DataUnit::Tile(i) | DataUnit::Plane(i) => Some(i),
}
}
pub fn kind(&self) -> &'static str {
match self {
DataUnit::Payload => "payload",
DataUnit::Strip(_) => "strip",
DataUnit::Tile(_) => "tile",
DataUnit::Plane(_) => "plane",
}
}
}
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.index() {
Some(i) => write!(f, "{} {i}", self.kind()),
None => f.write_str("image data"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Truncation {
pub unit: DataUnit,
pub offset: u64,
pub expected: u64,
pub available: u64,
pub file_len: u64,
}
impl Truncation {
pub fn new(unit: DataUnit, offset: u64, expected: u64, file_len: u64) -> Self {
let available = file_len.saturating_sub(offset).min(expected);
Truncation { unit, offset, expected, available, file_len }
}
pub fn missing(&self) -> u64 {
self.expected.saturating_sub(self.available)
}
pub fn starts_beyond_eof(&self) -> bool {
self.offset >= self.file_len
}
pub fn range_overflows(&self) -> bool {
self.offset.checked_add(self.expected).is_none_or(|e| usize::try_from(e).is_err())
}
}
impl fmt::Display for Truncation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let unit = match self.unit {
DataUnit::Payload => String::new(),
u => format!("{u} "),
};
if self.range_overflows() {
write!(
f,
"truncated image data: {unit}range of {} bytes at offset {} overflows (file is {} bytes)",
self.expected, self.offset, self.file_len
)
} else if self.starts_beyond_eof() {
write!(
f,
"truncated image data: {unit}expected {} bytes at offset {}, which is beyond the end of the file ({} bytes)",
self.expected, self.offset, self.file_len
)
} else {
write!(
f,
"truncated image data: {unit}expected {} bytes at offset {}, but only {} bytes remain before EOF",
self.expected, self.offset, self.available
)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum CorruptReason {
Truncated(Truncation),
InvalidUnit {
unit: DataUnit,
message: String,
},
PreviewFallback {
message: String,
},
}
impl CorruptReason {
pub fn unit(&self) -> DataUnit {
match self {
CorruptReason::Truncated(t) => t.unit,
CorruptReason::InvalidUnit { unit, .. } => *unit,
CorruptReason::PreviewFallback { .. } => DataUnit::Payload,
}
}
pub fn kind(&self) -> &'static str {
match self {
CorruptReason::Truncated(_) => "truncated",
CorruptReason::InvalidUnit { .. } => "invalid-unit",
CorruptReason::PreviewFallback { .. } => "preview-fallback",
}
}
pub fn truncation(&self) -> Option<&Truncation> {
match self {
CorruptReason::Truncated(t) => Some(t),
_ => None,
}
}
}
impl fmt::Display for CorruptReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CorruptReason::Truncated(t) => t.fmt(f),
CorruptReason::InvalidUnit { unit, message } => write!(f, "corrupt {unit}: {message}"),
CorruptReason::PreviewFallback { message } => write!(f, "sensor data not decoded: {message}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct DecodeWarning {
pub reason: CorruptReason,
pub samples_filled: u64,
}
impl DecodeWarning {
pub fn new(reason: CorruptReason, samples_filled: u64) -> Self {
DecodeWarning { reason, samples_filled }
}
pub fn kind(&self) -> &'static str {
self.reason.kind()
}
}
impl fmt::Display for DecodeWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.reason {
CorruptReason::Truncated(t) => write!(
f,
"recovered truncated {}: {} / {} bytes available; {} missing samples were zero-filled",
t.unit, t.available, t.expected, self.samples_filled
),
CorruptReason::InvalidUnit { unit, message } => {
write!(f, "recovered corrupt {unit} ({message}); its {} samples were zero-filled", self.samples_filled)
}
CorruptReason::PreviewFallback { message } => {
write!(f, "sensor data not decoded ({message}); the frame holds the embedded full-size preview")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RowGeometry {
pub rows: usize,
pub stride: usize,
pub row_bytes: usize,
pub samples: usize,
}
impl RowGeometry {
pub fn packed(rows: usize, samples: usize, bits: u32, stride: usize) -> Self {
let row_bytes = (samples.saturating_mul(bits as usize)).div_ceil(8);
RowGeometry { rows, stride: stride.max(row_bytes), row_bytes, samples }
}
pub fn tight(rows: usize, samples: usize, bits: u32) -> Self {
let row_bytes = (samples.saturating_mul(bits as usize)).div_ceil(8);
RowGeometry { rows, stride: row_bytes, row_bytes, samples }
}
pub fn needed(&self) -> usize {
if self.rows == 0 {
return 0;
}
(self.rows - 1).saturating_mul(self.stride).saturating_add(self.row_bytes)
}
pub fn total(&self) -> u64 {
self.rows as u64 * self.samples as u64
}
pub fn complete_rows(&self, avail: usize) -> usize {
if avail < self.row_bytes || self.stride == 0 {
return 0;
}
((avail - self.row_bytes) / self.stride + 1).min(self.rows)
}
pub fn complete_samples(&self, avail: usize) -> u64 {
let rows = self.complete_rows(avail);
if rows >= self.rows {
return self.total();
}
let rest = avail.saturating_sub(rows * self.stride).min(self.row_bytes);
let partial = if self.row_bytes == 0 { 0 } else { rest as u64 * self.samples as u64 / self.row_bytes as u64 };
rows as u64 * self.samples as u64 + partial
}
pub fn missing_samples(&self, avail: usize) -> u64 {
self.total() - self.complete_samples(avail)
}
}
pub fn check_range(file_len: usize, unit: DataUnit, offset: usize, needed: usize) -> Result<(), Truncation> {
if needed == 0 {
return Ok(());
}
match offset.checked_add(needed) {
Some(end) if end <= file_len => Ok(()),
_ => Err(Truncation::new(unit, offset as u64, needed as u64, file_len as u64)),
}
}
#[derive(Debug, Clone)]
pub struct SensorBytes<'a> {
pub bytes: &'a [u8],
pub truncation: Option<Truncation>,
}
impl<'a> SensorBytes<'a> {
pub fn is_complete(&self) -> bool {
self.truncation.is_none()
}
}
impl DecodeContext<'_> {
pub fn sensor_bytes<'a>(
&self,
data: &'a [u8],
unit: DataUnit,
offset: usize,
needed: usize,
) -> Result<SensorBytes<'a>> {
match check_range(data.len(), unit, offset, needed) {
Ok(()) => Ok(SensorBytes { bytes: data.get(offset..offset + needed).unwrap_or(&[]), truncation: None }),
Err(t) if self.recover => {
let start = offset.min(data.len());
let end = start.saturating_add(needed).min(data.len());
Ok(SensorBytes { bytes: &data[start..end], truncation: Some(t) })
}
Err(t) => Err(Error::truncated(t)),
}
}
pub fn sensor_rows<'a>(
&self,
data: &'a [u8],
unit: DataUnit,
offset: usize,
geometry: RowGeometry,
) -> Result<&'a [u8]> {
let s = self.sensor_bytes(data, unit, offset, geometry.needed())?;
if let Some(t) = s.truncation {
let filled = geometry.missing_samples(s.bytes.len());
self.recovered(CorruptReason::Truncated(t), filled);
}
Ok(s.bytes)
}
pub fn truncated(&self, t: Truncation, samples_filled: u64) -> Result<()> {
if self.recover {
self.recovered(CorruptReason::Truncated(t), samples_filled);
Ok(())
} else {
Err(Error::truncated(t))
}
}
pub fn recovered(&self, reason: CorruptReason, samples_filled: u64) {
self.warnings.lock().unwrap_or_else(|p| p.into_inner()).push(DecodeWarning::new(reason, samples_filled));
}
pub fn take_warnings(&self) -> Vec<DecodeWarning> {
let mut w = std::mem::take(&mut *self.warnings.lock().unwrap_or_else(|p| p.into_inner()));
w.sort_by_key(|w| {
let u = w.reason.unit();
(u.kind(), u.index())
});
w
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Profiler;
#[test]
fn messages() {
let t = Truncation::new(DataUnit::Strip(17), 24_576_000, 131_072, 24_576_000 + 48_291);
assert_eq!(t.available, 48_291);
assert_eq!(t.missing(), 131_072 - 48_291);
assert_eq!(
t.to_string(),
"truncated image data: strip 17 expected 131072 bytes at offset 24576000, but only 48291 bytes remain before EOF"
);
let e = Error::truncated(t.clone());
assert_eq!(e.kind(), "CorruptFile");
assert_eq!(e.truncation(), Some(&t));
assert!(e.to_string().starts_with("corrupt file: truncated image data: strip 17"));
let w = DecodeWarning::new(CorruptReason::Truncated(t), 10);
assert_eq!(
w.to_string(),
"recovered truncated strip 17: 48291 / 131072 bytes available; 10 missing samples were zero-filled"
);
let beyond = Truncation::new(DataUnit::Tile(2), 1000, 10, 500);
assert!(beyond.starts_beyond_eof());
assert_eq!(beyond.available, 0);
assert!(beyond.to_string().contains("beyond the end of the file"));
assert!(Truncation::new(DataUnit::Payload, u64::MAX - 1, 10, 5).range_overflows());
}
#[test]
fn geometry() {
let g = RowGeometry::packed(3, 4, 12, 8);
assert_eq!(g.needed(), 22);
assert_eq!(g.complete_rows(22), 3);
assert_eq!(g.complete_rows(21), 2);
assert_eq!(g.complete_samples(21), 8 + 3);
assert_eq!(g.missing_samples(0), 12);
assert_eq!(g.missing_samples(100), 0);
}
#[test]
fn policies() {
let p = Profiler::new();
let data = [0u8; 10];
let mut ctx = DecodeContext::new(&p);
assert!(ctx.recover, "recover is the default");
ctx.recover = false;
assert_eq!(ctx.sensor_bytes(&data, DataUnit::Payload, 2, 8).unwrap().bytes.len(), 8);
let e = ctx.sensor_bytes(&data, DataUnit::Strip(1), 4, 8).unwrap_err();
assert_eq!(e.truncation().unwrap().available, 6);
assert!(ctx.sensor_bytes(&data, DataUnit::Strip(1), usize::MAX, 8).is_err());
assert!(ctx.sensor_bytes(&data, DataUnit::Strip(1), 100, 0).unwrap().bytes.is_empty());
ctx.recover = true;
let s = ctx.sensor_bytes(&data, DataUnit::Strip(1), 4, 8).unwrap();
assert_eq!(s.bytes.len(), 6);
assert!(!s.is_complete());
let s = ctx.sensor_bytes(&data, DataUnit::Strip(2), usize::MAX, 8).unwrap();
assert!(s.bytes.is_empty());
assert_eq!(ctx.sensor_rows(&data, DataUnit::Tile(0), 4, RowGeometry::tight(2, 4, 8)).unwrap().len(), 6);
let w = ctx.take_warnings();
assert_eq!(w.len(), 1);
assert_eq!(w[0].samples_filled, 2);
}
}