196 lines
4.7 KiB
Ruby
196 lines
4.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative 'array'
|
|
|
|
SANITIZE = 1
|
|
SANITIZE_CC = 2
|
|
SKIP_START_WHITESPACES = 4
|
|
|
|
class Packer
|
|
# Format: ESDDDDDD EDDDDDDD EDD... Extended, Data, Sign
|
|
# @return [Array<int>]
|
|
def self.pack_int(num)
|
|
# the first byte can fit 6 bits
|
|
# because the first two bits are extended and sign
|
|
# so if you have a number bigger than 63
|
|
# it needs two bytes
|
|
# which are also least significant byte first etc
|
|
# so we do not support that YET
|
|
#
|
|
# the first too big number 64 is represented as those bits
|
|
# 10000000 00000001
|
|
# ^^^ ^ ^^ ^
|
|
# ||\ / | \ /
|
|
# || \ / not \ /
|
|
# || \ extended
|
|
# || \ /
|
|
# || \ /
|
|
# || \ /
|
|
# || \/
|
|
# || /\
|
|
# || / \
|
|
# || / \
|
|
# || 0000001 000000
|
|
# || |
|
|
# || v
|
|
# || 64
|
|
# ||
|
|
# |positive
|
|
# extended
|
|
sign = '0'
|
|
if num.negative?
|
|
sign = '1'
|
|
num += 1
|
|
end
|
|
num = num.abs
|
|
return pack_big_int(sign, num) if num > 63 || num < -63
|
|
|
|
ext = '0'
|
|
bits = ext + sign + num.to_s(2).rjust(6, '0')
|
|
[bits.to_i(2)]
|
|
end
|
|
|
|
def self.pack_big_int(sign, num)
|
|
num_bits = num.to_s(2)
|
|
first = "1#{sign}#{num_bits[-6..]}"
|
|
|
|
num_bits = num_bits[0..-7]
|
|
bytes = []
|
|
num_bits.chars.groups_of(7).each do |seven_bits|
|
|
# mark all as extended
|
|
bytes << "1#{seven_bits.join.rjust(7, '0')}"
|
|
end
|
|
# least significant first
|
|
bytes = bytes.reverse
|
|
# mark last byte as unextended
|
|
bytes[-1][0] = '0'
|
|
([first] + bytes).map { |b| b.to_i(2) }
|
|
end
|
|
|
|
def self.pack_str(str)
|
|
str.chars.map(&:ord) + [0x00]
|
|
end
|
|
end
|
|
|
|
class Unpacker
|
|
attr_reader :prev, :data, :parsed
|
|
|
|
def initialize(data)
|
|
@data = data
|
|
@prev = [] # all the data already unpacked
|
|
@parsed = []
|
|
# TODO: delete parsed or document it with examples
|
|
# @parsed.push({ type: 'string', value: str, raw:, len:, pos: })
|
|
@red_bytes = 0
|
|
# { type: 'int', value: 1, raw: "\x01", len: 1, pos: 0 }
|
|
if data.instance_of?(String)
|
|
@data = data.unpack('C*')
|
|
elsif data.instance_of?(Array)
|
|
@data = data
|
|
else
|
|
raise 'Error: Unpacker expects array of integers or byte string'
|
|
end
|
|
end
|
|
|
|
def str_sanitize(str)
|
|
letters = str.chars
|
|
letters.map! do |c|
|
|
c.ord < 32 && c != "\r" && c != "\n" && c != "\t" ? ' ' : c
|
|
end
|
|
letters.join
|
|
end
|
|
|
|
def str_sanitize_cc(str)
|
|
letters = str.chars
|
|
letters.map! do |c|
|
|
c.ord < 32 ? ' ' : c
|
|
end
|
|
letters.join
|
|
end
|
|
|
|
def get_string(sanitize = SANITIZE)
|
|
return nil if @data.nil?
|
|
|
|
str = ''
|
|
raw = []
|
|
len = 0
|
|
pos = @red_bytes
|
|
@data.each_with_index do |byte, index|
|
|
@prev.push(byte)
|
|
raw.push(raw)
|
|
@red_bytes += 1
|
|
len += 1
|
|
if byte.zero?
|
|
@data = index == @data.length - 1 ? nil : @data[(index + 1)..]
|
|
str = str_sanitize(str) unless (sanitize & SANITIZE).zero?
|
|
str = str_sanitize_cc(str) unless (sanitize & SANITIZE_CC).zero?
|
|
@parsed.push({ type: 'string', value: str, raw:, len:, pos: })
|
|
return str
|
|
end
|
|
str += byte.chr
|
|
end
|
|
# raise "get_string() failed to find null terminator"
|
|
# return empty string in case of error
|
|
''
|
|
end
|
|
|
|
def get_int
|
|
return nil if @data.nil?
|
|
return nil if @data.empty?
|
|
|
|
# TODO: make this more performant
|
|
# it should not read in ALL bytes
|
|
# of the WHOLE packed data
|
|
# it should be max 4 bytes
|
|
# because bigger ints are not sent anyways
|
|
bytes = @data.map { |byte| byte.to_s(2).rjust(8, '0') }
|
|
first = bytes[0]
|
|
|
|
sign = first[1]
|
|
bits = []
|
|
parsed = { type: 'int' }
|
|
|
|
# extended
|
|
if first[0] == '1'
|
|
bits << first[2..]
|
|
bytes = bytes[1..]
|
|
consumed = 1
|
|
bytes.each do |eigth_bits|
|
|
bits << eigth_bits[1..]
|
|
consumed += 1
|
|
|
|
break if eigth_bits[0] == '0'
|
|
end
|
|
bits = bits.reverse
|
|
@prev += @data[0...consumed]
|
|
parsed[:raw] = @data[0...consumed]
|
|
parsed[:len] = consumed
|
|
parsed[:pos] = @red_bytes
|
|
@red_bytes += consumed
|
|
@data = @data[consumed..]
|
|
else # single byte
|
|
bits = [first[2..]]
|
|
@prev.push(@data[0])
|
|
parsed[:raw] = [@data[0]]
|
|
parsed[:len] = 1
|
|
parsed[:pos] = @red_bytes
|
|
@red_bytes += 1
|
|
@data = @data[1..]
|
|
end
|
|
num = bits.join.to_i(2)
|
|
parsed[:value] = sign == '1' ? -(num + 1) : num
|
|
@parsed.push(parsed)
|
|
parsed[:value]
|
|
end
|
|
|
|
def get_raw(size = -1)
|
|
len = size == -1 ? @data.size : size
|
|
@prev += @data[...len]
|
|
pos = @red_bytes
|
|
@red_bytes += len
|
|
@parsed.push({ type: 'raw', value: @data[...len], raw: @data[...len], len:, pos: })
|
|
# TODO: error if size exceeds @data.size
|
|
@data.shift(len)
|
|
end
|
|
end
|