opentitanlib/image/
image.rs

1// Copyright lowRISC contributors (OpenTitan project).
2// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3// SPDX-License-Identifier: Apache-2.0
4
5use std::borrow::Cow;
6use std::collections::HashSet;
7use std::convert::TryInto;
8use std::fs::File;
9use std::io::{Read, Write};
10use std::mem::{align_of, offset_of, size_of};
11use std::path::{Path, PathBuf};
12
13use anyhow::{Result, bail, ensure};
14use sphincsplus::{SphincsPlus, SpxDomain, SpxPublicKey};
15use thiserror::Error;
16use zerocopy::FromBytes;
17
18use crate::crypto::ecdsa::{EcdsaPublicKey, EcdsaRawPublicKey, EcdsaRawSignature};
19use crate::crypto::sha256::Sha256Digest;
20use crate::image::manifest::{
21    CHIP_MANIFEST_VERSION_MAJOR2, CHIP_ROM_EXT_IDENTIFIER, CHIP_ROM_EXT_SIZE_MAX,
22    MANIFEST_EXT_ID_SPX_KEY, MANIFEST_EXT_ID_SPX_SIGNATURE, Manifest, ManifestKind,
23    SigverifySpxSignature,
24};
25use crate::image::manifest_def::{ManifestSigverifyBuffer, ManifestSpec};
26use crate::image::manifest_ext::{ManifestExtEntry, ManifestExtEntrySpec};
27use crate::util::file::{FromReader, ToWriter};
28use crate::util::parse_int::ParseInt;
29
30#[derive(Debug, Error)]
31pub enum ImageError {
32    #[error("Incomplete read: expected to read {0} bytes but read {1} bytes")]
33    IncompleteRead(usize, usize),
34    #[error("Failed to parse image manifest.")]
35    Parse,
36    #[error("Extension data overflows flash image.")]
37    ExtensionOverflow,
38    #[error("Extension table index is out of bounds.")]
39    BadExtensionTableIndex,
40    #[error("Extension ID 0x{0:x} not in manifest table.")]
41    NoExtensionTableEntry(u32),
42    #[error("Extension 0x{0:x} is not aligned to word boundary.")]
43    BadExtensionAlignment(u32),
44    #[error("Invalid placement of signed extension 0x{0:x}.")]
45    MisplacedSignedExtension(u32),
46    #[error("Invalid manifest major version: {0}. ECDSA support requires major version {1}.")]
47    InvalidManifestVersionforEcdsa(u16, u16),
48}
49
50pub enum MainSignatureParams {
51    Ecdsa(EcdsaRawPublicKey, EcdsaRawSignature),
52}
53
54pub struct SpxSignatureParams {
55    key: SpxPublicKey,
56    signature: [u8; 7856],
57}
58
59// Binary image is signed either RSA or ECDSA. SPX+ signature could be added as
60// an extension.
61pub struct SigverifyParams {
62    pub main_sig_params: MainSignatureParams,
63    pub spx_sig_params: Option<SpxSignatureParams>,
64    pub spx_hash_reversal_bug: bool,
65}
66
67impl SigverifyParams {
68    pub fn new(
69        main_sig_params: MainSignatureParams,
70        spx_sig_params: Option<SpxSignatureParams>,
71    ) -> Self {
72        SigverifyParams {
73            main_sig_params,
74            spx_sig_params,
75            spx_hash_reversal_bug: false,
76        }
77    }
78    pub fn with_hash_reversal_bug(mut self, bug: bool) -> Self {
79        self.spx_hash_reversal_bug = bug;
80        self
81    }
82
83    // Verify the main image signature.
84    pub fn verify(&self, digest: &Sha256Digest) -> Result<()> {
85        match &self.main_sig_params {
86            MainSignatureParams::Ecdsa(key, sig) => {
87                let ecdsa_key: EcdsaPublicKey = key.try_into()?;
88                ecdsa_key.verify(digest, sig)?;
89            }
90        }
91        Ok(())
92    }
93
94    // Verify the optional SPX+ signature.
95    pub fn spx_verify(&self, b: &[u8], domain: SpxDomain) -> Result<()> {
96        if let Some(spx) = &self.spx_sig_params {
97            let msg = match domain {
98                SpxDomain::PreHashedSha256 => Cow::from(if self.spx_hash_reversal_bug {
99                    Sha256Digest::hash(b).to_vec_rev()
100                } else {
101                    Sha256Digest::hash(b).to_vec()
102                }),
103                _ => Cow::from(b),
104            };
105            spx.key.verify(domain, &spx.signature, &msg)?;
106        } else {
107            bail!("No SPX signature found");
108        }
109
110        Ok(())
111    }
112}
113
114/// A buffer with the same alignment as `Manifest` for storing image data.
115#[repr(C)]
116#[derive(Debug)]
117pub struct ImageData {
118    pub bytes: [u8; Image::MAX_SIZE],
119    _align: [Manifest; 0],
120}
121
122impl Default for ImageData {
123    fn default() -> Self {
124        ImageData {
125            bytes: [0xFF; Image::MAX_SIZE],
126            _align: [],
127        }
128    }
129}
130
131#[derive(Debug, Default)]
132pub struct Image {
133    // TODO(cfrantz): We should use Box::new_uninit to create this, as the
134    // scheme of allowing Box::default to initialize this creates a copy on
135    // the stack and then copies it into the Box.  Unfortunately, Box::new_uninit
136    // is nightly-only (rust-lang#63291).
137    //
138    // For now, I've increased the thread stack size for opentitanlib_test to
139    // avoid the test overflowing its stack while initializing this item.
140    data: Box<ImageData>,
141    pub size: usize,
142}
143
144#[derive(Debug)]
145pub struct SubImage<'a> {
146    pub kind: ManifestKind,
147    pub offset: usize,
148    pub manifest: &'a Manifest,
149    pub data: &'a [u8],
150}
151
152#[derive(Debug)]
153pub enum ImageChunk {
154    Concat(PathBuf),
155    Offset(PathBuf, usize),
156}
157
158#[derive(Debug, Default)]
159pub struct ImageAssembler {
160    pub size: usize,
161    pub mirrored: bool,
162    pub chunks: Vec<ImageChunk>,
163}
164
165impl FromReader for Image {
166    /// Reads in an `Image`.
167    fn from_reader(mut r: impl Read) -> Result<Self> {
168        let mut image = Image::default();
169        image.size = r.read(&mut image.data.bytes)?;
170        Ok(image)
171    }
172}
173
174impl ToWriter for Image {
175    /// Writes out the `Image`.
176    fn to_writer(&self, w: &mut impl Write) -> Result<()> {
177        w.write_all(&self.data.bytes[..self.size])?;
178        Ok(())
179    }
180}
181
182impl Image {
183    pub const MAX_SIZE: usize = 1024 * 1024;
184
185    /// Perform a sanity check to ensure that the image comes with manifest.
186    ///
187    /// Note that passing the sanity check doesn't guarantee that the manifest is valid.
188    pub fn manifest_sanity_check(&self) -> Result<()> {
189        let manifest = self.borrow_manifest()?;
190        let len = self.data.bytes.len() as u32;
191
192        ensure!(manifest.signed_region_end <= len);
193        ensure!(manifest.length <= len);
194        ensure!(manifest.code_start < len);
195        ensure!(manifest.code_end < len);
196        ensure!(manifest.entry_point < len);
197        ensure!(manifest.extensions.entries.iter().all(|x| x.offset < len));
198
199        // We allow the ROM_EXT to be larger than maximum slot size to support provisioning flows
200        // and future ROM_EXT expansion if needed.
201        if (manifest.identifier == CHIP_ROM_EXT_IDENTIFIER)
202            && (manifest.length > CHIP_ROM_EXT_SIZE_MAX)
203        {
204            log::warn!("ROM_EXT is larger than 64k. Link offsets may need recalculating.");
205        }
206
207        Ok(())
208    }
209
210    // Retrieve the SPX+ signature from the image, if present.
211    fn get_spx_signature(&self) -> Result<Option<SpxSignatureParams>> {
212        let ext_tab = self.borrow_manifest()?.extensions.entries;
213        let key_o = ext_tab
214            .iter()
215            .find(|e| e.identifier == MANIFEST_EXT_ID_SPX_KEY);
216        let sig_o = ext_tab
217            .iter()
218            .find(|e| e.identifier == MANIFEST_EXT_ID_SPX_SIGNATURE);
219
220        match (key_o, sig_o) {
221            (Some(key_e), Some(sig_e)) => {
222                const KEY_SIZE: usize = 32; // SPX+ public key size.
223                const SIG_SIZE: usize = std::mem::size_of::<SigverifySpxSignature>();
224
225                let mut key_bytes = [0u8; KEY_SIZE];
226                let mut signature = [0u8; SIG_SIZE];
227                let k_ofs = (key_e.offset + 8) as usize;
228                let s_ofs = (sig_e.offset + 8) as usize;
229
230                key_bytes.copy_from_slice(&self.data.bytes[k_ofs..k_ofs + KEY_SIZE]);
231                signature.copy_from_slice(&self.data.bytes[s_ofs..s_ofs + SIG_SIZE]);
232
233                let key = SpxPublicKey::from_bytes(SphincsPlus::Sha2128sSimple, &key_bytes)?;
234
235                Ok(Some(SpxSignatureParams { key, signature }))
236            }
237            (_, _) => Ok(None),
238        }
239    }
240
241    pub fn get_sigverify_params_from_manifest(&self) -> Result<SigverifyParams> {
242        let manifest = self.borrow_manifest()?;
243        let manifest_def: ManifestSpec = manifest.try_into()?;
244
245        let spx_sig_params = self.get_spx_signature()?;
246
247        let pub_key = manifest_def
248            .pub_key()
249            .ok_or(ImageError::Parse)?
250            .to_le_bytes();
251
252        let signature = manifest_def
253            .signature()
254            .ok_or(ImageError::Parse)?
255            .to_le_bytes();
256
257        let ecdsa_pub_key = EcdsaRawPublicKey::read(&mut std::io::Cursor::new(pub_key))?;
258        let ecdsa_sig = EcdsaRawSignature::read(&mut std::io::Cursor::new(signature))?;
259
260        Ok(SigverifyParams::new(
261            MainSignatureParams::Ecdsa(ecdsa_pub_key, ecdsa_sig),
262            spx_sig_params,
263        ))
264    }
265
266    /// Overwrites all fields in the image's manifest that are defined in `other`.
267    pub fn overwrite_manifest(&mut self, other: ManifestSpec) -> Result<()> {
268        let manifest = self.borrow_manifest_mut()?;
269        let mut manifest_def: ManifestSpec = (&*manifest).try_into()?;
270        manifest_def.overwrite_fields(other);
271        *manifest = manifest_def.try_into()?;
272        Ok(())
273    }
274
275    /// Adds an extension to the signed region of this `Image`.
276    ///
277    /// This will take all the signed extensions in `spec` and append them to the image.
278    /// This should be called before adding any unsigned extensions to ensure all extensions that
279    /// are a part of the signature exist within the contiguous signed region of the image.
280    pub fn add_signed_manifest_extensions(&mut self, spec: &[ManifestExtEntrySpec]) -> Result<()> {
281        spec.iter()
282            .filter(|entry_spec| entry_spec.is_signed())
283            .try_for_each(|entry_spec| {
284                self.add_manifest_extension(ManifestExtEntry::from_spec(entry_spec)?)
285            })
286    }
287
288    /// Adds an extension to the unsigned region of this `Image`.
289    ///
290    /// This will take all the unsigned extensions in `spec` and append them to the image.
291    /// This should only be called once all signed extensions have been added.
292    pub fn add_unsigned_manifest_extensions(
293        &mut self,
294        spec: &[ManifestExtEntrySpec],
295    ) -> Result<()> {
296        spec.iter()
297            .filter(|entry_spec| !entry_spec.is_signed())
298            .try_for_each(|entry_spec| {
299                self.add_manifest_extension(ManifestExtEntry::from_spec(entry_spec)?)
300            })
301    }
302
303    /// Adds an extension to the end of this `Image`.
304    pub fn add_manifest_extension(&mut self, entry: ManifestExtEntry) -> Result<()> {
305        let manifest = self.borrow_manifest()?;
306
307        // A copy of the extension table since we can't borrow as mutable.
308        let mut ext_table = manifest.extensions.entries;
309
310        let entry_id = entry.header().identifier;
311
312        // Update the offset in the extension table.
313        let ext_table_entry = ext_table
314            .iter_mut()
315            .find(|e| e.identifier == entry_id)
316            .ok_or(ImageError::NoExtensionTableEntry(entry_id))?;
317
318        // If the extension already exists, overwrite it, else append it to the end of the
319        // image.
320        let offset = if ext_table_entry.offset != 0 {
321            ext_table_entry.offset
322        } else {
323            ensure!(
324                self.size.is_multiple_of(align_of::<u32>()),
325                ImageError::BadExtensionAlignment(entry_id)
326            );
327            self.size.try_into()?
328        };
329        ext_table_entry.offset = offset;
330
331        // Write the extension to the end of the image.
332        let ext_bytes = entry.to_vec();
333        let end_index = offset
334            .checked_add(ext_bytes.len().try_into()?)
335            .ok_or(ImageError::ExtensionOverflow)?;
336        let extension_slice = self
337            .data
338            .bytes
339            .get_mut(offset as usize..end_index as usize)
340            .ok_or(ImageError::ExtensionOverflow)?;
341        extension_slice.copy_from_slice(ext_bytes.as_slice());
342        self.size = std::cmp::max(end_index as usize, self.size);
343
344        let manifest = self.borrow_manifest_mut()?;
345        manifest.extensions.entries = ext_table;
346
347        Ok(())
348    }
349
350    /// Allocates space for a manifest extension and sets the offset in the extension table.
351    ///
352    /// This function is similar to `add_manifest_extension`, but doesn't populate any data for the
353    /// extension. This is necessary to properly set the `length` field of the manifest and the
354    /// `offset` field of the manifest extension entry for signing.
355    pub fn allocate_manifest_extension(&mut self, id: u32, len: usize) -> Result<()> {
356        let offset = self.size as u32;
357        self.borrow_manifest_mut()?
358            .extensions
359            .entries
360            .iter_mut()
361            .find(|e| e.identifier == id)
362            .ok_or(ImageError::NoExtensionTableEntry(id))?
363            .offset = offset;
364
365        self.size = self
366            .size
367            .checked_add(len)
368            .ok_or(ImageError::ExtensionOverflow)?;
369
370        Ok(())
371    }
372
373    /// Clears any entry in the manifest extension table that doesn't have an offset.
374    ///
375    /// This allows a manifest definition with a populated extension table to be used even when
376    /// extensions aren't provided.
377    pub fn drop_null_extensions(&mut self) -> Result<()> {
378        let manifest = self.borrow_manifest_mut()?;
379
380        manifest.extensions.entries.iter_mut().for_each(|e| {
381            if e.offset == 0 {
382                e.identifier = 0;
383            }
384        });
385
386        Ok(())
387    }
388
389    /// Updates the signature field in the `Manifest` with the provided ECDSA signature.
390    pub fn update_ecdsa_signature(&mut self, signature: EcdsaRawSignature) -> Result<()> {
391        let manifest = self.borrow_manifest_mut()?;
392
393        // TODO(moidx): Remove check once we have migrated away from RSA keys
394        // and start using a key type field in the manifest.
395        //
396        // Note(cfrantz): I have disabled this error return because we want to test
397        // manifests with bad version numbers.  I've replaced the error return with
398        // a log message.  As stated by moidx@, we'll remove this once we've
399        // completely migrated away from RSA keys.
400        // ensure!(
401        //     manifest.manifest_version.major == CHIP_MANIFEST_VERSION_MAJOR2,
402        //     ImageError::InvalidManifestVersionforEcdsa(
403        //         manifest.manifest_version.major,
404        //         CHIP_MANIFEST_VERSION_MAJOR2
405        //     )
406        // );
407        if manifest.manifest_version.major != CHIP_MANIFEST_VERSION_MAJOR2 {
408            log::error!(
409                "Invalid manifest version for ECDSA: {:?}",
410                manifest.manifest_version
411            );
412        }
413
414        let mut manifest_def: ManifestSpec = (&*manifest).try_into()?;
415
416        let signature_bytes = signature
417            .r
418            .iter()
419            .chain(signature.s.iter())
420            .copied()
421            .collect::<Vec<u8>>();
422
423        manifest_def.update_signature(ManifestSigverifyBuffer::from_le_bytes(signature_bytes)?);
424        *manifest = manifest_def.try_into()?;
425        Ok(())
426    }
427
428    /// Updates the pub_key field in the `Manifest` with the ECDSA public key.
429    pub fn update_ecdsa_public_key(&mut self, ecdsa_public_key: EcdsaRawPublicKey) -> Result<()> {
430        let manifest = self.borrow_manifest_mut()?;
431        let mut manifest_def: ManifestSpec = (&*manifest).try_into()?;
432
433        let key_bytes = ecdsa_public_key
434            .x
435            .iter()
436            .chain(ecdsa_public_key.y.iter())
437            .copied()
438            .collect::<Vec<u8>>();
439
440        manifest_def.update_pub_key(ManifestSigverifyBuffer::from_le_bytes(key_bytes)?);
441        *manifest = manifest_def.try_into()?;
442        Ok(())
443    }
444
445    pub fn subimages(&self) -> Result<Vec<SubImage<'_>>> {
446        let mut result = Vec::new();
447        let mut offset = 0;
448        while offset < self.size {
449            let m = &self.data.bytes[offset..offset + size_of::<Manifest>()];
450            let manifest = Manifest::ref_from_bytes(m).map_err(|_| ImageError::Parse)?;
451            let kind = ManifestKind(manifest.identifier);
452            let mut size = 1;
453            if kind.is_known_value() {
454                size = manifest.length as usize;
455                result.push(SubImage {
456                    kind,
457                    offset,
458                    manifest,
459                    data: &self.data.bytes[offset..offset + size],
460                });
461            }
462            // Round up to next 64K offset.
463            offset += (size + 65535) & !65535;
464        }
465        Ok(result)
466    }
467
468    pub fn bytes(&self) -> &[u8] {
469        &self.data.bytes[..self.size]
470    }
471
472    pub fn borrow_manifest(&self) -> Result<&Manifest> {
473        let manifest_slice = &self.data.bytes[0..size_of::<Manifest>()];
474        let manifest = Manifest::ref_from_bytes(manifest_slice).map_err(|_| ImageError::Parse)?;
475        Ok(manifest)
476    }
477
478    pub fn borrow_manifest_mut(&mut self) -> Result<&mut Manifest> {
479        let manifest_slice = &mut self.data.bytes[0..size_of::<Manifest>()];
480        let manifest = Manifest::mut_from_bytes(manifest_slice).map_err(|_| ImageError::Parse)?;
481        Ok(manifest)
482    }
483
484    /// Updates the length field in the `Manifest`.
485    pub fn update_length(&mut self) -> Result<usize> {
486        self.borrow_manifest_mut()?.length = self.size as u32;
487        Ok(self.size)
488    }
489
490    /// Sets the `signed_region_end` field to the end of the last signed extension.
491    ///
492    /// The end of the signed region is computed as the offset of the first extension that is not
493    /// in `signed_ids` or the size of the image if there is no such extension.
494    pub fn update_signed_region(&mut self, signed_ids: &HashSet<u32>) -> Result<()> {
495        let image_size = self.size as u32;
496        let mut first_unsigned_ext = 0u32;
497        let manifest = self.borrow_manifest_mut()?;
498
499        let mut ext_table = manifest.extensions.entries;
500        ext_table.sort_by(|a, b| a.offset.cmp(&b.offset));
501        for e in ext_table {
502            // Ignore any extensions that haven't been added to the image.
503            if e.offset == 0 {
504                continue;
505            }
506            if signed_ids.contains(&e.identifier) {
507                // Since extensions are sorted by offset, if we've already seen an unsigned
508                // extension then the signed region is discontinuous.
509                ensure!(
510                    first_unsigned_ext == 0,
511                    ImageError::MisplacedSignedExtension(e.identifier)
512                );
513            } else if first_unsigned_ext == 0 {
514                first_unsigned_ext = e.offset;
515            }
516        }
517
518        manifest.signed_region_end = if first_unsigned_ext == 0 {
519            image_size
520        } else {
521            first_unsigned_ext
522        };
523
524        Ok(())
525    }
526
527    /// Operates on the signed region of the image.
528    pub fn map_signed_region<F, R>(&self, f: F) -> Result<R>
529    where
530        F: FnOnce(&[u8]) -> R,
531    {
532        Ok(f(&self.data.bytes[offset_of!(Manifest, usage_constraints)
533            ..self.borrow_manifest()?.signed_region_end as usize]))
534    }
535
536    /// Compute the SHA256 digest for the signed portion of the `Image`.
537    pub fn compute_digest(&self) -> Result<Sha256Digest> {
538        self.map_signed_region(|v| Sha256Digest::hash(v))
539    }
540}
541
542impl SubImage<'_> {
543    /// Operates on the signed region of the image.
544    pub fn map_signed_region<F, R>(&self, f: F) -> Result<R>
545    where
546        F: FnOnce(&[u8]) -> R,
547    {
548        Ok(f(&self.data[offset_of!(Manifest, usage_constraints)
549            ..self.manifest.signed_region_end as usize]))
550    }
551
552    /// Compute the SHA256 digest for the signed portion of the `Image`.
553    pub fn compute_digest(&self) -> Result<Sha256Digest> {
554        self.map_signed_region(|v| Sha256Digest::hash(v))
555    }
556}
557
558impl ImageAssembler {
559    /// Creates an `ImageAssembler` with a given `size` and mirroring parameters.
560    pub fn with_params(size: usize, mirrored: bool) -> Self {
561        ImageAssembler {
562            size,
563            mirrored,
564            ..Default::default()
565        }
566    }
567
568    /// Creates an `ImageAssembler` with default parameters for OpenTitan: a 1MiB image which is mirrored.
569    pub fn new() -> Self {
570        Self::with_params(0x100000, true)
571    }
572
573    /// Parse a list of strings into chunks to be assembled.
574    /// Each string may be a filename or a filename@offset describing where in the assembled image the contents of the file should appear.
575    /// The offset is an integer expressed in any of the bases accepted by [`ParseInt`].
576    pub fn parse(&mut self, chunks: &[impl AsRef<str>]) -> Result<()> {
577        for chunk in chunks {
578            if let Some((file, offset)) = chunk.as_ref().split_once('@') {
579                self.chunks.push(ImageChunk::Offset(
580                    PathBuf::from(file),
581                    usize::from_str(offset)?,
582                ));
583            } else {
584                self.chunks
585                    .push(ImageChunk::Concat(PathBuf::from(chunk.as_ref())));
586            }
587        }
588        Ok(())
589    }
590
591    // Read a file into a buffer.  Ensure the entire file is read.
592    fn read(path: &Path, buf: &mut [u8]) -> Result<usize> {
593        let mut file = File::open(path)?;
594        let len = file.metadata()?.len() as usize;
595        ensure!(len <= buf.len(), ImageError::IncompleteRead(len, buf.len()));
596        if buf[..len].iter().any(|&c| c != 0xff) {
597            bail!("{path:?} Overlapped during image assembly");
598        }
599        let n = file.read(buf)?;
600        ensure!(len == n, ImageError::IncompleteRead(len, n));
601        Ok(n)
602    }
603
604    /// Assemble the image according to the parameters and parsed chunk specifications.
605    pub fn assemble(&self) -> Result<Vec<u8>> {
606        let size = if self.mirrored {
607            self.size / 2
608        } else {
609            self.size
610        };
611        let mut image = vec![0xff; size];
612        let mut pos = 0;
613        for chunk in &self.chunks {
614            match chunk {
615                ImageChunk::Concat(path) => {
616                    let n = Self::read(path, &mut image[pos..])?;
617                    pos += n;
618                }
619                ImageChunk::Offset(path, offset) => {
620                    let n = Self::read(path, &mut image[*offset..])?;
621                    pos = offset + n;
622                }
623            }
624        }
625        if self.mirrored {
626            image.extend_from_within(..size);
627        }
628        Ok(image)
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635    use crate::util::testdata;
636
637    #[test]
638    fn test_assemble_concat() -> Result<()> {
639        // Test image assembly by concatenation.
640        let mut image = ImageAssembler::with_params(16, false);
641        image.parse(&[
642            testdata("image/hello.txt").to_str().unwrap(),
643            testdata("image/world.txt").to_str().unwrap(),
644        ])?;
645        let data = image.assemble()?;
646        assert_eq!(data, b"HelloWorld\xff\xff\xff\xff\xff\xff");
647        Ok(())
648    }
649
650    #[test]
651    fn test_assemble_offset() -> Result<()> {
652        // Test image assembly by explicit offsets.
653        let mut image = ImageAssembler::with_params(16, false);
654        image.parse(&[
655            testdata("image/hello.txt@0").to_str().unwrap(),
656            testdata("image/world.txt@0x8").to_str().unwrap(),
657        ])?;
658        let data = image.assemble()?;
659        assert_eq!(data, b"Hello\xff\xff\xffWorld\xff\xff\xff");
660        Ok(())
661    }
662
663    #[test]
664    fn test_assemble_mirrored() -> Result<()> {
665        // Test image assembly with mirroring.
666        let mut image = ImageAssembler::with_params(20, true);
667        image.parse(&[
668            testdata("image/hello.txt").to_str().unwrap(),
669            testdata("image/world.txt").to_str().unwrap(),
670        ])?;
671        let data = image.assemble()?;
672        assert_eq!(data, b"HelloWorldHelloWorld");
673        Ok(())
674    }
675
676    #[test]
677    fn test_assemble_mirrored_offset_error() -> Result<()> {
678        // Test image assembly where one of the source files isn't read completely.
679        let mut image = ImageAssembler::with_params(16, true);
680        image.parse(&[
681            testdata("image/hello.txt@0").to_str().unwrap(),
682            testdata("image/world.txt@0x5").to_str().unwrap(),
683        ])?;
684        let err = image.assemble().unwrap_err();
685        assert_eq!(
686            err.to_string(),
687            "Incomplete read: expected to read 5 bytes but read 3 bytes"
688        );
689        Ok(())
690    }
691
692    #[test]
693    fn test_load_image() {
694        // Read and write back image.
695        let image = Image::read_from_file(&testdata("image/test_image.bin")).unwrap();
696        image
697            .write_to_file(&testdata("image/test_image_out.bin"))
698            .unwrap();
699
700        // Ensure the result is identical to the original.
701        let (mut orig_bytes, mut res_bytes) = (Vec::<u8>::new(), Vec::<u8>::new());
702        File::open(testdata("image/test_image.bin"))
703            .unwrap()
704            .read_to_end(&mut orig_bytes)
705            .unwrap();
706        File::open(testdata("image/test_image_out.bin"))
707            .unwrap()
708            .read_to_end(&mut res_bytes)
709            .unwrap();
710        assert_eq!(orig_bytes, res_bytes);
711    }
712}