zencodec 0.1.14

Shared traits and types for zen* image codecs
Documentation
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! Per-job security policy flags for decode and encode operations.
//!
//! These control what a codec is allowed to do on a given job.
//! All fields default to `None`, meaning the codec uses its own default.
//! `Some(true)` explicitly allows; `Some(false)` explicitly denies.
//!
//! # Named levels
//!
//! - [`DecodePolicy::none()`] / [`EncodePolicy::none()`] — all defaults
//! - [`DecodePolicy::strict()`] — minimal attack surface (no metadata, no progressive, no animation)
//! - [`DecodePolicy::permissive()`] — allow everything
//!
//! Individual flags can be overridden after constructing a named level.

/// Decode security policy.
///
/// Controls what features a decoder is permitted to use when processing
/// untrusted input. Codecs check these flags and skip or reject
/// accordingly; unrecognized flags are ignored.
///
/// # Example
///
/// ```
/// use zencodec::decode::DecodePolicy;
///
/// // Start strict, then allow ICC (needed for color management)
/// let policy = DecodePolicy::strict().with_allow_icc(true);
/// assert_eq!(policy.allow_icc, Some(true));
/// assert_eq!(policy.allow_exif, Some(false));
/// ```
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct DecodePolicy {
    /// Extract ICC color profiles. When `Some(false)`, the decoder
    /// skips ICC parsing and returns no profile in [`ImageInfo`](crate::ImageInfo).
    pub allow_icc: Option<bool>,
    /// Extract EXIF metadata.
    pub allow_exif: Option<bool>,
    /// Extract XMP metadata.
    pub allow_xmp: Option<bool>,
    /// Allow progressive / interlaced images.
    /// When `Some(false)`, the decoder rejects progressive input.
    pub allow_progressive: Option<bool>,
    /// Allow multi-frame / animated images.
    /// When `Some(false)`, only the first frame is decoded.
    pub allow_animation: Option<bool>,
    /// Accept truncated or partially corrupt input.
    /// When `Some(true)`, the decoder returns whatever it decoded so far.
    pub allow_truncated: Option<bool>,
    /// Strict spec compliance.
    /// When `Some(true)`, reject non-conformant inputs that would
    /// otherwise be accepted with workarounds.
    pub strict: Option<bool>,
}

// All Option<bool>, no pointers — same size on all platforms.
const _: () = assert!(core::mem::size_of::<DecodePolicy>() == 7);

impl DecodePolicy {
    /// No preferences — codec uses its own defaults.
    pub const fn none() -> Self {
        Self {
            allow_icc: None,
            allow_exif: None,
            allow_xmp: None,
            allow_progressive: None,
            allow_animation: None,
            allow_truncated: None,
            strict: None,
        }
    }

    /// Minimal attack surface: no metadata extraction, no progressive,
    /// no animation, strict parsing.
    pub const fn strict() -> Self {
        Self {
            allow_icc: Some(false),
            allow_exif: Some(false),
            allow_xmp: Some(false),
            allow_progressive: Some(false),
            allow_animation: Some(false),
            allow_truncated: Some(false),
            strict: Some(true),
        }
    }

    /// Allow everything.
    pub const fn permissive() -> Self {
        Self {
            allow_icc: Some(true),
            allow_exif: Some(true),
            allow_xmp: Some(true),
            allow_progressive: Some(true),
            allow_animation: Some(true),
            allow_truncated: Some(true),
            strict: Some(false),
        }
    }

    /// Override ICC profile extraction.
    pub const fn with_allow_icc(mut self, v: bool) -> Self {
        self.allow_icc = Some(v);
        self
    }

    /// Override EXIF extraction.
    pub const fn with_allow_exif(mut self, v: bool) -> Self {
        self.allow_exif = Some(v);
        self
    }

    /// Override XMP extraction.
    pub const fn with_allow_xmp(mut self, v: bool) -> Self {
        self.allow_xmp = Some(v);
        self
    }

    /// Override progressive/interlaced support.
    pub const fn with_allow_progressive(mut self, v: bool) -> Self {
        self.allow_progressive = Some(v);
        self
    }

    /// Override animation support.
    pub const fn with_allow_animation(mut self, v: bool) -> Self {
        self.allow_animation = Some(v);
        self
    }

    /// Override truncated input handling.
    pub const fn with_allow_truncated(mut self, v: bool) -> Self {
        self.allow_truncated = Some(v);
        self
    }

    /// Override strict parsing.
    pub const fn with_strict(mut self, v: bool) -> Self {
        self.strict = Some(v);
        self
    }

    /// Resolve a flag: return the explicit value, or fall back to `default`.
    pub const fn resolve_icc(&self, default: bool) -> bool {
        match self.allow_icc {
            Some(v) => v,
            None => default,
        }
    }

    /// Resolve EXIF flag.
    pub const fn resolve_exif(&self, default: bool) -> bool {
        match self.allow_exif {
            Some(v) => v,
            None => default,
        }
    }

    /// Resolve XMP flag.
    pub const fn resolve_xmp(&self, default: bool) -> bool {
        match self.allow_xmp {
            Some(v) => v,
            None => default,
        }
    }

    /// Resolve progressive flag.
    pub const fn resolve_progressive(&self, default: bool) -> bool {
        match self.allow_progressive {
            Some(v) => v,
            None => default,
        }
    }

    /// Resolve animation flag.
    pub const fn resolve_animation(&self, default: bool) -> bool {
        match self.allow_animation {
            Some(v) => v,
            None => default,
        }
    }

    /// Resolve truncated flag.
    pub const fn resolve_truncated(&self, default: bool) -> bool {
        match self.allow_truncated {
            Some(v) => v,
            None => default,
        }
    }

    /// Resolve strict flag.
    pub const fn resolve_strict(&self, default: bool) -> bool {
        match self.strict {
            Some(v) => v,
            None => default,
        }
    }
}

/// Encode metadata policy.
///
/// Controls which metadata an encoder embeds in the output.
/// All fields default to `None`, meaning the codec uses its own default.
/// `Some(true)` explicitly allows embedding; `Some(false)` explicitly strips.
///
/// # Example
///
/// ```
/// use zencodec::encode::EncodePolicy;
///
/// // Strip all metadata from output
/// let policy = EncodePolicy::strip_all();
///
/// // Or fine-grained: keep ICC, strip EXIF/XMP
/// let policy = EncodePolicy::none()
///     .with_embed_icc(true)
///     .with_embed_exif(false)
///     .with_embed_xmp(false);
/// ```
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct EncodePolicy {
    /// Embed ICC color profiles in the output.
    pub embed_icc: Option<bool>,
    /// Embed EXIF metadata in the output.
    pub embed_exif: Option<bool>,
    /// Embed XMP metadata in the output.
    pub embed_xmp: Option<bool>,
}

const _: () = assert!(core::mem::size_of::<EncodePolicy>() == 3);

impl EncodePolicy {
    /// No preferences — codec uses its own defaults.
    pub const fn none() -> Self {
        Self {
            embed_icc: None,
            embed_exif: None,
            embed_xmp: None,
        }
    }

    /// Strip all metadata from output.
    pub const fn strip_all() -> Self {
        Self {
            embed_icc: Some(false),
            embed_exif: Some(false),
            embed_xmp: Some(false),
        }
    }

    /// Preserve all metadata in output.
    pub const fn preserve_all() -> Self {
        Self {
            embed_icc: Some(true),
            embed_exif: Some(true),
            embed_xmp: Some(true),
        }
    }

    /// Override ICC embedding.
    pub const fn with_embed_icc(mut self, v: bool) -> Self {
        self.embed_icc = Some(v);
        self
    }

    /// Override EXIF embedding.
    pub const fn with_embed_exif(mut self, v: bool) -> Self {
        self.embed_exif = Some(v);
        self
    }

    /// Override XMP embedding.
    pub const fn with_embed_xmp(mut self, v: bool) -> Self {
        self.embed_xmp = Some(v);
        self
    }

    /// Resolve ICC embedding flag.
    pub const fn resolve_icc(&self, default: bool) -> bool {
        match self.embed_icc {
            Some(v) => v,
            None => default,
        }
    }

    /// Resolve EXIF embedding flag.
    pub const fn resolve_exif(&self, default: bool) -> bool {
        match self.embed_exif {
            Some(v) => v,
            None => default,
        }
    }

    /// Resolve XMP embedding flag.
    pub const fn resolve_xmp(&self, default: bool) -> bool {
        match self.embed_xmp {
            Some(v) => v,
            None => default,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn decode_none_is_all_none() {
        let p = DecodePolicy::none();
        assert_eq!(p.allow_icc, None);
        assert_eq!(p.allow_exif, None);
        assert_eq!(p.allow_xmp, None);
        assert_eq!(p.allow_progressive, None);
        assert_eq!(p.allow_animation, None);
        assert_eq!(p.allow_truncated, None);
        assert_eq!(p.strict, None);
    }

    #[test]
    fn decode_strict_denies_all() {
        let p = DecodePolicy::strict();
        assert_eq!(p.allow_icc, Some(false));
        assert_eq!(p.allow_exif, Some(false));
        assert_eq!(p.allow_animation, Some(false));
        assert_eq!(p.strict, Some(true));
    }

    #[test]
    fn decode_permissive_allows_all() {
        let p = DecodePolicy::permissive();
        assert_eq!(p.allow_icc, Some(true));
        assert_eq!(p.allow_truncated, Some(true));
        assert_eq!(p.strict, Some(false));
    }

    #[test]
    fn decode_builder_overrides() {
        let p = DecodePolicy::strict().with_allow_icc(true);
        assert_eq!(p.allow_icc, Some(true));
        assert_eq!(p.allow_exif, Some(false)); // still strict
    }

    #[test]
    fn decode_resolve_with_default() {
        let p = DecodePolicy::none();
        assert!(p.resolve_icc(true));
        assert!(!p.resolve_icc(false));

        let p = DecodePolicy::strict();
        assert!(!p.resolve_icc(true)); // explicit false overrides default true
    }

    #[test]
    fn encode_none_is_all_none() {
        let p = EncodePolicy::none();
        assert_eq!(p.embed_icc, None);
        assert_eq!(p.embed_exif, None);
        assert_eq!(p.embed_xmp, None);
    }

    #[test]
    fn encode_strip_all() {
        let p = EncodePolicy::strip_all();
        assert_eq!(p.embed_icc, Some(false));
        assert_eq!(p.embed_exif, Some(false));
        assert_eq!(p.embed_xmp, Some(false));
    }

    #[test]
    fn encode_preserve_all() {
        let p = EncodePolicy::preserve_all();
        assert_eq!(p.embed_icc, Some(true));
        assert_eq!(p.embed_exif, Some(true));
        assert_eq!(p.embed_xmp, Some(true));
    }

    #[test]
    fn encode_builder_overrides() {
        let p = EncodePolicy::strip_all().with_embed_icc(true);
        assert_eq!(p.embed_icc, Some(true));
        assert_eq!(p.embed_exif, Some(false)); // still stripped
    }

    #[test]
    fn encode_resolve_with_default() {
        let p = EncodePolicy::none();
        assert!(p.resolve_icc(true));
        assert!(!p.resolve_icc(false));

        let p = EncodePolicy::strip_all();
        assert!(!p.resolve_icc(true));
    }

    #[test]
    fn static_construction() {
        static _DECODE: DecodePolicy = DecodePolicy::strict().with_allow_icc(true);
        static _ENCODE: EncodePolicy = EncodePolicy::strip_all().with_embed_icc(true);
    }

    #[test]
    fn default_is_none() {
        assert_eq!(DecodePolicy::default(), DecodePolicy::none());
        assert_eq!(EncodePolicy::default(), EncodePolicy::none());
    }
}