Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
449c4d4
fix(file): keep the image layout when writing a partition back
JanZachmann Sep 28, 2026
5186d4b
chore: bump dependencies including major versions
JanZachmann Sep 28, 2026
2c7f060
style(device_update): drop redundant borrows in info! arguments
JanZachmann Sep 28, 2026
e9163a5
fix(file): keep holes when copying the image
JanZachmann Sep 28, 2026
e2443b2
fix(file): map the ext4 journals in a generated bmap
JanZachmann Sep 28, 2026
52fe21d
fix: copy image data segments larger than 2 GiB completely
JanZachmann Sep 28, 2026
19bcd30
fix(file): harden ext4 journal lookup for bmap generation
JanZachmann Sep 28, 2026
d7e7242
build(docker): install e2fsprogs in the builder
JanZachmann Sep 28, 2026
2912452
fix(file): keep zero blocks as holes when decompressing
JanZachmann Sep 28, 2026
84ba27e
refactor(file): move bmap generation into its own module
JanZachmann Sep 28, 2026
9842fef
test(file): cover ext4 journal edge cases and a flash onto old content
JanZachmann Sep 28, 2026
efea1b0
Merge remote-tracking branch 'upstream/main' into jz-2026-09-28-keep-…
JanZachmann Oct 23, 2026
68f21f4
fix(deps): resolve RUSTSEC-2026-0385 (ctutils)
JanZachmann Oct 23, 2026
7e75052
fix(file): detect truncated input and harden sparse decompress
JanZachmann Oct 28, 2026
a0da23c
fix(file): keep the ranges of the input bmap mapped
JanZachmann Oct 28, 2026
fb4441d
fix(file): address review findings on input bmap handling
JanZachmann Oct 28, 2026
cb3c476
feat(cli)!: rename bmap options to --input-bmap and --output-bmap
JanZachmann Oct 28, 2026
46d0362
fix(file): map zero blocks that a copy writes into a hole
JanZachmann Oct 28, 2026
02a712b
fix(file): reject an input bmap whose checksums do not match the image
JanZachmann Oct 28, 2026
5e827ac
docs(file): explain a checksum mismatch after a command without -b
JanZachmann Oct 28, 2026
279f8a8
feat(file): write the output bmap in Rust and keep the image sparse
JanZachmann Oct 28, 2026
57a1580
perf(file): compare only blocks that hold data in a partition write back
JanZachmann Oct 28, 2026
b59788c
test(file): check the bmaptool format only in the integration tests
JanZachmann Oct 28, 2026
53ebcd8
chore(docker): drop dd and sync from the image
JanZachmann Oct 28, 2026
d386816
fix(file): decompress images named .gz and .bz2
JanZachmann Oct 28, 2026
cdc299c
refactor(cli): share the bmap and compression options of image commands
JanZachmann Oct 28, 2026
92ce896
test(file): make the write back failure test work as root
JanZachmann Oct 28, 2026
04bb5f1
fix(file): name the output bmap after the output image
JanZachmann Oct 28, 2026
beafb45
perf(file): do not sync the temp image after a partition write
JanZachmann Oct 28, 2026
d7ba2da
test(file): cover a failed compress trailer and a partition past the …
JanZachmann Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 14 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
name = "omnect-cli"
readme = "README.md"
repository = "https://github.com/omnect/omnect-cli"
version = "0.28.5"
version = "0.29.0"

[dependencies]
actix-web = { version = "4.17", default-features = false, features = ["macros"] }
Expand All @@ -31,7 +31,6 @@ hmac = { version = "0.13", default-features = false }
mbrman = { version = "0.6", default-features = false }
omnect-crypto = { git = "https://github.com/omnect/omnect-crypto.git", tag = "0.5.0" }
keyring-core = { version = "1.0", default-features = false }
libfs = { version = "0.9", default-features = false }
log = { version = "0.4", default-features = false }
num_cpus = { version = "1.17", default-features = false }
oauth2 = { version = "5.0", default-features = false, features = [
Expand All @@ -44,6 +43,8 @@ reqwest = { version = "0.13", default-features = false, features = [
"json",
"native-tls",
] }
roxmltree = { version = "0.21", default-features = false, features = ["std"] }
rustix = { version = "1.1", default-features = false, features = ["fs", "std"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0", default-features = false }
serde_path_to_error = { version = "0.1", default-features = false }
Expand Down Expand Up @@ -81,5 +82,5 @@ tar = "0.4"

# metadata for building with cargo-deb (https://crates.io/crates/cargo-deb)
[package.metadata.deb]
depends = "bmap-tools, e2tools, keychain, libc6 (>= 2.39), libmagic1, libssl3 (>= 3.0.0), mtools"
depends = "e2tools, keychain, libc6 (>= 2.39), libmagic1, libssl3 (>= 3.0.0), mtools"
revision = ""
4 changes: 0 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ COPY ${debian_dir}/omnect-cli_${omnect_cli_version}_amd64.deb omnect-cli_${omnec

RUN apt-get update && \
apt-get install -y --no-install-recommends \
bmap-tools \
ca-certificates \
e2tools \
fdisk \
Expand All @@ -34,14 +33,11 @@ RUN <<EOT
mkdir -p /copy/status.d /copy/usr/lib

executables=(
/usr/bin/dd \
/usr/bin/e2cp \
/usr/bin/e2mkdir \
/usr/bin/fallocate \
/usr/bin/mcopy \
/usr/bin/omnect-cli \
/usr/bin/ssh-keygen \
/usr/bin/sync \
/usr/sbin/fdisk \
)

Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ docker run --rm -it \
omnect/omnect-cli:latest file copy-to-image --files /source/my-source-file,boot:/my-dest-file -i /source/my-image.wic
```

**Note1**: `-b` option to create bmap file is not supported by omnect-cli docker image.<br>
**Note2**: The ssh tunnel option requires some additional settings. See [here](Usage-with-docker) for more details.<br>
**Note3**: The docker inject command is not supported by omnect-cli docker image.<br>.
**Note1**: The ssh tunnel option requires some additional settings. See [here](Usage-with-docker) for more details.<br>
**Note2**: The docker inject command is not supported by omnect-cli docker image.<br>.

# Build from sources

Expand Down Expand Up @@ -184,6 +183,14 @@ omnect-cli file copy-to-image --help
- File permissions: inject `systemd-tmpfiles.d`
- Wifi: inject `wpa_supplicant-wlan0.conf`

## bmap files

Commands that change an image create a bmap file with `-b` (`--output-bmap`). It is written next to the output image as `<output image>.bmap`, e.g. `my-image.wic.bmap` for `my-image.wic` and `my-image.wic.xz.bmap` for `my-image.wic.xz`, and replaces a file with this name. `bmaptool` finds it there without `--bmap`.

The bmap of the input image is the base for it: `omnect-cli` takes the file given with `--input-bmap`, else `<image>.bmap` or the bmap of the uncompressed image next to the image, e.g. `my-image.wic.bmap` for `my-image.wic.xz` as the Yocto build creates it. The input bmap must have the size of the image and sha256 range checksums that match it, otherwise the command fails. A command without `-b` changes the image but not the bmap next to it, so when you run several commands on one image, use `-b` on each of them, or pass the matching bmap with `--input-bmap`. The created bmap maps every range of the input bmap, also when it holds only zeros, so a bmap-based flash overwrites what an earlier installation left there (partition table entries, FAT, ext4 journals). It also maps every block that the image file stores as data, which includes the blocks `omnect-cli` changed. The image file itself stays sparse.

Without an input bmap, the created bmap maps the whole image, and a flash writes every block.

## ssh tunnel

### Inject ssh tunnel credentials
Expand Down
6 changes: 4 additions & 2 deletions project-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,17 @@
- `src/docker.rs` — `docker pull --platform` + `docker save` for multi-arch images
- `src/image.rs` — firmware image architecture detection (ARM32/ARM64/x86_64)
- `src/file/mod.rs` — high-level image operations: identity config, certs, hostname patching
- `src/file/functions.rs` — partition read/modify/write via dd + e2cp/mcopy
- `src/file/functions.rs` — sparse partition read, modify via e2cp/mcopy, write back of changed blocks only
- `src/file/bmap.rs` — input bmap lookup and checksum check; writes the output bmap: input ranges (or the whole image) plus data blocks
- `src/file/sparse.rs` — data/hole ranges via `SEEK_DATA`/`SEEK_HOLE`, sparse range copy
- `src/file/partition.rs` — GPT/MBR partition table parsing
- `src/file/compression.rs` — xz/bzip2/gzip compress/decompress with auto-detection
- `src/validators/` — validation for identity config (TOML), device-update config (JSON), SSH keys
- `conf/*.template` — 11 config templates (identity, device-update, WiFi)

## 4. Repository-Specific Constraints

- External tools `e2cp`, `e2mkdir`, `mcopy`, `mmd`, `dd`, `fallocate`, `ssh-keygen`, `fdisk` must be available at runtime (Dockerfile copies them explicitly).
- External tools `e2cp`, `e2mkdir`, `mcopy`, `mmd`, `ssh-keygen`, `fdisk` must be available at runtime (Dockerfile copies them explicitly).
- Partition enum maps partition names to numbers differently for GPT vs MBR — see `file/functions.rs`.
- OAuth2 callback binds to `127.0.0.1:4000` and `[::1]:4000`; container mode overrides to `0.0.0.0`.
- `conf/` directory uses `.gitignore` to track only `*.template` files — actual configs are generated, never committed.
Expand Down
95 changes: 34 additions & 61 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,25 @@ use crate::file::{
compression::Compression,
functions::{FileCopyFromParams, FileCopyToParams, Partition},
};
use clap::Parser;
use clap::{Args, Parser};
use std::path::PathBuf;
use url::Url;

const COPYRIGHT: &str = "Copyright © 2021 by conplement AG";

#[derive(Args, Debug, Default)]
pub struct ImageOutput {
/// optional: create a bmap of the output image
#[arg(short = 'b', long = "output-bmap")]
pub output_bmap: bool,
/// optional: bmap of the input image; default: <image>.bmap or <uncompressed image>.bmap next to the image
#[arg(long = "input-bmap", requires = "output_bmap")]
pub input_bmap: Option<PathBuf>,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
pub compress_image: Option<Compression>,
}

// ToDo: command completion
#[derive(Parser, Debug)]
#[command(after_help = COPYRIGHT)]
Expand All @@ -34,12 +47,8 @@ pub enum Docker {
/// destination path of the docker image in the firmware image (must end in ".tar.gz")
#[clap(short = 'e', long = "dest")]
dest: PathBuf,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},
}

Expand All @@ -55,12 +64,8 @@ pub enum File {
/// path to wic image file (optionally compressed with xz, bzip2 or gzip)
#[arg(short = 'i', long = "image")]
image: PathBuf,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},
/// copy files from image
CopyFromImage {
Expand Down Expand Up @@ -88,12 +93,8 @@ pub enum IdentityConfig {
/// path to wic image file (optionally compressed with xz, bzip2 or gzip)
#[arg(short = 'i', long = "image")]
image: PathBuf,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},
/// EXPERIMENTAL: set transparent gateway config.toml file and additional certificates and keys
SetIotedgeGatewayConfig {
Expand All @@ -112,12 +113,8 @@ pub enum IdentityConfig {
/// path to device identity certificate key file
#[arg(short = 'k', long = "device_identity_key")]
device_identity_key: PathBuf,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},
/// EXPERIMENTAL: set leaf device config.toml file and additional certificate
SetIotLeafSasConfig {
Expand All @@ -130,12 +127,8 @@ pub enum IdentityConfig {
/// path to root ca certificate file
#[arg(short = 'r', long = "root_ca")]
root_ca: PathBuf,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},
/// set certificates in order to support X.509 based DPS provisioning and certificate renewal via EST
SetDeviceCertificate {
Expand All @@ -154,12 +147,8 @@ pub enum IdentityConfig {
/// period of validity in days
#[arg(short = 'D', long = "days")]
days: u32,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},
/// set certificates in order to support X.509 based DPS provisioning WITHOUT certificate renewal via EST
SetDeviceCertificateNoEst {
Expand All @@ -172,12 +161,8 @@ pub enum IdentityConfig {
/// path to wic image file (optionally compressed with xz, bzip2 or gzip)
#[arg(short = 'i', long = "image")]
image: PathBuf,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},
/// generate and set bootstrap certificate for edge ca issuance/renewal.
SetEdgeCaCertificate {
Expand All @@ -196,12 +181,8 @@ pub enum IdentityConfig {
/// period of validity in days
#[arg(short = 'D', long = "days")]
days: u32,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},
}

Expand All @@ -217,12 +198,8 @@ pub enum IotHubDeviceUpdate {
/// path to wic image file (optionally compressed with xz, bzip2 or gzip)
#[arg(short = 'i', long = "image")]
image: PathBuf,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},
/// import update to azure iot-hub
ImportUpdate {
Expand Down Expand Up @@ -336,12 +313,8 @@ pub enum SshConfig {
/// path to public key of the ssh root ca
#[arg(short = 'r', long = "root_ca")]
root_ca: PathBuf,
/// optional: generate bmap file (currently not working in docker image)
#[arg(short = 'b', long = "generate-bmap-file")]
generate_bmap: bool,
/// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=')
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
#[command(flatten)]
output: ImageOutput,
},

/// set ssh connection parameters (see
Expand Down
Loading
Loading