Class: Oxygene::CID

Inherits:
Object
  • Object
show all
Defined in:
lib/oxygene/cid.rb

Overview

Represents a Content Identifier (CID) of some piece of content like an ATProto record or a CAR section.

Only the DASL-compatible CID versions that are used in ATProto are supported:

  • only CIDv1, not v0
  • in binary CBOR form: with SHA-256 hash, 32 bytes hash size, and codec being either 0x71 (DRISL) or 0x55 (raw) (DRISL is used in CIDs of CAR sections, and raw codec is used in CIDs of blobs)
  • in JSON string form: with 'b' prefix and Base32-encoded data, using lowercase alphabet and no '=' padding, and the CBOR form parameters listed above, resulting in an either "bafyrei" or "bafkrei" prefix

The CID instances can be created either from JSON strings or binary CBOR form, and the binary form can optionally include the \x00 prefix byte that is used when the binary CID is encoded in a CBOR tag 42 object. The instances will lazily convert between the two forms only when needed.

The code in this class is heavily optimized for performance when creating and processing a large number of CIDs e.g. when parsing a CAR repo or processing firehose events, for example:

  • the input JSON or binary string is stored without copying or allocating new strings for modified versions, if possible
  • the other form (JSON/CBOR) is memoized and only created on demand, not up front
  • equality check tries to use whichever form exists in both instances if possible, to avoid unnecessary conversion
  • validation code uses pre-generated header constants to avoid checking the headers byte by byte, and operates mostly on byte code numbers to avoid unnecessary string allocations

Related specifications:

Constant Summary collapse

JSON_PREFIX =

Multibase prefix code for Base32, required for CIDs in JSON string form.

'b'
CBOR_TAG_PREFIX =

Multibase "identity" prefix code, required for binary CIDs stored in a CBOR tag 42 object.

"\x00".b.freeze
DRISL_CODEC_ID =

DAG-CBOR or DRISL (used e.g. in CIDs of CAR sections)

0x71
RAW_CODEC_ID =

Raw binary (used in CIDs of blobs)

0x55

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data, binary: true, cbor_prefix: false, codec: nil) ⇒ CID

Creates a CID instance from binary/CBOR data or a JSON string form.

Three possible kinds of values are accepted as input:

  • a binary string with \x00 identity prefix, as used when included in a CBOR tag object's value - use binary: true and cbor_prefix: true
  • a binary string without the identity prefix, as used e.g. when decoded from a CAR archive - use binary: true without cbor_prefix
  • a JSON string - use binary: false

For backwards compatibility, the default when only the input argument is passed is to assume the "binary without CBOR prefix" form. For performance, for the "binary with CBOR prefix" and the JSON form types, the input string itself is stored without copying and frozen to prevent modification. If you need to modify the string later at the call site, call dup when passing the input argument here. (For the non-prefixed binary version, the input string is copied to add the prefix.)

Optionally, the initializer can also enforce that the CID has to use a selected one of the two available codecs (DRISL/DAG-CBOR vs. raw binary).

Parameters:

  • data (String)

    raw CID bytes, identity-prefixed CBOR bytes, or a Base32 JSON string

  • binary (Boolean) (defaults to: true)

    true if data is in binary form, false if it's a JSON string

  • cbor_prefix (Boolean) (defaults to: false)

    true if the data includes the leading null byte used inside CBOR tag 42

  • codec (:drisl, :raw, nil) (defaults to: nil)

    required content codec, or nil to accept either of the two supported codecs

Raises:

  • (ArgumentError)

    if data is nil, codec param is invalid, or invalid combination of options is used

  • (DecodeError)

    if the CID has an invalid size or is missing an expected prefix/suffix

  • (UnsupportedError)

    if the CID is in a form that is technically valid, but not supported here



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/oxygene/cid.rb', line 140

def initialize(data, binary: true, cbor_prefix: false, codec: nil)
  raise ArgumentError.new("Data cannot be nil") if data.nil?

  codec_id = case codec
    when nil then nil
    when :drisl then DRISL_CODEC_ID
    when :raw then RAW_CODEC_ID
    else raise ArgumentError.new("Unexpected CID codec: #{codec.inspect}")
  end

  if binary
    if cbor_prefix
      validate_binary_form(data, true, codec_id)
      @cbor_form = data.freeze
    else
      validate_binary_form(data, false, codec_id)
      @cbor_form = (CBOR_TAG_PREFIX + data).freeze
    end
  else
    raise ArgumentError.new("cbor_prefix cannot be used with JSON input") if cbor_prefix

    validate_json_form(data, codec_id)
    @json_form = data.freeze
  end
end

Class Method Details

.from_cbor_tag(tag) ⇒ CID

Builds a CID from a CBOR tag 42 value.

Expects a CBOR::Tagged object returned from CBOR.decode or CBOR.decode_sequence.

Parameters:

  • tag (CBOR::Tagged)

    tagged CBOR object containing binary CID data with identity (0) prefix

Returns:

  • (CID)

    CID object wrapping that content identifier

Raises:

  • (DecodeError)

    if the CID has an invalid size or is missing an expected prefix/suffix

  • (UnsupportedError)

    if the CID is in a form that is technically valid, but not supported here



95
96
97
# File 'lib/oxygene/cid.rb', line 95

def self.from_cbor_tag(tag)
  CID.new(tag.value, binary: true, cbor_prefix: true)
end

.from_json(string) ⇒ CID

Builds a CID from a Base32-encoded JSON form string.

Parameters:

  • string (String)

    59-character string with a b prefix encoded with Base32

Returns:

  • (CID)

    CID object wrapping that content identifier

Raises:

  • (ArgumentError)

    if the input string is nil

  • (DecodeError)

    if the CID has an invalid size or is missing an expected prefix/suffix

  • (UnsupportedError)

    if the CID is in a form that is technically valid, but not supported here



107
108
109
# File 'lib/oxygene/cid.rb', line 107

def self.from_json(string)
  CID.new(string, binary: false)
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Compares this CID with another CID to see if they're equal.

If both CIDs have a generated CBOR or JSON form, those forms are used for comparison without conversion. If the two only have different forms, the JSON CID is converted to binary for comparison.

Parameters:

  • other (Object)

    object to compare

Returns:

  • (Boolean)

    whether other is a CID with the same value



229
230
231
232
233
234
235
236
237
238
239
# File 'lib/oxygene/cid.rb', line 229

def ==(other)
  return false unless other.is_a?(CID)

  if @cbor_form && (other_cbor = other.instance_variable_get('@cbor_form'))
    @cbor_form == other_cbor
  elsif @json_form && (other_json = other.instance_variable_get('@json_form'))
    @json_form == other_json
  else
    self.cbor_form == other.cbor_form
  end
end

#cbor_formString

Returns the CID data in binary form with a null byte identity prefix, as stored in CBOR tag 42.

If the CID was created from a JSON form, the data is decoded from Base32 (and memoized). If it was created from a CBOR tag, the input is returned directly.

Returns:

Raises:

  • (ArgumentError)

    if a JSON-form CID contains invalid Base32



174
175
176
# File 'lib/oxygene/cid.rb', line 174

def cbor_form
  @cbor_form ||= Base32.decode(@json_form, 1, CBOR_TAG_PREFIX).freeze
end

#hashInteger

Returns a hash code for the purposes of a Set or Hash.

The binary CBOR form of the CID is used to derive the hash.

Returns:

  • (Integer)

    hash code generated from the CID's binary form



249
250
251
# File 'lib/oxygene/cid.rb', line 249

def hash
  cbor_form.hash
end

#inspectString

Returns a representation of the CID object for debugging.

Returns:

  • (String)

    a representation of the CID object for debugging



216
217
218
# File 'lib/oxygene/cid.rb', line 216

def inspect
  "CID(\"#{json_form}\")"
end

#json_formString

Returns the CID's Base32-encoded JSON string representation.

If the CID was created from a binary form, the data is encoded into Base32 (and memoized). If it was created from a JSON form, the input is returned directly.

Returns:

  • (String)

    frozen CID string beginning with JSON_PREFIX



202
203
204
# File 'lib/oxygene/cid.rb', line 202

def json_form
  @json_form ||= Base32.encode(@cbor_form, 1, JSON_PREFIX).freeze
end

#raw_dataString Also known as: data

Returns the CID data in binary form, without the null byte identity prefix from CBOR tag.

If the CID was created from a JSON form, the data is decoded from Base32 (and memoized).

Returns:

  • (String)

    frozen binary CID bytes

Raises:

  • (ArgumentError)

    if a JSON-form CID contains invalid Base32



185
186
187
188
189
190
191
# File 'lib/oxygene/cid.rb', line 185

def raw_data
  @raw_data ||= if @cbor_form
    @cbor_form.byteslice(1, @cbor_form.bytesize - 1).freeze
  else
    Base32.decode(@json_form, 1).freeze
  end
end

#to_sString

Returns the CID's Base32-encoded JSON string representation (same as #json_form).

Returns:

  • (String)

    the CID in the JSON form



210
211
212
# File 'lib/oxygene/cid.rb', line 210

def to_s
  json_form
end