1use crate::qcow::Qcow3;
4use aes_gcm::KeyInit;
5use aes_gcm::{aead::Aead, Aes256Gcm, Key, Nonce};
6use anyhow::bail;
7use anyhow::Result;
8use binrw::{BinRead, BinReaderExt, BinWrite};
9use rand::Rng;
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use std::ffi::CStr;
14use std::path::PathBuf;
15use std::{
16 fs::File,
17 io::{BufReader, Cursor, Read, Seek, SeekFrom, Write},
18 path::Path,
19 time::{SystemTime, UNIX_EPOCH},
20};
21use strum::{Display, EnumIter};
22use tracing::{debug, info, trace};
23
24pub mod qcow;
25
26#[derive(
28 BinRead, BinWrite, Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, EnumIter, Display,
29)]
30#[serde(tag = "arch")]
31#[brw(repr(u8))]
32pub enum ImageArch {
33 Amd64,
34 Arm64,
35 I386,
36 Mips,
37 Mips64,
38 S390x,
39}
40
41impl ImageArch {
42 pub fn as_github_string(&self) -> String {
43 match self {
44 ImageArch::Amd64 => "x86_64",
45 ImageArch::Arm64 => todo!(),
46 ImageArch::I386 => todo!(),
47 ImageArch::Mips => todo!(),
48 ImageArch::Mips64 => todo!(),
49 ImageArch::S390x => todo!(),
50 }
51 .to_string()
52 }
53}
54
55impl Default for ImageArch {
56 fn default() -> Self {
57 match std::env::consts::ARCH {
58 "x86" => ImageArch::I386,
59 "x86_64" => ImageArch::Amd64,
60 "aarch64" => ImageArch::Arm64,
61 "mips" => ImageArch::Mips,
62 "mips64" => ImageArch::Mips64,
63 "s390x" => ImageArch::S390x,
64 _ => panic!("Unknown CPU architecture: {}", std::env::consts::ARCH),
65 }
66 }
67}
68
69impl TryFrom<String> for ImageArch {
70 type Error = anyhow::Error;
71 fn try_from(s: String) -> Result<Self> {
72 match s.to_lowercase().as_str() {
73 "amd64" => Ok(ImageArch::Amd64),
74 "x86_64" => Ok(ImageArch::Amd64),
75 "arm64" => Ok(ImageArch::Arm64),
76 "aarch64" => Ok(ImageArch::Arm64),
77 "i386" => Ok(ImageArch::I386),
78 _ => bail!("Unknown architecture: {s}"),
79 }
80 }
81}
82
83#[derive(Debug)]
102pub struct ImageHandle {
103 pub primary_header: PrimaryHeader,
105
106 pub protected_header: Option<ProtectedHeader>,
108
109 pub config: Option<Vec<u8>>,
111
112 pub digest_table: Option<DigestTable>,
114
115 pub directory: Option<Directory>,
117
118 pub path: std::path::PathBuf,
120
121 pub file_size: u64,
123
124 pub id: String,
126}
127
128#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
130#[brw(repr(u8))]
131pub enum ClusterCompressionType {
132 None = 0,
134
135 Zstd = 1,
137}
138
139#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
141#[brw(repr(u8))]
142pub enum ClusterEncryptionType {
143 None = 0,
145
146 Aes256 = 1,
148}
149
150#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
152#[brw(repr(u8))]
153pub enum HeaderEncryptionType {
154 None = 0,
156
157 Aes256 = 1,
159}
160
161#[derive(BinRead, BinWrite, Debug, Eq, PartialEq)]
165#[brw(magic = b"\xc0\x1d\xb0\x01", big)]
166pub struct PrimaryHeader {
167 #[br(assert(version == 1))]
169 pub version: u8,
170
171 pub size: u64,
173
174 pub timestamp: u64,
176
177 pub encryption_type: HeaderEncryptionType,
179
180 pub name: [u8; 64],
182
183 pub arch: ImageArch,
185
186 pub directory_nonce: [u8; 12],
188
189 pub directory_offset: u64,
191
192 pub directory_size: u32,
194
195 pub public: u8,
197
198 pub reserved: [u8; 64],
200}
201
202impl PrimaryHeader {
203 pub fn name(&self) -> String {
204 unsafe { CStr::from_ptr(self.name.as_ptr() as *const std::ffi::c_char) }
205 .to_string_lossy()
206 .into_owned()
207 }
208
209 pub fn is_public(&self) -> bool {
210 return self.public == 1u8;
211 }
212}
213
214#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
216#[brw(big)]
217pub struct ProtectedHeader {
218 pub block_size: u32,
220
221 pub cluster_count: u32,
223
224 pub cluster_compression: ClusterCompressionType,
226
227 pub cluster_encryption: ClusterEncryptionType,
229
230 pub nonce_count: u32,
232
233 #[br(count = nonce_count)]
235 pub nonce_table: Vec<[u8; 12]>,
236
237 pub cluster_key: [u8; 32],
239}
240
241#[derive(BinRead, BinWrite, Debug)]
242#[brw(big)]
243pub struct Directory {
244 pub protected_nonce: [u8; 12],
246
247 pub protected_size: u32,
249
250 pub config_nonce: [u8; 12],
252
253 pub config_offset: u64,
255
256 pub config_size: u32,
258
259 pub digest_table_nonce: [u8; 12],
261
262 pub digest_table_offset: u64,
264
265 pub digest_table_size: u32,
267}
268
269#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
270#[brw(big)]
271pub struct DigestTable {
272 pub digest_count: u32,
274
275 #[br(count = digest_count)]
277 pub digest_table: Vec<DigestTableEntry>,
278}
279
280#[derive(BinRead, BinWrite, Debug, Eq, PartialEq, Clone)]
282#[brw(big)]
283pub struct DigestTableEntry {
284 pub cluster_offset: u64,
286
287 pub block_offset: u64,
289
290 pub digest: [u8; 32],
292}
293
294#[derive(BinRead, BinWrite, Debug)]
297#[brw(big)]
298pub struct Cluster {
299 pub size: u32,
301
302 #[br(count = size)]
304 pub data: Vec<u8>,
305}
306
307fn new_key(password: String) -> Aes256Gcm {
309 Aes256Gcm::new(&Sha256::new().chain_update(password.as_bytes()).finalize())
311}
312
313pub fn compute_id(path: impl AsRef<Path>) -> Result<String> {
315 let mut file = File::open(&path)?;
316 let mut hasher = Sha256::new();
317
318 std::io::copy(&mut file, &mut hasher)?;
319 Ok(hex::encode(hasher.finalize()))
320}
321
322impl ImageHandle {
323 pub fn load(&mut self, password: Option<String>) -> Result<()> {
326 let mut file = File::open(&self.path)?;
327
328 let cipher = new_key(password.unwrap_or("".to_string()));
329
330 file.seek(SeekFrom::Start(self.primary_header.directory_offset))?;
332 let directory: Directory = match self.primary_header.encryption_type {
333 HeaderEncryptionType::None => file.read_be()?,
334 HeaderEncryptionType::Aes256 => {
335 let mut directory_bytes = vec![0u8; self.primary_header.directory_size as usize];
336 file.read_exact(&mut directory_bytes)?;
337
338 let directory_bytes = cipher.decrypt(
339 Nonce::from_slice(&self.primary_header.directory_nonce),
340 directory_bytes.as_ref(),
341 )?;
342 Cursor::new(directory_bytes).read_be()?
343 }
344 };
345
346 file.seek(SeekFrom::Start(0))?;
348
349 let _primary: PrimaryHeader = file.read_be()?;
351 let protected_header: ProtectedHeader = match self.primary_header.encryption_type {
352 HeaderEncryptionType::None => file.read_be()?,
353 HeaderEncryptionType::Aes256 => {
354 let mut protected_header_bytes = vec![0u8; directory.protected_size as usize];
355 file.read_exact(&mut protected_header_bytes)?;
356
357 let protected_header_bytes = cipher.decrypt(
358 Nonce::from_slice(&directory.protected_nonce),
359 protected_header_bytes.as_ref(),
360 )?;
361 Cursor::new(protected_header_bytes).read_be()?
362 }
363 };
364
365 file.seek(SeekFrom::Start(directory.config_offset))?;
367 let mut config_bytes = vec![0u8; directory.config_size as usize];
368 file.read_exact(&mut config_bytes)?;
369
370 self.config = match self.primary_header.encryption_type {
371 HeaderEncryptionType::None => Some(config_bytes),
372 HeaderEncryptionType::Aes256 => Some(cipher.decrypt(
373 Nonce::from_slice(&directory.config_nonce),
374 config_bytes.as_ref(),
375 )?),
376 };
377
378 file.seek(SeekFrom::Start(directory.digest_table_offset))?;
380 let digest_table: DigestTable = match self.primary_header.encryption_type {
381 HeaderEncryptionType::None => file.read_be()?,
382 HeaderEncryptionType::Aes256 => {
383 let mut digest_table_bytes = vec![0u8; directory.digest_table_size as usize];
384 file.read_exact(&mut digest_table_bytes)?;
385
386 let digest_table_bytes = cipher.decrypt(
387 Nonce::from_slice(&directory.digest_table_nonce),
388 digest_table_bytes.as_ref(),
389 )?;
390 Cursor::new(digest_table_bytes).read_be()?
391 }
392 };
393
394 self.directory = Some(directory);
396 self.protected_header = Some(protected_header);
397 self.digest_table = Some(digest_table);
398 Ok(())
399 }
400
401 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
403 let path = path.as_ref();
404 let mut file = File::open(path)?;
405
406 debug!(path = ?path, "Opening image");
407
408 let primary_header: PrimaryHeader = file.read_be()?;
410 debug!(primary_header = ?primary_header, "Primary header");
411
412 let id = if let Some(stem) = path.file_stem() {
414 if Regex::new("[A-Fa-f0-9]{64}")?.is_match(stem.to_str().unwrap()) {
415 stem.to_str().unwrap().to_string()
416 } else {
417 compute_id(&path).unwrap()
418 }
419 } else {
420 compute_id(&path).unwrap()
421 };
422
423 if primary_header.encryption_type == HeaderEncryptionType::None {
424 let protected_header: ProtectedHeader = file.read_be()?;
426
427 file.seek(SeekFrom::Start(primary_header.directory_offset))?;
429 let directory: Directory = file.read_be()?;
430
431 let mut config = vec![0u8; directory.config_size as usize];
433 file.seek(SeekFrom::Start(directory.config_offset))?;
434 file.read_exact(&mut config)?;
435
436 Ok(Self {
437 id,
438 primary_header,
439 protected_header: Some(protected_header),
440 config: Some(config),
441 digest_table: None,
442 directory: Some(directory),
443 path: path.to_path_buf(),
444 file_size: std::fs::metadata(&path)?.len(),
445 })
446 } else {
447 Ok(Self {
448 id,
449 primary_header,
450 protected_header: None,
451 config: None,
452 digest_table: None,
453 directory: None,
454 path: path.to_path_buf(),
455 file_size: std::fs::metadata(&path)?.len(),
456 })
457 }
458 }
459
460 pub fn change_password(&self, _old_password: String, new_password: String) -> Result<()> {
463 let _cipher = new_key(new_password);
465 let _rng = rand::thread_rng();
466
467 todo!()
468 }
469
470 pub fn write<F: Fn(u64, u64) -> ()>(&self, dest: impl AsRef<Path>, progress: F) -> Result<()> {
475 if self.protected_header.is_none() || self.digest_table.is_none() {
476 bail!("Image not loaded");
477 }
478
479 let protected_header = self.protected_header.clone().unwrap();
480 let digest_table = self.digest_table.clone().unwrap().digest_table;
481
482 let cluster_cipher =
483 Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&protected_header.cluster_key));
484
485 let dest = dest.as_ref();
486 info!(image = ?self, dest = ?dest, "Writing goldboot image");
487
488 let mut dest = std::fs::OpenOptions::new()
489 .create(true)
490 .write(true)
491 .read(true)
492 .open(dest)?;
493
494 let mut cluster_table = BufReader::new(File::open(&self.path)?);
496
497 let dest_metadata = dest.metadata()?;
500 if dest_metadata.is_file() && dest_metadata.len() < self.primary_header.size {
501 dest.set_len(self.primary_header.size)?;
502 }
503
504 let mut block = vec![0u8; protected_header.block_size as usize];
505
506 for i in 0..protected_header.cluster_count as usize {
508 let entry = &digest_table[i];
510
511 dest.seek(SeekFrom::Start(entry.block_offset))?;
513
514 let hash: [u8; 32] = match dest.read_exact(&mut block) {
516 Ok(_) => Sha256::new().chain_update(&block).finalize().into(),
517 Err(_) => {
518 [0u8; 32]
520 }
521 };
522
523 if hash != entry.digest {
524 cluster_table.seek(SeekFrom::Start(entry.cluster_offset))?;
526 let mut cluster: Cluster = cluster_table.read_be()?;
527
528 trace!(
529 cluster_size = cluster.size,
530 cluster_offset = entry.cluster_offset,
531 "Read dirty cluster",
532 );
533
534 cluster.data = match protected_header.cluster_encryption {
536 ClusterEncryptionType::None => cluster.data,
537 ClusterEncryptionType::Aes256 => cluster_cipher.decrypt(
538 Nonce::from_slice(&protected_header.nonce_table[i]),
539 cluster.data.as_ref(),
540 )?,
541 };
542
543 cluster.data = match protected_header.cluster_compression {
545 ClusterCompressionType::None => cluster.data,
546 ClusterCompressionType::Zstd => {
547 zstd::decode_all(std::io::Cursor::new(&cluster.data))?
548 }
549 };
550
551 trace!(
552 block_offset = entry.block_offset,
553 block_size = cluster.data.len(),
554 "Writing block",
555 );
556
557 dest.seek(SeekFrom::Start(entry.block_offset))?;
559 dest.write_all(&cluster.data)?;
560 }
561
562 progress(
563 protected_header.block_size as u64,
564 protected_header.cluster_count as u64 * protected_header.block_size as u64,
565 );
566 }
567
568 Ok(())
569 }
570}
571
572pub struct ImageBuilder {
573 name: String,
574 config: Vec<u8>,
575 password: Option<String>,
576 public: bool,
577 dest: PathBuf,
578 progress: Box<dyn Fn(u64, u64) -> ()>,
579}
580
581impl ImageBuilder {
582 pub fn new(dest: impl AsRef<Path>) -> Self {
583 Self {
584 name: "".into(),
585 config: Vec::new(),
586 password: None,
587 public: false,
588 dest: dest.as_ref().to_path_buf(),
589 progress: Box::new(|_, _| {}),
590 }
591 }
592
593 pub fn name(mut self, name: &str) -> Self {
594 self.name = name.to_string();
595 self
596 }
597
598 pub fn config(mut self, config: Vec<u8>) -> Self {
599 self.config = config;
600 self
601 }
602
603 pub fn public(mut self, public: bool) -> Self {
604 self.public = public;
605 self
606 }
607
608 pub fn password(mut self, password: &str) -> Self {
609 self.password = Some(password.to_string());
610 self
611 }
612
613 pub fn password_opt(mut self, password: Option<String>) -> Self {
614 self.password = password;
615 self
616 }
617
618 pub fn progress<'a, F>(&'a mut self, progress: F) -> &'a mut Self
619 where
620 F: Fn(u64, u64) + 'static,
621 {
622 self.progress = Box::new(progress);
623 self
624 }
625
626 pub fn convert(self, source: &Qcow3, size: u64) -> Result<ImageHandle> {
628 info!(name = self.name, "Exporting storage to goldboot image");
629
630 assert!(
631 source.header.size >= size,
632 "source.header.size = {}, size = {}",
633 source.header.size,
634 size
635 );
636
637 let mut dest_file = File::create(&self.dest)?;
638 let mut source_file = File::open(&source.path)?;
639
640 let header_cipher = new_key(self.password.clone().unwrap_or("".to_string()));
642 let mut rng = rand::thread_rng();
643
644 let mut directory = Directory {
646 protected_nonce: rng.gen::<[u8; 12]>(),
647 protected_size: 0,
648 config_nonce: rng.gen::<[u8; 12]>(),
649 config_offset: 0,
650 config_size: 0,
651 digest_table_nonce: rng.gen::<[u8; 12]>(),
652 digest_table_offset: 0,
653 digest_table_size: 0,
654 };
655
656 let mut primary_header = PrimaryHeader {
658 version: 1,
659 arch: ImageArch::Amd64, size,
661 directory_nonce: rng.gen::<[u8; 12]>(),
662 directory_offset: 0,
663 directory_size: 0,
664 timestamp: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
665 encryption_type: if self.password.is_some() {
666 HeaderEncryptionType::Aes256
667 } else {
668 HeaderEncryptionType::None
669 },
670 public: if self.public { 1u8 } else { 0u8 },
671 name: [0u8; 64],
672 reserved: [0u8; 64],
673 };
674
675 primary_header.name[0..self.name.len()].copy_from_slice(&self.name.clone().as_bytes()[..]);
676
677 let mut protected_header = ProtectedHeader {
679 block_size: source.header.cluster_size() as u32,
680 cluster_count: source.count_clusters()? as u32,
681 cluster_compression: ClusterCompressionType::None, cluster_encryption: if self.password.is_some() {
683 ClusterEncryptionType::Aes256
684 } else {
685 ClusterEncryptionType::None
686 },
687 cluster_key: rng.gen::<[u8; 32]>(),
688 nonce_count: 0,
689 nonce_table: vec![],
690 };
691
692 if self.password.is_some() {
693 protected_header.nonce_count = protected_header.cluster_count;
694 protected_header.nonce_table = (0..protected_header.cluster_count)
695 .map(|_| rng.gen::<[u8; 12]>())
696 .collect();
697 }
698
699 let cluster_cipher =
701 Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&protected_header.cluster_key));
702
703 dest_file.seek(SeekFrom::Start(0))?;
705 primary_header.write(&mut dest_file)?;
706
707 {
709 debug!(protected_header = ?protected_header, "Writing protected header");
710 let mut protected_header_bytes = Cursor::new(Vec::new());
711 protected_header.write(&mut protected_header_bytes)?;
712
713 let protected_header_bytes = match primary_header.encryption_type {
714 HeaderEncryptionType::None => protected_header_bytes.into_inner(),
715 HeaderEncryptionType::Aes256 => header_cipher.encrypt(
716 Nonce::from_slice(&directory.protected_nonce),
717 protected_header_bytes.into_inner()[..].as_ref(),
718 )?,
719 };
720
721 directory.protected_size = protected_header_bytes.len() as u32;
722 dest_file.write_all(&protected_header_bytes)?;
723 }
724
725 {
727 let config_bytes = match primary_header.encryption_type {
728 HeaderEncryptionType::None => self.config.clone(),
729 HeaderEncryptionType::Aes256 => header_cipher.encrypt(
730 Nonce::from_slice(&directory.config_nonce),
731 self.config.as_ref(),
732 )?,
733 };
734
735 directory.config_offset = dest_file.stream_position()?;
736 directory.config_size = config_bytes.len() as u32;
737 dest_file.write_all(&config_bytes)?;
738 }
739
740 let mut digest_table = DigestTable {
742 digest_count: protected_header.cluster_count,
743 digest_table: vec![],
744 };
745
746 let mut block_offset: u64 = 0;
748
749 let mut cluster_count = 0;
751
752 let mut cluster_offset = dest_file.stream_position()?;
754
755 for l1_entry in &source.l1_table {
757 if let Some(l2_table) = l1_entry.read_l2(&mut source_file, source.header.cluster_bits) {
758 for l2_entry in l2_table {
759 if l2_entry.is_used {
760 let mut cluster = Cluster {
762 size: 0,
764 data: l2_entry.read_contents(
765 &mut source_file,
766 source.header.cluster_size(),
767 source.header.compression_type,
768 )?,
769 };
770
771 cluster
773 .data
774 .truncate((primary_header.size - block_offset) as usize);
775
776 let digest = Sha256::new().chain_update(&cluster.data).finalize();
778
779 digest_table.digest_table.push(DigestTableEntry {
780 digest: digest.into(),
781 block_offset,
782 cluster_offset,
783 });
784
785 cluster.data = match protected_header.cluster_compression {
787 ClusterCompressionType::None => cluster.data,
788 ClusterCompressionType::Zstd => {
789 zstd::encode_all(std::io::Cursor::new(cluster.data), 0)?
790 }
791 };
792
793 cluster.data = match protected_header.cluster_encryption {
795 ClusterEncryptionType::None => cluster.data,
796 ClusterEncryptionType::Aes256 => cluster_cipher.encrypt(
797 Nonce::from_slice(&protected_header.nonce_table[cluster_count]),
798 cluster.data.as_ref(),
799 )?,
800 };
801
802 cluster.size = cluster.data.len() as u32;
803
804 trace!(
806 cluster_size = cluster.size,
807 cluster_offset = cluster_offset,
808 "Recording cluster",
809 );
810 cluster.write(&mut dest_file)?;
811
812 cluster_offset += 4; cluster_offset += cluster.size as u64;
815 cluster_count += 1;
816 }
817 block_offset += source.header.cluster_size();
818 }
820 } else {
821 block_offset +=
822 source.header.cluster_size() * source.header.l2_entries_per_cluster();
823 }
828 }
829
830 {
832 let mut digest_table_bytes = Cursor::new(Vec::new());
833 digest_table.write(&mut digest_table_bytes)?;
834
835 let digest_table_bytes = match primary_header.encryption_type {
836 HeaderEncryptionType::None => digest_table_bytes.into_inner(),
837 HeaderEncryptionType::Aes256 => header_cipher.encrypt(
838 Nonce::from_slice(&directory.digest_table_nonce),
839 digest_table_bytes.into_inner()[..].as_ref(),
840 )?,
841 };
842
843 directory.digest_table_offset = dest_file.stream_position()?;
844 directory.digest_table_size = digest_table_bytes.len() as u32;
845 dest_file.write_all(&digest_table_bytes)?;
846 }
847
848 {
850 let mut directory_bytes = Cursor::new(Vec::new());
851 directory.write(&mut directory_bytes)?;
852
853 let directory_bytes = match primary_header.encryption_type {
854 HeaderEncryptionType::None => directory_bytes.into_inner(),
855 HeaderEncryptionType::Aes256 => header_cipher.encrypt(
856 Nonce::from_slice(&primary_header.directory_nonce),
857 directory_bytes.into_inner()[..].as_ref(),
858 )?,
859 };
860
861 primary_header.directory_offset = dest_file.stream_position()?;
862 primary_header.directory_size = directory_bytes.len() as u32;
863 dest_file.write_all(&directory_bytes)?;
864 }
865
866 dest_file.seek(SeekFrom::Start(0))?;
868 primary_header.write(&mut dest_file)?;
869
870 Ok(ImageHandle {
871 id: compute_id(&self.dest)?,
872 primary_header,
873 protected_header: Some(protected_header),
874 config: Some(self.config),
875 digest_table: Some(digest_table),
876 directory: Some(directory),
877 file_size: std::fs::metadata(&self.dest)?.len(),
878 path: self.dest,
879 })
880 }
881}
882
883#[cfg(test)]
884mod tests {
885 use std::process::Command;
886
887 use super::*;
888 use sha1::Sha1;
889 use test_log::test;
890
891 #[test]
892 fn convert_random_data() -> Result<()> {
893 let tmp = tempfile::tempdir()?;
894
895 let size = rand::thread_rng().gen_range(512..=1000);
897 let mut raw: Vec<u8> = Vec::new();
898 for _ in 0..size {
899 raw.push(rand::thread_rng().gen());
900 }
901
902 std::fs::write(tmp.path().join("file.raw"), &raw)?;
904
905 Command::new("qemu-img")
907 .arg("convert")
908 .arg("-f")
909 .arg("raw")
910 .arg("-O")
911 .arg("qcow2")
912 .arg(tmp.path().join("file.raw"))
913 .arg(tmp.path().join("file.qcow2"))
914 .spawn()?
915 .wait()?;
916
917 let image = ImageBuilder::new(tmp.path().join("file.gb"))
919 .convert(&Qcow3::open(tmp.path().join("file.qcow2"))?, size)?;
920
921 image.write(tmp.path().join("output.raw"), |_, _| {})?;
923 assert_eq!(
924 std::fs::read(tmp.path().join("file.raw"))?,
925 std::fs::read(tmp.path().join("output.raw"))?,
926 );
927
928 Ok(())
929 }
930
931 #[test]
932 fn convert_small_qcow2_to_unencrypted_image() -> Result<()> {
933 let tmp = tempfile::tempdir()?;
934
935 let image = ImageBuilder::new(tmp.path().join("small.gb"))
937 .convert(&Qcow3::open("test/small.qcow2")?, 4194304)?;
938
939 let mut loaded_image = ImageHandle::open(tmp.path().join("small.gb"))?;
941 assert_eq!(loaded_image.primary_header, image.primary_header);
942 assert_eq!(loaded_image.protected_header, image.protected_header);
943
944 assert_eq!(loaded_image.digest_table, None);
946 loaded_image.load(None)?;
947 assert_eq!(loaded_image.digest_table.unwrap().digest_count, 2);
948
949 image.write(tmp.path().join("small.raw"), |_, _| {})?;
951 assert_eq!(
952 hex::encode(
953 Sha1::new()
954 .chain_update(&std::fs::read(tmp.path().join("small.raw"))?)
955 .finalize()
956 ),
957 "34e1c79c80941e5519ec76433790191318a5c77b"
958 );
959
960 Ok(())
961 }
962
963 #[test]
964 fn convert_small_qcow2_to_encrypted_image() -> Result<()> {
965 let tmp = tempfile::tempdir()?;
966
967 let image = ImageBuilder::new(tmp.path().join("small.gb"))
969 .password("1234")
970 .convert(&Qcow3::open("test/small.qcow2")?, 4194304)?;
971
972 let mut loaded_image = ImageHandle::open(tmp.path().join("small.gb"))?;
974 assert_eq!(loaded_image.primary_header, image.primary_header);
975 assert_eq!(loaded_image.protected_header, None);
976
977 loaded_image.load(Some("1234".to_string()))?;
979
980 image.write(tmp.path().join("small.raw"), |_, _| {})?;
982 assert_eq!(
983 hex::encode(
984 Sha1::new()
985 .chain_update(&std::fs::read(tmp.path().join("small.raw"))?)
986 .finalize()
987 ),
988 "34e1c79c80941e5519ec76433790191318a5c77b"
989 );
990
991 Ok(())
992 }
993}