Login
writer.cr at tip
Login

File src/remilib/compression/bzip/writer.cr from the latest check-in


     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
   100
   101
   102
   103
   104
   105
   106
   107
   108
   109
   110
   111
   112
   113
   114
   115
   116
   117
   118
   119
   120
   121
   122
   123
   124
   125
   126
   127
   128
   129
   130
   131
   132
   133
   134
   135
   136
   137
   138
   139
   140
   141
   142
   143
   144
   145
   146
   147
#### 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