////------------------------------------------------------------------------------------------------------------------------------ /// <summary> /// Initializes a new instance of the <see cref="AudioTagOffset" /> class. /// </summary> /// <param name="tagOrigin">The tag origin.</param> /// <param name="startOffset">The start offset.</param> /// <param name="endOffset">The end offset.</param> /// <param name="audioTag">The audio tag.</param> public AudioTagOffset(TagOrigin tagOrigin, long startOffset, long endOffset, IAudioTag audioTag) { TagOrigin = tagOrigin; StartOffset = startOffset; EndOffset = endOffset; AudioTag = audioTag; }
////------------------------------------------------------------------------------------------------------------------------------ /// <inheritdoc/> public IAudioTagOffset ReadFromStream(Stream stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanRead) throw new InvalidOperationException("stream can not be read"); if (!stream.CanSeek) throw new InvalidOperationException("stream can not be seeked"); // This tag can only be located at the end; the header does not contain a size field, so we're not able to calculate the end. if (tagOrigin == TagOrigin.Start) return null; StreamBuffer sb = stream as StreamBuffer ?? new StreamBuffer(stream); Lyrics3Tag tag = new Lyrics3Tag { Encoding = Encoding }; long footerOffset = FindFooterIdentifier(sb); if (footerOffset < 0) return null; sb.Position = Math.Max(footerOffset - Lyrics3Tag.MaxLyricsSize, 0); long headerOffset = FindHeaderIdentifier(sb); if (headerOffset < 0) return null; // Read lyrics int lyricsLength = (int)(footerOffset - (headerOffset + HeaderIdentifierBytes.Length)); string lyrics = sb.ReadString(lyricsLength); tag.Lyrics = GetLyrics(lyrics); // Read footer sb.ReadString(FooterIdentifierBytes.Length); return new AudioTagOffset(tagOrigin, headerOffset, footerOffset, tag); }
////------------------------------------------------------------------------------------------------------------------------------ /// <inheritdoc/> public IAudioTagOffset ReadFromStream(Stream stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanRead) throw new InvalidOperationException("stream can not be read"); if (!stream.CanSeek) throw new InvalidOperationException("stream can not be seeked"); StreamBuffer sb = stream as StreamBuffer ?? new StreamBuffer(stream); long startOffset = FindIdentifier(sb, tagOrigin); if (startOffset < 0) return null; long endOffset = 0; if (tagOrigin == TagOrigin.End) { endOffset = startOffset + FooterIdentifierBytes.Length; sb.Position = Math.Max(sb.Position - FooterIdentifierBytes.Length - Lyrics3v2Tag.TagSizeLength, 0); string strTagSize = sb.ReadString(Lyrics3v2Tag.TagSizeLength); long tagSize; if (!Int64.TryParse(strTagSize, out tagSize)) { #if DEBUG throw new InvalidDataException(String.Format("Can't parse size value {0} before footer.", strTagSize)); #else return null; #endif } long currentPosition = sb.Position; // The Lyrics3 block ends with a six character size descriptor and the string "LYRICS200". // The size value includes the "LYRICSBEGIN" string, but does not include the 6 character size descriptor and the trailing "LYRICS200" string. sb.Position = Math.Max(sb.Position - tagSize - Lyrics3v2Tag.TagSizeLength, 0); startOffset = FindIdentifier(sb, TagOrigin.Start); if (startOffset < 0) { // Header could be off sb.Position = Math.Max(sb.Position - (tagSize - 5), 0); if (sb.Position > 0) { startOffset = FindIdentifier(sb, TagOrigin.End); if (startOffset < 0) startOffset = sb.Position = currentPosition; } } } // Read the fields. List<Lyrics3v2Field> fields = new List<Lyrics3v2Field>(); long bytesRead = HeaderIdentifierBytes.Length; long streamLength = (endOffset > 0) ? endOffset : Math.Max(sb.Length, endOffset); while (startOffset + bytesRead < streamLength) { long startPosition = sb.Position; // Read field if needed. Lyrics3v2Field field = Lyrics3v2Field.ReadFromStream(sb, streamLength - bytesRead); if (field != null) { Lyrics3v2FieldParseEventArgs fieldParseEventArgs = new Lyrics3v2FieldParseEventArgs(field); OnFieldParse(fieldParseEventArgs); if (!fieldParseEventArgs.Cancel) { // Call after read event. Lyrics3v2FieldParsedEventArgs parsedEventArgs = new Lyrics3v2FieldParsedEventArgs(field); OnFieldParsed(parsedEventArgs); fields.Add(field); } } else { sb.Position = startPosition; break; } bytesRead += (sb.Position - startPosition); } if (endOffset == 0) { // The Lyrics3 block ends with a six character size descriptor and the string "LYRICS200". // The size value includes the "LYRICSBEGIN" string, but does not include the 6 character size descriptor and the trailing "LYRICS200" string. string strTagSize = sb.ReadString(6); long tagSize; if (!Int64.TryParse(strTagSize, out tagSize)) { #if DEBUG throw new InvalidDataException(String.Format("Can't parse size value {0} before footer.", strTagSize)); #else return null; #endif } int bRead; string footerIdentifier = sb.ReadString(FooterIdentifierBytes.Length, true, out bRead); if (!String.Equals(footerIdentifier, Lyrics3v2Tag.FooterIdentifier)) { #if DEBUG throw new InvalidDataException( String.Format("Footer identifier {0} not recognized; expected {1}", footerIdentifier, Lyrics3v2Tag.FooterIdentifier)); #else sb.Position -= bRead; #endif } endOffset = sb.Position; } #if DEBUG long totalDataSize = (endOffset - startOffset) - Lyrics3v2Tag.TagSizeLength - FooterIdentifierBytes.Length; //if (totalDataSize != bytesRead) //{ // // If the amount of bytes we read doesn't match the size of the tag indicated in the header, // throw new InvalidDataException( // String.Format( // "Amount of bytes read ({0}) does not match size indicated before footer ({1}).", // totalDataSize, // bytesRead)); //} #endif Lyrics3v2Tag tag = new Lyrics3v2Tag(); tag.SetFields(fields); return new AudioTagOffset(tagOrigin, startOffset, endOffset, tag); }
////------------------------------------------------------------------------------------------------------------------------------ private static long FindIdentifier(Stream stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); long startPosition = stream.Position; long streamLength = stream.Length; int headerSize = HeaderIdentifierBytes.Length; if (streamLength < headerSize) return -1; if (tagOrigin == TagOrigin.Start) { // Look for a header at the current position long startPositionHeader = startPosition; long endPositionHeader = Math.Min(startPositionHeader + headerSize, streamLength); long tagPosition = ReadIdentifier(stream, startPositionHeader, endPositionHeader, HeaderIdentifierBytes); if (tagPosition >= 0) return tagPosition; // Look for a header past the current position startPositionHeader = endPositionHeader; endPositionHeader = Math.Min(startPositionHeader + headerSize, streamLength); tagPosition = ReadIdentifier(stream, startPositionHeader, endPositionHeader, HeaderIdentifierBytes); if (tagPosition >= 0) return tagPosition; } else if (tagOrigin == TagOrigin.End) { // The Lyrics3 block ends with a six character size descriptor and the string "LYRICS200". // The size value includes the "LYRICSBEGIN" string, but does not include the 6 character size descriptor and the trailing "LYRICS200" string. int footerSize = FooterIdentifierBytes.Length; // Look for a footer at before current position long startPositionHeader = Math.Max(startPosition - footerSize, 0); long endPositionHeader = Math.Min(startPosition, streamLength); long tagPosition = ReadIdentifier(stream, startPositionHeader, endPositionHeader, FooterIdentifierBytes); if (tagPosition >= 0) return tagPosition; // Look for a footer before the previous start position startPositionHeader = Math.Max(startPositionHeader - footerSize, 0); endPositionHeader = Math.Min(startPositionHeader, streamLength); tagPosition = ReadIdentifier(stream, startPositionHeader, endPositionHeader, FooterIdentifierBytes); if (tagPosition >= 0) return tagPosition; } return -1; }
/// <inheritdoc/> public IAudioTagOffset ReadFromStream(Stream stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanRead) throw new InvalidOperationException("stream can not be read"); if (!stream.CanSeek) throw new InvalidOperationException("stream can not be seeked"); StreamBuffer sb = stream as StreamBuffer ?? new StreamBuffer(stream); ApeHeader headerOrFooter = ReadHeader(sb, tagOrigin); if (headerOrFooter == null) return null; // Don't throw an exception; just return here. The read header could be a false match. if (headerOrFooter.Size > ApeTag.MaxAllowedSize) return null; ApeTag tag = new ApeTag(headerOrFooter.Version, headerOrFooter.Flags); long startOffset, endOffset; ApeHeader header = null, footer = null; if (tag.IsHeader) { header = headerOrFooter; startOffset = header.Position; endOffset = Math.Min(startOffset + ApeTag.HeaderSize + header.Size, sb.Length); if (endOffset > sb.Length) { #if DEBUG throw new EndOfStreamException("Tag at start could not be read: stream is truncated."); #else return null; #endif } tag.UseHeader = true; } else { footer = headerOrFooter; endOffset = Math.Min(footer.Position + ApeTag.FooterSize, sb.Length); startOffset = Math.Max(endOffset - footer.Size - (tag.UseHeader ? ApeTag.HeaderSize : 0), 0); if (endOffset > sb.Length) { #if DEBUG throw new EndOfStreamException("Tag at end could not be read: stream is truncated."); #else return null; #endif } tag.UseFooter = true; // Seek to the start of the tag. sb.Seek(startOffset, SeekOrigin.Begin); if (tag.UseHeader) { // Read the 'header' as if it was the start of the tag header = ReadHeader(sb, TagOrigin.Start); if (header == null) { // Size in the header could be off. // Read the 'footer' as if it was at the end of the tag sb.Seek(startOffset, SeekOrigin.Begin); header = ReadHeader(sb, TagOrigin.End); if (header == null) { tag.UseHeader = false; sb.Seek(startOffset, SeekOrigin.Begin); } } if (header != null) { startOffset = header.Position; header.Size = (int)Math.Max(endOffset - (startOffset + ApeTag.HeaderSize), 0); footer.Size = Math.Max(header.Size, footer.Size); } if (footer.Size > ApeTag.MaxAllowedSize) { #if DEBUG throw new InvalidDataException( String.Format("Size ({0}) is larger than the max allowed size ({1})", footer.Size, ApeTag.MaxAllowedSize)); #else return null; #endif } } ValidateHeader(header, footer); } int totalSizeItems = Math.Max(headerOrFooter.Size - ApeTag.FooterSize, 0); if ((sb.Length - sb.Position) < totalSizeItems) { #if DEBUG throw new IndexOutOfRangeException( String.Format("Ape field ({0}) is bigger than amount of bytes left in stream ({1}).", totalSizeItems, sb.Length - sb.Position)); #else return null; #endif } // Parse the individual frames. List<ApeItem> items = new List<ApeItem>(); long bytesRead = 0; while (bytesRead < totalSizeItems) { long startPosition = sb.Position; ApeItem item = ApeItem.ReadFromStream(tag.Version, sb, totalSizeItems - bytesRead); if (item != null) { ApeItemParseEventArgs parseEventArgs = new ApeItemParseEventArgs(item); OnItemParse(parseEventArgs); if (!parseEventArgs.Cancel) { if (parseEventArgs.Item != null) item = parseEventArgs.Item; if (item != null) { // Call after read event. ApeItemParsedEventArgs parsedEventArgs = new ApeItemParsedEventArgs(item); OnItemParsed(parsedEventArgs); items.Add(parsedEventArgs.Item); } } } bytesRead += (sb.Position - startPosition); } #if DEBUG //if (items.Count != headerOrFooter.FrameCount) //throw new InvalidDataException("items.Count does not match FrameCount"); if (items.Count > ApeTag.MaxAllowedFields) { throw new InvalidDataException( String.Format("Tag has more fields ('{0}') than the allowed max fields count ('{1}').", items.Count, ApeTag.MaxAllowedFields)); } //if (bytesRead != totalSizeItems) //{ // throw new InvalidDataException( // String.Format("Amount of bytes read ({0}) does not match expected size ({1}).", bytesRead, totalSizeItems)); //} #endif if (tag.IsHeader && tag.UseFooter) { sb.Position += ApeTag.FooterSize; footer = ReadHeader(sb, TagOrigin.End); } ValidateHeader(header, footer); tag.SetItems(items); return new AudioTagOffset(tagOrigin, startOffset, endOffset, tag); }
////------------------------------------------------------------------------------------------------------------------------------ private static ApeHeader ReadHeader(StreamBuffer stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); long startPosition = stream.Position; long streamLength = stream.Length; if (streamLength < ApeTag.HeaderSize) return null; // Look for a header at the current position if (tagOrigin == TagOrigin.Start) { // Look for a header at the current position long startPositionHeader = startPosition; long endPositionHeader = Math.Min(startPositionHeader + ApeTag.HeaderSize, streamLength); ApeHeader hdr = ReadHeader(stream, startPositionHeader, endPositionHeader); if (hdr != null) return hdr; // Look for a header past the current position startPositionHeader = endPositionHeader; endPositionHeader = Math.Min(startPositionHeader + ApeTag.HeaderSize, streamLength); hdr = ReadHeader(stream, startPositionHeader, endPositionHeader); if (hdr != null) return hdr; } else if (tagOrigin == TagOrigin.End) { // Look for a footer before the current position long startPositionHeader = Math.Max(startPosition - ApeTag.FooterSize, 0); long endPositionHeader = Math.Min(startPositionHeader + ApeTag.FooterSize, streamLength); ApeHeader hdr = ReadHeader(stream, startPositionHeader, endPositionHeader); if (hdr != null) return hdr; // Look for a footer before the previous start position startPositionHeader = Math.Max(startPositionHeader - ApeTag.FooterSize, 0); endPositionHeader = Math.Min(startPositionHeader + ApeTag.FooterSize, streamLength); hdr = ReadHeader(stream, startPositionHeader, endPositionHeader); if (hdr != null) return hdr; } return null; }
////------------------------------------------------------------------------------------------------------------------------------ /// <inheritdoc/> public IAudioTagOffset ReadFromStream(Stream stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanRead) throw new InvalidOperationException("stream can not be read"); if (!stream.CanSeek) throw new InvalidOperationException("stream can not be seeked"); StreamBuffer sb = stream as StreamBuffer ?? new StreamBuffer(stream); long startOffset = FindIdentifier(sb, tagOrigin); if (startOffset < 0) return null; Id3v1Tag tag = new Id3v1Tag(); string extendedTrackTitle = String.Empty; string extendedArtist = String.Empty; string extendAlbumTitle = String.Empty; if ((startOffset - Id3v1Tag.ExtendedSize) >= 0) { sb.Position = startOffset - Id3v1Tag.ExtendedSize; string extendedHeaderIdentifier = sb.ReadString(ExtendedHeaderIdentifierBytes.Length); if (String.Equals(extendedHeaderIdentifier, Id3v1Tag.ExtendedHeaderIdentifier, StringComparison.OrdinalIgnoreCase)) { startOffset = startOffset - ExtendedHeaderIdentifierBytes.Length; extendedTrackTitle = sb.ReadString(60, _encoding); extendedArtist = sb.ReadString(60, _encoding); extendAlbumTitle = sb.ReadString(60, _encoding); Id3v1TrackSpeed trackSpeed = (Id3v1TrackSpeed)sb.ReadByte(); tag.TrackSpeed = Id3v1Tag.IsValidTrackSpeed(trackSpeed) ? trackSpeed : Id3v1TrackSpeed.Unset; tag.ExtendedTrackGenre = sb.ReadString(30, _encoding); tag.StartTime = GetTimeSpan(sb.ReadString(6, _encoding)); tag.EndTime = GetTimeSpan(sb.ReadString(6, _encoding)); tag.UseExtendedTag = true; } else sb.Position = startOffset + HeaderIdentifierBytes.Length; } // Read the rest of the tag. tag.TrackTitle = sb.ReadString(30, _encoding) + extendedTrackTitle; tag.Artist = sb.ReadString(30, _encoding) + extendedArtist; tag.AlbumTitle = sb.ReadString(30, _encoding) + extendAlbumTitle; tag.AlbumYear = sb.ReadString(4); byte[] comment = new byte[30]; if ((sb.Read(comment, 30) == 30) && (comment[28] == '\0') && (comment[29] != '\0')) { tag.TrackNumber = comment[29]; tag.TrackComment = _encoding.GetString(comment, 0, 28); } else tag.TrackComment = _encoding.GetString(comment); Id3v1Genre genre = (Id3v1Genre)sb.ReadByte(); tag.Genre = Id3v1Tag.IsValidGenre(genre) ? genre : Id3v1Genre.Unknown; long endOffset = sb.Position; return new AudioTagOffset(tagOrigin, startOffset, endOffset, tag); }
////------------------------------------------------------------------------------------------------------------------------------ private static long FindIdentifier(Stream stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); long startPosition = stream.Position; long streamLength = stream.Length; if (streamLength < Id3v1Tag.TotalSize) return -1; if (tagOrigin == TagOrigin.Start) { // Look for the tag at the current position long startPositionTag = Math.Max(startPosition, 0); long endPositionTag = Math.Min(startPosition + HeaderIdentifierBytes.Length, streamLength); long tagPosition = ReadIdentifier(stream, startPositionTag, endPositionTag); if (tagPosition >= 0) return tagPosition; } else if (tagOrigin == TagOrigin.End) { // Look for tag before the current position long startPositionTag = (startPosition - Id3v1Tag.TotalSize) >= 0 ? startPosition - Id3v1Tag.TotalSize : Math.Max(startPosition - HeaderIdentifierBytes.Length, 0); long endPositionTag = Math.Min(startPositionTag + HeaderIdentifierBytes.Length, streamLength); long tagPosition = ReadIdentifier(stream, startPositionTag, endPositionTag); if (tagPosition >= 0) return tagPosition; } return -1; }
////------------------------------------------------------------------------------------------------------------------------------ /// <inheritdoc/> public IAudioTagOffset ReadFromStream(Stream stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanRead) throw new InvalidOperationException("stream can not be read"); if (!stream.CanSeek) throw new InvalidOperationException("stream can not be seeked"); StreamBuffer sb = stream as StreamBuffer ?? new StreamBuffer(stream); MusicMatchTag tag = new MusicMatchTag(); // Should we read the header or footer? MusicMatchHeader headerOrFooter = (tagOrigin == TagOrigin.Start) ? ReadHeader(sb, tagOrigin) : ReadFooter(sb, tagOrigin); if (headerOrFooter == null) return null; long startOffset = 0, endOffset = 0, streamLength = sb.Length; MusicMatchHeader header = null, footer = null, versionInformation = null; if (tagOrigin == TagOrigin.Start) { header = headerOrFooter; tag.UseHeader = true; startOffset = header.Position; } else if (tagOrigin == TagOrigin.End) { footer = headerOrFooter; endOffset = Math.Min(footer.Position + MusicMatchTag.FooterSize, streamLength); // Seek to the Audio metadata offset struct sb.Position = Math.Max(endOffset - MusicMatchTag.FooterSize - MusicMatchTag.DataOffsetFieldsSize, 0); // Read the Audio Meta-data Offsets MusicMatchDataOffsets dataOffsets = ReadDataOffsets(sb); long startPosition = Math.Max(sb.Position + MusicMatchTag.FooterSize, 0); foreach (int audioMetaDataOffset in AudioMetaDataSizes.OrderBy(a => a)) { // Seek to the start of the version information, based on the audioMetaDataOffset long startPositionHeader = Math.Max(startPosition - audioMetaDataOffset - MusicMatchTag.HeaderSize, 0); long endPositionHeader = startPositionHeader + HeaderIdentifierBytes.Length; // Read the version information versionInformation = ReadHeaderFooter(sb, startPositionHeader, endPositionHeader, HeaderIdentifierBytes, TagOrigin.Start); if (versionInformation == null) continue; startOffset = (versionInformation.Position + MusicMatchTag.HeaderSize) - dataOffsets.TotalDataOffsetsSize; sb.Position = Math.Max(startOffset - MusicMatchTag.HeaderSize, 0); header = ReadHeader(sb, TagOrigin.Start); break; } // We've got a problem, Houston. if (versionInformation == null) return null; // Header found? set new startPosition if (header != null) { if (header.Position == versionInformation.Position) { header = null; } else { tag.UseHeader = true; startOffset = header.Position; } } } sb.Position = startOffset + (tag.UseHeader ? MusicMatchTag.HeaderSize : 0); // Read image tag.Image = MusicMatchImage.ReadFromStream(sb); // Unused, null padding byte[] nullPadding = new byte[4]; sb.Read(nullPadding, nullPadding.Length); // Read version info if (versionInformation == null) { versionInformation = ReadHeader(sb, TagOrigin.Start); if (versionInformation == null) { #if DEBUG throw new InvalidDataException("Version information not found in the tag."); #else return null; #endif } } else { // version information has the same size as the header size. sb.Position += MusicMatchTag.HeaderSize; } if (tag.UseHeader && !versionInformation.Equals(header)) { #if DEBUG throw new InvalidDataException("Version information field(s) mismatch the header fields."); #else return null; #endif } tag.XingEncoderVersion = versionInformation.XingEncoderVersion; tag.Version = versionInformation.MusicMatchVersion; // Read the Audio meta-data ReadAudioMetaFields(sb, tag); // Padding sb.Position += 16; if (tagOrigin == TagOrigin.Start) { long startPosition = sb.Position; foreach (int audioMetaDataOffset in AudioMetaDataSizes.OrderBy(a => a)) { //Look for the start of the footer. long startPositionFooter = Math.Max(startPosition + (audioMetaDataOffset - (sb.Position - startOffset) - (tag.UseHeader ? MusicMatchTag.HeaderSize : 0)), 0); long endPositionFooter = startPositionFooter + FooterIdentifierBytes.Length; // Read the footer. footer = ReadHeaderFooter(sb, startPositionFooter, endPositionFooter, FooterIdentifierBytes, TagOrigin.End); if (footer != null) break; } // We've got a problem, Houston. if (footer == null) return null; } return new AudioTagOffset(tagOrigin, startOffset, endOffset, tag); }
private static MusicMatchHeader ReadHeaderFooter(StreamBuffer stream, long startHeaderPosition, long endHeaderPosition, IList<byte> identifierBytes, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); stream.Position = startHeaderPosition; while (startHeaderPosition < endHeaderPosition) { int y = 0; while (stream.ReadByte() == identifierBytes[y++]) { startHeaderPosition++; if (y != identifierBytes.Count) continue; if (tagOrigin == TagOrigin.Start) { MusicMatchHeader header = new MusicMatchHeader { Position = stream.Position - identifierBytes.Count, Padding1 = new byte[2], Padding2 = new byte[2], Padding3 = new byte[2], SpacePadding2 = new byte[226] }; // 0x00 0x00 Padding stream.Read(header.Padding1, header.Padding1.Length); // <8-byte numerical ASCII string> header.XingEncoderVersion = stream.ReadString(8); // 0x00 0x00 Padding stream.Read(header.Padding2, header.Padding2.Length); // Xing encoder version <8-byte numerical ASCII string> header.MusicMatchVersion = stream.ReadString(8); // 0x00 0x00 Padding stream.Read(header.Padding3, header.Padding3.Length); // Space padding <226 * 0x20 > stream.Read(header.SpacePadding2, header.SpacePadding2.Length); ValidateHeader(header, null); return header; } if (tagOrigin == TagOrigin.End) { MusicMatchHeader footer = new MusicMatchHeader { Position = stream.Position - identifierBytes.Count, SpacePadding1 = new byte[13], SpacePadding2 = new byte[12] }; // Space padding <13 * 0x20> stream.Read(footer.SpacePadding1, 13); // version <4-byte numerical ASCII string> e.g. 3.05 footer.MusicMatchVersion = stream.ReadString(4); // Space padding <12 * 0x20> stream.Read(footer.SpacePadding2, 12); ValidateHeader(null, footer); return footer; } } startHeaderPosition++; } return null; }
private static MusicMatchHeader ReadHeader(StreamBuffer stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); long startPosition = stream.Position; long streamLength = stream.Length; if (streamLength < MusicMatchTag.HeaderSize) return null; if (tagOrigin == TagOrigin.Start) { // Look for a header at the current position long startPositionHeader = startPosition; long endPositionHeader = Math.Min(startPositionHeader + HeaderIdentifierBytes.Length, streamLength); MusicMatchHeader header = ReadHeaderFooter(stream, startPositionHeader, endPositionHeader, HeaderIdentifierBytes, TagOrigin.Start); if (header != null) return header; // Look for a header past the current position startPositionHeader = endPositionHeader; endPositionHeader = Math.Min(startPositionHeader + HeaderIdentifierBytes.Length, streamLength); header = ReadHeaderFooter(stream, startPositionHeader, endPositionHeader, HeaderIdentifierBytes, TagOrigin.Start); if (header != null) return header; } else if (tagOrigin == TagOrigin.End) { // Look for a footer before the current position long startPositionHeader = Math.Max(startPosition - HeaderIdentifierBytes.Length, 0); long endPositionHeader = Math.Min(startPositionHeader + HeaderIdentifierBytes.Length, streamLength); MusicMatchHeader footer = ReadHeaderFooter(stream, startPositionHeader, endPositionHeader, HeaderIdentifierBytes, TagOrigin.End); if (footer != null) return footer; // Look for a footer before the previous start position startPositionHeader = Math.Max(startPositionHeader - HeaderIdentifierBytes.Length, 0); endPositionHeader = Math.Min(startPositionHeader + HeaderIdentifierBytes.Length, streamLength); footer = ReadHeaderFooter(stream, startPositionHeader, endPositionHeader, HeaderIdentifierBytes, TagOrigin.End); if (footer != null) return footer; } return null; }
////------------------------------------------------------------------------------------------------------------------------------ /// <inheritdoc/> public IAudioTagOffset ReadFromStream(Stream stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanRead) throw new InvalidOperationException("stream can not be read"); if (!stream.CanSeek) throw new InvalidOperationException("stream can not be seeked"); StreamBuffer sb = stream as StreamBuffer ?? new StreamBuffer(stream); // Try to read the header. Id3v2Header headerOrFooter = ReadHeader(sb, tagOrigin); if (headerOrFooter == null) return null; // Don't throw an exception; just return here. The read header could be a false match. if (headerOrFooter.Size > Id3v2Tag.MaxAllowedSize) return null; // Copy header values. Id3v2Tag tag = new Id3v2Tag(headerOrFooter.Version, headerOrFooter.Flags); bool isHeader = String.Equals(Id3v2Tag.HeaderIdentifier, headerOrFooter.Identifier, StringComparison.OrdinalIgnoreCase); // startOffset: offset the header starts // endOffset: offset the footer ends if available, or when the tag ends long startOffset, endOffset; Id3v2Header header, footer = null; if (isHeader) { header = headerOrFooter; startOffset = header.Position; endOffset = Math.Min(startOffset + Id3v2Tag.HeaderSize + header.Size + (tag.UseFooter ? Id3v2Tag.FooterSize : 0), sb.Length); if (endOffset > sb.Length) { #if DEBUG throw new EndOfStreamException("Tag at start could not be read: stream is truncated."); #else return null; #endif } } else { // We've read the footer. footer = headerOrFooter; endOffset = footer.Position + Id3v2Tag.FooterSize; startOffset = Math.Max(endOffset - Id3v2Tag.FooterSize - footer.Size - Id3v2Tag.HeaderSize, 0); // Seek to the start of the tag. sb.Seek(startOffset, SeekOrigin.Begin); // Read the header; it's location could be off. header = ReadHeader(sb, TagOrigin.Start); if (header == null) { // Seek back a bit more and try again. startOffset = Math.Max(startOffset - Id3v2Tag.HeaderSize, 0); sb.Seek(startOffset, SeekOrigin.Begin); header = ReadHeader(sb, TagOrigin.End); // If we still didn't find a header, seek to the start offset; the header could be missing. if (header == null) sb.Seek(startOffset, SeekOrigin.Begin); } // If we've found a header. if (header != null) { startOffset = header.Position; // Calculate the tag size (i.e. all data, excluding the header and footer) headerOrFooter.Size = (int)Math.Max(endOffset - (startOffset + Id3v2Tag.HeaderSize + Id3v2Tag.FooterSize), 0); } else { // No header found. tag.UseHeader = false; } // Return if this is the case. if (headerOrFooter.Size > Id3v2Tag.MaxAllowedSize) { #if DEBUG throw new InvalidDataException(String.Format("Size ({0}) is larger than the max allowed size ({1})", footer.Size, Id3v2Tag.MaxAllowedSize)); #else return null; #endif } // A footer is read at this point. tag.UseFooter = true; } // At this point, the stream position is located right at the start of the data after the header. // The size is the sum of the byte length of the extended header, the padding and the frames after unsynchronization. // This does not include the footer size nor the header size. int totalSizeItems = Math.Min(headerOrFooter.Size, (int)(sb.Length - sb.Position)); // If the tag has been unsynchronized, synchronize it again. // For version Id3v2.4.0 and later we don't do the unsynch here: // unsynchronization [S:6.1] is done on frame level, instead of on tag level, making it easier to skip frames, increasing the stream ability of the tag. // The unsynchronization flag in the header [S:3.1] indicates if all frames has been unsynchronized, // while the new unsynchronization flag in the frame header [S:4.1.2] indicates unsynchronization. if (tag.UseUnsynchronization && (tag.Version < Id3v2Version.Id3v240)) { int unsynchronizedDataSize = totalSizeItems; byte[] unsynchronizedData = new byte[unsynchronizedDataSize]; unsynchronizedDataSize = sb.Read(unsynchronizedData, 0, unsynchronizedDataSize); byte[] data = Id3v2Tag.GetSynchronizedData(unsynchronizedData, 0, unsynchronizedDataSize); sb = new StreamBuffer(data); // Update the total size of the items with the length of the synchronized data totalSizeItems = data.Length; } // Calculate the padding size. int paddingSize = 0; // Check for an extended header. if (tag.UseExtendedHeader) { int crc; tag.ExtendedHeader = ReadExtendedHeader(sb, tag, out crc); if (tag.ExtendedHeader != null) { if (tag.UseFooter && (tag.ExtendedHeader.PaddingSize != 0)) { // Id3v2 tag can not have padding when an Id3v2 footer has been added. tag.ExtendedHeader.PaddingSize = 0; } paddingSize += tag.ExtendedHeader.PaddingSize; // Calculate CRC if needed. if (tag.ExtendedHeader.CrcDataPresent) { long currentPosition = sb.Position; // Id3v2.3.0: // The CRC should be calculated before unsynchronisation on the data between the extended header and the padding, i.e. the frames and only the frames. // Id3v2.4.0: // The CRC is calculated on all the data between the header and footer as indicated by the header's tag length field, minus the extended header. // Note that this includes the padding (if there is any), but excludes the footer. int dataLength = ((tag.Version >= Id3v2Version.Id3v240) ? totalSizeItems : totalSizeItems - (tag.PaddingSize + tag.ExtendedHeader.PaddingSize) - 4) - tag.ExtendedHeader.GetHeaderSize(tag.Version); byte[] crcData = sb.ToByteArray().Skip((int)currentPosition).Take(dataLength).ToArray(); int calculatedCrc = Cryptography.Crc32.Calculate(crcData); if (calculatedCrc != crc) throw new InvalidDataException(String.Format("CRC {0:X} in tag does not match calculated CRC {1:X}", crc, calculatedCrc)); sb.Position = currentPosition; } } } // Bytes we won't read or have already read, excluding the header/footer. totalSizeItems -= (tag.ExtendedHeader != null ? tag.ExtendedHeader.GetHeaderSize(tag.Version) + paddingSize : 0); // Now let's get to the good part, and start parsing the frames! List<Id3v2Frame> frames = new List<Id3v2Frame>(); long bytesRead = 0; while (bytesRead < totalSizeItems) { long startPosition = sb.Position; // See if the next byte is a padding byte. int identifierByte = sb.ReadByte(false); if (identifierByte == 0x00) { // Rest is padding. // We add up to the padding rather than assigning it, because the ExtendedHeader might include padding as well. tag.PaddingSize += (int)(totalSizeItems - bytesRead); bytesRead += tag.PaddingSize; break; } Id3v2Frame frame = Id3v2Frame.ReadFromStream(tag.Version, sb, totalSizeItems - bytesRead); if (frame != null) { Id3v2FrameParseEventArgs frameParseEventArgs = new Id3v2FrameParseEventArgs(frame); OnFrameParse(frameParseEventArgs); if (!frameParseEventArgs.Cancel) { // If the event has 'replaced' the frame, assign the 'new' frame here and continue. if (frameParseEventArgs.Frame != null) frame = frameParseEventArgs.Frame; // Assign the cryptor instance of the tag to the frame, and decrypt if necessary. bool isEncrypted = frame.UseEncryption; if (isEncrypted) isEncrypted = frame.Decrypt(); // Do the same with decompressing, if necessary. if (!isEncrypted && frame.UseCompression) frame.Decompress(); // Call after read event. Id3v2FrameParsedEventArgs frameParsedEventArgs = new Id3v2FrameParsedEventArgs(frame); OnFrameParsed(frameParsedEventArgs); // Add the 'final' frame from the event. frames.Add(frameParsedEventArgs.Frame); } } else { // Skip next byte. sb.Position = (startPosition + 1); } //Reading is always done forward; not backwards. bytesRead += (sb.Position - startPosition); } paddingSize += tag.PaddingSize; #if DEBUG if (frames.Count() > Id3v2Tag.MaxAllowedFrames) { throw new InvalidDataException( String.Format("Tag has more frames ('{0}') than the allowed max frames count ('{1}').", frames.Count(), Id3v2Tag.MaxAllowedFrames)); } if (bytesRead != totalSizeItems) { throw new InvalidDataException( String.Format("Amount of bytes read ({0}) does not match expected size ({1}).", bytesRead, totalSizeItems)); } // Id3v2.4.0 and later do not have a field for padding size. if (!isHeader && (paddingSize > 0)) throw new InvalidDataException("Id3v2 tag can not have padding when footer is set."); #endif // If the tag is at the start of the stream, is the tag allowed to have a footer? if (isHeader && tag.UseFooter) { sb.Position += Id3v2Tag.FooterSize; footer = ReadHeader(sb, TagOrigin.End); } // Validate the padding. if (paddingSize > 0) { byte[] padding = new byte[paddingSize]; sb.Read(padding, paddingSize); #if DEBUG if (!padding.Any(b => b == 0x00)) throw new InvalidDataException("Padding contains one or more invalid padding bytes."); #endif } ValidateHeader(tag, header, footer); // Sort frames and add them to the tag. AddRequiredFrames(tag.Version, frames); tag.SetFrames(frames); return new AudioTagOffset(tagOrigin, startOffset, endOffset, tag); }
/// <summary> /// Initializes a new instance of the <see cref="AudioTagParseEventArgs" /> class. /// </summary> /// <param name="audioTagReader">The audio tag.</param> /// <param name="tagOrigin">The tag origin.</param> public AudioTagParseEventArgs(IAudioTagReader audioTagReader, TagOrigin tagOrigin) { AudioTagReader = audioTagReader; TagOrigin = tagOrigin; }