Module: FormatParser::MP3Parser::ID3Extraction

Extended by:
IOUtils, ID3Extraction
Included in:
ID3Extraction
Defined in:
lib/parsers/mp3_parser/id3_extraction.rb

Constant Summary collapse

ID3V1_TAG_SIZE_BYTES =
128
ID3V2_MINOR_TAG_VERSIONS =

it supports 2.4.x, 2.3.x, 2.2.x which are supported by the gem id3tag see id3.org/Developer%20Information for more details of each version

[2, 3, 4]
MAX_SIZE_FOR_ID3V2 =
1 * 1024 * 1024

Constants included from IOUtils

IOUtils::INTEGER_DIRECTIVES

Instance Method Summary collapse

Methods included from IOUtils

read_bytes, read_fixed_point, read_int, safe_read, safe_skip, skip_bytes

Instance Method Details

#attempt_id3_v1_extraction(io) ⇒ Object



10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/parsers/mp3_parser/id3_extraction.rb', line 10

def attempt_id3_v1_extraction(io)
  return if io.size < ID3V1_TAG_SIZE_BYTES # Won't fit the ID3v1 regardless

  io.seek(io.size - 128)
  trailer_bytes = io.read(128)

  return unless trailer_bytes && trailer_bytes.bytesize == ID3V1_TAG_SIZE_BYTES
  return unless trailer_bytes.byteslice(0, 3) == 'TAG'

  buf = StringIO.new(trailer_bytes)
  swallow_exceptions { ID3Tag.read(buf, :v1) }
end

#attempt_id3_v2_extraction(io) ⇒ Object



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
# File 'lib/parsers/mp3_parser/id3_extraction.rb', line 23

def attempt_id3_v2_extraction(io)
  io.seek(0) # Only support header ID3v2
  header = parse_id3_v2_header(io)
  return unless header[:tag] == 'ID3' && header[:size] > 0
  return unless ID3V2_MINOR_TAG_VERSIONS.include?(header[:version].unpack('C').first)

  id3_tag_size = io.pos + header[:size]

  # Here we got to pay attention. The tag size encoded in
  # the ID3 header is a 4-byte unsigned int. Meaning it
  # can hold values up to 256 MB. We do not want to read
  # that much since we are pulling that data into memory -
  # and it would also make the parser easily exploitable.
  # We will set a "hard" limit beyound which we will simply
  # refuse to read those tags at all.
  if id3_tag_size > MAX_SIZE_FOR_ID3V2
    io.seek(id3_tag_size) # For reading the frames
    return
  end

  io.seek(0)
  blob = safe_read(io, id3_tag_size)

  swallow_exceptions { ID3Tag.read(StringIO.new(blob), :v2) }
rescue FormatParser::IOUtils::InvalidRead
  nil
end

#parse_id3_v2_header(io) ⇒ Object



60
61
62
63
64
65
# File 'lib/parsers/mp3_parser/id3_extraction.rb', line 60

def parse_id3_v2_header(io)
  fields = {tag: :a3, version: :a2, flags: :a1, syncsafe_size: :N1}
  header_data = read_and_unpack_packspec(io, **fields)
  header_data[:size] = ID3Tag::SynchsafeInteger.decode(header_data.delete(:syncsafe_size))
  header_data
end

#read_and_unpack_packspec(io, **packspec) ⇒ Object



51
52
53
54
55
56
57
58
# File 'lib/parsers/mp3_parser/id3_extraction.rb', line 51

def read_and_unpack_packspec(io, **packspec)
  sizes = {'a' => 1, 'N' => 4}
  n = packspec.values.map { |e| sizes.fetch(e[0]) * e[1].to_i }.inject(&:+)
  byte_str = safe_read(io, n)

  unpacked_values = byte_str.unpack(packspec.values.join)
  Hash[packspec.keys.zip(unpacked_values)]
end

#swallow_exceptionsObject

We swallow exceptions from ID3Tag primarily because it does not have a single wrapping error class we could capture. We also do not touch our original IO object when working with ID3Tag



70
71
72
73
74
75
# File 'lib/parsers/mp3_parser/id3_extraction.rb', line 70

def swallow_exceptions
  yield
rescue => e
  warn(e)
  nil
end