Class: Omnizip::Parity::Models::Packet

Inherits:
Lutaml::Model::Serializable
  • Object
show all
Defined in:
lib/omnizip/parity/models/packet.rb

Overview

Base class for PAR2 packets

All PAR2 packets share a common 64-byte header structure:

  • magic (8 bytes): "PAR2\0PKT"
  • length (8 bytes): Total packet length including header (Q<)
  • packet_hash (16 bytes): MD5 hash of (set_id + type + data)
  • set_id (16 bytes): Recovery set identifier
  • type (16 bytes): Packet type identifier

The packet body follows the header and varies by packet type.

Constant Summary collapse

PACKET_SIGNATURE =

PAR2 packet signature

"PAR2\x00PKT".b.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**attributes) ⇒ Packet

Initialize packet

Parameters:

  • attributes (Hash)

    Packet attributes



43
44
45
46
# File 'lib/omnizip/parity/models/packet.rb', line 43

def initialize(**attributes)
  @body_data = "" # Initialize body_data with empty binary string
  super
end

Instance Attribute Details

#body_dataObject

Packet body data (variable length, subclass-specific) NOT a lutaml-model attribute because it contains raw binary data that can include null bytes and invalid UTF-8 sequences



38
39
40
# File 'lib/omnizip/parity/models/packet.rb', line 38

def body_data
  @body_data
end

Class Method Details

.read_from(io) ⇒ Packet?

Read packet from IO stream

Parameters:

  • io (IO)

    Input stream

Returns:

  • (Packet, nil)

    Parsed packet or nil if EOF/invalid



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
# File 'lib/omnizip/parity/models/packet.rb', line 52

def self.read_from(io)
  # Read header (64 bytes total)
  magic = io.read(8)
  return nil unless magic == PACKET_SIGNATURE

  length = io.read(8).unpack1("Q<")
  packet_hash = io.read(16)
  set_id = io.read(16)
  type = io.read(16)

  # Read body data
  data_length = length - 64
  body_data = io.read(data_length)

  # Create instance
  packet = new(
    magic: magic,
    length: length,
    packet_hash: packet_hash,
    set_id: set_id,
    type: type,
  )

  # Set body_data directly to avoid lutaml-model attribute
  # corruption of binary data
  packet.body_data = body_data

  # Verify packet hash
  # NOTE: Hash validation temporarily disabled for par2cmdline compatibility testing
  # unless packet.valid_hash?
  #   warn "Invalid packet hash detected"
  # end

  packet
end

Instance Method Details

#build_bodyObject

Build body data from structured attributes

Subclasses must implement this to build body_data from their specific attributes.

Raises:

  • (NotImplementedError)


149
150
151
152
# File 'lib/omnizip/parity/models/packet.rb', line 149

def build_body
  raise NotImplementedError,
        "#{self.class} must implement build_body"
end

#calculate_hashString

Calculate MD5 hash of packet body

Hash is computed over: set_id + type + body_data

Returns:

  • (String)

    16-byte MD5 digest



114
115
116
117
118
119
120
# File 'lib/omnizip/parity/models/packet.rb', line 114

def calculate_hash
  data = +""
  data << set_id
  data << type
  data << body_data
  Digest::MD5.digest(data)
end

#packet_typeString

Get packet type identifier

Returns:

  • (String)

    16-byte type identifier



132
133
134
# File 'lib/omnizip/parity/models/packet.rb', line 132

def packet_type
  type
end

#parse_bodyObject

Parse body data into structured attributes

Subclasses must implement this to parse body_data into their specific attributes.

Raises:

  • (NotImplementedError)


140
141
142
143
# File 'lib/omnizip/parity/models/packet.rb', line 140

def parse_body
  raise NotImplementedError,
        "#{self.class} must implement parse_body"
end

#valid_hash?Boolean

Verify packet hash is correct

Returns:

  • (Boolean)

    true if hash matches



125
126
127
# File 'lib/omnizip/parity/models/packet.rb', line 125

def valid_hash?
  calculate_hash == packet_hash
end

#write_to(io) ⇒ Object

Write packet to IO stream

Parameters:

  • io (IO)

    Output stream



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/omnizip/parity/models/packet.rb', line 91

def write_to(io)
  # Calculate total length
  self.length = 64 + body_data.bytesize

  # Calculate packet hash (MD5 of set_id + type + body_data)
  self.packet_hash = calculate_hash

  # Write header
  io.write(magic)
  io.write([length].pack("Q<"))
  io.write(packet_hash)
  io.write(set_id)
  io.write(type)

  # Write body
  io.write(body_data)
end