Documentation
Patient data
Every message these crates touch is a clinical record. Here is what they do with it, what they never do with it, and the one place a value can escape into somewhere you did not intend.
The short version
A message goes in as text, stays in memory as text, and comes back out when you ask for it. Nothing is written to disk, sent over a network, logged, counted, or cached. The libraries do not open files, read environment variables, spawn processes, or open sockets — not conditionally, not on a feature flag, not at all.
The one thing to know before you log an error: error and diagnostic messages can quote a value from the message. That is the section worth reading twice.
What the libraries never do
| Not done | How to check |
|---|---|
| No logging or tracing | No log, tracing, or any logging facade in any Cargo.toml in the workspace |
| No telemetry, analytics, or phone-home | No HTTP client anywhere; no network dependency of any kind |
| No filesystem access from library code | std::fs and File::open appear in no library source, only in the CLI |
| No environment variables | std::env appears in no library source, only in the CLI’s argument parsing |
| No sockets opened | std::net appears in no library source; MLLP is generic over a byte stream you supply |
| No subprocesses | std::process appears in no library source |
| No serialization framework | No serde; the JSON reader is hand-written and reads only dictionaries |
The whole runtime dependency surface of the workspace is er7, plus chrono in hl7-2-mllp — optional, off
by default, used only to stamp a generated acknowledgement with the wall clock. The macro crates
run at compile time only. Criterion and the fuzzing harness are development dependencies and are
never linked into anything you ship.
The release dictionaries are compiled into the binary and parsed on first use, so no file is opened for them at run time. A dictionary you supply comes from a string or from bytes you read yourself — the crate never fetches one.
Where a value can escape
This is the part that matters in practice, because the usual way PHI leaves a well-behaved system is a log line.
Errors that carry message content:
Error::BadValue— itsfoundfield is the offending text from the message, verbatim.Error::BadMshHeader— the detail can quote part of the malformed header.Error::Invalid— carries every error-severity diagnostic, with the caveat below.
Diagnostics that carry message content: a ValueFormat finding
formats the offending value into its detail string. Every other diagnostic kind
reports a location and a description of the problem, not the content at that location.
Display for both types reproduces those strings — so to_string(), {:?}, println!, panic!, unwrap(), expect(), and any logging call you make yourself will carry a value if the error is
one of those kinds.
A path is not a value, but it is not nothing. OBX[200]-5 says
nothing about a patient. The path on a diagnostic is safe to log in a way the detail is not.
If your logs are less trusted than your message store — which is usual, because logs are shipped, aggregated, and retained differently — match on the error and log the parts you want:
match message.get("PID-5.1") {
Err(hl7_2::Error::BadValue { path, expected, .. }) => {
// Deliberately drops `found`, which is text from the message.
eprintln!("{path}: expected {expected}");
}
Err(error) => eprintln!("{error}"),
Ok(value) => { /* ... */ }
}The same applies to validate: filter on severity
and kind, log the path, and treat the detail as message content.
What is not defended against
Stated plainly, because a security review will ask and a vague answer is worse than a limitation.
- Memory is not zeroed. Strings holding message text are freed normally; the bytes are not overwritten first. A core dump, a swap file, or a heap inspection can contain message content.
- No constant-time anything. Nothing here is a cryptographic operation.
- No access control. These are parsing libraries. Who may read which message is entirely your question.
- No encryption, at rest or in transit. MLLP is plaintext framing on whatever stream you give it. If that stream should be TLS, you supply the TLS stream.
- No de-identification. There is no scrub, redact, or anonymise function, and none is planned. Redaction is a policy decision about a particular data set, not a library default.
- No audit trail. Nothing records that a message was read. If your environment requires an access log, that is above this layer.
The command-line tools
The binaries do what you point them at and nothing else: read a named file or standard input, write a named file or standard output, exit. No config file is searched for, no environment variable is consulted, no history is kept, no temporary file is written.
Two ordinary shell hazards are worth naming anyway, because they are how PHI most often leaks from a command line and neither is something a program can prevent. Shell history: a message passed as an argument lands in your history file — pipe it or redirect from a file instead. Terminal scrollback: output persists in the buffer, and in whatever recording of the session exists.
The transports
Both transport crates are deliberately narrower than they sound. hl7-2-mllp implements framing over any byte
stream; it does not open, bind, connect, or configure anything. MLLP by itself provides no
confidentiality, integrity, or authentication — a property of the protocol as specified, not a
shortcut taken here. hl7-2-soap builds and parses
envelopes and contains no HTTP client or server.
So the network posture of a system built on these crates is decided entirely by the code around them, which is where a reviewer should look.
This project's own data
Every sample, test fixture, and benchmark input in the repository is synthetic. There is no real patient data anywhere in the history, and none may be added — not in an issue, a pull request, a test that reproduces a bug, or a benchmark corpus. Benchmark inputs are either generated in code or are the repository's own synthetic sample files, so there is no external corpus that could quietly acquire a real message.
Reports must be redacted. Keep the structure, replace the values — structure is what reproduces a parsing bug; names, identifiers, dates of birth, and addresses are not.
Redacted, and still a usable bug report
MSH|^~\&|LAB|ACME|EHR|CLINIC|20260814080000||ORU^R01|MSG00042|P|2.5
PID|1||REDACTED^^^ACME&1.2.3.4&ISO^MR||REDACTED^REDACTED||REDACTED|F
OBX|1|NM|2093-3^Cholesterol^LN||187|mg/dL|<200|N|||FThis website sets no cookies, runs no analytics, and loads no third-party script. The only browser storage it uses is one key remembering a light or dark theme choice.
If real patient data does reach an issue, a pull request, or a commit, say so at joel@joelparkerhenderson.com and it will be handled as an incident: removed, and the history rewritten if it landed in one.
If you are reviewing this for a deployment
The three checks that answer most of the questionnaire
cargo tree -p hl7-2 # one dependency: er7, itself dependency-free
grep -rn "std::net\|std::fs\|std::env" hl7-2/src/ # only main.rs, the CLI
grep -rn "^log = \|^tracing" --include=Cargo.toml . # nothingThen read the Error enum in hl7-2's src/lib.rs and Diagnostic in src/validate.rs against Where a value can escape above, and check the licensing: MIT, Apache-2.0,
BSD-3-Clause, GPL-2.0-only, or GPL-3.0-only at your option, the same five in every crate, byte
for byte.