require'stringio'moduleBinData# A wrapper around an IO object. The wrapper provides a consistent# interface for BinData objects to use when accessing the IO.moduleIO# Common operations for both Read and Write.moduleCommondefinitialize(io)ifself.class===ioraiseArgumentError,"io must not be a #{self.class}"end# wrap strings in a StringIOifio.respond_to?(:to_str)io=BinData::IO.create_string_io(io.to_str)end@raw_io=io@buffer_end_points=nilextendseekable??SeekableStream:UnSeekableStreamstream_initend#-------------privatedefseekable?@raw_io.posrescueNoMethodError,Errno::ESPIPE,Errno::EPIPEnilenddefseek(n)seek_raw(buffer_limited_n(n))enddefbuffer_limited_n(n)if@buffer_end_pointsifn.nil?orn>0max=@buffer_end_points[1]-offsetn=maxifn.nil?orn>maxelsemin=@buffer_end_points[0]-offsetn=minifn<minendendnenddefwith_buffer_common(n,&block)prev=@buffer_end_pointsifprevavail=prev[1]-offsetn=availifn>availend@buffer_end_points=[offset,offset+n]beginblock.call(*@buffer_end_points)ensure@buffer_end_points=prevendend# Use #seek and #pos on seekable streamsmoduleSeekableStream# The number of bytes remaining in the input stream.defnum_bytes_remainingstart_mark=@raw_io.pos@raw_io.seek(0,::IO::SEEK_END)end_mark=@raw_io.posif@buffer_end_pointsif@buffer_end_points[1]<end_markend_mark=@buffer_end_points[1]endendbytes_remaining=end_mark-start_mark@raw_io.seek(start_mark,::IO::SEEK_SET)bytes_remainingend# All io calls in +block+ are rolled back after this# method completes.defwith_readahead(&block)mark=@raw_io.posbeginblock.callensure@raw_io.seek(mark,::IO::SEEK_SET)endend#-----------privatedefstream_init@initial_pos=@raw_io.posenddefoffset_raw@raw_io.pos-@initial_posenddefseek_raw(n)@raw_io.seek(n,::IO::SEEK_CUR)enddefread_raw(n)@raw_io.read(n)enddefwrite_raw(data)@raw_io.write(data)endend# Manually keep track of offset for unseekable streams.moduleUnSeekableStreamdefoffset_raw@offsetend# The number of bytes remaining in the input stream.defnum_bytes_remainingraiseIOError,"stream is unseekable"end# All io calls in +block+ are rolled back after this# method completes.defwith_readahead(&block)mark=@offset@read_data=""@in_readahead=trueclass<<selfalias_method:read_raw_without_readahead,:read_rawalias_method:read_raw,:read_raw_with_readaheadendbeginblock.callensure@offset=mark@in_readahead=falseendend#-----------privatedefstream_init@offset=0enddefread_raw(n)data=@raw_io.read(n)@offset+=data.sizeifdatadataenddefread_raw_with_readahead(n)data=""if@read_data.length>0andnot@in_readaheadbytes_to_consume=[n,@read_data.length].mindata<<@read_data.slice!(0,bytes_to_consume)n-=bytes_to_consumeif@read_data.length==0class<<selfalias_method:read_raw,:read_raw_without_readaheadendendendraw_data=@raw_io.read(n)data<<raw_dataifraw_dataif@in_readahead@read_data<<dataend@offset+=data.sizedataenddefwrite_raw(data)@offset+=data.size@raw_io.write(data)enddefseek_raw(n)raiseIOError,"stream is unseekable"ifn<0# NOTE: how do we seek on a writable stream?# skip over data in 8k blockswhilen>0bytes_to_read=[n,8192].minread_raw(bytes_to_read)n-=bytes_to_readendendendend# Creates a StringIO around +str+.defself.create_string_io(str="")StringIO.new(str.dup.force_encoding(Encoding::BINARY))end# Create a new IO Read wrapper around +io+. +io+ must provide #read,# #pos if reading the current stream position and #seek if setting the# current stream position. If +io+ is a string it will be automatically# wrapped in an StringIO object.## The IO can handle bitstreams in either big or little endian format.## M byte1 L M byte2 L# S 76543210 S S fedcba98 S# B B B B## In big endian format:# readbits(6), readbits(5) #=> [765432, 10fed]## In little endian format:# readbits(6), readbits(5) #=> [543210, a9876]#classReadincludeCommondefinitialize(io)super(io)# bits when reading@rnbits=0@rval=0@rendian=nilend# Sets a buffer of +n+ bytes on the io stream. Any reading or seeking# calls inside the +block+ will be contained within this buffer.defwith_buffer(n,&block)with_buffer_common(n)doblock.callreadendend# Returns the current offset of the io stream. Offset will be rounded# up when reading bitfields.defoffsetoffset_rawend# Seek +n+ bytes from the current position in the io stream.defseekbytes(n)reset_read_bitsseek(n)end# Reads exactly +n+ bytes from +io+.## If the data read is nil an EOFError is raised.## If the data read is too short an IOError is raised.defreadbytes(n)reset_read_bitsstr=read(n)raiseEOFError,"End of file reached"ifstr.nil?raiseIOError,"data truncated"ifstr.size<nstrend# Reads all remaining bytes from the stream.defread_all_bytesreset_read_bitsreadend# Reads exactly +nbits+ bits from the stream. +endian+ specifies whether# the bits are stored in +:big+ or +:little+ endian format.defreadbits(nbits,endian)if@rendian!=endian# don't mix bits of differing endianreset_read_bits@rendian=endianendifendian==:bigread_big_endian_bits(nbits)elseread_little_endian_bits(nbits)endend# Discards any read bits so the stream becomes aligned at the# next byte boundary.defreset_read_bits@rnbits=0@rval=0end#---------------privatedefread(n=nil)read_raw(buffer_limited_n(n))enddefread_big_endian_bits(nbits)while@rnbits<nbitsaccumulate_big_endian_bitsendval=(@rval>>(@rnbits-nbits))&mask(nbits)@rnbits-=nbits@rval&=mask(@rnbits)valenddefaccumulate_big_endian_bitsbyte=read(1)raiseEOFError,"End of file reached"ifbyte.nil?byte=byte.unpack('C').at(0)&0xff@rval=(@rval<<8)|byte@rnbits+=8enddefread_little_endian_bits(nbits)while@rnbits<nbitsaccumulate_little_endian_bitsendval=@rval&mask(nbits)@rnbits-=nbits@rval>>=nbitsvalenddefaccumulate_little_endian_bitsbyte=read(1)raiseEOFError,"End of file reached"ifbyte.nil?byte=byte.unpack('C').at(0)&0xff@rval=@rval|(byte<<@rnbits)@rnbits+=8enddefmask(nbits)(1<<nbits)-1endend# Create a new IO Write wrapper around +io+. +io+ must provide #write.# If +io+ is a string it will be automatically wrapped in an StringIO# object.## The IO can handle bitstreams in either big or little endian format.## See IO::Read for more information.classWriteincludeCommondefinitialize(io)super(io)@wnbits=0@wval=0@wendian=nilend# Sets a buffer of +n+ bytes on the io stream. Any writes inside the# +block+ will be contained within this buffer. If less than +n+ bytes# are written inside the block, the remainder will be padded with '\0'# bytes.defwith_buffer(n,&block)with_buffer_common(n)do|buf_start,buf_end|block.callwrite("\0"*(buf_end-offset))endend# Returns the current offset of the io stream. Offset will be rounded# up when writing bitfields.defoffsetoffset_raw+(@wnbits>0?1:0)end# Seek +n+ bytes from the current position in the io stream.defseekbytes(n)flushbitsseek(n)end# Writes the given string of bytes to the io stream.defwritebytes(str)flushbitswrite(str)end# Writes +nbits+ bits from +val+ to the stream. +endian+ specifies whether# the bits are to be stored in +:big+ or +:little+ endian format.defwritebits(val,nbits,endian)if@wendian!=endian# don't mix bits of differing endianflushbits@wendian=endianendclamped_val=val&mask(nbits)ifendian==:bigwrite_big_endian_bits(clamped_val,nbits)elsewrite_little_endian_bits(clamped_val,nbits)endend# To be called after all +writebits+ have been applied.defflushbitsraise"Internal state error nbits = #{@wnbits}"if@wnbits>=8if@wnbits>0writebits(0,8-@wnbits,@wendian)endendalias_method:flush,:flushbits#---------------privatedefwrite(data)n=buffer_limited_n(data.size)ifn<data.sizedata=data[0,n]endwrite_raw(data)enddefwrite_big_endian_bits(val,nbits)whilenbits>0bits_req=8-@wnbitsifnbits>=bits_reqmsb_bits=(val>>(nbits-bits_req))&mask(bits_req)nbits-=bits_reqval&=mask(nbits)@wval=(@wval<<bits_req)|msb_bitswrite(@wval.chr)@wval=0@wnbits=0else@wval=(@wval<<nbits)|val@wnbits+=nbitsnbits=0endendenddefwrite_little_endian_bits(val,nbits)whilenbits>0bits_req=8-@wnbitsifnbits>=bits_reqlsb_bits=val&mask(bits_req)nbits-=bits_reqval>>=bits_req@wval=@wval|(lsb_bits<<@wnbits)write(@wval.chr)@wval=0@wnbits=0else@wval=@wval|(val<<@wnbits)@wnbits+=nbitsnbits=0endendenddefmask(nbits)(1<<nbits)-1endendendend