xslt

Production-grade, memory-safe, zero-trust XSLT 1.0 and EXSLT shard for Crystal.

xslt.cr

xslt.cr logo

Production-grade, memory-safe, and secure-by-default XSLT 1.0 and EXSLT transformation library for Crystal.

Wraps native libxslt and libexslt through low-level C FFI bindings seamlessly integrated with Crystal standard library XML::Document and IO.

Features

  • Memory Safety & Zero Double-Frees: Deep-clones Crystal GC-managed XML::Document trees (xmlCopyDoc) when compiling stylesheets, eliminating double-free segfaults between xsltFreeStylesheet and Crystal Boehm GC finalizers.
  • Secure Sandboxing by Default: Implements zero-trust sandboxing via Xslt::SecurityPolicy, strictly forbidding Local File Inclusion (LFI) via document() or <xsl:include>, Server-Side Request Forgery (SSRF) over network URIs, arbitrary file writes via <exsl:document>, and unauthorized directory creation.
  • Full EXSLT Integration: Out-of-the-box support for math (math:max, math:min, math:sqrt), sets (set:distinct, set:difference), strings (str:tokenize, str:replace), dates (date:year), dynamic expressions (dyn:evaluate), and result tree fragments (exsl:node-set).
  • Flexible Parameter Handling: Automatic parameter escaping and quoting via params: and dynamic XPath expression evaluation via xpath_params:.
  • Streaming & Multi-Target Serialization: Transform directly to XML::Document, formatted String, or stream directly into any Crystal IO (e.g., IO::Memory, File, HTTP::Server::Response).
  • Thread/Fiber Concurrency Safe: Compiled stylesheets are re-entrant and can be shared across multiple fibers concurrently without locks or data races.
  • Zero Stderr Leakage: C-level callbacks intercept all diagnostics cleanly into typed exceptions (Xslt::ParseError, Xslt::TransformError, Xslt::SecurityError).

System Requirements

Requires libxslt and libxml2 development headers:

# Fedora/RHEL/CentOS
sudo dnf install libxslt-devel libxml2-devel

# Debian/Ubuntu
sudo apt-get install libxslt1-dev libxml2-dev

# Arch Linux
sudo pacman -S libxslt libxml2

Installation

Add the dependency to your shard.yml:

dependencies:
  xslt:
    gitlab: renich/xslt

Run shards install.

Usage

Quick String Transformation

require "xslt"

xml = "<greeting><message>Hello, World!</message></greeting>"
xsl = <<-XSL
  <?xml version="1.0" encoding="UTF-8"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="no" omit-xml-declaration="yes"/>
    <xsl:template match="/greeting">
      <result><xsl:value-of select="message"/></result>
    </xsl:template>
  </xsl:stylesheet>
XSL

output = Xslt.transform_to_string(xml, xsl)
# => "<result>Hello, World!</result>"

Precompiled Stylesheets & Re-use

For repeated transformations, precompile stylesheets with Xslt::Stylesheet for maximum throughput:

sheet = Xslt::Stylesheet.new(Path["templates/report.xsl"])

# Transform multiple documents concurrently across fibers
100.times do |i|
  spawn do
    xml = fetch_xml_for(i)
    result = sheet.transform_to_string(xml)
    process(result)
  end
end

Transforming into XML::Document

doc = sheet.transform(xml)
puts doc.root.try(&.name)

Streaming Directly to IO

Avoid intermediate heap allocations by streaming directly to files, memory buffers, or sockets:

File.open("output.html", "w") do |file|
  sheet.transform(xml, file)
end

Parameters & Dynamic XPath

# String parameters are auto-quoted safely
result = sheet.transform_to_string(
  xml,
  params: {
    "title"   => "User's Report & Summary",
    "section" => "Overview",
  }
)

# XPath parameters evaluate dynamic XPath expressions at runtime
result = sheet.transform_to_string(
  xml,
  xpath_params: {
    "multiplier" => "2 * 5",
    "flag"       => "true()",
    "count_expr" => "count(//item)",
  }
)

Security Sandboxing & Granular Whitelisting

By default, all external file/network reads, file writes, and directory creation are strictly denied:

# Denied by default -> raises Xslt::SecurityError
begin
  Xslt.transform_to_string(xml, malicious_xsl)
rescue ex : Xslt::SecurityError
  puts "Access denied to: #{ex.target}"
end

# Selectively whitelist specific read paths or network hosts
policy = Xslt::SecurityPolicy.sandbox(
  allowed_read_paths: ["/var/data/catalog.xml", Path["/var/data/shared"]],
  allowed_read_hosts: ["api.example.com"],
)

sheet = Xslt::Stylesheet.new(xsl, security_policy: policy)
result = sheet.transform_to_string(xml, security_policy: policy)

Exception Hierarchy

All exceptions inherit from Xslt::Error:

  • Xslt::Error: Base exception class carrying structured diagnostic messages from LibXSLT/LibXML.
    • Xslt::ParseError: Raised when parsing source XML or compiling XSLT stylesheets fails.
    • Xslt::TransformError: Raised when runtime template execution or dynamic XPath evaluation fails.
    • Xslt::SecurityError: Raised when an operation violates security policy boundaries (operation, target).

Contributing

See CONTRIBUTING.rst for development setup, testing, formatting, and submission guidelines.

Changelog

See CHANGELOG.rst for release history and version notes.

License

MIT License. Copyright (c) 2026 Rénich Bon Ćirić.

Repository

xslt

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • 26 days ago
  • October 2, 2026
License

MIT License

Links
Synced at

Sun, 04 Oct 2026 20:11:05 GMT

Languages