android_sparse_image/
split.rs

1use crate::{
2    ChunkHeader, ChunkType, FileHeader, CHUNK_HEADER_BYTES_LEN, DEFAULT_BLOCKSIZE,
3    FILE_HEADER_BYTES_LEN,
4};
5use thiserror::Error;
6
7/// A definition of one chunk of a split image; When writing out or downloading to a device the
8/// (chunk) header should be written out first followed by size bytes from the original file from
9/// offset (in bytes) onwards
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct SplitChunk {
12    /// Chunk header
13    pub header: ChunkHeader,
14    /// Offset in the input file for the chunk data
15    pub offset: usize,
16    /// Amount of data to be copied from the input file (in bytes)
17    pub size: usize,
18}
19
20/// A definition of a split sparse image; When writing out or downloading to a device the  (file)
21/// header should be written first followed by each chunk
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct Split {
24    /// Global file header
25    pub header: FileHeader,
26    /// List of data chunks
27    pub chunks: Vec<SplitChunk>,
28}
29
30impl Split {
31    fn from_chunks(chunks: Vec<SplitChunk>, block_size: u32) -> Self {
32        let n_chunks = chunks.len() as u32;
33        let blocks = chunks.iter().map(|c| c.header.chunk_size).sum();
34
35        let header = FileHeader {
36            block_size,
37            blocks,
38            chunks: n_chunks,
39            checksum: 0,
40        };
41
42        Split { header, chunks }
43    }
44
45    /// Total size of the sparse image that would be generated when writing out the split
46    pub fn sparse_size(&self) -> usize {
47        FILE_HEADER_BYTES_LEN
48            + self
49                .chunks
50                .iter()
51                .map(|c| c.header.total_size as usize)
52                .sum::<usize>()
53    }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57struct SplitBuilder {
58    space: u32,
59    block_size: u32,
60    chunks: Vec<SplitChunk>,
61}
62
63impl SplitBuilder {
64    fn new(block_size: u32, mut space: u32, blocks_offset: u32) -> Self {
65        space -= FILE_HEADER_BYTES_LEN as u32;
66        let chunks = if blocks_offset == 0 {
67            vec![]
68        } else {
69            // Seek to the offset first
70            let header = ChunkHeader::new_dontcare(blocks_offset);
71            space -= header.total_size;
72            vec![SplitChunk {
73                header,
74                offset: 0,
75                size: 0,
76            }]
77        };
78        Self {
79            space,
80            block_size,
81            chunks,
82        }
83    }
84
85    fn try_add_chunk(&mut self, chunk: &ChunkHeader, image_offset: usize) -> bool {
86        if self.space > chunk.total_size {
87            let split = SplitChunk {
88                header: chunk.clone(),
89                offset: image_offset,
90                size: chunk.data_size(),
91            };
92            self.chunks.push(split);
93            self.space -= chunk.total_size;
94            true
95        } else {
96            false
97        }
98    }
99
100    /// Add as much raw data as possible, returning the blocks taken up)
101    fn add_raw(&mut self, image_offset: usize, blocks: u32) -> u32 {
102        let left = self.space.saturating_sub(CHUNK_HEADER_BYTES_LEN as u32);
103        let blocks_left = left / self.block_size;
104
105        if blocks_left > 0 {
106            let blocks = blocks.min(blocks_left);
107            let header = ChunkHeader::new_raw(blocks, self.block_size);
108            self.space -= header.total_size;
109
110            self.chunks.push(SplitChunk {
111                size: header.data_size(),
112                offset: image_offset,
113                header,
114            });
115
116            blocks
117        } else {
118            0
119        }
120    }
121
122    fn finish(self) -> Split {
123        Split::from_chunks(self.chunks, self.block_size)
124    }
125}
126
127#[derive(Debug, Error)]
128pub enum SplitError {
129    #[error("Size is too small to fit chunks")]
130    TooSmall,
131}
132
133fn check_minimal_size(size: u32, block_size: u32) -> Result<(), SplitError> {
134    // At the very list the size we split into should be enough to have:
135    // * A file header
136    // * A Chunk header for an initial don't care block
137    // * A Chunk header for a raw block and a single block
138    if size < FILE_HEADER_BYTES_LEN as u32 + 2 * CHUNK_HEADER_BYTES_LEN as u32 + block_size {
139        return Err(SplitError::TooSmall);
140    }
141    Ok(())
142}
143
144/// Split an existing sparse image based on its file header and chunks into multiple splits fitting
145/// into the given `size`
146pub fn split_image(
147    header: &FileHeader,
148    chunks: &[ChunkHeader],
149    size: u32,
150) -> Result<Vec<Split>, SplitError> {
151    check_minimal_size(size, header.block_size)?;
152    let (_, _, builder, mut splits) = chunks.iter().try_fold(
153        (
154            // output offset in blocks
155            0,
156            // Start of the first data area (after initial file and chunk header
157            FILE_HEADER_BYTES_LEN + CHUNK_HEADER_BYTES_LEN,
158            SplitBuilder::new(header.block_size, size, 0),
159            // Splits collector
160            vec![],
161        ),
162        |(block_offset, image_offset, mut builder, mut splits), chunk| {
163            if !builder.try_add_chunk(chunk, image_offset) {
164                if chunk.chunk_type == ChunkType::Raw {
165                    // Try packing in partial chunks
166                    let mut blocks = 0;
167                    loop {
168                        blocks += builder.add_raw(
169                            image_offset + (blocks * header.block_size) as usize,
170                            chunk.chunk_size - blocks,
171                        );
172
173                        if blocks >= chunk.chunk_size {
174                            break;
175                        } else {
176                            splits.push(builder.finish());
177                            builder =
178                                SplitBuilder::new(header.block_size, size, block_offset + blocks);
179                        }
180                    }
181                } else {
182                    splits.push(builder.finish());
183                    builder = SplitBuilder::new(header.block_size, size, block_offset);
184                    if !builder.try_add_chunk(chunk, image_offset) {
185                        return Err(SplitError::TooSmall);
186                    }
187                }
188            }
189            Ok((
190                block_offset + chunk.chunk_size,
191                image_offset + chunk.total_size as usize,
192                builder,
193                splits,
194            ))
195        },
196    )?;
197    splits.push(builder.finish());
198    Ok(splits)
199}
200
201/// Generate a set of splits for a raw image of a given `raw_size` each fitting within `size`; The
202/// raw size is rounded up to multiple of [DEFAULT_BLOCKSIZE] as that's the minimal granularity.
203/// When writing out the android sparse image the data should just be padded as needed as well!
204pub fn split_raw(raw_size: usize, size: u32) -> Result<Vec<Split>, SplitError> {
205    check_minimal_size(size, DEFAULT_BLOCKSIZE)?;
206    let raw_blocks = raw_size.div_ceil(DEFAULT_BLOCKSIZE as usize) as u32;
207
208    let mut block_offset = 0;
209    let mut splits = vec![];
210
211    while raw_blocks > block_offset {
212        let mut builder = SplitBuilder::new(DEFAULT_BLOCKSIZE, size, block_offset);
213        block_offset += builder.add_raw(
214            (block_offset * DEFAULT_BLOCKSIZE) as usize,
215            raw_blocks - block_offset,
216        );
217        splits.push(builder.finish());
218    }
219    Ok(splits)
220}
221
222#[cfg(test)]
223mod test {
224    use super::*;
225
226    #[test]
227    fn split_simple() {
228        let header = FileHeader {
229            block_size: 4096,
230            blocks: 1024,
231            chunks: 2,
232            checksum: 0,
233        };
234        let chunks = [
235            ChunkHeader::new_fill(8),
236            ChunkHeader::new_raw(1024 - 8, 4096),
237        ];
238
239        let split = split_image(&header, &chunks, 1024 * 4096).unwrap();
240        assert_eq!(split.len(), 1);
241        let split = &split[0];
242
243        assert_eq!(split.header, header);
244        assert_eq!(split.chunks.len(), 2);
245        assert_eq!(
246            &split.chunks[0],
247            &SplitChunk {
248                header: chunks[0].clone(),
249                offset: FILE_HEADER_BYTES_LEN + CHUNK_HEADER_BYTES_LEN,
250                size: chunks[0].data_size()
251            }
252        );
253        assert_eq!(
254            &split.chunks[1],
255            &SplitChunk {
256                header: chunks[1].clone(),
257                offset: FILE_HEADER_BYTES_LEN + 2 * CHUNK_HEADER_BYTES_LEN + 4,
258                size: chunks[1].data_size(),
259            }
260        );
261    }
262
263    #[test]
264    fn split_multiple() {
265        let header = FileHeader {
266            block_size: 4096,
267            blocks: 2048,
268            chunks: 2,
269            checksum: 0,
270        };
271        let chunks = [
272            ChunkHeader::new_fill(8),
273            ChunkHeader::new_raw(1024 - 8, 4096),
274            ChunkHeader::new_raw(1024 - 8, 4096),
275            ChunkHeader::new_fill(8),
276        ];
277        let expected = [
278            Split {
279                header: FileHeader {
280                    block_size: 4096,
281                    blocks: 519,
282                    chunks: 2,
283                    checksum: 0,
284                },
285                chunks: vec![
286                    SplitChunk {
287                        header: ChunkHeader::new_fill(8),
288                        offset: FILE_HEADER_BYTES_LEN + CHUNK_HEADER_BYTES_LEN,
289                        size: 4,
290                    },
291                    SplitChunk {
292                        header: ChunkHeader::new_raw(511, 4096),
293                        offset: FILE_HEADER_BYTES_LEN + 2 * CHUNK_HEADER_BYTES_LEN + 4,
294                        size: 511 * 4096,
295                    },
296                ],
297            },
298            Split {
299                header: FileHeader {
300                    block_size: 4096,
301                    blocks: 519 + 511,
302                    chunks: 3,
303                    checksum: 0,
304                },
305                chunks: vec![
306                    SplitChunk {
307                        header: ChunkHeader::new_dontcare(519),
308                        offset: 0,
309                        size: 0,
310                    },
311                    // Finalizing first raw block, 1024 - 519 left: 505
312                    SplitChunk {
313                        header: ChunkHeader::new_raw(505, 4096),
314                        offset: FILE_HEADER_BYTES_LEN + 2 * CHUNK_HEADER_BYTES_LEN + 4 + 511 * 4096,
315                        size: 505 * 4096,
316                    },
317                    // First part of the second raw chunk, 511 - 505 left: 6
318                    SplitChunk {
319                        header: ChunkHeader::new_raw(6, 4096),
320                        offset: FILE_HEADER_BYTES_LEN
321                            + 3 * CHUNK_HEADER_BYTES_LEN
322                            + 4
323                            + 1016 * 4096,
324                        size: 6 * 4096,
325                    },
326                ],
327            },
328            Split {
329                header: FileHeader {
330                    block_size: 4096,
331                    blocks: 519 + 511 + 511,
332                    chunks: 2,
333                    checksum: 0,
334                },
335                chunks: vec![
336                    SplitChunk {
337                        header: ChunkHeader::new_dontcare(519 + 511),
338                        offset: 0,
339                        size: 0,
340                    },
341                    // Second part of the second raw chunk, 6 were in the last chunk
342                    SplitChunk {
343                        header: ChunkHeader::new_raw(511, 4096),
344                        offset: FILE_HEADER_BYTES_LEN
345                            + 3 * CHUNK_HEADER_BYTES_LEN
346                            + 4
347                            + 1016 * 4096
348                            + 6 * 4096,
349                        size: 511 * 4096,
350                    },
351                ],
352            },
353            Split {
354                header: FileHeader {
355                    block_size: 4096,
356                    blocks: 2048,
357                    chunks: 3,
358                    checksum: 0,
359                },
360                chunks: vec![
361                    SplitChunk {
362                        header: ChunkHeader::new_dontcare(519 + 511 + 511),
363                        offset: 0,
364                        size: 0,
365                    },
366                    // Final part of the second raw chunk, 6 + 511 already accounted for, so 499
367                    // left of 1016
368                    SplitChunk {
369                        header: ChunkHeader::new_raw(499, 4096),
370                        offset: FILE_HEADER_BYTES_LEN
371                            + 3 * CHUNK_HEADER_BYTES_LEN
372                            + 4
373                            + 1016 * 4096
374                            + 517 * 4096,
375                        size: 499 * 4096,
376                    },
377                    // Second fill
378                    SplitChunk {
379                        header: ChunkHeader::new_fill(8),
380                        offset: FILE_HEADER_BYTES_LEN
381                            + 4 * CHUNK_HEADER_BYTES_LEN
382                            + 4
383                            + 1016 * 4096
384                            + 1016 * 4096,
385                        size: 4,
386                    },
387                ],
388            },
389        ];
390
391        let splits = split_image(&header, &chunks, 512 * 4096).unwrap();
392        for (i, (split, expected)) in splits.iter().zip(expected.iter()).enumerate() {
393            assert_eq!(split, expected, "split {i} mismatch");
394        }
395        assert_eq!(splits.len(), expected.len());
396    }
397
398    #[test]
399    fn test_split_raw() {
400        let splits = split_raw(8 * DEFAULT_BLOCKSIZE as usize, 3 * DEFAULT_BLOCKSIZE).unwrap();
401        assert_eq!(splits.len(), 4, "Incorrect parts: {splits:?}");
402        for (i, split) in splits.iter().enumerate() {
403            assert_eq!(split.header.block_size, 4096);
404            assert_eq!(split.header.checksum, 0);
405            let raw = if i == 0 {
406                assert_eq!(split.header.chunks, 1);
407                assert_eq!(split.chunks.len(), 1);
408                &split.chunks[0]
409            } else {
410                assert_eq!(split.header.chunks, 2);
411                assert_eq!(split.chunks.len(), 2);
412                assert_eq!(
413                    split.chunks[0],
414                    SplitChunk {
415                        header: ChunkHeader {
416                            chunk_type: ChunkType::DontCare,
417                            chunk_size: 2 * i as u32,
418                            total_size: CHUNK_HEADER_BYTES_LEN as u32
419                        },
420                        offset: 0,
421                        size: 0
422                    },
423                    "chunk {i}"
424                );
425                &split.chunks[1]
426            };
427            assert_eq!(
428                raw,
429                &SplitChunk {
430                    header: ChunkHeader {
431                        chunk_type: ChunkType::Raw,
432                        chunk_size: 2,
433                        total_size: 2 * DEFAULT_BLOCKSIZE + CHUNK_HEADER_BYTES_LEN as u32
434                    },
435                    offset: 2 * i * DEFAULT_BLOCKSIZE as usize,
436                    size: 2 * DEFAULT_BLOCKSIZE as usize
437                },
438                "chunk {i}"
439            );
440        }
441    }
442}