pay10ad-dumper 0.1.3

Feature-rich Android OTA payload dumper written in Rust
Documentation
use std::{fs, path::Path};

use anyhow::Result;

use crate::{
    proto::{DeltaArchiveManifest, DynamicPartitionMetadata, install_operation},
    structs::{
        ApexInfoMetadata, DynamicPartitionGroupInfo, DynamicPartitionInfo, PartitionMetadata,
        PayloadMetadata, VabcFeatureSetInfo,
    },
    utils::format_size,
};

pub fn save_metadata(
    manifest: &DeltaArchiveManifest,
    output_dir: &Path,
    data_offset: u64,
) -> Result<String> {
    let mut partitions = Vec::new();
    for partition in &manifest.partitions {
        if let Some(info) = &partition.new_partition_info {
            let size_in_bytes = info.size.unwrap_or(0);
            let block_size = u64::from(manifest.block_size.unwrap_or(4096));
            let size_in_blocks = size_in_bytes / block_size;
            let total_blocks = size_in_bytes / block_size;
            let hash = info.hash.as_ref().map(hex::encode);
            let mut start_offset = data_offset;
            for op in &partition.operations {
                if let Some(_first_extent) = op.dst_extents.first() {
                    // 2
                    start_offset = data_offset + op.data_offset.unwrap_or(0);
                    break;
                }
            }
            let end_offset = start_offset + size_in_bytes;
            let compression_type = partition
                .operations
                .iter()
                .find_map(|op| match op.r#type() {
                    install_operation::Type::ReplaceXz => Some("xz"),
                    install_operation::Type::ReplaceBz => Some("bz2"),
                    install_operation::Type::Zstd => Some("zstd"),
                    _ => None,
                })
                .unwrap_or("none")
                .to_string();
            let encryption = if partition.partition_name.contains("userdata") {
                "AES"
            } else {
                "none"
            };

            partitions.push(PartitionMetadata {
                partition_name: partition.partition_name.clone(),
                size_in_blocks,
                size_in_bytes,
                size_readable: format_size(size_in_bytes),
                hash,
                start_offset,
                end_offset,
                data_offset,
                partition_type: partition.partition_name.clone(),
                operations_count: partition.operations.len(),
                compression_type,
                encryption: encryption.to_string(),
                block_size,
                total_blocks,
                run_postinstall: partition.run_postinstall,
                postinstall_path: partition.postinstall_path.clone(),
                filesystem_type: partition.filesystem_type.clone(),
                postinstall_optional: partition.postinstall_optional,
                hash_tree_algorithm: partition.hash_tree_algorithm.clone(),
                version: partition.version.clone(),
            });
        }
    }

    let dynamic_partition_metadata = manifest
        .dynamic_partition_metadata
        .as_ref()
        .map(extract_dynamic_partition_info);
    let apex_info: Vec<ApexInfoMetadata> = manifest
        .apex_info
        .iter()
        .map(|info| ApexInfoMetadata {
            package_name: info.package_name.clone(),
            version: info.version,
            is_compressed: info.is_compressed,
            decompressed_size: info.decompressed_size,
        })
        .collect();

    let payload_metadata = PayloadMetadata {
        security_patch_level: manifest.security_patch_level.clone(),
        block_size: manifest.block_size.unwrap_or(4096),
        minor_version: manifest.minor_version.unwrap_or(0),
        max_timestamp: manifest.max_timestamp,
        dynamic_partition_metadata,
        partial_update: manifest.partial_update,
        apex_info,
        partitions,
    };

    let json = serde_json::to_string_pretty(&payload_metadata)?;

    if output_dir.to_string_lossy() == "-" {
        return Ok(json);
    }

    let metadata_path = output_dir.join("payload_metadata.json");
    fs::write(metadata_path, &json)?;

    Ok(json)
}

/// Extracts [`DynamicPartitionInfo`] from [`DynamicPartitionMetadata`].
fn extract_dynamic_partition_info(dpm: &DynamicPartitionMetadata) -> DynamicPartitionInfo {
    let groups: Vec<DynamicPartitionGroupInfo> = dpm
        .groups
        .iter()
        .map(|group| DynamicPartitionGroupInfo {
            name: group.name.clone(),
            size: group.size,
            partition_names: group.partition_names.clone(),
        })
        .collect();

    let vabc_feature_set = dpm.vabc_feature_set.as_ref().map(|fs| VabcFeatureSetInfo {
        threaded: fs.threaded,
        batch_writes: fs.batch_writes,
    });

    DynamicPartitionInfo {
        groups,
        snapshot_enabled: dpm.snapshot_enabled,
        vabc_enabled: dpm.vabc_enabled,
        vabc_compression_param: dpm.vabc_compression_param.clone(),
        cow_version: dpm.cow_version,
        vabc_feature_set,
        compression_factor: dpm.compression_factor,
    }
}