use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{copy, Seek, SeekFrom, Write};
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use nix::mount;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use xz2::write::XzEncoder;
use crate::blockdev::*;
use crate::cmdline::*;
use crate::io::*;
mod fiemap;
mod file;
mod io_helpers;
mod unpacker;
use crate::osmet::fiemap::*;
use crate::osmet::file::*;
use crate::osmet::io_helpers::*;
use crate::osmet::unpacker::*;
pub use crate::osmet::unpacker::OsmetUnpacker;
#[derive(Serialize, Deserialize, Debug)]
struct Mapping {
extent: Extent,
object: Sha256Digest,
}
#[derive(Serialize, Deserialize, Debug)]
struct OsmetPartition {
start_offset: u64,
end_offset: u64,
mappings: Vec<Mapping>,
}
#[derive(Serialize, Deserialize, Debug)]
struct Osmet {
partitions: Vec<OsmetPartition>,
checksum: Sha256Digest,
size: u64,
}
#[derive(Serialize)]
struct FiemapOutput {
extents: Vec<Extent>,
}
pub fn dev_show_fiemap(config: DevShowFiemapConfig) -> Result<()> {
let output = FiemapOutput {
extents: fiemap_path(config.file.as_str().as_ref())?,
};
let stdout = std::io::stdout();
let mut out = stdout.lock();
serde_json::to_writer_pretty(&mut out, &output).context("failed to serialize extents")?;
out.write_all(b"\n").context("failed to write newline")?;
Ok(())
}
pub fn pack_osmet(config: PackOsmetConfig) -> Result<()> {
let disk = Disk::new(&config.device)?;
let boot = disk.mount_partition_by_label("boot", mount::MsFlags::MS_RDONLY)?;
let root = disk.mount_partition_by_label("root", mount::MsFlags::MS_RDONLY)?;
let boot_files = prescan_boot_partition(&boot)?;
let (root_partition, mapped_boot_files) = scan_root_partition(&root, boot_files)?;
let boot_partition = scan_boot_partition(&boot, mapped_boot_files)?;
let partitions = vec![boot_partition, root_partition];
eprintln!("Packing image");
let (mut xzpacked_image, size) =
write_xzpacked_image_to_file(Path::new(&config.device), &partitions, config.fast)?;
eprintln!("Verifying that repacked image matches digest");
let (checksum, unpacked_size) =
get_unpacked_image_digest(&mut xzpacked_image, &partitions, &root)?;
xzpacked_image
.seek(SeekFrom::Start(0))
.context("seeking back to start of xzpacked image")?;
if unpacked_size != size {
bail!(
"unpacking test: got {} bytes but expected {}",
unpacked_size,
size
);
}
let checksum_str = checksum.to_hex_string()?;
if checksum_str != config.checksum {
bail!(
"unpacking test: got checksum {} but expected {}",
checksum_str,
&config.checksum
);
}
let sector_size = get_sector_size_for_path(Path::new(&config.device))?.get();
let header = OsmetFileHeader::new(sector_size, &config.description)?;
let osmet = Osmet {
partitions,
checksum,
size,
};
osmet_file_write(Path::new(&config.output), header, osmet, xzpacked_image)?;
eprintln!("Packing successful!");
Ok(())
}
pub fn dev_extract_osmet(config: DevExtractOsmetConfig) -> Result<()> {
let mut dev = OpenOptions::new()
.write(true)
.open(Path::new(&config.device))
.with_context(|| format!("opening {:?}", &config.device))?;
if !dev
.metadata()
.with_context(|| format!("getting metadata for {:?}", &config.device))?
.file_type()
.is_block_device()
{
bail!("{:?} is not a block device", &config.device);
}
let mut unpacker = OsmetUnpacker::new(Path::new(&config.osmet), Path::new(&config.repo))?;
copy(&mut unpacker, &mut dev)
.with_context(|| format!("copying to block device {}", &config.device))?;
Ok(())
}
pub fn find_matching_osmet_in_dir(
osmet_dir: &Path,
architecture: &str,
sector_size: u32,
) -> Result<Option<(PathBuf, String)>> {
for entry in WalkDir::new(osmet_dir).max_depth(1) {
let entry = entry.with_context(|| format!("walking {:?}", osmet_dir))?;
if !entry.file_type().is_file() {
continue;
}
let header = osmet_file_read_header(entry.path())?;
if header.os_architecture == architecture && header.sector_size == sector_size {
return Ok(Some((entry.into_path(), header.os_description)));
}
}
Ok(None)
}
fn scan_root_partition(
root: &Mount,
mut boot_files: HashMap<u64, PathBuf>,
) -> Result<(OsmetPartition, HashMap<PathBuf, Sha256Digest>)> {
let (start_offset, end_offset) = root.get_partition_offsets()?;
let mut cached_boot_files_digests: HashMap<u64, Sha256Digest> = HashMap::new();
let mut mapped_boot_files: HashMap<PathBuf, Sha256Digest> = HashMap::new();
let objects_dir = root.mountpoint().join("ostree/repo/objects");
let mut mappings: Vec<Mapping> = vec![];
let mut mapped_file_count = 0;
let mut empty_file_count = 0;
for entry in WalkDir::new(objects_dir) {
let entry = entry.context("walking objects/ dir")?;
if !entry.file_type().is_file() {
continue;
}
if entry.path().extension() != Some("file".as_ref()) {
continue;
}
let extents = fiemap_path(entry.path().as_os_str())?;
if extents.is_empty() {
empty_file_count += 1;
continue;
}
let object = object_path_to_checksum(entry.path())
.with_context(|| format!("invalid object path {:?}", entry.path()))?;
for extent in extents {
mappings.push(Mapping {
extent,
object: object.clone(),
});
}
let len = entry
.metadata()
.with_context(|| format!("getting metadata for {:?}", entry.path()))?
.len();
if let Entry::Occupied(boot_entry) = boot_files.entry(len) {
let boot_file_digest = match cached_boot_files_digests.entry(len) {
Entry::Vacant(e) => e.insert(Sha256Digest::from_path(boot_entry.get())?),
Entry::Occupied(e) => e.into_mut(),
};
if Sha256Digest::from_path(entry.path())? == *boot_file_digest {
mapped_boot_files.insert(boot_entry.remove(), object.clone());
}
}
mapped_file_count += 1;
}
eprintln!(
"Total OSTree objects scanned from /root: {} ({} mapped, {} empty)",
mapped_file_count + empty_file_count,
mapped_file_count,
empty_file_count
);
eprintln!(
"Total OSTree objects found in /boot: {}",
mapped_boot_files.len()
);
canonicalize(&mut mappings);
eprintln!("Total /root extents: {}", mappings.len());
Ok((
OsmetPartition {
start_offset,
end_offset,
mappings,
},
mapped_boot_files,
))
}
fn prescan_boot_partition(boot: &Mount) -> Result<HashMap<u64, PathBuf>> {
let mut files: HashMap<u64, PathBuf> = HashMap::new();
for entry in WalkDir::new(boot.mountpoint()) {
let entry = entry.context("walking /boot")?;
if !entry.file_type().is_file() {
continue;
}
let len = entry
.metadata()
.with_context(|| format!("getting metadata for {:?}", entry.path()))?
.len();
if len > 1024 {
files.entry(len).or_insert_with(|| entry.into_path());
}
}
Ok(files)
}
fn scan_boot_partition(
boot: &Mount,
mut boot_files: HashMap<PathBuf, Sha256Digest>,
) -> Result<OsmetPartition> {
let (start_offset, end_offset) = boot.get_partition_offsets()?;
let mut mappings: Vec<Mapping> = vec![];
for (path, object) in boot_files.drain() {
for extent in fiemap_path(path.as_path().as_os_str())? {
mappings.push(Mapping {
extent,
object: object.clone(),
});
}
}
canonicalize(&mut mappings);
eprintln!("Total /boot extents: {}", mappings.len());
Ok(OsmetPartition {
start_offset,
end_offset,
mappings,
})
}
fn write_xzpacked_image_to_file(
block_device: &Path,
partitions: &[OsmetPartition],
fast: bool,
) -> Result<(File, u64)> {
let mut xz_tmpf = XzEncoder::new(
tempfile::Builder::new()
.prefix("coreos-installer-xzpacked")
.suffix(".raw.xz")
.tempfile()
.context("allocating packed image tempfile")?
.into_file(),
if fast { 0 } else { 9 },
);
let mut dev = OpenOptions::new()
.read(true)
.open(&block_device)
.with_context(|| format!("opening {:?}", block_device))?;
let total_bytes_skipped = write_packed_image(&mut dev, &mut xz_tmpf, partitions)?;
xz_tmpf.try_finish().context("trying to finish xz stream")?;
let blksize = get_block_device_size(&dev)
.with_context(|| format!("querying block device size of {:?}", block_device))?;
let total_bytes_written = xz_tmpf.total_in();
if total_bytes_written + total_bytes_skipped != blksize.get() {
bail!(
"bytes written + bytes skipped != block device size: {} + {} vs {}",
total_bytes_written,
total_bytes_skipped,
blksize
);
}
eprintln!("Total bytes skipped: {}", total_bytes_skipped);
eprintln!("Total bytes written: {}", total_bytes_written);
eprintln!("Total bytes written (compressed): {}", xz_tmpf.total_out());
let mut tmpf = xz_tmpf.finish().context("finishing xz stream")?;
tmpf.seek(SeekFrom::Start(0))
.context("seeking back to start of tempfile")?;
Ok((tmpf, blksize.get()))
}
fn write_packed_image(
dev: &mut File,
w: &mut impl Write,
partitions: &[OsmetPartition],
) -> Result<u64> {
let mut buf = [0u8; 8192];
let mut cursor: u64 = 0;
let mut total_bytes_skipped: u64 = 0;
for (i, partition) in partitions.iter().enumerate() {
assert!(partition.start_offset >= cursor);
copy_exactly_n(dev, w, partition.start_offset - cursor, &mut buf)?;
total_bytes_skipped += write_packed_image_partition(dev, w, partition, &mut buf)
.with_context(|| format!("packing partition {}", i))?;
cursor = partition.end_offset;
}
copy(dev, w).context("copying remainder of disk")?;
Ok(total_bytes_skipped)
}
fn write_packed_image_partition(
dev: &mut File,
w: &mut impl Write,
partition: &OsmetPartition,
buf: &mut [u8],
) -> Result<u64> {
let mut total_bytes_skipped = 0;
let mut cursor = partition.start_offset;
for mapping in partition.mappings.iter() {
let extent_start = mapping.extent.physical + partition.start_offset;
assert!(extent_start >= cursor);
if cursor < extent_start {
cursor += copy_exactly_n(dev, w, extent_start - cursor, buf)
.context("while writing in between extents")?;
}
dev.seek(SeekFrom::Current(mapping.extent.length.try_into().unwrap()))
.with_context(|| format!("while skipping extent: {:?}", mapping.extent))?;
total_bytes_skipped += mapping.extent.length;
cursor += mapping.extent.length;
}
assert!(cursor <= partition.end_offset);
copy_exactly_n(dev, w, partition.end_offset - cursor, buf)
.context("copying remainder of partition")?;
Ok(total_bytes_skipped)
}
fn canonicalize(mappings: &mut Vec<Mapping>) {
if mappings.is_empty() {
eprintln!("No mappings to canonicalize");
return;
}
mappings.sort_unstable_by(|a, b| {
a.extent
.physical
.cmp(&b.extent.physical)
.then_with(|| a.extent.length.cmp(&b.extent.length).reverse())
});
let mut clamped_mappings_count = 0;
let mut mappings_to_delete: Vec<usize> = Vec::new();
let mut last_mapping_physical_end = mappings[0].extent.physical + mappings[0].extent.length;
for (i, mapping) in mappings.iter_mut().enumerate().skip(1) {
let mapping_physical_end = mapping.extent.physical + mapping.extent.length;
if mapping_physical_end <= last_mapping_physical_end {
mappings_to_delete.push(i);
} else {
if mapping.extent.physical < last_mapping_physical_end {
let n = last_mapping_physical_end - mapping.extent.physical;
mapping.extent.logical += n;
mapping.extent.physical += n;
mapping.extent.length -= n;
clamped_mappings_count += 1;
}
last_mapping_physical_end = mapping_physical_end;
}
}
eprintln!("Duplicate extents dropped: {}", mappings_to_delete.len());
eprintln!("Overlapping extents clamped: {}", clamped_mappings_count);
for i in mappings_to_delete.into_iter().rev() {
mappings.remove(i);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::default::Default;
#[test]
fn test_canonicalize() {
let mut mappings: Vec<Mapping> = Vec::new();
mappings.push(Mapping {
extent: Extent {
logical: 100,
physical: 100,
length: 50,
},
object: Sha256Digest::default(),
});
canonicalize(&mut mappings);
assert_eq!(mappings.len(), 1);
assert_eq!(
mappings[0].extent,
Extent {
logical: 100,
physical: 100,
length: 50
}
);
mappings.push(Mapping {
extent: Extent {
logical: 100,
physical: 100,
length: 10,
},
object: Sha256Digest::default(),
});
mappings.push(Mapping {
extent: Extent {
logical: 110,
physical: 110,
length: 10,
},
object: Sha256Digest::default(),
});
mappings.push(Mapping {
extent: Extent {
logical: 140,
physical: 140,
length: 10,
},
object: Sha256Digest::default(),
});
canonicalize(&mut mappings);
assert_eq!(mappings.len(), 1);
assert_eq!(
mappings[0].extent,
Extent {
logical: 100,
physical: 100,
length: 50
}
);
mappings.push(Mapping {
extent: Extent {
logical: 140,
physical: 140,
length: 20,
},
object: Sha256Digest::default(),
});
mappings.push(Mapping {
extent: Extent {
logical: 150,
physical: 150,
length: 20,
},
object: Sha256Digest::default(),
});
canonicalize(&mut mappings);
assert_eq!(mappings.len(), 3);
assert_eq!(
mappings[0].extent,
Extent {
logical: 100,
physical: 100,
length: 50
}
);
assert_eq!(
mappings[1].extent,
Extent {
logical: 150,
physical: 150,
length: 10
}
);
assert_eq!(
mappings[2].extent,
Extent {
logical: 160,
physical: 160,
length: 10
}
);
}
}