Alioth Code Coverage

img.rs0.00%

1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fs::File;
16use std::io::Read;
17use std::os::unix::fs::FileExt;
18use std::path::Path;
19
20use alioth::blk::qcow2::{
21 QCOW2_MAGIC, Qcow2CmprDesc, Qcow2Hdr, Qcow2IncompatibleFeatures, Qcow2L1, Qcow2L2, Qcow2StdDesc,
22};
23use alioth::errors::{DebugTrace, trace_error};
24use alioth::utils::endian::Bu64;
25use clap::{Args, Subcommand};
26use miniz_oxide::inflate::TINFLStatus;
27use miniz_oxide::inflate::core::inflate_flags::TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF;
28use miniz_oxide::inflate::core::{DecompressorOxide, decompress};
29use serde::Deserialize;
30use snafu::{ResultExt, Snafu};
31use zerocopy::{FromZeros, IntoBytes};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
34pub enum ImageFormat {
35 #[serde(alias = "qcow2")]
36 Qcow2,
37 #[serde(alias = "raw")]
38 Raw,
39}
40
41#[derive(Args, Debug)]
42pub struct ImgArgs {
43 #[command(subcommand)]
44 cmd: Command,
45}
46
47#[derive(Subcommand, Debug)]
48enum Command {
49 /// Convert an image from one format to another.
50 Convert(ConvertArgs),
51}
52
53#[derive(Args, Debug)]
54struct ConvertArgs {
55 /// Input file format
56 #[arg(short = 'f', long, default_value = "qcow2")]
57 source_format: Box<str>,
58
59 /// Output file format
60 #[arg(short = 'O', long, default_value = "raw")]
61 target_format: Box<str>,
62
63 /// Input file
64 input: Box<Path>,
65
66 /// Output file
67 output: Box<Path>,
68}
69
70#[trace_error]
71#[derive(Snafu, DebugTrace)]
72#[snafu(module, context(suffix(false)))]
73pub enum Error {
74 #[snafu(display("Error from OS"), context(false))]
75 System { error: std::io::Error },
76 #[snafu(display("Failed to parse {arg}"))]
77 ParseArg {
78 arg: String,
79 error: serde_aco::Error,
80 },
81 #[snafu(display("Failed to convert image from {from:?} to {to:?}"))]
82 Conversion { from: ImageFormat, to: ImageFormat },
83 #[snafu(display("Missing magic number {magic:x?}, found {found:x?}"))]
84 MissingMagic { magic: [u8; 4], found: [u8; 4] },
85 #[snafu(display("Unsupported qcow2 features: {features:?}"))]
86 Features { features: Qcow2IncompatibleFeatures },
87 #[snafu(display("Decompression failed: {:?}", status))]
88 DecompressionFailed { status: TINFLStatus },
89}
90
91type Result<T> = std::result::Result<T, Error>;
92
93pub fn exec(args: ImgArgs) -> Result<()> {
94 match args.cmd {
95 Command::Convert(args) => convert(args),
96 }
97}
98
99fn convert(args: ConvertArgs) -> Result<()> {
100 let from: ImageFormat = serde_aco::from_arg(&args.source_format).context(error::ParseArg {
101 arg: args.source_format,
102 })?;
103 let to: ImageFormat = serde_aco::from_arg(&args.target_format).context(error::ParseArg {
104 arg: args.target_format,
105 })?;
106 if from == ImageFormat::Qcow2 && to == ImageFormat::Raw {
107 convert_qcow2_to_raw(&args.input, &args.output)
108 } else {
109 error::Conversion { from, to }.fail()
110 }
111}
112
113fn convert_qcow2_to_raw(input: &Path, output: &Path) -> Result<()> {
114 let mut hdr = Qcow2Hdr::new_zeroed();
115 let mut f = File::open(input)?;
116 f.read_exact(hdr.as_mut_bytes())?;
117 if hdr.magic != QCOW2_MAGIC {
118 return error::MissingMagic {
119 magic: QCOW2_MAGIC,
120 found: hdr.magic,
121 }
122 .fail();
123 }
124 let features = hdr.incompatible_features.to_ne();
125 if hdr.version.to_ne() > 2 && features != 0 {
126 let features = Qcow2IncompatibleFeatures::from_bits_retain(features);
127 return error::Features { features }.fail();
128 }
129 let cluster_bits = hdr.cluster_bits.to_ne();
130 let cluster_size = 1 << cluster_bits;
131 let l2_size = cluster_size / std::mem::size_of::<Bu64>() as u64;
132
133 let mut l1_table = vec![Bu64::new_zeroed(); hdr.l1_size.to_ne() as usize];
134 f.read_exact_at(l1_table.as_mut_bytes(), hdr.l1_table_offset.to_ne())?;
135
136 let output = File::create(output)?;
137 output.set_len(hdr.size.to_ne())?;
138
139 let mut data = vec![0u8; cluster_size as usize];
140 let mut tmp_buf = vec![0u8; cluster_size as usize];
141 let mut l2_table = vec![Bu64::new_zeroed(); l2_size as usize];
142
143 let mut decompressor = DecompressorOxide::new();
144
145 for (l1_index, l1_entry) in l1_table.iter().enumerate() {
146 let l1_entry = Qcow2L1(l1_entry.to_ne());
147 let l2_offset = l1_entry.l2_offset();
148 if l2_offset == 0 {
149 continue;
150 }
151 f.read_exact_at(l2_table.as_mut_bytes(), l2_offset)?;
152 for (l2_index, l2_entry) in l2_table.iter().enumerate() {
153 let l2_entry = Qcow2L2(l2_entry.to_ne());
154 if l2_entry.compressed() {
155 let l2_desc = Qcow2CmprDesc(l2_entry.desc());
156 let (offset, size) = l2_desc.offset_size(cluster_bits);
157 let buf = if let Some(buf) = tmp_buf.get_mut(..size as usize) {
158 buf
159 } else {
160 tmp_buf.resize(size as usize, 0);
161 tmp_buf.as_mut()
162 };
163 f.read_exact_at(buf, offset)?;
164 decompressor.init();
165 let flag = TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF;
166 let (status, _, _) = decompress(&mut decompressor, buf, &mut data, 0, flag);
167 if status != TINFLStatus::Done {
168 return error::DecompressionFailed { status }.fail();
169 }
170 } else {
171 let l2_desc = Qcow2StdDesc(l2_entry.desc());
172 if l2_desc.zero() {
173 continue;
174 }
175 if !l2_entry.rc1() && l2_desc.offset() == 0 {
176 continue;
177 }
178 let offset = l2_desc.cluster_offset();
179 f.read_exact_at(&mut data, offset)?;
180 }
181 let output_offset = (l1_index as u64 * l2_size + l2_index as u64) << cluster_bits;
182 output.write_all_at(&data, output_offset)?;
183 }
184 }
185 Ok(())
186}
187