xslt

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

xslt.cr

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).

License

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

Repository

xslt

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

MIT License

Links
Synced at

Fri, 02 Oct 2026 02:59:23 GMT

Languages