img.rs0.00%
1
// Copyright 2026 Google LLC2
//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 at6
//7
// https://www.apache.org/licenses/LICENSE-2.08
//9
// Unless required by applicable law or agreed to in writing, software10
// 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 and13
// limitations under the License.14
15
use std::fs::File;16
use std::io::Read;17
use std::os::unix::fs::FileExt;18
use std::path::Path;19
20
use alioth::blk::qcow2::{21
QCOW2_MAGIC, Qcow2CmprDesc, Qcow2Hdr, Qcow2IncompatibleFeatures, Qcow2L1, Qcow2L2, Qcow2StdDesc,22
};23
use alioth::errors::{DebugTrace, trace_error};24
use alioth::utils::endian::Bu64;25
use clap::{Args, Subcommand};26
use miniz_oxide::inflate::TINFLStatus;27
use miniz_oxide::inflate::core::inflate_flags::TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF;28
use miniz_oxide::inflate::core::{DecompressorOxide, decompress};29
use serde::Deserialize;30
use snafu::{ResultExt, Snafu};31
use zerocopy::{FromZeros, IntoBytes};32
33
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]34
pub enum ImageFormat {35
#[serde(alias = "qcow2")]36
Qcow2,37
#[serde(alias = "raw")]38
Raw,39
}40
41
#[derive(Args, Debug)]42
pub struct ImgArgs {43
#[command(subcommand)]44
cmd: Command,45
}46
47
#[derive(Subcommand, Debug)]48
enum Command {49
/// Convert an image from one format to another.50
Convert(ConvertArgs),51
}52
53
#[derive(Args, Debug)]54
struct ConvertArgs {55
/// Input file format56
#[arg(short = 'f', long, default_value = "qcow2")]57
source_format: Box<str>,58
59
/// Output file format60
#[arg(short = 'O', long, default_value = "raw")]61
target_format: Box<str>,62
63
/// Input file64
input: Box<Path>,65
66
/// Output file67
output: Box<Path>,68
}69
70
#[trace_error]71
#[derive(Snafu, DebugTrace)]72
#[snafu(module, context(suffix(false)))]73
pub 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
91
type Result<T> = std::result::Result<T, Error>;92
93
pub fn exec(args: ImgArgs) -> Result<()> {94
match args.cmd {95
Command::Convert(args) => convert(args),96
}97
}98
99
fn 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
113
fn 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
buf159
} 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