Compare commits

...

49 commits

Author SHA1 Message Date
Imbus
1ca6f8dfdf Fix warning in musl related to ioctl
Ioctl's in musl take ints, while glibc take unsigned longs. When not
using glibc, default to casting the ioctl number to an int. Tests
passing.
2026-02-21 08:06:37 +01:00
Imbus
d90c05ca77 Tests: test.sh does not use sudo internally, now runs under alpine with musl 2026-02-21 08:05:09 +01:00
Imbus
8601db8811 Initial bash completion 2026-02-21 07:24:25 +01:00
Imbus
4b853f9c16 Spelling errors 2026-02-21 06:17:07 +01:00
Imbus
febcedc2f3 Provide a more descriptive introduction and a summary of writeimg's features 2026-02-21 06:11:40 +01:00
Imbus
5e9245f059 Bump the help section in the readme to latest version 2026-02-21 06:05:02 +01:00
Imbus
d02fc5f386 Provide a detailed description of design considerations and future plans 2026-02-21 06:03:41 +01:00
Imbus
2e02a056cc Now also reports the size of the block device before asking to continue 2026-02-21 03:57:30 +01:00
Imbus
60e7179766 Fix forgotten assignment to last 2026-02-15 03:22:26 +01:00
Imbus
bf55c2d0a9 Performance: Update progress bar only when changed 2026-02-15 03:10:54 +01:00
Imbus
e038ab224d Simplify: fprintf -> printf 2026-02-15 02:45:12 +01:00
Imbus
c1e38b365c Uninstall target, PHONY 2026-02-14 16:02:47 +01:00
Imbus
d0728a87c0 Manpage permissions to 644 2026-02-14 15:55:35 +01:00
Imbus
71ef502cea Include manpage in 'make install' 2026-02-13 17:33:43 +01:00
Imbus
a62728e9db Initial manpage 2026-02-13 17:30:42 +01:00
Imbus
f37e28c4ef Remove accidental inclusion of unfinished WI_FILE check 2026-02-09 14:44:42 +01:00
Imbus
4127ad6333 More tests
Include some notes about cleanup and accidentally deleting loop-control
2026-02-09 14:42:56 +01:00
Imbus
d1d3b3cd45 Rename dev_name and filename to iname/oname in preparation for enabling file outputs 2026-02-09 14:42:28 +01:00
Imbus
36a98b2630 Simple test 2026-02-09 13:59:10 +01:00
Imbus
9723924222 Use stylized name in help section 2026-02-09 13:53:46 +01:00
Imbus
d0a3db77e9 Remove commented CFLAGS appends for libudev 2026-02-09 13:53:26 +01:00
Imbus
078ba792f0 Get version string from git, instead of hardcoded in makefile 2026-02-09 13:52:57 +01:00
Imbus
734c1c10ec Use bitmasking to keep track of flags 2026-02-09 13:36:59 +01:00
Imbus
117d01fcbe v0.2.1 bump 2026-02-07 18:52:56 +01:00
Imbus
692f89d6c2 Formatting 2026-02-07 18:44:40 +01:00
Imbus
5cbfc38e62 Add some introduction to readme and brush up the formatting 2026-02-07 18:42:38 +01:00
Imbus
008e9e7ca9 Readme txt -> md 2026-02-07 18:39:18 +01:00
Imbus
9d2645ad65 Readme 2026-02-07 18:38:48 +01:00
Imbus
53239324ef Tell the user how much data will be written beforehand 2026-02-07 18:24:32 +01:00
Imbus
a1556ca383 Set (y/n) prompt to (y/N) to better reflect behaviour 2026-02-07 18:23:59 +01:00
Imbus
0a4626e7d1 Fix formatting when error occurs 2026-02-07 18:23:33 +01:00
Imbus
ab43512e42 Bump to v0.2.0 2026-02-07 17:36:37 +01:00
Imbus
3bfbfd46fa Move noisy metadata info into help section 2026-02-07 17:36:29 +01:00
Imbus
c67c68615a Add noconfirm flag 2026-02-07 17:24:31 +01:00
Imbus
13e27bbcc1 Flush block device with ioctl before reading it back 2026-02-07 17:24:16 +01:00
Imbus
c08ee6a738 Guard for verify only 2026-02-07 17:23:27 +01:00
Imbus
19bf88b2e6 Block size set to 1MiB 2026-02-07 17:22:06 +01:00
Imbus
824f975510 Include EXTRA_CFLAGS for optional extras 2026-02-07 17:21:48 +01:00
Imbus
be2f00b91c Clang-format 2026-02-07 17:20:00 +01:00
Imbus
5277b78fd3 Prompt user before doing anything 2026-02-07 14:55:24 +01:00
Imbus
21f2420d42 Now includes a simple progress bar, percent only 2026-02-07 14:55:05 +01:00
Imbus
9e96ebaca0 Assertion on file descriptor write access 2026-02-07 14:54:25 +01:00
Imbus
804a6b1485 Include the file size in the write job struct for future reference in data rate and progress bar 2026-02-07 13:52:13 +01:00
Imbus
c11bdd7f47 Assert that the file size is above zero 2026-02-07 13:51:44 +01:00
Imbus
88a1a87ca6 Guard for block device name string starting with "/dev/" 2026-02-07 13:51:16 +01:00
Imbus
cf85ca788a Verification logic now verifies both by-block and crc32 2026-02-07 13:23:53 +01:00
Imbus
2c2fcd6c8e Bring in single-header crc32 2026-02-07 13:22:29 +01:00
Imbus
3d61eeb1ab Make block size a compile-time tunable 2026-02-07 13:22:05 +01:00
Imbus
1a73c5a248 Rename: BUFSIZE -> BLOCKSIZE 2026-02-07 13:06:52 +01:00
9 changed files with 544 additions and 62 deletions

20
.clang-format Normal file
View file

@ -0,0 +1,20 @@
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: Never
ColumnLimit: 120
AllowShortLoopsOnASingleLine: true
AllowShortFunctionsOnASingleLine: false
AlwaysBreakTemplateDeclarations: true
BreakConstructorInitializers: BeforeComma
AlignConsecutiveDeclarations:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignFunctionPointers: false
PadOperators: false
AlignConsecutiveMacros: true
AllowShortCaseLabelsOnASingleLine: true
SeparateDefinitionBlocks: Always
BinPackArguments: false

View file

@ -1,17 +1,14 @@
GITREV ?= $(shell git describe --dirty --always)
BLDDATE ?= $(shell date -I)
CR_YEAR ?= $(shell date +%Y)
VERSION ?= "v0.1.2"
VERSION ?= "$(shell git describe --tags --always --abbrev=0 2>/dev/null || git rev-parse --short HEAD)"
CFLAGS ?= -Wall -Wextra -Wpedantic -O2 -std=gnu99
CFLAGS += -DGITREV='"$(GITREV)"'
CFLAGS += -DBLDDATE='"$(BLDDATE)"'
CFLAGS += -DCR_YEAR='"$(CR_YEAR)"'
CFLAGS += -DVERSION='$(VERSION)'
# Soon...
# CFLAGS += $(shell pkg-config --cflags libudev)
# LIBS += $(shell pkg-config --libs libudev)
CFLAGS += $(EXTRA_CFLAGS)
PREFIX ?= /usr/local
DESTDIR ?=
@ -24,4 +21,19 @@ clean:
install: writeimg
install -d $(DESTDIR)$(PREFIX)/bin
install -d $(DESTDIR)$(PREFIX)/share/man/man1
install -m 0755 $< $(DESTDIR)$(PREFIX)/bin/$<
install -m 0644 $<.1 $(DESTDIR)$(PREFIX)/share/man/man1/$<.1
# Note that this bypasses PREFIX, since
# bash does not source /usr/local by default
install_bash: writeimg_completion_bash.sh
install -d $(DESTDIR)/etc/bash_completion.d
install -m 0644 $< $(DESTDIR)/etc/bash_completion.d/$<
uninstall:
rm -f $(DESTDIR)$(PREFIX)/bin/writeimg
rm -f $(DESTDIR)$(PREFIX)/share/man/man1/writeimg.1
rm -f $(DESTDIR)/etc/bash_completion.d/writeimg*
.PHONY: clean install uninstall

117
README.md Normal file
View file

@ -0,0 +1,117 @@
# WriteIMG Image Writer
WriteIMG is a simple dd-like image writer with additional security checks for
Linux and soon FreeBSD. It tells you what you're about to do and warns you
about potential problems before they occur. It also features automatic image
verifications, and uses a suitable block size for fast and gentle writing.
Say goodbye to fried flash and overwritten bootloaders, no more footguns!
```
WriteIMG v0.2.3, Rev. d02fc5f
In honor of SwePwnage - the OG disk destroyer
Copyright (C) 2026 Imbus, BSD-2-Clause
Build date: 2026-02-21
Usage:
writeimg [-v] -d <device> <file.img>
Args:
<file.img> Binary image file
-v Verify only
-d device Target block device
-h, --help Print this help message
-n, --noconfirm Do not ask for permission
-V, --version Print version
```
We also provide a manpage, which targets OpenBSD-level quality. Incorrect or
outdated information is a bug.
## Testing
For testing, there is a shell script "test.sh" included. It sets up a block dev
and writes (and verifies) a bunch of nonsense to it. It boils down to:
```
dd if=/dev/zero of=./disk.img bs=1M count=1024
losetup -fP ./disk.img
losetup -a
losedup -d /dev/loop0
```
Writes to a regular files is not currently in scope, although it would simplify
testing.
## Design Considerations
Most of the sanity checking is currently highly Linux specific. We should
prefer general/posix solutions that reach **at least** FreeBSD, preferably
OpenBSD and Solaris as well. FreeBSD did implement procfs, but its a Linux-ism
and it has since been deprecated. I would prefer not to turn this rather simple
code into macro-mozaic, as i've seen other similar projects do. After all, this
is just a juiced-up dd-rewrite at its core.
**Apple products are unsupported.** Im simply not interested in ensuring
compatibility with a walled-garden ecosystem. If *you* are, we can change that.
At the time of writing, my FreeBSD server is down for maintenance, which means
all of my development and testing is focused on AMD64, AArch64 and AArch32
Linux.
In the setup phase of the program, we can absolutely afford to do lots of
sanity checking via syscalls. Between each block write, we flush the disk
buffers. These flushes are larger (1 MiB when buffer is full) than the sector
size (often 4k or 64k) of the flash, so as far as i know, this is a gentle way
to write flash, and should not incur any significant performance overhead. 1
MiB is also a multiple of the most common sector sizes. We could write it all
with no flushing, but that would mean the progress indicator will measure
buffered writes, which is useless.
It should be possible to induce optimal block size, but this idea has not been
explored yet.
Currently when verifying the written data, we read from both the input file and
the output block device and do a byte-by-byte comparison. A CRC32 is also
calculated in the first pass of the input file read. In the second pass (the
verification stage), we calculate the CRC32 of the block device data and
compare that to our previous result. **This means that we currently use two
separate methods of verification**. The program allocates **two** separate
buffers in the startup phase for comparisons. This will change in the coming
releases, and we will rely only on the CRC.
De-allocation is handled in the interrupt vector as well as in the 'sad-paths',
but ultimately this program can be regarded as samurai-principled. We try to
handle deallocation, but exit on failure and let the kernel handle the rest.
The use of a crypto-grade checksumming algorithm was considered, but was
ultimately rejected in favour of a lookup based CRC32. Its simpler, faster and
easier to understand (See: [crc32.h](./crc32.h)). We may include a compile-time
option to disable the lookup table to reduce size for really small targets, but
we speculate that those targets are already satisfied with busybox-dd.
We also considered using libudev or any of its analogues, to determine the type
of block device (spinning or flash), but my somewhat inconclusive research
indicates that it does not include functionality to determine the medium type
(usb/sd/sata), which is ultimately what i would like to warn the user about.
The libudev library is also Linux specific.
We also need completion scripts for the most common shells. This includes csh,
bash and zsh. Should be easy enough when we set our minds to it.
See:
- `grep -nE 'BLK[A-Za-z0-9]+' /usr/include/linux/fs.h`
In particular, we're interested in BLKGETSIZE64 and BLKFLSBUF for now.
We can read the device info from:
- /sys/class/block/[name]/*
Like:
- /sys/class/block/[name]/device/model
- /sys/class/block/[name]/size
## Inspiration
See:
- https://github.com/landley/toybox/blob/master/toys/posix/dd.c
- https://github.com/illiliti/libudev-zero
- https://github.com/LoupVaillant/Monocypher

View file

@ -1,11 +0,0 @@
Simple dd-like image writer with additional security checks.
dd if=/dev/zero of=./disk.img bs=1M count=1024
losetup -fP ./disk.img
losetup -a
losedup -d /dev/loop0
See:
https://github.com/landley/toybox/blob/master/toys/posix/dd.c
https://github.com/illiliti/libudev-zero
https://github.com/LoupVaillant/Monocypher

78
crc32.h Normal file
View file

@ -0,0 +1,78 @@
#ifndef CRC32_H
#define CRC32_H
#include <stddef.h>
#include <stdint.h>
/*
* CRC-32 (IEEE 802.3, gzip, zip, etc.)
* Polynomial: 0xEDB88320 (reflected)
* Init value: 0xFFFFFFFF
* Final XOR: 0xFFFFFFFF
*/
#define CRC32_INIT ((uint32_t)0xFFFFFFFFU)
#define CRC32_XOR ((uint32_t)0xFFFFFFFFU)
static const uint32_t crc32_table[256] = {
0x00000000U, 0x77073096U, 0xEE0E612CU, 0x990951BAU, 0x076DC419U, 0x706AF48FU, 0xE963A535U, 0x9E6495A3U, 0x0EDB8832U,
0x79DCB8A4U, 0xE0D5E91EU, 0x97D2D988U, 0x09B64C2BU, 0x7EB17CBDU, 0xE7B82D07U, 0x90BF1D91U, 0x1DB71064U, 0x6AB020F2U,
0xF3B97148U, 0x84BE41DEU, 0x1ADAD47DU, 0x6DDDE4EBU, 0xF4D4B551U, 0x83D385C7U, 0x136C9856U, 0x646BA8C0U, 0xFD62F97AU,
0x8A65C9ECU, 0x14015C4FU, 0x63066CD9U, 0xFA0F3D63U, 0x8D080DF5U, 0x3B6E20C8U, 0x4C69105EU, 0xD56041E4U, 0xA2677172U,
0x3C03E4D1U, 0x4B04D447U, 0xD20D85FDU, 0xA50AB56BU, 0x35B5A8FAU, 0x42B2986CU, 0xDBBBC9D6U, 0xACBCF940U, 0x32D86CE3U,
0x45DF5C75U, 0xDCD60DCFU, 0xABD13D59U, 0x26D930ACU, 0x51DE003AU, 0xC8D75180U, 0xBFD06116U, 0x21B4F4B5U, 0x56B3C423U,
0xCFBA9599U, 0xB8BDA50FU, 0x2802B89EU, 0x5F058808U, 0xC60CD9B2U, 0xB10BE924U, 0x2F6F7C87U, 0x58684C11U, 0xC1611DABU,
0xB6662D3DU, 0x76DC4190U, 0x01DB7106U, 0x98D220BCU, 0xEFD5102AU, 0x71B18589U, 0x06B6B51FU, 0x9FBFE4A5U, 0xE8B8D433U,
0x7807C9A2U, 0x0F00F934U, 0x9609A88EU, 0xE10E9818U, 0x7F6A0DBBU, 0x086D3D2DU, 0x91646C97U, 0xE6635C01U, 0x6B6B51F4U,
0x1C6C6162U, 0x856530D8U, 0xF262004EU, 0x6C0695EDU, 0x1B01A57BU, 0x8208F4C1U, 0xF50FC457U, 0x65B0D9C6U, 0x12B7E950U,
0x8BBEB8EAU, 0xFCB9887CU, 0x62DD1DDFU, 0x15DA2D49U, 0x8CD37CF3U, 0xFBD44C65U, 0x4DB26158U, 0x3AB551CEU, 0xA3BC0074U,
0xD4BB30E2U, 0x4ADFA541U, 0x3DD895D7U, 0xA4D1C46DU, 0xD3D6F4FBU, 0x4369E96AU, 0x346ED9FCU, 0xAD678846U, 0xDA60B8D0U,
0x44042D73U, 0x33031DE5U, 0xAA0A4C5FU, 0xDD0D7CC9U, 0x5005713CU, 0x270241AAU, 0xBE0B1010U, 0xC90C2086U, 0x5768B525U,
0x206F85B3U, 0xB966D409U, 0xCE61E49FU, 0x5EDEF90EU, 0x29D9C998U, 0xB0D09822U, 0xC7D7A8B4U, 0x59B33D17U, 0x2EB40D81U,
0xB7BD5C3BU, 0xC0BA6CADU, 0xEDB88320U, 0x9ABFB3B6U, 0x03B6E20CU, 0x74B1D29AU, 0xEAD54739U, 0x9DD277AFU, 0x04DB2615U,
0x73DC1683U, 0xE3630B12U, 0x94643B84U, 0x0D6D6A3EU, 0x7A6A5AA8U, 0xE40ECF0BU, 0x9309FF9DU, 0x0A00AE27U, 0x7D079EB1U,
0xF00F9344U, 0x8708A3D2U, 0x1E01F268U, 0x6906C2FEU, 0xF762575DU, 0x806567CBU, 0x196C3671U, 0x6E6B06E7U, 0xFED41B76U,
0x89D32BE0U, 0x10DA7A5AU, 0x67DD4ACCU, 0xF9B9DF6FU, 0x8EBEEFF9U, 0x17B7BE43U, 0x60B08ED5U, 0xD6D6A3E8U, 0xA1D1937EU,
0x38D8C2C4U, 0x4FDFF252U, 0xD1BB67F1U, 0xA6BC5767U, 0x3FB506DDU, 0x48B2364BU, 0xD80D2BDAU, 0xAF0A1B4CU, 0x36034AF6U,
0x41047A60U, 0xDF60EFC3U, 0xA867DF55U, 0x316E8EEFU, 0x4669BE79U, 0xCB61B38CU, 0xBC66831AU, 0x256FD2A0U, 0x5268E236U,
0xCC0C7795U, 0xBB0B4703U, 0x220216B9U, 0x5505262FU, 0xC5BA3BBEU, 0xB2BD0B28U, 0x2BB45A92U, 0x5CB36A04U, 0xC2D7FFA7U,
0xB5D0CF31U, 0x2CD99E8BU, 0x5BDEAE1DU, 0x9B64C2B0U, 0xEC63F226U, 0x756AA39CU, 0x026D930AU, 0x9C0906A9U, 0xEB0E363FU,
0x72076785U, 0x05005713U, 0x95BF4A82U, 0xE2B87A14U, 0x7BB12BAEU, 0x0CB61B38U, 0x92D28E9BU, 0xE5D5BE0DU, 0x7CDCEFB7U,
0x0BDBDF21U, 0x86D3D2D4U, 0xF1D4E242U, 0x68DDB3F8U, 0x1FDA836EU, 0x81BE16CDU, 0xF6B9265BU, 0x6FB077E1U, 0x18B74777U,
0x88085AE6U, 0xFF0F6A70U, 0x66063BCAU, 0x11010B5CU, 0x8F659EFFU, 0xF862AE69U, 0x616BFFD3U, 0x166CCF45U, 0xA00AE278U,
0xD70DD2EEU, 0x4E048354U, 0x3903B3C2U, 0xA7672661U, 0xD06016F7U, 0x4969474DU, 0x3E6E77DBU, 0xAED16A4AU, 0xD9D65ADCU,
0x40DF0B66U, 0x37D83BF0U, 0xA9BCAE53U, 0xDEBB9EC5U, 0x47B2CF7FU, 0x30B5FFE9U, 0xBDBDF21CU, 0xCABAC28AU, 0x53B39330U,
0x24B4A3A6U, 0xBAD03605U, 0xCDD70693U, 0x54DE5729U, 0x23D967BFU, 0xB3667A2EU, 0xC4614AB8U, 0x5D681B02U, 0x2A6F2B94U,
0xB40BBE37U, 0xC30C8EA1U, 0x5A05DF1BU, 0x2D02EF8DU,
};
static inline uint32_t crc32_update_raw(uint32_t crc, const void *data, unsigned int length) {
const uint8_t *p = (const uint8_t *)data;
while (length--) {
crc = (crc >> 8) ^ crc32_table[(crc ^ *p++) & 0xFFU];
}
return crc;
}
static inline uint32_t crc32(const void *data, unsigned int length) {
uint32_t crc = CRC32_INIT;
crc = crc32_update_raw(crc, data, length);
return crc ^ CRC32_XOR;
}
static inline uint32_t crc32_init(void) {
return CRC32_INIT;
}
static inline uint32_t crc32_update(uint32_t crc, const void *data, unsigned int length) {
return crc32_update_raw(crc, data, length);
}
static inline uint32_t crc32_finalize(uint32_t crc) {
return crc ^ CRC32_XOR;
}
#undef CRC32_XOR
#undef CRC32_INIT
#endif // CRC32_H

72
test.sh Normal file
View file

@ -0,0 +1,72 @@
#!/usr/env/bin bash
set -e
# set -x # For debugging
# If you ever mess this up:
# $ mknod /dev/loop-control c 10 237
# $ chmod 600 /dev/loop-control
# $ chown root:root /dev/loop-control
#
# For cleanup:
# $ find /dev -maxdepth 1 -type b -name 'loop[0-9]*' -exec rm -f {} \;
if [ "$(id -u)" -ne 0 ]; then
echo "Run as root. We need permissions to write and setup a loop device."
exit 1
fi
DISKFILE="/tmp/disk.img"
BINFILE="/tmp/file.bin"
LOOPNUM=$((RANDOM % 156 + 100))
LOOPDEV="/dev/loop${LOOPNUM}"
echo "Using device: ${LOOPDEV}"
cleanup() {
echo "Cleaning up..."
set +e -x
losetup -d ${LOOPDEV}
rm ${LOOPDEV}
rm ${BINFILE} ${DISKFILE}
}
trap cleanup EXIT INT TERM
if losetup ${LOOPDEV} >/dev/null 2>&1; then
echo "${LOOPDEV} already in use" >&2
cleanup
exit 1
fi
if [ ! -f ${DISKFILE} ]; then
echo "Creating ${DISKFILE}"
dd if=/dev/zero of=${DISKFILE} bs=1M count=256
fi
if [ ! -f ${BINFILE} ]; then
echo "Creating ${BINFILE}"
dd if=/dev/urandom of=${BINFILE} bs=1M count=64
fi
if [ ! -e ${LOOPDEV} ]; then
mknod ${LOOPDEV} b 7 ${LOOPNUM} # busybox needs this
losetup ${LOOPDEV} ${DISKFILE}
fi
./writeimg -nd ${LOOPDEV} ${BINFILE}
./writeimg -vnd ${LOOPDEV} ${BINFILE}
./writeimg -nd ${LOOPDEV} ./writeimg
./writeimg -vnd ${LOOPDEV} ./writeimg
./writeimg -nd ${LOOPDEV} ./LICENSE
./writeimg -vnd ${LOOPDEV} ./LICENSE
# Redirect this to avoid confusion
! ./writeimg -vnd ${LOOPDEV} ./crc32.h 2>/dev/null
GREEN="\e[32m"
RESET="\e[0m"
echo -e "\n\n${GREEN}Looks good!${RESET}"

38
writeimg.1 Normal file
View file

@ -0,0 +1,38 @@
.TH WRITEIMG 1 "February 2026" "writeimg" "User Commands"
.SH NAME
writeimg \- Write an image to a block device
.SH SYNOPSIS
.B writeimg
\fB\-d\fR \fIDEVICE\fR \fIIMAGE\fR
.br
.B writeimg
\fB\-\-device\fR \fIDEVICE\fR \fIIMAGE\fR
.SH DESCRIPTION
.B writeimg
reads the input
.I IMAGE
and writes its contents to
.I DEVICE
The output device must be specified explicitly using the
.B \-d
or
.B \-\-device
option. The image file is given as a positional argument.
.SH OPTIONS
.TP
.BR \-d ", " \-\-device" " DEVICE
Device to write to.
.TP
.BR \-n ", " \-\-noconfirm
Do not require confirmation from user.
.TP
.BR \-v ", " \-\-verify
Verify only, do not write
.SH EXAMPLES
Write an image to a block device:
.PP
.nf
writeimg -d /dev/sdx file.img
.fi

View file

@ -1,7 +1,9 @@
#include "crc32.h"
#include <assert.h>
#include <fcntl.h>
#include <getopt.h>
#include <libgen.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
@ -10,6 +12,11 @@
#include <sys/stat.h>
#include <unistd.h>
#if __linux__
#include <linux/fs.h> /* IOTCL flush number */
#include <sys/ioctl.h> /* IOCTL */
#endif
#ifndef GITREV
#define GITREV "unknown"
#endif
@ -22,32 +29,65 @@
#define CR_YEAR "2026"
#endif
#ifndef BLOCKSIZE
#define BLOCKSIZE (1024 * 1024)
#endif
#define WI_VERIFY (1 << 0)
#define WI_WRITE (1 << 1)
#define WI_ASK (1 << 2)
#define BYTES_TO_MIB(bts) ((double)bts / (1024 * 1024))
#define BAR_WIDTH 50
void print_progress(int current, int total) {
static int last = INT_MAX;
float fraction = (float)current / total;
int filled = (int)(fraction * BAR_WIDTH);
if (filled == last)
return; /* Avoid unnecessary io/flushes */
last = filled;
printf("\r[");
for (int i = 0; i < BAR_WIDTH; i++) {
if (i < filled)
putchar('#');
else
putchar(' ');
}
printf("] %3d%%", (int)(fraction * 100));
fflush(stdout);
}
// clang-format off
const char help[] =
"Usage:\n"
" writeimg [-v] -d <device> <file.img>\n"
"\n"
"Args:\n"
" <file.img> Binary image file\n"
" -v Verify only\n"
" -d device Target block device\n"
" -h, --help Print this help message\n"
" -V, --version Print version\n"
" <file.img> Binary image file\n"
" -v Verify only\n"
" -d device Target block device\n"
" -h, --help Print this help message\n"
" -n, --noconfirm Do not ask for permission\n"
" -V, --version Print version\n"
"\0";
// clang-format on
const char copyright[] = "Copyright (C) %s Imbus, BSD-2-Clause\n";
#define BUFSIZE (4 * 1024 * 1024)
struct write_job {
char *filename;
char *dev_name;
char *iname;
char *oname;
char *buffer;
char *buffer2; /* For memcmp integrity checks */
size_t bufsize;
size_t block_size;
char verify_only;
size_t total_bytes;
char flags;
} wjob = {0};
typedef struct write_job write_job_t;
@ -65,38 +105,65 @@ void int_handler(int signum) {
}
int perform_write(write_job_t *job) {
int block_fd = open(job->dev_name, O_RDWR);
int file_fd = open(job->filename, O_RDONLY);
int block_fd = open(job->oname, O_RDWR);
int file_fd = open(job->iname, O_RDONLY);
assert(block_fd >= 0);
assert(file_fd >= 0);
/* TODO: Checks */
assert(job->bufsize >= job->block_size);
while (!job->verify_only) {
int crc = crc32_init();
size_t b_written = 0;
while (42) {
fsync(block_fd);
ssize_t read_bytes = read(file_fd, job->buffer, job->block_size);
assert(read_bytes >= 0);
if (read_bytes == 0) {
fsync(block_fd);
crc = crc32_finalize(crc);
if (job->flags & WI_WRITE) {
printf("\nWriting done...\n");
assert(job->total_bytes == b_written);
} else
assert(0 == b_written);
break; /* Finished */
}
if (read_bytes < 0) {
fprintf(stderr, "%s: Read error\n", job->filename);
fprintf(stderr, "%s: Read error\n", job->iname);
perror("Read");
exit(EXIT_FAILURE);
}
ssize_t written_bytes = write(block_fd, job->buffer, read_bytes);
if (written_bytes < 0) {
fprintf(stderr, "%s: Write error\n", job->dev_name);
perror("Write");
exit(EXIT_FAILURE);
}
crc = crc32_update(crc, job->buffer, read_bytes);
if (job->flags & WI_WRITE) {
ssize_t written_bytes = write(block_fd, job->buffer, read_bytes);
if (written_bytes < 0) {
fprintf(stderr, "%s: Write error\n", job->oname);
perror("Write");
exit(EXIT_FAILURE);
}
print_progress(b_written += written_bytes, job->total_bytes);
} /* Else maybe give some helpful insights? */
}
lseek(block_fd, 0, SEEK_SET);
lseek(file_fd, 0, SEEK_SET);
memset(job->buffer, 0, BUFSIZE);
memset(job->buffer2, 0, BUFSIZE);
memset(job->buffer, 0, BLOCKSIZE);
memset(job->buffer2, 0, BLOCKSIZE);
/* This is essentially $ blockdev --flushbufs */
ioctl(block_fd, BLKFLSBUF);
int crc_back = crc32_init();
b_written = 0;
while (1) {
ssize_t read_file = read(file_fd, job->buffer2, job->block_size);
@ -106,12 +173,21 @@ int perform_write(write_job_t *job) {
assert(read_block >= 0);
assert(read_file >= 0);
if (read_file == 0)
if (read_file == 0) {
crc_back = crc32_finalize(crc_back);
assert(crc_back == crc);
assert(job->total_bytes == b_written);
printf("\nVerification done!\n");
break;
}
crc_back = crc32_update(crc_back, job->buffer, read_file);
print_progress(b_written += read_block, job->total_bytes);
int cmp = memcmp((const void *)job->buffer2, (const void *)job->buffer, (size_t)read_file);
if (0 != cmp) {
fprintf(stderr, "WARNING: File did not verify correctly!\n");
fflush(stdout);
fprintf(stderr, "\nWARNING: File did not verify correctly!\n");
exit(EXIT_FAILURE);
}
}
@ -125,16 +201,12 @@ static const struct option longopts[] = {
{"help", 0, 0, 'h'},
{"version", 0, 0, 'V'},
{"device", 1, 0, 'd'},
{"noconfirm", 0, 0, 'n'},
{NULL, 0, 0, 0},
};
int main(int argc, char *argv[]) {
printf("%s %s, Rev. %s\n", basename(argv[0]), VERSION, GITREV);
fprintf(stdout, copyright, CR_YEAR);
#ifdef BLDDATE
printf("Build date: %s\n", BLDDATE);
#endif
printf("\n");
printf("%s %s, Rev. %s\n", "WriteIMG", VERSION, GITREV);
/* Line buffering, system allocated */
setvbuf(stdout, NULL, _IOLBF, 0);
@ -145,14 +217,26 @@ int main(int argc, char *argv[]) {
signal(SIGHUP, int_handler);
signal(SIGTERM, int_handler);
wjob.flags = WI_VERIFY | WI_WRITE | WI_ASK;
int c = {0};
while ((c = getopt_long(argc, argv, "vd:hV", longopts, 0)) != -1) {
while ((c = getopt_long(argc, argv, "vd:hnV", longopts, 0)) != -1) {
switch (c) {
case 'v': ++wjob.verify_only; continue;
case 'd': wjob.dev_name = optarg; continue;
case 'v':
wjob.flags |= WI_VERIFY;
wjob.flags &= ~WI_WRITE;
continue;
case 'd': wjob.oname = optarg; continue;
case 'h': break;
case 'n': wjob.flags &= ~WI_ASK; continue;
case 'V': exit(EXIT_SUCCESS);
}
printf("In honor of SwePwnage - the OG disk destroyer\n");
printf(copyright, CR_YEAR);
#ifdef BLDDATE
printf("Build date: %s\n", BLDDATE);
#endif
printf("\n");
printf("%s\n", help);
exit(EXIT_SUCCESS);
}
@ -167,25 +251,72 @@ int main(int argc, char *argv[]) {
exit(EXIT_FAILURE);
}
wjob.filename = argv[0];
wjob.iname = argv[0];
struct stat file_stat = {0};
if (0 != stat(wjob.filename, &file_stat)) {
if (0 != stat(wjob.iname, &file_stat)) {
printf("File does not exist...\n");
exit(EXIT_FAILURE);
}
if (NULL == wjob.dev_name) {
printf("Device required...\n");
if (NULL == wjob.oname) {
printf("You need to specify a device.\n");
exit(EXIT_FAILURE);
}
printf("Writing %s to %s\n", wjob.filename, wjob.dev_name);
if (0 != strncmp(wjob.oname, "/dev/", 4)) {
printf("\"%s\" does not appear to be a block device...\n", wjob.oname);
exit(EXIT_FAILURE);
}
wjob.buffer = malloc(BUFSIZE);
wjob.buffer2 = malloc(BUFSIZE);
uint64_t device_size;
{
/* Seems to be the cleanest way to check for write perm on a blockdev */
int fd = open(wjob.oname, O_WRONLY);
if (fd < 0) {
printf("Cannot write to \"%s\", do you have write permissions?\n", wjob.oname);
close(fd);
exit(EXIT_FAILURE);
}
wjob.bufsize = BUFSIZE;
wjob.block_size = BUFSIZE;
#ifdef __GLIBC__
if (ioctl(fd, BLKGETSIZE64, &device_size) < 0) {
#else /* With musl, ioctl's take ints, kheaders provide unsigned longs. Passes tests. */
if (ioctl(fd, (int)BLKGETSIZE64, &device_size) < 0) {
#endif
perror("ioctl");
close(fd);
exit(EXIT_FAILURE);
}
close(fd);
}
wjob.total_bytes = file_stat.st_size;
assert(file_stat.st_size >= 0);
if (wjob.flags & WI_WRITE)
printf("Writing \"%s\" (%.1f MiB) to \"%s\" (%.1f MiB)\n",
basename(wjob.iname),
BYTES_TO_MIB(wjob.total_bytes),
wjob.oname,
BYTES_TO_MIB(device_size));
if ((wjob.flags & WI_ASK) && (wjob.flags & WI_WRITE)) {
printf("Is this okay? (y/N): ");
fflush(stdout);
if ('y' != getchar()) {
printf("Aborting...\n");
exit(EXIT_SUCCESS);
}
}
wjob.buffer = malloc(BLOCKSIZE);
wjob.buffer2 = malloc(BLOCKSIZE);
assert(wjob.buffer);
assert(wjob.buffer2);
wjob.bufsize = BLOCKSIZE;
wjob.block_size = BLOCKSIZE;
perform_write(&wjob);
@ -194,7 +325,7 @@ int main(int argc, char *argv[]) {
if (wjob.buffer2)
free(wjob.buffer2);
printf("\nOkay!\n");
exit(0);
return 0;
printf("\n%.1f MiB's verified.\nAll good!\n", BYTES_TO_MIB(wjob.total_bytes));
exit(EXIT_SUCCESS);
}

View file

@ -0,0 +1,25 @@
_writeimg_completion() {
local cur prev opts
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="-v -d -h --help -n --noconfirm -V --version"
# Devices
if [[ "$prev" == "-d" ]]; then
COMPREPLY=( $(compgen -W "$(ls -d /dev/sd* /dev/nvme* /dev/mmcblk* 2>/dev/null)" -- "$cur") )
return 0
fi
# Flags
if [[ "$cur" != -* ]]; then
COMPREPLY=( $(compgen -f -- "$cur") )
return 0
fi
# Files
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
}
complete -F _writeimg_completion writeimg