1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//! Error codes and details.

use std::{collections::TryReserveError, fmt::Display};

use zip::result::ZipError;

use crate::{validate, SubblockMode};

/// The types of limit that may be exceeded.
///
/// Variants name the field in [`Limits`](crate::file::Limits) that was exceeded.
#[derive(Debug, thiserror::Error)]
pub enum Limit {
    #[error("uncompressed JSON too big")]
    JsonBytes,
    #[error("uncompressed array or image too big")]
    ArrayBytes,
    #[error("image width or height is too large")]
    ImageDim,
}

fn format_corners<T: Display>(corners: &[T; 6]) -> String {
    format!(
        "[{}, {}, {}] to [{}, {}, {}]",
        corners[0], corners[1], corners[2], corners[3], corners[4], corners[5],
    )
}

/// Ways that data, either in a file or being written to one, can be invalid.
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum InvalidData {
    /// Data length does not match the Array.
    #[error("Error: array length {found} does not the declared length {expected}")]
    LengthMismatch { found: u64, expected: u64 },
    /// A size is <= 0.
    #[error("size value {value} is zero or less")]
    SizeZeroOrLess { value: f64 },
    /// A discrete colormap boundary is less than the previous boundary.
    #[error("discrete colormap boundary decreases")]
    BoundaryDecreases,
    /// A segment, triangle, or category index is out of range.
    #[error("index value {value} exceeds the maximum index {maximum}")]
    IndexOutOfRange { value: u64, maximum: u64 },
    /// A block index is out of range.
    #[error("block index {value:?} exceeds the maximum index {maximum:?}")]
    BlockIndexOutOfRange { value: [u32; 3], maximum: [u32; 3] },
    /// A regular sub-block has zero or negative size.
    #[error("sub-block {} has zero or negative size", format_corners(corners))]
    RegularSubblockZeroSize { corners: [u32; 6] },
    /// A regular sub-block extends outside the parent.
    #[error(
        "sub-block {} exceeds the maximum {maximum:?}",
        format_corners(corners)
    )]
    RegularSubblockOutsideParent {
        corners: [u32; 6],
        maximum: [u32; 3],
    },
    /// A regular sub-block doesn't match the octree or full sub-block mode.
    #[error("sub-block {} is invalid for {mode:?} mode", format_corners(corners))]
    RegularSubblockNotInMode {
        corners: [u32; 6],
        mode: SubblockMode,
    },
    /// A free-form sub-block has zero or negative size.
    #[error("sub-block {} has zero or negative size", format_corners(corners))]
    FreeformSubblockZeroSize { corners: [f64; 6] },
    /// A free-form sub-block is outside the [0.0, 1.0] parent range.
    #[error(
        "sub-block {} is outside the valid range of 0.0 to 1.0",
        format_corners(corners)
    )]
    FreeformSubblockOutsideParent { corners: [f64; 6] },
}

/// Errors generated by this crate.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// Used when memory allocation fails.
    #[error("Memory allocation failed")]
    OutOfMemory,
    /// Forward errors from file operations.
    #[error("File IO error: {0}")]
    IoError(std::io::Error),
    /// When the correct file header is not detected.
    #[error("Error: the zip comment does not identify this as an OMF file: '{0}'")]
    NotOmf(String),
    /// When the file version is newer than the library.
    #[error("Version error: the file uses OMF v{0}.{1} but this version library can only read 0.9 and 2.0")]
    NewerVersion(u32, u32),
    /// The file version is pre-release and can't be loaded by release versions.
    #[error("Version error: the file uses pre-release OMF v{0}.{1}-{2} and can't be loaded")]
    PreReleaseVersion(u32, u32, String),
    /// Forwards `serde_json` errors when deserializing.
    #[error("JSON deserialization error: {0}")]
    DeserializationFailed(#[from] serde_json::Error),
    /// Forwards `serde_json` errors when serializing.
    #[error("JSON serialization error: {0}")]
    SerializationFailed(serde_json::Error),
    /// Passes out errors detected during OMF validation.
    #[error("Validation failed")]
    ValidationFailed(#[from] validate::Problems),
    /// When trying to cast `f64` to `f32` for example, as that would lose precision.
    #[error("Error: can't cast from {0} to {1} without losing data")]
    UnsafeCast(&'static str, &'static str),
    /// Writing an image that isn't in PNG or JPEG format.
    #[error("Error: image is not in PNG or JPEG encoding")]
    NotImageData,
    /// Writing an array that isn't in Parquet format.
    #[error("Error: image is not in Parquet encoding")]
    NotParquetData,
    /// Tried to read something that exceeds the provided limits.
    #[error("Error: safety limit exceeded")]
    LimitExceeded(Limit),
    /// Array data errors, when reading or writing.
    #[error("Data error: {0}")]
    InvalidData(Box<InvalidData>),
    /// A data file or index is missing from the zip.
    #[error("Error: missing archive member '{0}'")]
    ZipMemberMissing(String),
    /// Zip read or write failed.
    #[error("Zip error: {0}")]
    ZipError(String),
    /// When a Parquet schema doesn't match.
    #[cfg(feature = "parquet")]
    #[error("{}", mismatch_string(.0, .1))]
    ParquetSchemaMismatch(
        std::sync::Arc<parquet::schema::types::Type>,
        std::sync::Arc<Vec<parquet::schema::types::Type>>,
    ),
    /// Forward errors from array operations.
    #[cfg(feature = "parquet")]
    #[error("{0}")]
    ParquetError(Box<parquet::errors::ParquetError>),
    /// Forward errors from image operations.
    #[cfg(feature = "image")]
    #[error("Image processing error: {0}")]
    ImageError(Box<image::ImageError>),
    /// Errors from OMF1 conversion.
    #[cfg(feature = "omf1")]
    #[error("OMF1 conversion failed: {0}")]
    Omf1Error(#[from] crate::omf1::Omf1Error),
}

impl From<InvalidData> for Error {
    fn from(value: InvalidData) -> Self {
        Self::InvalidData(value.into())
    }
}

impl From<std::io::Error> for Error {
    fn from(error: std::io::Error) -> Self {
        match error.kind() {
            std::io::ErrorKind::OutOfMemory => Error::OutOfMemory,
            std::io::ErrorKind::Other if error.get_ref().is_some_and(|r| r.is::<Error>()) => {
                *error.into_inner().unwrap().downcast().unwrap()
            }
            _ => Error::IoError(error),
        }
    }
}

impl From<TryReserveError> for Error {
    fn from(_: TryReserveError) -> Self {
        Error::OutOfMemory
    }
}

impl From<ZipError> for Error {
    fn from(value: ZipError) -> Self {
        match value {
            ZipError::Io(e) => Self::IoError(e),
            other => Self::ZipError(other.to_string()),
        }
    }
}

#[cfg(feature = "image")]
impl From<image::ImageError> for Error {
    fn from(value: image::ImageError) -> Self {
        use image::error::LimitErrorKind;
        match &value {
            image::ImageError::Limits(err @ image::error::LimitError { .. }) => match err.kind() {
                LimitErrorKind::DimensionError => Error::LimitExceeded(Limit::ImageDim),
                LimitErrorKind::InsufficientMemory => Error::LimitExceeded(Limit::ArrayBytes),
                _ => Error::ImageError(value.into()),
            },
            _ => Error::ImageError(value.into()),
        }
    }
}

#[cfg(feature = "parquet")]
impl From<parquet::errors::ParquetError> for Error {
    fn from(value: parquet::errors::ParquetError) -> Self {
        Self::ParquetError(value.into())
    }
}

#[cfg(feature = "parquet")]
fn mismatch_string(
    found: &parquet::schema::types::Type,
    expected: &[parquet::schema::types::Type],
) -> String {
    use parquet::schema::{printer::print_schema, types::Type};

    fn schema_string(ty: &Type) -> String {
        let mut buf = Vec::new();
        print_schema(&mut buf, ty);
        String::from_utf8_lossy(&buf).trim_end().to_owned()
    }
    let mut out = format!(
        "Parquet schema mismatch, found:\n{found}\n\n{expected_label}:\n",
        found = schema_string(found),
        expected_label = if expected.len() == 1 {
            "Expected"
        } else {
            "Expected one of"
        }
    );
    for ty in expected {
        out.push_str(&schema_string(ty));
        out.push('\n');
    }
    out
}