Skip to content

Commit 2d49d0f

Browse files
authored
[LibLzma] Add XZ format support (#86)
* [LibLzma] Add XZ format support * fix encode_bound clamping for Julia 1.6 and remove redundant test * improve edge case testing * Add copilot suggestions
1 parent c0b02e4 commit 2d49d0f

16 files changed

Lines changed: 804 additions & 1 deletion

.github/workflows/CI.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ jobs:
5555
- ChunkCodecCore/**
5656
- ChunkCodecTests/**
5757
- LibLz4/**
58+
LibLzma:
59+
- ChunkCodecCore/**
60+
- ChunkCodecTests/**
61+
- LibLzma/**
5862
LibSnappy:
5963
- ChunkCodecCore/**
6064
- ChunkCodecTests/**

LibLzma/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Release Notes
2+
3+
All notable changes to this package will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6+
7+
## Unreleased
8+
9+
### Added
10+
11+
- Initial release

LibLzma/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Nathan Zimmerberg
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

LibLzma/Project.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name = "ChunkCodecLibLzma"
2+
uuid = "e95d29e5-19c5-4afd-ae0f-beb790efacdf"
3+
version = "0.1.0"
4+
authors = ["nhz2 <nhz2@cornell.edu>"]
5+
6+
[workspace]
7+
projects = ["test"]
8+
9+
[deps]
10+
ChunkCodecCore = "0b6fb165-00bc-4d37-ab8b-79f91016dbe1"
11+
XZ_jll = "ffd25f8a-64ca-5728-b0f7-c24cf3aae800"
12+
13+
[compat]
14+
ChunkCodecCore = "1"
15+
XZ_jll = "5"
16+
julia = "1.6"

LibLzma/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ChunkCodecLibLzma
2+
3+
This package implements the ChunkCodec interface for the following encoders and decoders
4+
using the liblzma C library <https://tukaani.org/xz/>
5+
6+
1. `XZCodec`, `XZEncodeOptions`, `XZDecodeOptions`
7+
8+
## Example
9+
10+
```julia-repl
11+
julia> using ChunkCodecLibLzma
12+
13+
julia> data = [0x00, 0x01, 0x02, 0x03];
14+
15+
julia> compressed_data = encode(XZEncodeOptions(;preset=UInt32(6), check=ChunkCodecLibLzma.LZMA_CHECK_CRC64), data);
16+
17+
julia> decompressed_data = decode(XZCodec(), compressed_data; max_size=length(data), size_hint=length(data));
18+
19+
julia> data == decompressed_data
20+
true
21+
```
22+
23+
The low level interface is defined in the `ChunkCodecCore` package.
24+

LibLzma/src/ChunkCodecLibLzma.jl

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
module ChunkCodecLibLzma
2+
3+
using XZ_jll: liblzma
4+
5+
using ChunkCodecCore:
6+
Codec,
7+
EncodeOptions,
8+
DecodeOptions,
9+
check_in_range,
10+
check_contiguous,
11+
grow_dst!,
12+
DecodingError,
13+
MaybeSize,
14+
NOT_SIZE
15+
import ChunkCodecCore:
16+
decode_options,
17+
can_concatenate,
18+
try_decode!,
19+
try_resize_decode!,
20+
try_encode!,
21+
encode_bound,
22+
try_find_decoded_size,
23+
decoded_size_range
24+
25+
export XZCodec,
26+
XZEncodeOptions,
27+
XZDecodeOptions,
28+
LZMADecodingError
29+
30+
if VERSION >= v"1.11.0-DEV.469"
31+
eval(Meta.parse("""
32+
public
33+
LZMA_PRESET_LEVEL_MASK,
34+
LZMA_PRESET_EXTREME,
35+
LZMA_CHECK_NONE,
36+
LZMA_CHECK_CRC32,
37+
LZMA_CHECK_CRC64,
38+
LZMA_CHECK_SHA256
39+
"""))
40+
end
41+
42+
43+
44+
# reexport ChunkCodecCore
45+
using ChunkCodecCore: ChunkCodecCore, encode, decode
46+
export ChunkCodecCore, encode, decode
47+
48+
49+
include("liblzma.jl")
50+
51+
"""
52+
struct XZCodec <: Codec
53+
XZCodec()
54+
55+
xz compression using the liblzma C library <https://tukaani.org/xz/>
56+
57+
See also [`XZEncodeOptions`](@ref) and [`XZDecodeOptions`](@ref)
58+
"""
59+
struct XZCodec <: Codec
60+
end
61+
decode_options(::XZCodec) = XZDecodeOptions()
62+
63+
include("encode.jl")
64+
include("decode.jl")
65+
66+
end # module ChunkCodecLibLzma

LibLzma/src/decode.jl

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
LZMADecodingError(code)
3+
4+
Error for data that cannot be decoded.
5+
"""
6+
struct LZMADecodingError <: DecodingError
7+
code::Cint
8+
end
9+
10+
function Base.showerror(io::IO, err::LZMADecodingError)
11+
print(io, "LZMADecodingError: ")
12+
if err.code == LZMA_DATA_ERROR
13+
print(io, "LZMA_DATA_ERROR: data is corrupt")
14+
elseif err.code == LZMA_FORMAT_ERROR
15+
print(io, "LZMA_FORMAT_ERROR: file format not recognized")
16+
elseif err.code == LZMA_OPTIONS_ERROR
17+
print(io, "LZMA_OPTIONS_ERROR: reserved bits set in headers. Data corrupt, or upgrading liblzma may help")
18+
elseif err.code == LZMA_BUF_ERROR
19+
print(io, "LZMA_BUF_ERROR: the compressed stream may be truncated or corrupt")
20+
else
21+
print(io, "unknown lzma error code: ")
22+
print(io, err.code)
23+
end
24+
nothing
25+
end
26+
27+
"""
28+
struct XZDecodeOptions <: DecodeOptions
29+
XZDecodeOptions(; kwargs...)
30+
31+
xz decompression using the liblzma C library <https://tukaani.org/xz/>
32+
33+
Like the command line tool `xz`, decoding accepts concatenated and padded compressed data and returns the decompressed data concatenated.
34+
35+
# Keyword Arguments
36+
37+
- `codec::XZCodec=XZCodec()`
38+
"""
39+
struct XZDecodeOptions <: DecodeOptions
40+
codec::XZCodec
41+
end
42+
function XZDecodeOptions(;
43+
codec::XZCodec=XZCodec(),
44+
kwargs...
45+
)
46+
XZDecodeOptions(codec)
47+
end
48+
can_concatenate(::XZDecodeOptions) = true
49+
50+
function try_find_decoded_size(::XZDecodeOptions, src::AbstractVector{UInt8})::Nothing
51+
# Potentially this could be found by parsing through the index
52+
# This is complicated by potential padding and concatenated streams
53+
nothing
54+
end
55+
56+
function try_decode!(d::XZDecodeOptions, dst::AbstractVector{UInt8}, src::AbstractVector{UInt8}; kwargs...)::MaybeSize
57+
try_resize_decode!(d, dst, src, Int64(length(dst)))
58+
end
59+
60+
function try_resize_decode!(d::XZDecodeOptions, dst::AbstractVector{UInt8}, src::AbstractVector{UInt8}, max_size::Int64; kwargs...)::MaybeSize
61+
dst_size::Int64 = length(dst)
62+
src_size::Int64 = length(src)
63+
src_left::Int64 = src_size
64+
dst_left::Int64 = dst_size
65+
check_contiguous(dst)
66+
check_contiguous(src)
67+
if isempty(src)
68+
throw(LZMADecodingError(LZMA_BUF_ERROR))
69+
end
70+
cconv_src = Base.cconvert(Ptr{UInt8}, src)
71+
# We start by allocating our allocator
72+
cconv_allocator = Base.cconvert(Ref{lzma_allocator}, default_allocator())
73+
GC.@preserve cconv_allocator begin
74+
allocator_p = Base.unsafe_convert(Ref{lzma_allocator}, cconv_allocator)
75+
stream = lzma_stream()
76+
stream.allocator = allocator_p
77+
ret = @ccall liblzma.lzma_stream_decoder(
78+
stream::Ref{lzma_stream},
79+
typemax(UInt64)::UInt64,
80+
LZMA_CONCATENATED::UInt32,
81+
)::Cint
82+
if ret == LZMA_MEM_ERROR
83+
throw(OutOfMemoryError())
84+
elseif ret != LZMA_OK
85+
error("Unknown lzma error code: $(ret)")
86+
end
87+
try
88+
while true # Loop for resizing dst
89+
# dst may get resized, so cconvert needs to be redone on each iteration.
90+
cconv_dst = Base.cconvert(Ptr{UInt8}, dst)
91+
GC.@preserve cconv_src cconv_dst begin
92+
src_p = Base.unsafe_convert(Ptr{UInt8}, cconv_src)
93+
dst_p = Base.unsafe_convert(Ptr{UInt8}, cconv_dst)
94+
stream.avail_in = src_left
95+
stream.avail_out = dst_left
96+
stream.next_in = src_p + (src_size - src_left)
97+
stream.next_out = dst_p + (dst_size - dst_left)
98+
ret = @ccall liblzma.lzma_code(
99+
stream::Ref{lzma_stream},
100+
LZMA_FINISH::Cint,
101+
)::Cint
102+
if ret == LZMA_OK || ret == LZMA_STREAM_END
103+
@assert stream.avail_in src_left
104+
@assert stream.avail_out dst_left
105+
src_left = stream.avail_in
106+
dst_left = stream.avail_out
107+
@assert src_left 0:src_size
108+
@assert dst_left 0:dst_size
109+
end
110+
if ret == LZMA_OK
111+
# Likely not enough output space
112+
# but also potentially the input is truncated
113+
# Unlike zlib, we can keep trying until we get LZMA_BUF_ERROR
114+
if iszero(dst_left)
115+
# Give more space and try again
116+
# This might result in returning a NOT_SIZE
117+
# when instead the actual issue is that the input is truncated.
118+
local next_size = grow_dst!(dst, max_size)
119+
if isnothing(next_size)
120+
return NOT_SIZE
121+
end
122+
dst_left += next_size - dst_size
123+
dst_size = next_size
124+
@assert dst_left > 0
125+
end
126+
elseif ret == LZMA_STREAM_END
127+
@assert iszero(src_left)
128+
# yay done return decompressed size
129+
real_dst_size = dst_size - dst_left
130+
@assert real_dst_size 0:length(dst)
131+
return real_dst_size
132+
elseif ret == LZMA_DATA_ERROR || ret == LZMA_FORMAT_ERROR || ret == LZMA_OPTIONS_ERROR || ret == LZMA_BUF_ERROR
133+
throw(LZMADecodingError(ret))
134+
elseif ret == LZMA_MEM_ERROR
135+
throw(OutOfMemoryError())
136+
else
137+
error("Unknown lzma error code: $(ret)")
138+
end
139+
end
140+
end
141+
finally
142+
@ccall liblzma.lzma_end(stream::Ref{lzma_stream})::Cvoid
143+
end
144+
end
145+
end

0 commit comments

Comments
 (0)