| Crates.io | icy_sauce |
| lib.rs | icy_sauce |
| version | 0.3.5 |
| created_at | 2024-04-08 19:00:12.65082+00 |
| updated_at | 2025-12-07 19:15:29.278389+00 |
| description | Library for handling SAUCE – Standard Architecture for Universal Comment Extensions |
| homepage | https://github.com/mkrueger/icy_sauce |
| repository | https://github.com/mkrueger/icy_sauce |
| max_upload_size | |
| id | 1200929 |
| size | 406,758 |
A Rust library for reading and writing SAUCE (Standard Architecture for Universal Comment Extensions) metadata records. SAUCE is a metadata protocol widely used in the ANSI art and BBS scenes to embed information about artwork files.
SAUCE is a metadata format created in 1994 by ACiD Productions to standardize how information about digital artwork and other files is stored. The SAUCE record is appended to the end of files and contains:
bstr for proper DOS codepage handlingAdd this to your Cargo.toml:
[dependencies]
icy_sauce = "0.3.2"
use icy_sauce::prelude::*; // brings common types into scope
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let data = fs::read("artwork.ans")?;
if let Some(sauce) = SauceRecord::from_bytes(&data)? {
println!("Title: {}", sauce.title());
println!("Author: {}", sauce.author());
println!("Group: {}", sauce.group());
// Get format-specific information
if let Some(caps) = sauce.capabilities() {
match caps {
Capabilities::Character(c) => {
println!("Character format: {:?} ({}x{})", c.format, c.columns, c.lines);
}
Capabilities::Bitmap(b) => {
println!("Bitmap: {:?} ({}x{} @ {}bpp)", b.format, b.width, b.height, b.pixel_depth);
}
Capabilities::Binary(b) => {
match b.format {
BinaryFormat::BinaryText => {
println!("BinaryText width: {}", b.columns);
if let Some(h) = b.binary_text_height_from_file_size(sauce.file_size()) {
println!("Derived height: {}", h);
}
println!("ICE colors: {}", b.ice_colors);
println!("Letter spacing: {:?}", b.letter_spacing);
println!("Aspect ratio: {:?}", b.aspect_ratio);
if let Some(font) = b.font() {
println!("Font: {}", font.to_str_lossy());
}
}
BinaryFormat::XBin => {
println!("XBin dimensions: {}x{}", b.columns, b.lines);
}
}
}
Capabilities::Vector(v) => {
println!("Vector: {:?}", v.format);
}
_ => {}
}
}
}
Ok(())
}
use icy_sauce::prelude::*;
use bstr::BString;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create character capabilities for an 80x25 ANSI file
let mut caps = CharacterCapabilities::new(CharacterFormat::Ansi)
.dimensions(80, 25);
caps.set_font(BString::from("IBM VGA"))?;
let sauce = SauceRecordBuilder::default()
.title(BString::from("My Artwork"))?
.author(BString::from("Artist"))?
.group(BString::from("Art Group"))?
.date(SauceDate::new(2024, 1, 15))
.capabilities(Capabilities::Character(caps))?
.add_comment(BString::from("Created with love"))?
.build();
// Write to file with content
let mut output = Vec::new();
output.extend_from_slice(b"Your file content here...");
sauce.write(&mut output)?;
Ok(())
}
You can remove one or more SAUCE records (and optionally their preceding EOF 0x1A marker) from the end of a file buffer without copying the data using strip_sauce.
StripMode variants:
| Mode | Removes | EOF Handling | Use Case |
|---|---|---|---|
Last |
Last SAUCE record | Preserves all EOF bytes | Keep legacy EOF marker but drop metadata |
LastStripFinalEof (default) |
Last SAUCE record | Removes a single EOF directly before the record | Clean view of payload |
All |
All contiguous SAUCE records (separated by ≤1 EOF each) | Preserves trailing EOF bytes | Multi-edit cleanup while keeping final EOF |
AllStripFinalEof |
All contiguous SAUCE records | Also strips a single trailing EOF after last removed record | Aggressive full cleanup |
Contiguous multi-record stripping stops if more than one consecutive EOF (0x1A 0x1A ...) separates records—stacked EOFs form a barrier.
use icy_sauce::{strip_sauce, StripMode};
// Assume `data` contains file payload + EOF + SAUCE
let cleaned = strip_sauce(&data, StripMode::default()); // LastStripFinalEof
// Keep EOF marker but remove SAUCE
let keep_eof = strip_sauce(&data, StripMode::Last);
// Remove multiple contiguous SAUCE records, keep trailing EOF(s)
let multi = strip_sauce(&data, StripMode::All);
// Most aggressive: remove all contiguous SAUCE records and one trailing EOF
let aggressive = strip_sauce(&data, StripMode::AllStripFinalEof);
Multi-record example:
"Content" 0x1A SAUCE1 0x1A SAUCE2 0x1A -> StripMode::All -> "Content" 0x1A
"Content" 0x1A SAUCE1 0x1A 0x1A SAUCE2 -> StripMode::All -> "Content" 0x1A 0x1A SAUCE2 (double EOF blocks chain)
Use strip_sauce_ex for metadata about the operation:
use icy_sauce::{strip_sauce_ex, StripMode};
let result = strip_sauce_ex(&data, StripMode::AllStripFinalEof);
println!("Removed {} record(s), {} EOF byte(s); new length {}",
result.records_removed, result.eof_bytes_removed, result.data.len());
If no SAUCE record is found, the original slice is returned unchanged.
This library includes a command-line utility for inspecting SAUCE records in files. You can use it directly with cargo run --example:
cargo run --example print_sauce <FILE>
cargo install --path . --example print_sauce
cargo run --example print_sauce artwork.ans
cargo run --example print_sauce artwork.ans --comments
cargo run --example print_sauce artwork.ans --raw
cargo run --example print_sauce artwork.ans -c -r
SAUCE Information for 'demo.ans'
============================================================
Title: Winter Scene
Author: ArtistName
Group: Cool Group
Date: 2024-01-15
Type: Character
Character File Information:
Format: Ansi
Dimensions: 80x25
iCE Colors: Yes
Letter Spacing: NinePixel
Aspect Ratio: Legacy
Font: IBM VGA
Comments (2):
----------------------------------------
1: Created for the winter artpack
2: Inspired by snowy mountains
use icy_sauce::prelude::*;
use bstr::BString;
let sauce = SauceRecordBuilder::default()
.title(BString::from("Art"))?
.add_comment(BString::from("First comment"))?
.add_comment(BString::from("Second comment"))?
.build();
for comment in sauce.comments() {
println!("Comment: {}", comment);
}
use icy_sauce::prelude::*;
use bstr::BString;
use icy_sauce::{LetterSpacing, AspectRatio};
// BinaryText (width must be even; height can be derived from file size)
let mut bin_caps = BinaryCapabilities::binary_text(160)?; // 160 columns
bin_caps.ice_colors = true;
bin_caps.letter_spacing = LetterSpacing::NinePixel;
bin_caps.aspect_ratio = AspectRatio::Legacy;
bin_caps.set_font(BString::from("IBM VGA"))?;
// XBin with explicit dimensions
let xbin_caps = BinaryCapabilities::xbin(80, 50)?;
To compute height of a BinaryText file after parsing:
if let Some(h) = bin_caps.binary_text_height_from_file_size(record.file_size()) {
println!("Derived height: {}", h);
}
use icy_sauce::prelude::*;
let mut caps = BitmapCapabilities::new(BitmapFormat::Png);
caps.width = 640;
caps.height = 480;
caps.pixel_depth = 24;
use icy_sauce::prelude::*;
let caps = AudioCapabilities { format: AudioFormat::S3m, sample_rate: 0 }; // tracker formats ignore sample_rate
use icy_sauce::prelude::*;
let caps = ArchiveCapabilities { format: ArchiveFormat::Zip };
SAUCE strings are typically encoded in CP437 (DOS codepage). This library uses bstr::BString for all text fields:
use bstr::BString;
let title = BString::from(b"My \x01 ASCII Art");
println!("Title: {}", title.to_str_lossy());
use icy_sauce::SauceError;
match sauce_result {
Err(SauceError::TitleTooLong(len)) => println!("Title is {} bytes, max is 35", len),
Err(SauceError::CommentLimitExceeded) => println!("Cannot add more than 255 comments"),
_ => {}
}
use icy_sauce::prelude::*;
let char_caps = CharacterCapabilities::new(CharacterFormat::Ansi).dimensions(80, 25);
let caps = Capabilities::Character(char_caps);
match caps {
Capabilities::Character(c) => println!("Character format with {} columns", c.columns),
Capabilities::Bitmap(b) => println!("Bitmap format: {:?}", b.format),
Capabilities::Vector(v) => println!("Vector format: {:?}", v.format),
Capabilities::Audio(a) => println!("Audio format: {:?}", a.format),
_ => {}
}
| Field | BinaryText Meaning | XBin Meaning |
|---|---|---|
columns |
Width (even 2–510) | Width (0–65535, >0 recommended) |
lines |
Always 0 (height derived from file size) | Explicit height |
ice_colors |
Enables 16 background colors (non-blink mode) | Ignored |
letter_spacing |
8/9 pixel or legacy spacing | Ignored (always legacy) |
aspect_ratio |
Legacy / LegacyDevice / Square | Ignored (legacy) |
font() |
Optional font name (≤22 bytes) | Always None |
Implements SAUCE v00.5 Spec:
Licensed under the Apache License, Version 2.0. See LICENSE.
Issues and PRs welcome: https://github.com/mkrueger/icy_sauce.