libyml 0.0.6

DEPRECATED — `libyml` is unmaintained. This release is a thin compatibility shim that forwards every call to `unsafe-libyaml` (the upstream C-libyaml translation `libyml` was originally forked from). Please migrate to `unsafe-libyaml`, `yaml-rust2`, or `noyalib`.
Documentation

Contents

Getting started

Choosing a replacement

Deprecation reference

Operational


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.

[dependencies]
libyml = "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::string module — 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-libyaml crate, which has an actively-maintained, independently-audited translation of the C parser.
  • The yaml_string_extend symbol no longer exists in this crate at any path. Code that depended on it won't compile — which is exactly the desired outcome.

Verification:

$ cargo update -p libyml --precise 0.0.6
$ grep -r yaml_string_extend $(cargo metadata --format-version 1 \
    | jq -r '.packages[] | select(.name=="libyml") | .manifest_path | rtrimstr("/Cargo.toml")')/src
# (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
[advisories]
# 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.
ignore = ["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

#![allow(deprecated)]
use core::mem::MaybeUninit;
use libyml::success::is_success;
use libyml::{
    yaml_parser_delete, yaml_parser_initialize, yaml_parser_parse,
    yaml_parser_set_input_string, YamlEventT, YamlParserT,
    YamlStreamEndEvent,
};

fn main() {
    let yaml = b"name: myapp\nport: 8080\n";
    unsafe {
        let mut parser = MaybeUninit::<YamlParserT>::uninit();
        assert!(is_success(yaml_parser_initialize(parser.as_mut_ptr()).ok));
        let mut parser = parser.assume_init();
        yaml_parser_set_input_string(&mut parser, yaml.as_ptr(), yaml.len() as u64);

        loop {
            let mut event = MaybeUninit::<YamlEventT>::uninit();
            assert!(is_success(yaml_parser_parse(&mut parser, event.as_mut_ptr()).ok));
            let event = event.assume_init();
            if event.type_ == YamlStreamEndEvent { break; }
        }
        yaml_parser_delete(&mut parser);
    }
}

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 — pick unsafe-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 a Yaml enum AST.
  • You can move to typed deserialisation — pick noyalib. Modern serde-integrated API, no unsafe, configurable parser limits and YAML 1.2 strict resolution. Best long-term landing spot for any code that was using libyml to 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:

  1. Boolean parameters take bool, not c_int. Previously yaml_scalar_event_initialize(..., 1, 1, style) compiled with c_int arguments. Under the shim the function signature comes from unsafe-libyaml, so the same call site needs true / false instead of 1 / 0. This is a hard compile error, not a silent change.

  2. Success is no longer a nameable type. The upstream keeps its Success struct in a private module — the value still flows out of yaml_* calls and you can still read .ok, but you can no longer write fn foo() -> libyml::success::Success. The retained is_success / is_failure helpers now take bool directly, so chain them as is_success(call(...).ok).

  3. Enum variants rename PascalCase → SCREAMING_SNAKE_CASE in match arms. The shim defines pub const YamlUtf8Encoding etc. 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-libyamlGitHub Upstream destination — same FFI shape, maintained
yaml-rust2 Pure-Rust low-level parser destination
noyalibGitHub 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.