// Event called when the Id3v2 tag has read a frame.
        // This event can be used to modify a read frame, as we do here, to further parse known frames.
        private static void Id3v2FrameParsed(object sender, Id3v2FrameParsedEventArgs e)
        {
            // See if the frame is a comment frame.
            if (!(e.Frame is Id3v2CommentFrame))
                return;

            // Safe-cast the frame to a comment frame.
            Id3v2CommentFrame frame = e.Frame as Id3v2CommentFrame;

            // See if the frame is an iTunes normalization frame.
            if (!String.Equals(frame.ShortContentDescription, "iTunNORM", StringComparison.OrdinalIgnoreCase))
                return;

            byte[] frameData = e.Frame.ToByteArray();

            // Parse the frame as an iTunes normalization frame.
            Id3v2iTunesNormalizationFrame normalizationFrame = Id3v2Frame.ReadFromStream<Id3v2iTunesNormalizationFrame>(
                e.Frame.Version,
                new MemoryStream(frameData),
                frameData.Length);

            // Set the new normalization frame as the new frame; this will replace the comment frame.
            e.Frame = normalizationFrame;
        }
 /// <summary>
 /// Raises the <see cref="FrameParsed"/> event.
 /// </summary>
 /// <param name="e">The <see cref="AudioVideoLib.Tags.Id3v2FrameParsedEventArgs"/> instance containing the event data.</param>
 private void OnFrameParsed(Id3v2FrameParsedEventArgs e)
 {
     EventHandler<Id3v2FrameParsedEventArgs> eventHandlers = FrameParsed;
     if (eventHandlers != null)
         eventHandlers(this, e);
 }
예제 #3
0
        ////------------------------------------------------------------------------------------------------------------------------------
        /// <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);
        }