Skip to content

Commit

Permalink
Add ZlibError type and move initialize into startproc
Browse files Browse the repository at this point in the history
  • Loading branch information
nhz2 committed Jan 13, 2025
1 parent 98ccccd commit 84de9cd
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 42 deletions.
46 changes: 30 additions & 16 deletions src/compression.jl
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,6 @@ end
# Methods
# -------

function TranscodingStreams.initialize(codec::CompressorCodec)
code = deflate_init!(codec.zstream, codec.level, codec.windowbits)
if code != Z_OK
zerror(codec.zstream, code)
end
return
end

function TranscodingStreams.finalize(codec::CompressorCodec)
zstream = codec.zstream
if zstream.state != C_NULL
Expand All @@ -174,33 +166,55 @@ function TranscodingStreams.finalize(codec::CompressorCodec)
return
end

function TranscodingStreams.startproc(codec::CompressorCodec, state::Symbol, error::Error)
code = deflate_reset!(codec.zstream)
if code == Z_OK
return :ok
function TranscodingStreams.startproc(codec::CompressorCodec, state::Symbol, error_ref::Error)
if codec.zstream.state == C_NULL
code = deflate_init!(codec.zstream, codec.level, codec.windowbits)
# errors in deflate_init! do not require clean up, so just throw
if code == Z_OK
return :ok
elseif code == Z_MEM_ERROR
throw(OutOfMemoryError())
elseif code == Z_STREAM_ERROR
error("Z_STREAM_ERROR: invalid parameter, this should be caught in the codec constructor")
elseif code == Z_VERSION_ERROR
error("Z_VERSION_ERROR: zlib library version is incompatible")

Check warning on line 180 in src/compression.jl

View check run for this annotation

Codecov / codecov/patch

src/compression.jl#L175-L180

Added lines #L175 - L180 were not covered by tests
else
error("unexpected libz error code: $(code)")

Check warning on line 182 in src/compression.jl

View check run for this annotation

Codecov / codecov/patch

src/compression.jl#L182

Added line #L182 was not covered by tests
end
else
error[] = ErrorException(zlib_error_message(codec.zstream, code))
return :error
code = deflate_reset!(codec.zstream)
# errors in deflate_reset! do not require clean up, so just throw
if code == Z_OK
return :ok
elseif code == Z_STREAM_ERROR
error("Z_STREAM_ERROR: the source stream state was inconsistent")

Check warning on line 190 in src/compression.jl

View check run for this annotation

Codecov / codecov/patch

src/compression.jl#L189-L190

Added lines #L189 - L190 were not covered by tests
else
error("unexpected libz error code: $(code)")

Check warning on line 192 in src/compression.jl

View check run for this annotation

Codecov / codecov/patch

src/compression.jl#L192

Added line #L192 was not covered by tests
end
end
end

function TranscodingStreams.process(codec::CompressorCodec, input::Memory, output::Memory, error::Error)
function TranscodingStreams.process(codec::CompressorCodec, input::Memory, output::Memory, error_ref::Error)
zstream = codec.zstream
if zstream.state == C_NULL
error("startproc must be called before process")

Check warning on line 200 in src/compression.jl

View check run for this annotation

Codecov / codecov/patch

src/compression.jl#L200

Added line #L200 was not covered by tests
end
zstream.next_in = input.ptr
avail_in = min(input.size, typemax(UInt32))
zstream.avail_in = avail_in
zstream.next_out = output.ptr
avail_out = min(output.size, typemax(UInt32))
zstream.avail_out = avail_out
code = deflate!(zstream, zstream.avail_in > 0 ? Z_NO_FLUSH : Z_FINISH)
@assert code != Z_STREAM_ERROR # state not clobbered
Δin = Int(avail_in - zstream.avail_in)
Δout = Int(avail_out - zstream.avail_out)
if code == Z_OK
return Δin, Δout, :ok
elseif code == Z_STREAM_END
return Δin, Δout, :end
else
error[] = ErrorException(zlib_error_message(zstream, code))
error_ref[] = ErrorException(zlib_error_message(zstream, code))

Check warning on line 217 in src/compression.jl

View check run for this annotation

Codecov / codecov/patch

src/compression.jl#L217

Added line #L217 was not covered by tests
return Δin, Δout, :error
end
end
54 changes: 38 additions & 16 deletions src/decompression.jl
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,6 @@ end
# Methods
# -------

function TranscodingStreams.initialize(codec::DecompressorCodec)
code = inflate_init!(codec.zstream, codec.windowbits)
if code != Z_OK
zerror(codec.zstream, code)
end
return
end

function TranscodingStreams.finalize(codec::DecompressorCodec)
zstream = codec.zstream
if zstream.state != C_NULL
Expand All @@ -162,18 +154,42 @@ function TranscodingStreams.finalize(codec::DecompressorCodec)
return
end

function TranscodingStreams.startproc(codec::DecompressorCodec, ::Symbol, error::Error)
code = inflate_reset!(codec.zstream)
if code == Z_OK
return :ok
function TranscodingStreams.startproc(codec::DecompressorCodec, ::Symbol, error_ref::Error)
# indicate that no input data is being provided for future zlib compat
codec.zstream.next_in = C_NULL
codec.zstream.avail_in = 0
if codec.zstream.state == C_NULL
code = inflate_init!(codec.zstream, codec.windowbits)
# errors in inflate_init! do not require clean up, so just throw
if code == Z_OK
return :ok
elseif code == Z_MEM_ERROR
throw(OutOfMemoryError())
elseif code == Z_STREAM_ERROR
error("Z_STREAM_ERROR: invalid parameter, this should be caught in the codec constructor")
elseif code == Z_VERSION_ERROR
error("Z_VERSION_ERROR: zlib library version is incompatible")

Check warning on line 171 in src/decompression.jl

View check run for this annotation

Codecov / codecov/patch

src/decompression.jl#L166-L171

Added lines #L166 - L171 were not covered by tests
else
error("unexpected libz error code: $(code)")

Check warning on line 173 in src/decompression.jl

View check run for this annotation

Codecov / codecov/patch

src/decompression.jl#L173

Added line #L173 was not covered by tests
end
else
error[] = ErrorException(zlib_error_message(codec.zstream, code))
return :error
code = inflate_reset!(codec.zstream)
# errors in deflate_reset! do not require clean up, so just throw
if code == Z_OK
return :ok
elseif code == Z_STREAM_ERROR
error("Z_STREAM_ERROR: the source stream state was inconsistent")

Check warning on line 181 in src/decompression.jl

View check run for this annotation

Codecov / codecov/patch

src/decompression.jl#L180-L181

Added lines #L180 - L181 were not covered by tests
else
error("unexpected libz error code: $(code)")

Check warning on line 183 in src/decompression.jl

View check run for this annotation

Codecov / codecov/patch

src/decompression.jl#L183

Added line #L183 was not covered by tests
end
end
end

function TranscodingStreams.process(codec::DecompressorCodec, input::Memory, output::Memory, error::Error)
function TranscodingStreams.process(codec::DecompressorCodec, input::Memory, output::Memory, error_ref::Error)
zstream = codec.zstream
if zstream.state == C_NULL
error("startproc must be called before process")

Check warning on line 191 in src/decompression.jl

View check run for this annotation

Codecov / codecov/patch

src/decompression.jl#L191

Added line #L191 was not covered by tests
end
zstream.next_in = input.ptr

avail_in = min(input.size, typemax(UInt32))
Expand All @@ -182,14 +198,20 @@ function TranscodingStreams.process(codec::DecompressorCodec, input::Memory, out
avail_out = min(output.size, typemax(UInt32))
zstream.avail_out = avail_out
code = inflate!(zstream, Z_NO_FLUSH)
@assert code != Z_STREAM_ERROR # state not clobbered
Δin = Int(avail_in - zstream.avail_in)
Δout = Int(avail_out - zstream.avail_out)
if code == Z_OK
return Δin, Δout, :ok
elseif code == Z_STREAM_END
return Δin, Δout, :end
elseif code == Z_MEM_ERROR
throw(OutOfMemoryError())

Check warning on line 209 in src/decompression.jl

View check run for this annotation

Codecov / codecov/patch

src/decompression.jl#L209

Added line #L209 was not covered by tests
elseif code == Z_BUF_ERROR && iszero(input.size)
error_ref[] = ZlibError("the compressed stream may be truncated")
return Δin, Δout, :error
else
error[] = ErrorException(zlib_error_message(zstream, code))
error_ref[] = ZlibError(zlib_error_message(zstream, code))
return Δin, Δout, :error
end
end
38 changes: 31 additions & 7 deletions src/libz.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,17 @@ end

const Z_DEFAULT_COMPRESSION = Cint(-1)

const Z_OK = Cint(0)
const Z_STREAM_END = Cint(1)
const Z_BUF_ERROR = Cint(-5)
const Z_OK = Cint(0)
const Z_STREAM_END = Cint(1)
const Z_NEED_DICT = Cint(2)
const Z_ERRNO = Cint(-1)
const Z_STREAM_ERROR = Cint(-2)
const Z_DATA_ERROR = Cint(-3)
const Z_MEM_ERROR = Cint(-4)
const Z_BUF_ERROR = Cint(-5)
const Z_VERSION_ERROR = Cint(-6)
# Return codes for the compression/decompression functions. Negative values
# are errors, positive values are used for special but normal events.

const Z_NO_FLUSH = Cint(0)
const Z_SYNC_FLUSH = Cint(2)
Expand All @@ -59,7 +67,9 @@ function version()
return unsafe_string(ccall((:zlibVersion, libz), Ptr{UInt8}, ()))
end

const zlib_version = version()
# This is the version of zlib used to make this wrapper.
# The `_init!` functions will return an error if the library is not compatible.
const zlib_version = "1.3.1"

function deflate_init!(zstream::ZStream, level::Integer, windowbits::Integer)
return ccall((:deflateInit2_, libz), Cint, (Ref{ZStream}, Cint, Cint, Cint, Cint, Cint, Cstring, Cint), zstream, level, Z_DEFLATED, windowbits, #=default memlevel=#8, #=default strategy=#0, zlib_version, sizeof(ZStream))
Expand Down Expand Up @@ -93,14 +103,28 @@ function inflate!(zstream::ZStream, flush::Integer)
return ccall((:inflate, libz), Cint, (Ref{ZStream}, Cint), zstream, flush)
end

# Error
# -----

struct ZlibError <: Exception
msg::String
end

function Base.showerror(io::IO, err::ZlibError)
print(io, "ZlibError: ")
print(io, err.msg)
nothing
end


function zerror(zstream::ZStream, code::Integer)
return throw(ErrorException(zlib_error_message(zstream, code)))
throw(ZlibError(zlib_error_message(zstream, code)))

Check warning on line 121 in src/libz.jl

View check run for this annotation

Codecov / codecov/patch

src/libz.jl#L121

Added line #L121 was not covered by tests
end

function zlib_error_message(zstream::ZStream, code::Integer)
if zstream.msg == C_NULL
return "zlib error: <no message> (code: $(code))"
return "<no message> (code: $(code))"

Check warning on line 126 in src/libz.jl

View check run for this annotation

Codecov / codecov/patch

src/libz.jl#L126

Added line #L126 was not covered by tests
else
return "zlib error: $(unsafe_string(zstream.msg)) (code: $(code))"
return "$(unsafe_string(zstream.msg)) (code: $(code))"
end
end
36 changes: 33 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CodecZlib
using CodecZlib: ZlibError
using Test
using Aqua: Aqua
using TranscodingStreams:
Expand Down Expand Up @@ -56,7 +57,7 @@ const testdir = @__DIR__
gzip_data_corrupted[1] = 0x00 # corrupt header
file = IOBuffer(gzip_data_corrupted)
stream = GzipDecompressorStream(file)
@test_throws ErrorException read(stream)
@test_throws ZlibError read(stream)
@test_throws ArgumentError read(stream)
@test !isopen(stream)
@test isopen(file)
Expand Down Expand Up @@ -160,7 +161,7 @@ end
close(stream)

stream = TranscodingStream(GzipDecompressor(gziponly=true), IOBuffer(zlib_data))
@test_throws Exception read(stream)
@test_throws ZlibError read(stream)
close(stream)

file = IOBuffer(b"foo")
Expand Down Expand Up @@ -251,7 +252,7 @@ end

@testset "panic" begin
stream = TranscodingStream(GzipDecompressor(), IOBuffer("some invalid data"))
@test_throws ErrorException read(stream)
@test_throws ZlibError read(stream)
@test_throws ArgumentError eof(stream)
end

Expand Down Expand Up @@ -304,3 +305,32 @@ end
@test_throws ArgumentError TranscodingStreams.stats(stream)
end
end

@testset "unexpected end of stream errors" begin
tests = [
(ZlibCompressor, ZlibDecompressor),
(DeflateCompressor, DeflateDecompressor),
(GzipCompressor, GzipDecompressor),
]
@testset "$(encoder)" for (encoder, decoder) in tests
local uncompressed = rand(UInt8, 1000)
local compressed = transcode(encoder, uncompressed)
for i in 0:length(compressed)-1
@test_throws ZlibError("the compressed stream may be truncated") transcode(decoder, compressed[1:i])
end
@test transcode(decoder, compressed) == uncompressed
# compressing empty vector should still work
@test transcode(decoder, transcode(encoder, UInt8[])) == UInt8[]
end
end
@testset "data errors" begin
@test_throws ZlibError transcode(ZlibDecompressor, zeros(UInt8, 10))
local uncompressed = rand(UInt8, 1000)
local compressed = transcode(ZlibCompressor, uncompressed)
compressed[70] ⊻= 0x01
@test_throws ZlibError transcode(ZlibDecompressor, compressed)
end
@testset "error printing" begin
@test sprint(Base.showerror, ZlibError("test error message")) ==
"ZlibError: test error message"
end

0 comments on commit 84de9cd

Please sign in to comment.