Writing a file inspector: the questions before the first line¶
Level: 201 → 301 · a project brief, not a lesson
One line: xxd, hexdump, od, strings and file are one program with five opinions — take some bytes from a file or from standard input, decide what they are, print a view — and every decision the finished tool makes is a question you can ask before writing it; this page is those questions, the page in this library or its encodings sibling ↗ that answers each, and the concepts nobody here has written yet.
There is no program on this page, on purpose. Reading a line from standard input is where the code starts; this is where the design starts, and the two habits it is trying to hand you are older than Rust — ask what the input is before asking what to do with it, and ask what you can measure before asking what to print.
1. Where do the bytes come from?¶
The tools you are imitating all accept a filename and a pipe, and behave differently on each in ways their man pages do not lead with.
- Which spellings do you accept?
tool FILE,tool < FILE,cat FILE | tool,tool -, andtoolalone. The first hands you a path; the other four hand you file descriptor 0 and nothing else. A file or stdin is the page for making one function serve both. - What do you lose when it is stdin? A name, a size, a position you can move, and everything
statknows.file -prints/dev/stdin: ASCII textbecause it has no other name to print;xxd -s 100on a pipe has to read and throw away 100 bytes, because a pipe cannot be rewound. Decide what the tool prints in the name column when there is no name, and whether--skipon a pipe is an error or a slow read. - Is anyone typing?
toolalone at an interactive prompt:xxdwaits silently for Ctrl-D, which reads as a hang to anyone who did not mean it.IsTerminal↗ is the one question that tells the two apart, and A pipe is not a terminal ↗ measures what else changes when the answer is no. Print a usage line, areading from stdin — Ctrl-D ends itnote on stderr, or nothing: choose, and say so in--help. - The same program, two behaviours by input shape is not hypothetical. Apple's
stringsreads a named file's sections and reads all of stdin, sostrings a.outandstrings - < a.outgive different answers — measured in the encodings library ↗. Whatever your tool does, do it the same way for both. - Several files?
hexdump a bis one stream with cumulative offsets and no boundary marker;xxdtakes one file.cat a - bputs stdin between two files, and it works. Decide whether offsets restart, and whether a filename header is printed when there is more than one.
2. Bytes, or text?¶
The 101 page reads lines into a String, and that is exactly the wrong unit for this tool.
- A
Stringpromises UTF-8, soread_linerefuses a PNG at its first byte that does not decode — the one failure you will actually meet, and A file is bytes; aStringis a promise. A dump reads[u8]throughRead, neverstr: Reading bytes. - A line is not a unit either. A binary has
0abytes that are not line ends, and a 4 GiB file with no0aat all is one "line". Sixteen bytes is the rowxxdandhexdump -Cchose, and it is a display decision, not a property of the file. readmay hand you fewer bytes than you asked for, on a pipe especially, and not only at the end. A row builder that assumes sixteen arrive at once prints wrong rows on a slow pipe and right ones on a file;read_exactfills the buffer or fails withUnexpectedEofon a ten-byte file, which is the wrong error for the last row. The loop that accumulates a row across short reads is the heart of the tool.- What does the last row look like?
hexdumpzero-pads the block and prints spaces for units past the end, which is where every dump's trailing spaces come from ↗;xxdpads with spaces to keep the text column aligned. A file of 15, 16 and 17 bytes are three different last rows, and all three go in the tests. - Offsets.
usizeis the length of a slice in memory; the offset into a 5 GiB stream is au64, and on a 32-bit target those differ. Eight hex digits (xxd,hexdump -C), seven (barehexdump), or octal (od's default) —odreads types, not bytes ↗ is the page on why a dump's offset base is a decision that surprises people. - If you group bytes into words, which end comes first? Bare
hexdumpprints6163for a file that starts61 62, because it read a 16-bit number in CPU order — the swap, at length ↗. In Rust the question isu16::from_le_bytesagainstfrom_be_bytes, and Meet the byte is where those live. A dump that never groups never has to answer. - The text gutter is ASCII and only ASCII.
0x20..=0x7eprints itself and everything else is a dot:u8::is_ascii_graphic() || b == b' ', decided per byte, and never a UTF-8 decode — the column cannot mislead you because it never had an opinion ↗.od -ainvents names for bytes it cannot draw ↗ and is the cautionary tale.
3. Binary, or text — and says who?¶
Binary is a verdict, not a property ↗: nothing in a file says binary, so every tool that says it ran a test, and they run different ones.
- Which test does your inspector run, and does it say? A
00byte in the first block is whatgit,diffandgreplook for — anddifflooks at the whole file on macOS and the first block on Linux ↗, so the same file gets two verdicts. Valid UTF-8 from start to end is a stronger test and a slower one. A BOM is three bytes of evidence.filereads the first 64 KiB and reports what an encoding looks like, not what it is ↗. - If it is text, what is worth counting? Lines — where
wc -landgrep -c ''disagree by one on a file with no trailing newline ↗. Line endings —\n,\r\n, a lone\r, or a mix, which RFC 1212 is about. The longest line, the control characters present by name (ht,lf,nul,del—hexdump's%_unames them), and whether any byte above7fdecodes. - If it decodes as UTF-8, is that an answer?
us-asciiis a statement no experiment could contradict, andiso-8859-1is a proof of a negative ↗ — a Latin-2 file of Polish text is valid Latin-1, valid Windows-1250 and invalid UTF-8 all at once. Report the test's result, not a name for the encoding, unless the name is what the test proved. In Rust the test isfrom_utf8and its lossy twin ↗. - The first two bytes decide whether it runs at all: a BOM in front of
#!makes the kernel refuse the file ↗, so an inspector that shows the first row in hex shows the bug thatcathides.
4. What kind of file is it?¶
A path names one of seven kinds of thing, and file answers a different question from all of them.
- The seven kinds, and the first character of
ls -l:-regular,ddirectory,lsymbolic link,pFIFO,ssocket,ccharacter device,bblock device. In Rust,fs::metadata(path)?.file_type()answers three of them (is_file,is_dir,is_symlink) andFileTypeExt↗ the other four on Unix — andmetadatafollows a symlink wheresymlink_metadatadoes not, so on a Mac/tmpis a directory to the first and a link to the second. No page in this library covers this yet: Seven kinds of file is the stub for it. - What does your tool do with each?
File::openon a directory succeeds on Unix, and the firstreadfails withIsADirectory— so the check has to be on the read, not on the open. A FIFO blocks until somebody writes./dev/zeronever ends, sotool /dev/zerowithout a--lengthisyeswith extra steps. A socket cannot be opened this way at all. A missing file and an empty file are different answers, and only one is an error. file's answer is one of four, and File type is four questions ↗ separates them: how is it stored (the inode, above), how do I run it (mode bits and the first two bytes), what is in it (magic numbers — whatfilereads), what did the user mean (the extension). Decide which your inspector answers, and print the question beside the answer.- On a Mac, two more questions, neither of which the seven kinds or
filesees: does the path carry extended attributes (xattr -l, wherecom.apple.quarantineis the one that stops a download from running), and what type does Launch Services think it is (mdls -name kMDItemContentType)? An.appis a directory to every Unix tool and a program to the Finder. - Magic numbers are a table, and the table is data.
89 50 4e 47at offset 0 is PNG;ustarat offset 257 is tar;CD001at offset 32769 is an ISO image — so "read the first sixteen bytes" is not enough for the third, and the order of the rules matters when a ZIP is also a DOCX, a JAR and an APK.libmagic↗ is that table with thirty years of rules; wiza ↗ and libmagic-rs ↗ are two Rust readings of it, on the shelf. Decide how many signatures your table holds, and whether a match is a verdict or a guess in the output.
5. The first bytes, the first lines¶
Every inspector needs a bounded read, and the bound is a design decision.
- In the shell, four spellings of the same sixteen bytes:
head -c 16 f | xxd,xxd -l 16 f,hexdump -n 16 -C f,od -N 16 -An -tx1 f— measured side by side on Feeding stdin, whereodis the one that pads. Lines:head -3andsed -n '1,3p'. - In Rust:
Read::take(n)↗ bounds any reader,read_exactinto a[u8; 16]fills it or fails, andlines().take(3)stops the line iterator — Reading lines efficiently is the page on what each costs. - A pipe cannot be rewound, so if the type-sniffing step consumed the first sixteen bytes, the dump still has to show them. Keep the prefix in a
Vec<u8>andRead::chain↗ it back in front of the rest; skipping isio::copyfromtake(n)intoio::sink(). On a real fileSeekdoes both for free, and no page here coversSeekyet. - How many bytes are enough?
filereads 64 KiB and a file that is UTF-8 from byte 65536 onward reports as ASCII ↗. Any bound you pick has a file that fools it; the honest output says what the bound was.
6. How do you test the content?¶
- The function takes
impl Readandimpl Write. Then a test hands it&[u8]and aVec<u8>— no temporary file, no process, no keyboard — which is the whole ofReadandWriteand the move the 101 page's example already makes. Temporary directories in tests is for the day a test needs a real path after all. - Which oracle? The obvious test compares your dump against
xxd's. The encodings library measured what that oracle cannot promise:odpads on macOS and not on Linux ↗,xxd -e's padding changed between two xxd versions ↗,hexdump -cchanges notation with the locale, andfile's English wording moved between releases while--mime-typedid not.hexdump -Cis the one view byte-identical across platforms and locales, so if a dump tool is your oracle, that is the flag. - The inputs to keep forever: empty; one byte; 15, 16 and 17 bytes; 64 zero bytes (does your tool squeeze repeated rows into a
*, and is that off by default, since it destroys data that will be parsed ↗?); a00in the first row and one 200 KiB in; invalid UTF-8;\r\n; no trailing newline; a directory; a FIFO; a missing file; a name that is not UTF-8. Each is one assertion, and what a test asserts is worth reading before writing the first. - The binary is a separate test. Unit tests prove the function; only running the program proves that
tool -andtool FILEreach it, that the exit status is non-zero on a missing file, and which stream the complaint went to — Testing a command.
7. The other end of the pipe¶
- stdout is the answer, stderr is everything else, and the exit status is the only part a script reads — Standard error, and exit status. A dump that prints
warning:on stdout has just become data. - Thousands of rows through
println!lock stdout per call; aBufWriteraroundstdout().lock()is the fix, and it needs aflushbeforeprocess::exit. tool big.bin | headcloses the pipe after ten rows, and Rust's runtime ignoresSIGPIPE, so the eleventhprintln!panics withfailed printing to stdout: Broken pipe— measured for this page on 1.98.0, and the reason a command-line tool writes withwriteln!and matches onErrorKind::BrokenPipe. No page covers it yet: Broken pipe is the stub.- Colour only when stdout is a terminal, which is
IsTerminalagain, on fd 1 this time;hexyldoes it and so doesls.--color=neveris the flag for the reader who pipes.
Concepts still missing¶
What the questions above need, and where each stands. A stub has a URL and an outline and no example yet; none means the page has not been started.
| Concept | Status | Page |
|---|---|---|
| Reading stdin line by line | written | Reading a line from standard input |
One function for a file or stdin; -; IsTerminal |
stub | A file or stdin |
Read, short reads, take, chain, the sixteen-byte row |
stub | Reading bytes |
The seven kinds of file, metadata, FileTypeExt, symlink_metadata |
stub | Seven kinds of file |
SIGPIPE, BrokenPipe, and println! in a pipeline |
stub | Broken pipe |
| A keypress without Enter; a prompt that does not echo | stub | Raw mode and passwords |
| A read you can give up on | stub | Reading without blocking |
Seek, and skipping on a pipe |
none | belongs beside Reading bytes |
| A magic-number table, and the order of its rules | none | the tool question is answered on file guesses ↗ |
Byte order, from_le_bytes |
partly | Meet the byte; Byte order and the BOM ↗ |
| The binary-or-text verdict, in Rust | written, next door | Binary is a verdict ↗, In Rust section |
Arguments, flags, clap |
stubs | Command-line arguments, Flags by hand, Deriving a parser with clap |
| Running the binary in a test | stub | Testing a command |
| The shell side: making, feeding and looking at the bytes | written | Feeding stdin |
| The tools this one imitates, and what to install | written | Byte tools |
Terms¶
Seven words the tools on the shelf use as if everyone knew them. Each is also in the glossary.
- Magic number — bytes at a known offset that identify a format:
89 50 4e 47is PNG. Whatfilereads and the extension does not. - Polyglot file — one file that is valid in two formats at once (a ZIP that is also a JAR, a GIF that is also JavaScript). The reason a signature table's order is part of its answer.
- Hex viewer / hex editor — a viewer prints the bytes (
xxd,hexyl); an editor writes them back (xxd -r, ImHex, 010 Editor). The column you are most tempted to edit in a dump is the onexxd -rthrows away ↗. - Disassembler / decompiler — a disassembler turns machine code into assembly, one instruction per line (
radare2,objdump); a decompiler guesses the C that produced it (Ghidra). Both start from the bytes a dump shows. - Firmware image — a file that is a whole device's flash: a boot loader, a kernel, a filesystem and padding, end to end.
binwalkfinds the boundaries by scanning for magic numbers. - Homoglyph — two characters that look alike and are not (
аU+0430 againstaU+0061). Invisible in the text column, obvious in the hex. - BiDi override — a Unicode control (U+202E and its family) that reverses the displayed order of what follows, so source code can read differently from how it runs.
strauditandunifind them; a dump shows them ase2 80 ae.
If you are coming from another language¶
Python. The tool is sys.stdin.buffer (bytes, not sys.stdin, which decodes) or open(path, 'rb'), os.stat with the stat.S_ISFIFO family for the seven kinds, and bytes.hex(' ') for the middle column — the ten-line dump() in the encodings library ↗ is the whole of xxd minus the flags. The question that does not transfer is the short read: f.read(16) on a regular file returns sixteen bytes or the end, and Python programmers rarely meet the pipe that returns three. And print() under | head raises BrokenPipeError, which is the same event with a Python name.
ABAP. OPEN DATASET ... IN BINARY MODE and READ DATASET ... INTO xstr are the bytes; xstring is the Vec<u8>, and the debugger's hex view is the dump. What is absent is the whole of section 1 — there is no standard input, no pipe, and no - — and section 4's seven kinds, since an application server file is regular or it is not there. The nearest thing to a magic number is cl_abap_zip refusing a file that does not start 50 4b, and it is the same idea: the format is decided by the bytes, not by the name.
See also¶
- Reading a line from standard input — the first line of the program this page declines to write
- Feeding stdin —
printf, pipes, redirects andxxd -r: how the bytes get to fd 0, and how to look at them before your program does - Byte tools —
xxd,hexdump,od,strings,file, and the tools that replace them, with what each is written in - Inspecting a file ↗ — the same tools inside a session, and the one column that is the file
hexdumpis a format engine ↗ ·xxdis the dump you can put back ↗ ·odreads types, not bytes ↗ ·stringshas a printable set, not an encoding ↗ ·fileguesses ↗std::io::Read↗ ·std::fs::FileType↗ ·std::io::ErrorKind↗
Po polsku¶
Ta strona celowo nie zawiera programu. xxd, hexdump, od, strings i file to jeden program z pięcioma opiniami — weź bajty z pliku albo ze standardowego wejścia, zdecyduj, czym są, wypisz widok — a każda decyzja gotowego narzędzia jest pytaniem, które można zadać przed napisaniem pierwszej linijki. Siedem grup pytań powyżej idzie w kolejności, w jakiej bajty przechodzą przez program: skąd pochodzą (plik czy potok — i co tracisz, gdy to potok: nazwę, rozmiar, możliwość przewinięcia), czy to bajty, czy tekst (zrzut szesnastkowy czyta [u8], nigdy String, bo String obiecuje UTF-8, a PNG tej obietnicy nie składa), kto orzeka, że plik jest binarny (to werdykt testu, nie własność pliku — bajt 00, poprawne UTF-8 i BOM to trzy różne testy z trzema różnymi odpowiedziami), jakiego rodzaju to plik (siedem rodzajów uniksowych, pierwsza litera ls -l, i cztery osobne pytania, na które file, rozszerzenie, bity trybu i i-węzeł odpowiadają każde po swojemu), ile bajtów wystarczy obejrzeć, jak przetestować zawartość bez klawiatury i bez pliku tymczasowego (funkcja bierze impl Read, test podaje &[u8]), i co się dzieje na drugim końcu potoku (tool big.bin | head zamyka potok, a println! w Ruście wtedy panikuje, bo środowisko uruchomieniowe ignoruje SIGPIPE).
Polski czytelnik ma tu jeden przykład pod ręką, którego angielski nie ma: plik z polskim tekstem zapisany w Latin-2 albo w stronie kodowej 1250 jest jednocześnie poprawnym Latin-1, poprawnym Windows-1250 i niepoprawnym UTF-8 — więc narzędzie, które wypisuje nazwę kodowania, zgaduje, a to, które wypisuje wynik testu, mówi prawdę. Tabela „Concepts still missing” wylicza, które z potrzebnych stron w tej bibliotece już są, które są tylko szkicami (stubs) z gotowym adresem, a których nie ma wcale — Seek i tablica liczb magicznych to dwa największe braki.
Szukaj po polsku: zrzut szesnastkowy pliku · liczba magiczna pliku · plik binarny a tekstowy · rodzaje plików w Uniksie · rust impl Read hex dump · rust BrokenPipe println panic · rust FileType is_fifo