Fix ID3v2 frame size calculation

Frame size uses 7-out-of-8-Bits notation.

Signed-off-by: Markus Birth <markus@birth-online.de>
This commit is contained in:
Markus Birth 2024-05-27 15:28:22 +01:00
parent 48edb42c1e
commit d6977ef4dc

View File

@ -492,11 +492,10 @@ class Mp3Info {
{ {
// read the rest of the id3v2 header // read the rest of the id3v2 header
$raw = $this->fileObj->getBytes(7); $raw = $this->fileObj->getBytes(7);
$data = unpack('cmajor_version/cminor_version/H*', $raw); $data = unpack('Cmajor_version/Cminor_version/Cflags/C4size', $raw);
$this->id3v2MajorVersion = $data['major_version']; $this->id3v2MajorVersion = $data['major_version'];
$this->id3v2MinorVersion = $data['minor_version']; $this->id3v2MinorVersion = $data['minor_version'];
$data = str_pad(base_convert($data[1], 16, 2), 40, 0, STR_PAD_LEFT); $flags = decbin($data['flags']);
$flags = substr($data, 0, 8);
if ($this->id3v2MajorVersion == 2) { // parse id3v2.2.0 header flags if ($this->id3v2MajorVersion == 2) { // parse id3v2.2.0 header flags
$this->id3v2Flags = array( $this->id3v2Flags = array(
'unsynchronisation' => (bool)substr($flags, 0, 1), 'unsynchronisation' => (bool)substr($flags, 0, 1),
@ -522,24 +521,15 @@ class Mp3Info {
if ($this->id3v2Flags['footer_present']) if ($this->id3v2Flags['footer_present'])
throw new \Exception('NEED TO PARSE id3v2.4 FOOTER!'); throw new \Exception('NEED TO PARSE id3v2.4 FOOTER!');
} }
$size = substr($data, 8, 32); $size = $data['size1'] << 21 | $data['size2'] << 14 | $data['size3'] << 7 | $data['size4'];
// some fucking shit
// getting only 7 of 8 bits of size bytes
$sizes = str_split($size, 8);
array_walk($sizes, function (&$value) {
$value = substr($value, 1);
});
$size = implode($sizes);
$size = bindec($size);
if ($this->id3v2MajorVersion == 2) { if ($this->id3v2MajorVersion == 2) {
// parse id3v2.2.0 body // parse id3v2.2.0 body
/*throw new \Exception('NEED TO PARSE id3v2.2.0 flags!');*/ /*throw new \Exception('NEED TO PARSE id3v2.2.0 flags!');*/
} else if ($this->id3v2MajorVersion == 3) { } elseif ($this->id3v2MajorVersion == 3) {
// parse id3v2.3.0 body // parse id3v2.3.0 body
$this->parseId3v23Body(10 + $size); $this->parseId3v23Body(10 + $size);
} else if ($this->id3v2MajorVersion == 4) { } elseif ($this->id3v2MajorVersion == 4) {
// parse id3v2.4.0 body // parse id3v2.4.0 body
$this->parseId3v24Body(10 + $size); $this->parseId3v24Body(10 + $size);
} }
@ -557,7 +547,7 @@ class Mp3Info {
$frame_id = substr($raw, 0, 4); $frame_id = substr($raw, 0, 4);
if ($frame_id == str_repeat(chr(0), 4)) { if ($frame_id == str_repeat(chr(0), 4)) {
fseek($fp, $lastByte); $this->fileObj->seekTo($lastByte);
break; break;
} }
@ -697,15 +687,15 @@ class Mp3Info {
// break; // break;
case 'APIC': # Attached picture case 'APIC': # Attached picture
$this->hasCover = true; $this->hasCover = true;
$last_byte = $this->fileObj->getFilePos() + $frame_size; $dataEnd = $this->fileObj->getFilePos() + $frame_size;
$this->coverProperties = ['text_encoding' => ord($this->fileObj->getBytes(1))]; $this->coverProperties = ['text_encoding' => ord($this->fileObj->getBytes(1))];
// fseek($fp, $frame_size - 4, SEEK_CUR); // fseek($fp, $frame_size - 4, SEEK_CUR);
$this->coverProperties['mime_type'] = $this->readTextUntilNull($last_byte); $this->coverProperties['mime_type'] = $this->readTextUntilNull($dataEnd);
$this->coverProperties['picture_type'] = ord($this->fileObj->getBytes(1)); $this->coverProperties['picture_type'] = ord($this->fileObj->getBytes(1));
$this->coverProperties['description'] = $this->readTextUntilNull($last_byte); $this->coverProperties['description'] = $this->readTextUntilNull($dataEnd);
$this->coverProperties['offset'] = $this->fileObj->getFilePos(); $this->coverProperties['offset'] = $this->fileObj->getFilePos();
$this->coverProperties['size'] = $last_byte - $this->fileObj->getFilePos(); $this->coverProperties['size'] = $dataEnd - $this->fileObj->getFilePos();
$this->fileObj->seekTo($last_byte); $this->fileObj->seekTo($dataEnd);
break; break;
// case 'GEOB': # General encapsulated object // case 'GEOB': # General encapsulated object
// break; // break;
@ -744,7 +734,7 @@ class Mp3Info {
/** /**
* Parses id3v2.4.0 tag body. * Parses id3v2.4.0 tag body.
* @param $fp *
* @param $lastByte * @param $lastByte
*/ */
protected function parseId3v24Body($lastByte) protected function parseId3v24Body($lastByte)
@ -758,10 +748,12 @@ class Mp3Info {
break; break;
} }
$data = unpack('Nframe_size/H2flags', substr($raw, 4)); $data = unpack('C4frame_size/H2flags', substr($raw, 4));
$frame_size = $data['frame_size']; $frame_size = $data['frame_size1'] << 21 | $data['frame_size2'] << 14 | $data['frame_size3'] << 7 | $data['frame_size4'];
$flags = base_convert($data['flags'], 16, 2); $flags = base_convert($data['flags'], 16, 2);
$this->id3v2TagsFlags[$frame_id] = array( $this->id3v2TagsFlags[$frame_id] = array(
'frame_size' => $frame_size,
'flags' => array( 'flags' => array(
'tag_alter_preservation' => (bool)substr($flags, 1, 1), 'tag_alter_preservation' => (bool)substr($flags, 1, 1),
'file_alter_preservation' => (bool)substr($flags, 2, 1), 'file_alter_preservation' => (bool)substr($flags, 2, 1),
@ -790,6 +782,7 @@ class Mp3Info {
case 'TCOM': # Composer case 'TCOM': # Composer
case 'TCOP': # Copyright message case 'TCOP': # Copyright message
case 'TDAT': # Date case 'TDAT': # Date
case 'TDRC': # Recording time
case 'TDLY': # Playlist delay case 'TDLY': # Playlist delay
case 'TENC': # Encoded by case 'TENC': # Encoded by
case 'TEXT': # Lyricist/Text writer case 'TEXT': # Lyricist/Text writer
@ -870,15 +863,16 @@ class Mp3Info {
$char = fgetc($fp); $char = fgetc($fp);
if ($char == "\00" && $actual_text === false) { if ($char == "\00" && $actual_text === false) {
if ($data['encoding'] == 0x1) { # two null-bytes for utf-16 if ($data['encoding'] == 0x1) { # two null-bytes for utf-16
if ($last_null) if ($last_null) {
$actual_text = null; $actual_text = null;
else } else {
$last_null = true; $last_null = true;
} else # no condition for iso-8859-1 }
} else { # no condition for iso-8859-1
$actual_text = null; $actual_text = null;
}
} }
else if ($actual_text !== false) $actual_text .= $char; elseif ($actual_text !== false) $actual_text .= $char;
else $short_description .= $char; else $short_description .= $char;
} }
if ($actual_text === false) $actual_text = $short_description; if ($actual_text === false) $actual_text = $short_description;
@ -897,15 +891,15 @@ class Mp3Info {
// break; // break;
case 'APIC': # Attached picture case 'APIC': # Attached picture
$this->hasCover = true; $this->hasCover = true;
$last_byte = $this->fileObj->getFilePos() + $frame_size; $dataEnd = $this->fileObj->getFilePos() + $frame_size;
$this->coverProperties = ['text_encoding' => ord($this->fileObj->getBytes(1))]; $this->coverProperties = ['text_encoding' => ord($this->fileObj->getBytes(1))];
// $this->fileObj->seekForward($frame_size - 4); // $this->fileObj->seekForward($frame_size - 4);
$this->coverProperties['mime_type'] = $this->readTextUntilNull($fp, $last_byte); $this->coverProperties['mime_type'] = $this->readTextUntilNull($dataEnd);
$this->coverProperties['picture_type'] = ord($this->fileObj->getBytes(1)); $this->coverProperties['picture_type'] = ord($this->fileObj->getBytes(1));
$this->coverProperties['description'] = $this->readTextUntilNull($fp, $last_byte); $this->coverProperties['description'] = $this->readTextUntilNull($dataEnd);
$this->coverProperties['offset'] = $this->fileObj->getFilePos(); $this->coverProperties['offset'] = $this->fileObj->getFilePos();
$this->coverProperties['size'] = $last_byte - $this->fileObj->getFilePos(); $this->coverProperties['size'] = $dataEnd - $this->fileObj->getFilePos();
$this->fileObj->seekTo($last_byte); $this->fileObj->seekTo($dataEnd);
break; break;
// case 'GEOB': # General encapsulated object // case 'GEOB': # General encapsulated object
// break; // break;