#### Bzip2 Implementation
#### Copyright (C) 2023-2024 Remilia Scarlet
#### Copyright (c) 2022 drone1400
#### Copyright (C) 2015 Jaime Olivares
#### Copyright (c) 2011 Matthew Francis
#### MIT License
####
#### Ported from the Java implementation by Matthew Francis:
#### https://github.com/MateuszBartosiewicz/bzip2.
####
#### Modified by drone1400
####
#### Ported by Remilia Scarlet from the C# implementation by Jamie Olivares:
#### http://github.com/jaime-olivares/bzip2
require "./blockcompressor"
module RemiLib::Compression::BZip2
# A write-only `IO` object to compress data in the BZip2 format.
#
# Instances of this class wrap another IO object. When you write to this
# instance, it compresses the data and writes it to the underlying IO.
#
# NOTE: unless created with a block, `close` must be invoked after all data
# has been written to an instance.
#
# Example of a simple compressor:
#
# ```crystal
# # Compress `file` and save to `output`.
# File.open(bzip2file, "rb") do |file|
# File.open(outputfile, "wb") do |output|
# RemiLib::Compression::BZip2::Writer.open(output) do |bzio|
# IO.copy(bzio, output)
# end
# end
# end
#```
class Writer < IO
# If `#sync_close?` is `true`, closing this IO will close the underlying IO.
property? sync_close : Bool
@output : IO
@bitWriter : BitWriter
@streamBlockSize : Int32
@streamCRC : UInt32 = 0
@int32Pool : ArrayPool(Int32) = ArrayPool(Int32).new(0)
private getter! blockComp : BlockCompressor?
# Creates an instance of `Writer`. `#close` must be invoked after all data has
# written. `level` will be clamped to `MIN_COMPRESSION_LEVEL` and
# `MAX_COMPRESSION_LEVEL` if it is outside those values.
def initialize(@output : IO, level : Int32 = DEFAULT_COMPRESSION_LEVEL, @sync_close : Bool = false)
level = level.clamp(MIN_COMPRESSION_LEVEL, MAX_COMPRESSION_LEVEL)
@closed = false
@streamBlockSize = level * 100_000
@bitWriter = BitWriter.new(@output)
@bitWriter.writeBits(16, STREAM_START_MARKER_1)
@bitWriter.writeBits(8, STREAM_START_MARKER_2)
@bitWriter.writeBits(8, ('0'.ord + level).to_u32)
initNextBlock
end
# Creates a new writer for the given *io*, yields it to the given block,
# then closes it at its end.
def self.open(io : IO, level : Int32 = DEFAULT_COMPRESSION_LEVEL, sync_close : Bool = false, &)
writer = new(io, level: level, sync_close: sync_close)
begin
yield writer
ensure
writer.close
end
end
# Always raises `IO::Error` because this is a write-only `IO`.
def read(slice : Bytes) : NoReturn
raise "Can't read from BZip2::Writer"
end
# See `IO#write`.
def write(slice : Bytes) : Nil
check_open
pos = 0
offset = 0
while offset < slice.size
pos = blockComp.write(slice[offset..])
if pos < slice.size
compressBlock
initNextBlock
end
offset += pos
end
end
# Not implemented for a BZip2 `Writer`, always raises an exception.
def flush : Nil
raise "Cannot #flush a BZip2::Writer"
end
# Closes this writer. Must be invoked after all data has been written.
def close : Nil
return if @closed || @output.closed?
finish
@output.close if @sync_close
end
# Returns `true` if this IO is closed.
def closed? : Bool
@closed
end
def inspect(io : IO) : Nil
to_s(io)
end
@[AlwaysInline]
private def initNextBlock : Nil
@blockComp = BlockCompressor.new(@bitWriter, @streamBlockSize, @int32Pool)
end
@[AlwaysInline]
private def compressBlock : Nil
return if blockComp.empty?
blockComp.closeBlock
@streamCRC = ((@streamCRC << 1) | (@streamCRC >> 31)) ^ blockComp.crc
end
private def finish : Nil
unless @closed
begin
compressBlock
@bitWriter.writeBits(24, STREAM_END_MARKER_1)
@bitWriter.writeBits(24, STREAM_END_MARKER_2)
@bitWriter.writeInt32(@streamCRC)
@bitWriter.flush
@output.flush
ensure
@blockComp = nil
@closed = true
end
end
end
end
end
|