Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 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
1ce9e1a
refactor(file): share the sparse copy into a new file
JanZachmann Oct 29, 2026
c18fc17
fix(file): do not write the image back after copy-from-image
JanZachmann Oct 29, 2026
c51066f
feat(file)!: fail on output name conflicts, add --output-image and --…
JanZachmann Oct 29, 2026
f0ead21
perf(file): check the input bmap of an uncompressed image before the …
JanZachmann Oct 29, 2026
be84b33
fix(file): read concatenated compressed streams
JanZachmann Oct 29, 2026
453b124
fix(file): check every bmap that bmaptool would use
JanZachmann Oct 29, 2026
d0800f8
fix(file)!: name a bzip2 image .bz2
JanZachmann Oct 29, 2026
c477f9a
fix(file): check the extension of the --output-image name
JanZachmann Oct 29, 2026
7ab73f8
fix(file): write the output image through a rename
JanZachmann Oct 29, 2026
20c114b
refactor(file): use tempfile::TempDir for the temp image
JanZachmann Oct 29, 2026
342bea1
fix(file): keep the partition files in their own temp dir
JanZachmann Oct 29, 2026
53672ec
perf(file): run copy-from-image on an uncompressed image directly
JanZachmann Oct 29, 2026
b328b45
fix(file): read partitions of a GPT with 4096-byte sectors
JanZachmann Oct 29, 2026
ee181b4
docs(file): describe the --output-image extension rule and the rename
JanZachmann Oct 29, 2026
961f95b
fix(file): keep the name of an image with any extension of the -p com…
JanZachmann Oct 29, 2026
3504b5a
fix(file): find the input bmap the way bmaptool does
JanZachmann Oct 29, 2026
3da5a4d
fix(file): sync the output image and remove the old bmap before the r…
JanZachmann Oct 29, 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
19 changes: 16 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,20 @@ omnect-cli file copy-to-image --help
- File permissions: inject `systemd-tmpfiles.d`
- Wifi: inject `wpa_supplicant-wlan0.conf`

## Output image

A command that changes an image writes the result to the input image. For a compressed image it writes the uncompressed image next to it, e.g. `my-image.wic` for `my-image.wic.xz`. With `-p` (`--pack-image`) it adds the extension of the compression (`.xz`, `.bz2` or `.gz`), e.g. `my-image.wic.xz` for `my-image.wic`. An image that already has an extension of that compression keeps its name. `-o` (`--output-image`) writes the result to the given path instead, and the input image stays unchanged. Its name must have the extension of the `-p` compression, or no compression extension without `-p`.

The command fails instead of overwriting an existing file that is not the input image. To overwrite it, name it with `-o`. The output image is written to a new file in its directory and then renamed, so the directory needs write access and space for one more image. The new file belongs to the user who runs `omnect-cli`. `copy-from-image` only reads the image and writes nothing.

## 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 command fails if this file is the bmap of an image that stays, e.g. `my-image.wic.bmap` when `-i my-image.wic.xz` writes `my-image.wic`, also with `--input-bmap` or `--no-input-bmap`. Choose another output with `-o` then.

The bmap of the input image is the base for it: `omnect-cli` takes the file given with `--input-bmap`, else the bmap next to the image that `bmaptool` would use: `<image>.bmap`, else the same with one extension of the image removed at a time, 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` fails if such a bmap exists for the output image (e.g. `my-image.wic.bmap` for `my-image.wic.xz`), because `bmaptool` would use it and it would not match the image. So when you run several commands on one image, use `-b` on each of them. `--no-input-bmap` skips a bmap next to the image that does not match, and the created bmap then maps the whole image. 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
105 changes: 44 additions & 61 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,35 @@ 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)]
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: the bmap next to the image that bmaptool would use
#[arg(long = "input-bmap", requires = "output_bmap")]
pub input_bmap: Option<PathBuf>,
/// optional: do not use a bmap next to the image, so the bmap maps the whole image
#[arg(
long = "no-input-bmap",
requires = "output_bmap",
conflicts_with = "input_bmap"
)]
pub no_input_bmap: bool,
/// optional: write the output image to this path, with the --pack-image extension or no compression extension; default: the input image, without the compression extension, plus the --pack-image extension
#[arg(short = 'o', long = "output-image")]
pub output_image: 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 +57,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 +74,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 +103,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 +123,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 +137,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 +157,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 +171,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 +191,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 +208,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 +323,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