Contents
Getting started
- Install — stop-gap shim usage
- Security: RUSTSEC-2025-0067 fixed in 0.0.6
- Quick Start — shim usage in twenty lines
Choosing a replacement
- Maintained alternatives — picking a destination crate
- One-minute migration paths — diff snippets per destination
Deprecation reference
- What changed in 0.0.6 — the shim, in one paragraph
- What still works in 0.0.6 — surviving tests and examples
- What was removed in 0.0.6 — the hand-translated C copy
- Behavioural notes — three intentional deltas worth knowing
Operational
- MSRV — Rust 1.56.0 floor
- Documentation — migration guide, alternative-crate docs
- License
Install
libyml = "0.0.6" is a stop-gap so an in-flight migration doesn't
block your release. Existing call sites compile unchanged; the
compiler emits a deprecation warning at each use libyml::* import
pointing at the migration guide.
[]
= "0.0.6"
The shim itself depends on unsafe-libyaml for its implementation
— the upstream libyml was originally forked from. The 18 000+
lines of hand-translated C-libyaml code that previous releases
shipped are no longer in the source tree; downstream users link
the upstream's audited copy through this re-export. Whether your
eventual destination is unsafe-libyaml, yaml-rust2, or
noyalib is your call.
Security: RUSTSEC-2025-0067 fixed in 0.0.6
RUSTSEC-2025-0067
flagged all libyml versions ≤ 0.0.5 as unsound — the
libyml::string::yaml_string_extend function had a code path
that could trigger undefined behaviour.
Upgrading to libyml = "0.0.6" removes the vulnerable
surface entirely:
- The entire
libyml::stringmodule — along with the rest of the hand-translated C-libyaml copy — is gone from the source tree. - Every public function is now a re-export from the upstream
unsafe-libyamlcrate, which has an actively-maintained, independently-audited translation of the C parser. - The
yaml_string_extendsymbol no longer exists in this crate at any path. Code that depended on it won't compile — which is exactly the desired outcome.
Verification:
# (no output — the function is no longer in the source tree)
The same structural fix flows through to any maintained alternative you eventually migrate to.
cargo audit will still warn — here's why and how to handle it
The RustSec advisory database has chosen not to mark 0.0.6
as patched. The decision was made on the basis that libyml is
unmaintained regardless of which version you pick — a position
that applies to the crate as a whole rather than to a specific
code path. Practical consequence: cargo audit and cargo deny
will emit RUSTSEC-2025-0067 against libyml = "0.0.6" even though
the unsound surface no longer exists in this release.
This repo's own CI suppresses the warning via .cargo/audit.toml
and deny.toml. If you're a downstream user who
wants the same suppression in your own project, copy the snippet
below — and feel free to remove it whenever you migrate fully.
# .cargo/audit.toml — at the workspace root
[]
# RUSTSEC-2025-0067 affects libyml's `yaml_string_extend` helper.
# The 0.0.6 deprecation shim removes that surface entirely; verify
# locally with `grep -r yaml_string_extend src/` (no matches).
# The advisory database tracks the crate's unmaintained status,
# not code presence — so we ignore the advisory ourselves and
# document the structural fix here.
= ["RUSTSEC-2025-0067"]
For cargo deny, add the same RUSTSEC-2025-0067 ID to your
deny.toml's [advisories] ignore list with the same rationale.
The cleanest long-term path is still to migrate off libyml
entirely — the maintained alternatives are listed above.
Quick Start
use MaybeUninit;
use is_success;
use ;
Run the bundled examples with cargo run --example example (parse
- emit demo) or
cargo run --example migration(single-file shim demo).
Maintained alternatives
Three crates are realistic destinations for a libyml user. None
is being prescribed — pick the one that fits the codebase.
| Crate | Latest | Migration shape | Best fit |
|---|---|---|---|
unsafe-libyaml |
0.2 | Drop-in upstream — rename PascalCase types/consts to snake_case / SCREAMING_SNAKE_CASE | Codebases that want to stay on the raw libyaml-shaped FFI API on a maintained backend |
yaml-rust2 |
0.9 | Not FFI-shaped — YamlLoader::load_from_str returns a Yaml AST |
Users who want to drop the C-libyaml model entirely while keeping a low-level parser primitive in pure Rust |
noyalib |
0.0 | Higher-level typed API (from_str::<T> / Value); pure-Rust, #![forbid(unsafe_code)] |
Users who can move from event-stream parsing to typed deserialisation — usually the cleanest end-state |
Decision guide
- You used
yaml_parser_*/yaml_emitter_*and want to keep the same shape — pickunsafe-libyaml. Same functions, PascalCase → snake_case rename on types and event/style constants. The shim is already pulling it in transitively, so switching is a flat cost. - You want to leave the C model behind but stay low-level —
pick
yaml-rust2. Pure-Rust parser, no FFI shapes, produces aYamlenum AST. - You can move to typed deserialisation — pick
noyalib. Modern serde-integrated API, nounsafe, configurable parser limits and YAML 1.2 strict resolution. Best long-term landing spot for any code that was usinglibymlto back a config loader or document model.
One-minute migration paths
Side-by-side diff snippets for each destination. The full
function-mapping tables are in MIGRATION.md.
→ unsafe-libyaml
-[dependencies]
-libyml = "0.0"
+[dependencies]
+unsafe-libyaml = "0.2"
-use libyml::{
- yaml_parser_initialize, YamlParserT, YamlUtf8Encoding,
- YamlPlainScalarStyle,
-};
+use unsafe_libyaml::{
+ yaml_parser_initialize, yaml_parser_t as YamlParserT,
+ YAML_UTF8_ENCODING, YAML_PLAIN_SCALAR_STYLE,
+};
Functions keep the same names. Types rename from PascalCase
(YamlParserT) to snake_case (yaml_parser_t). Enum variants
rename from PascalCase (YamlUtf8Encoding) to
SCREAMING_SNAKE_CASE (YAML_UTF8_ENCODING). Boolean arguments
change from c_int (0/1) to Rust bool (false/true).
→ yaml-rust2
-[dependencies]
-libyml = "0.0"
+[dependencies]
+yaml-rust2 = "0.9"
-// Event-stream loop with yaml_parser_parse(...)
+use yaml_rust2::YamlLoader;
+let docs = YamlLoader::load_from_str(yaml_str)?;
+let v = &docs[0];
yaml-rust2 returns a Yaml enum AST instead of streaming
events. Code that walked the libyml event stream needs a
restructure to walk the AST. This is the right choice when you
want pure-Rust parser primitives without the C-libyaml model.
→ noyalib
-[dependencies]
-libyml = "0.0"
+[dependencies]
+noyalib = "0.0.5"
-// Manual event-stream walk to read keys
+use noyalib::{from_str, Value};
+let cfg: MyConfig = from_str(yaml_str)?;
+// or, untyped:
+let v: Value = from_str(yaml_str)?;
If your libyml usage was indirect — backing a config loader,
RPC payload codec, or document model — noyalib is the cleanest
end-state. Pure-Rust, #![forbid(unsafe_code)], YAML 1.2 strict
resolver, configurable parser limits.
What changed in 0.0.6
libyml 0.0.6 is a thin compatibility shim. The hand-translated
copy of C libyaml that previous releases shipped — ~18 000 lines
across api.rs, scanner.rs, parser.rs, emitter.rs,
dumper.rs, loader.rs, and friends — has been deleted. Every
public function is now re-exported from unsafe-libyaml (the
upstream libyml was originally forked from); the historical
PascalCase type aliases (YamlParserT, YamlEventT, …) and the
common PascalCase enum-variant constants (YamlUtf8Encoding,
YamlPlainScalarStyle, …) are restored on top so existing call
sites compile unchanged.
The path-form sub-modules also survive: libyml::api::*,
libyml::decode::*, libyml::document::*, libyml::dumper::*,
libyml::loader::*, libyml::yaml::*, and libyml::success::*
keep resolving through thin re-export modules. Two former
sub-modules — libyml::memory and libyml::string — are retained
as empty stub modules with docs explaining the removal, so
use libyml::memory; keeps compiling even though every former
item under it is gone. The full inventory is in
What was removed in 0.0.6.
The shim being backed by unsafe-libyaml internally is an
implementation detail, not a recommendation to use
unsafe-libyaml specifically. The
Maintained alternatives section above
covers the choice.
What still works in 0.0.6
The shim is wire-compatible with typical user code. The original
test files from libyml ≤ 0.0.5 are kept under tests/ (with
small adaptation comments noting the patches) so users can see
their own code's migration shape side-by-side. Verified by
cargo test --all-targets + cargo run --example example +
cargo run --example migration:
| Surface | Status |
|---|---|
tests/test_lib.rs — retained from 0.0.5, two-line patch (is_success(call) → is_success(call.ok), drop #![no_std]) |
5 / 5 pass |
tests/test_decode.rs — retained from 0.0.5 verbatim — the libyml::decode::* path module re-exports through the shim |
8 / 8 pass |
tests/shim.rs — new smoke suite: parser init/delete, parse-first-event, emit a {greeting: hello} mapping round-trip, type-alias resolution, success helpers |
5 / 5 pass |
examples/example.rs — retained 0.0.5 aggregator shape — runs examples/apis/main.rs then a parse + emit demo |
exits 0 |
examples/apis/main.rs — retained from 0.0.5; the memory + string slabs are kept as comments with Rust-native replacements |
exits 0 |
examples/migration.rs — single-file shim demo |
exits 0 |
The full per-file inventory of retained / patched / removed tests
and examples is in MIGRATION.md § "Test and example coverage in
0.0.6".
What was removed in 0.0.6
Most of the previous public surface is retained through the
shim — the historical libyml::api::*, libyml::decode::*,
libyml::document::*, libyml::dumper::*, libyml::loader::*,
and libyml::yaml::* path-form imports keep resolving via thin
re-export modules that point at the upstream unsafe-libyaml
functions. The table below covers the items that don't
survive — the implementation-detail helpers that depended on the
hand-translated C copy.
Removed from libyml |
What it was | Where it goes |
|---|---|---|
libyml::memory::* |
yaml_malloc / yaml_free / yaml_realloc / yaml_strdup allocator wrappers |
None — the upstream uses Rust's alloc directly. Allocate with std::alloc::{alloc, dealloc} or Vec/Box instead. libyml::memory is retained as an empty stub module so use libyml::memory; keeps resolving — every former item under it is gone |
libyml::string::* |
yaml_string_extend / yaml_string_join — the unsound helper RUSTSEC-2025-0067 flags |
None — build strings with Rust's Vec / String. libyml::string is retained as an empty stub module for the same source-compatibility reason as memory |
libyml::internal::* |
Hand-translated internal helpers (parser/emitter state machines) | None — unsafe-libyaml keeps its equivalents private |
libyml::macros::* |
Internal __assert! / do_loop! macros |
None — implementation details of the C copy |
libyml::externs::* |
C-style malloc / free / memcpy / memmove re-exports |
None — the upstream uses Rust's alloc directly |
libyml::utils::* |
Internal memory_macros module |
None — implementation detail of the C copy |
libyml::libc |
Re-exports of core::ffi::c_* primitives |
Use core::ffi::* (or libc::* if you depend on the libc crate) directly |
libyml::loader::yaml_parser_set_composer_error |
Internal composer-error injection helper | None — inspect parser.problem after yaml_parser_parse / yaml_parser_load returns failure instead |
libyml::dumper::yaml_emitter_dump_node / _scalar / _sequence / _mapping |
Internal sub-routines of yaml_emitter_dump |
None — the upstream keeps these private; drive emission through the public yaml_emitter_dump entry point |
libyml::success::Success (as a nameable type) |
#[derive(PartialEq, Debug)] struct wrapping bool |
Read .ok on the upstream's return value directly; the shim retains libyml::success::is_success(bool) and is_failure(bool) helpers |
libyml::yaml::yaml_char_t |
C-libyaml byte type | Retained as a pub type yaml_char_t = u8; alias under libyml::yaml so the path keeps resolving |
src/bin/run-emitter-test-suite.rs, src/bin/run-parser-test-suite.rs, src/bin/cstr/* |
yaml-test-suite harness binaries | Upstream unsafe-libyaml runs its own equivalent test suite |
The full table is in MIGRATION.md.
Behavioural notes
The shim is backed by unsafe-libyaml's upstream code, which has
diverged from the fork's snapshot in three user-visible ways:
-
Boolean parameters take
bool, notc_int. Previouslyyaml_scalar_event_initialize(..., 1, 1, style)compiled withc_intarguments. Under the shim the function signature comes fromunsafe-libyaml, so the same call site needstrue/falseinstead of1/0. This is a hard compile error, not a silent change. -
Successis no longer a nameable type. The upstream keeps itsSuccessstruct in a private module — the value still flows out ofyaml_*calls and you can still read.ok, but you can no longer writefn foo() -> libyml::success::Success. The retainedis_success/is_failurehelpers now takebooldirectly, so chain them asis_success(call(...).ok). -
Enum variants rename PascalCase → SCREAMING_SNAKE_CASE in
matcharms. The shim definespub const YamlUtf8Encodingetc. so the names still work in value position (yaml_emitter_set_encoding(emit, YamlUtf8Encoding)). In refutable patterns (match enc { YamlUtf8Encoding => … }) the upstream's SCREAMING_SNAKE_CASE name is required (YAML_UTF8_ENCODING). Both spellings are re-exported.
The full mapping is in MIGRATION.md.
MSRV
libyml 0.0.6 requires Rust 1.56.0 — the same floor as
unsafe-libyaml. The previous releases also required 1.56, so
this is not a bump. Users on older toolchains should pin
libyml = "=0.0.5" until they can move forward.
Documentation
| Document | Covers |
|---|---|
MIGRATION.md |
Find/replace tables per destination, full removed-surface mapping, test/example coverage triage |
unsafe-libyaml — GitHub |
Upstream destination — same FFI shape, maintained |
yaml-rust2 |
Pure-Rust low-level parser destination |
noyalib — GitHub |
Modern pure-Rust typed-API destination |
| docs.rs/libyml | API reference for this shim — every item carries the #[deprecated] banner |
License
Dual-licensed under Apache 2.0 or MIT, at your option.