// // File.cs: Provides a basic framework for reading from and writing to // a file, as well as accessing basic tagging and media properties. // // Author: // Brian Nickel (brian.nickel@gmail.com) // Aaron Bockover (abockover@novell.com) // // Original Source: // tfile.cpp from TagLib // // Copyright (C) 2005, 2007 Brian Nickel // Copyright (C) 2006 Novell, Inc. // Copyright (C) 2002,2003 Scott Wheeler (Original Implementation) // // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License version // 2.1 as published by the Free Software Foundation. // // This library is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 // USA // using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Runtime.Serialization; namespace TagLib { /// /// Specifies the options to use when reading the media. /// [Flags] public enum ReadStyle { /// /// The media properties will not be read. /// None = 0, // Fast = 1, /// /// The media properties will be read with average accuracy. /// Average = 2, /// /// Use the class in the /// the property . /// This will avoid loading picture content when reading the Tag. /// Picture will be read lazily, when the picture content is /// accessed. /// PictureLazy = 4 } /// /// This abstract class provides a basic framework for reading from /// and writing to a file, as well as accessing basic tagging and /// media properties. /// /// /// This class is agnostic to all specific media types. Its /// child classes, on the other hand, support the the intricacies of /// different media and tagging formats. For example, supports the MPEG-4 specificication and /// Apple's tagging format. /// Each file type can be created using its format specific /// constructors, ie. , but the /// preferred method is to use or one of its /// variants, as it automatically detects the appropriate class from /// the file extension or provided mime-type. /// public abstract class File : IDisposable { #region Enums /// /// Specifies the type of file access operations currently /// permitted on an instance of . /// public enum AccessMode { /// /// Read operations can be performed. /// Read, /// /// Read and write operations can be performed. /// Write, /// /// The file is closed for both read and write /// operations. /// Closed } #endregion #region Delegates /// /// This delegate is used for intervening in by resolving the file type /// before any standard resolution operations. /// /// /// A object representing the /// file to be read. /// /// /// A object containing the mime-type /// of the file. /// /// /// A value specifying how to read /// media properties from the file. /// /// /// A new instance of or if the resolver could not match it. /// /// /// A is one way of /// altering the behavior of /// . /// When is called, the /// registered resolvers are invoked in the reverse order in /// which they were registered. The resolver may then perform /// any operations necessary, including other type-finding /// methods. /// If the resolver returns a new , /// it will instantly be returned, by . If it returns , will /// continue to process. If the resolver throws an exception /// it will be uncaught. /// To register a resolver, use . /// public delegate File FileTypeResolver (IFileAbstraction abstraction, string mimetype, ReadStyle style); #endregion #region Private Properties /// /// Contains the current stream used in reading/writing. /// Stream file_stream; /// /// Contains the internal file abstraction. /// protected IFileAbstraction file_abstraction; /// /// Contains buffer size to use when reading. /// static readonly int buffer_size = 1024; /// /// Contains the file type resolvers to use in . /// static readonly List file_type_resolvers = new List (); /// /// The reasons (if any) why this file is marked as corrupt. /// List corruption_reasons; #endregion #region Public Static Properties /// /// The buffer size to use when reading large blocks of data /// in the class. /// /// /// A containing the buffer size to use /// when reading large blocks of data. /// public static uint BufferSize => (uint)buffer_size; #endregion #region Constructors /// /// Constructs and initializes a new instance of for a specified path in the local file /// system. /// /// /// A object containing the path of the /// file to use in the new instance. /// /// /// is . /// protected File (string path) { if (path == null) throw new ArgumentNullException (nameof (path)); file_abstraction = new LocalFileAbstraction (path); } /// /// Constructs and initializes a new instance of for a specified file abstraction. /// /// /// A object to use when /// reading from and writing to the file. /// /// /// is . /// protected File (IFileAbstraction abstraction) { if (abstraction == null) throw new ArgumentNullException (nameof (abstraction)); file_abstraction = abstraction; } #endregion #region Public Properties /// /// Gets a abstract representation of all tags stored in the /// current instance. /// /// /// A object representing all tags /// stored in the current instance. /// /// /// This property provides generic and general access /// to the most common tagging features of a file. To access /// or add a specific type of tag in the file, use . /// public abstract Tag Tag { get; } /// /// Gets the media properties of the file represented by the /// current instance. /// /// /// A object containing the /// media properties of the file represented by the current /// instance. /// public abstract Properties Properties { get; } /// /// Gets the tag types contained in the physical file /// represented by the current instance. /// /// /// A bitwise combined value /// containing the tag types stored in the physical file as /// it was read or last saved. /// public TagTypes TagTypesOnDisk { get; protected set; } = TagTypes.None; /// /// Gets the tag types contained in the current instance. /// /// /// A bitwise combined value /// containing the tag types stored in the current instance. /// public TagTypes TagTypes => Tag?.TagTypes ?? TagTypes.None; /// /// Gets the name of the file as stored in its file /// abstraction. /// /// /// A object containing the name of the /// file as stored in the /// object used to create it or the path if created with a /// local path. /// public string Name => file_abstraction.Name; /// /// Gets the mime-type of the file as determined by if /// that method was used to create the current instance. /// /// /// A object containing the mime-type /// used to create the file or if was /// not used to create the current instance. /// public string MimeType { get; internal set; } /// /// Gets the seek position in the internal stream used by the /// current instance. /// /// /// A value representing the seek /// position, or 0 if the file is not open for reading. /// public long Tell => (Mode == AccessMode.Closed) ? 0 : file_stream.Position; /// /// Gets the length of the file represented by the current /// instance. /// /// /// A value representing the size of the /// file, or 0 if the file is not open for reading. /// public long Length => (Mode == AccessMode.Closed) ? 0 : file_stream.Length; /// /// Gets the position at which the invariant portion of the /// current instance begins. /// /// /// A value representing the seek /// position at which the file's invariant (media) data /// section begins. If the value could not be determined, /// -1 is returned. /// public long InvariantStartPosition { get; protected set; } = -1; /// /// Gets the position at which the invariant portion of the /// current instance ends. /// /// /// A value representing the seek /// position at which the file's invariant (media) data /// section ends. If the value could not be determined, /// -1 is returned. /// public long InvariantEndPosition { get; protected set; } = -1; /// /// Gets and sets the file access mode in use by the current /// instance. /// /// /// A value describing the features /// of stream currently in use by the current instance. /// /// /// Changing the value will cause the stream currently in use /// to be closed, except when a change is made from to which has no effect. /// public AccessMode Mode { get { if (file_stream == null) return AccessMode.Closed; if (file_stream.CanWrite) return AccessMode.Write; return AccessMode.Read; } set { if (Mode == value || (Mode == AccessMode.Write && value == AccessMode.Read)) return; if (file_stream != null) file_abstraction.CloseStream (file_stream); file_stream = null; if (value == AccessMode.Read) file_stream = file_abstraction.ReadStream; else if (value == AccessMode.Write) file_stream = file_abstraction.WriteStream; Mode = value; } } /// /// Gets the representing the file. /// public IFileAbstraction FileAbstraction => file_abstraction; /// /// Indicates if tags can be written back to the current file or not /// /// /// A which is true if tags can be written to the /// current file, otherwise false. /// public virtual bool Writeable => !PossiblyCorrupt; /// /// Indicates whether or not this file may be corrupt. /// /// /// true if possibly corrupt; otherwise, false. /// /// /// Files with unknown corruptions should not be written. /// public bool PossiblyCorrupt => corruption_reasons != null; /// /// The reasons for which this file is marked as corrupt. /// public IEnumerable CorruptionReasons => corruption_reasons; #endregion #region Public Methods /// /// Mark the file as corrupt. /// /// /// The reason why this file is considered to be corrupt. /// internal void MarkAsCorrupt (string reason) { if (corruption_reasons == null) corruption_reasons = new List (); corruption_reasons.Add (reason); } /// /// Dispose the current file. Equivalent to setting the /// mode to closed /// public void Dispose () { Mode = AccessMode.Closed; } /// /// Saves the changes made in the current instance to the /// file it represents. /// public abstract void Save (); /// /// Removes a set of tag types from the current instance. /// /// /// A bitwise combined value /// containing tag types to be removed from the file. /// /// /// In order to remove all tags from a file, pass as . /// public abstract void RemoveTags (TagTypes types); /// /// Gets a tag of a specified type from the current instance, /// optionally creating a new tag if possible. /// /// /// A value indicating the /// type of tag to read. /// /// /// A value specifying whether or not to /// try and create the tag if one is not found. /// /// /// A object containing the tag that was /// found in or added to the current instance. If no /// matching tag was found and none was created, is returned. /// /// /// Passing to does not guarantee the tag will be /// created. For example, trying to create an ID3v2 tag on an /// OGG Vorbis file will always fail. /// It is safe to assume that if is not returned, the returned tag can be cast to the /// appropriate type. /// /// /// The following example sets the mood of a file to /// several tag types. /// string [] SetMoods (TagLib.File file, params string[] moods) ///{ /// TagLib.Id3v2.Tag id3 = file.GetTag (TagLib.TagTypes.Id3v2, true); /// if (id3 != null) /// id3.SetTextFrame ("TMOO", moods); /// /// TagLib.Asf.Tag asf = file.GetTag (TagLib.TagTypes.Asf, true); /// if (asf != null) /// asf.SetDescriptorStrings (moods, "WM/Mood", "Mood"); /// /// TagLib.Ape.Tag ape = file.GetTag (TagLib.TagTypes.Ape); /// if (ape != null) /// ape.SetValue ("MOOD", moods); /// /// // Whatever tag types you want... ///} /// public abstract Tag GetTag (TagTypes type, bool create); /// /// Gets a tag of a specified type from the current instance. /// /// /// A value indicating the /// type of tag to read. /// /// /// A object containing the tag that was /// found in the current instance. If no matching tag /// was found, is returned. /// /// /// This class merely accesses the tag if it exists. /// provides the option /// of adding the tag to the current instance if it does not /// exist. /// It is safe to assume that if is not returned, the returned tag can be cast to the /// appropriate type. /// /// /// The following example reads the mood of a file from /// several tag types. /// static string [] GetMoods (TagLib.File file) ///{ /// TagLib.Id3v2.Tag id3 = file.GetTag (TagLib.TagTypes.Id3v2); /// if (id3 != null) { /// TextIdentificationFrame f = TextIdentificationFrame.Get (this, "TMOO"); /// if (f != null) /// return f.FieldList.ToArray (); /// } /// /// TagLib.Asf.Tag asf = file.GetTag (TagLib.TagTypes.Asf); /// if (asf != null) { /// string [] value = asf.GetDescriptorStrings ("WM/Mood", "Mood"); /// if (value.Length > 0) /// return value; /// } /// /// TagLib.Ape.Tag ape = file.GetTag (TagLib.TagTypes.Ape); /// if (ape != null) { /// Item item = ape.GetItem ("MOOD"); /// if (item != null) /// return item.ToStringArray (); /// } /// /// // Whatever tag types you want... /// /// return new string [] {}; ///} /// public Tag GetTag (TagTypes type) { return GetTag (type, false); } /// /// Reads a specified number of bytes at the current seek /// position from the current instance. /// /// /// A value specifying the number of bytes /// to read. /// /// /// A object containing the data /// read from the current instance. /// /// /// This method reads the block of data at the current /// seek position. To change the seek position, use . /// /// /// is less than zero. /// public ByteVector ReadBlock (int length) { if (length < 0) throw new ArgumentException ("Length must be non-negative", nameof (length)); if (length == 0) return new ByteVector (); Mode = AccessMode.Read; byte[] buffer = new byte[length]; int count = 0, read = 0, needed = length; do { count = file_stream.Read (buffer, read, needed); read += count; needed -= count; } while (needed > 0 && count != 0); return new ByteVector (buffer, read); } /// /// Writes a block of data to the file represented by the /// current instance at the current seek position. /// /// /// A object containing data to be /// written to the current instance. /// /// /// This will overwrite any existing data at the seek /// position and append new data to the file if writing past /// the current end. /// /// /// is . /// public void WriteBlock (ByteVector data) { if (data == null) throw new ArgumentNullException (nameof (data)); Mode = AccessMode.Write; file_stream.Write (data.Data, 0, data.Count); } /// /// Searches forwards through a file for a specified /// pattern, starting at a specified offset. /// /// /// A object containing a pattern /// to search for in the current instance. /// /// /// A value specifying at what /// seek position to start searching. /// /// /// A object specifying a pattern /// that the searched for pattern must appear before. If this /// pattern is found first, -1 is returned. /// /// /// A value containing the index at which /// the value was found. If not found, -1 is returned. /// /// /// is . /// public long Find (ByteVector pattern, long startPosition, ByteVector before) { if (pattern == null) throw new ArgumentNullException (nameof (pattern)); Mode = AccessMode.Read; if (pattern.Count > buffer_size) return -1; // The position in the file that the current buffer // starts at. long buffer_offset = startPosition; long original_position = file_stream.Position; try { // Start the search at the offset. file_stream.Position = startPosition; for (var buffer = ReadBlock (buffer_size); buffer.Count > 0; buffer = ReadBlock (buffer_size)) { var location = buffer.Find (pattern); if (before != null) { var beforeLocation = buffer.Find (before); if (beforeLocation < location) return -1; } if (location >= 0) return buffer_offset + location; // Ensure that we always rewind the stream a little so we never have a partial // match where our data exists between the end of read A and the start of read B. buffer_offset += buffer_size - pattern.Count; if (before != null && before.Count > pattern.Count) buffer_offset -= before.Count - pattern.Count; file_stream.Position = buffer_offset; } return -1; } finally { file_stream.Position = original_position; } } /// /// Searches forwards through a file for a specified /// pattern, starting at a specified offset. /// /// /// A object containing a pattern /// to search for in the current instance. /// /// /// A value specifying at what /// seek position to start searching. /// /// /// A value containing the index at which /// the value was found. If not found, -1 is returned. /// /// /// is . /// public long Find (ByteVector pattern, long startPosition) { return Find (pattern, startPosition, null); } /// /// Searches forwards through a file for a specified /// pattern, starting at the beginning of the file. /// /// /// A object containing a pattern /// to search for in the current instance. /// /// /// A value containing the index at which /// the value was found. If not found, -1 is returned. /// /// /// is . /// public long Find (ByteVector pattern) { return Find (pattern, 0); } /// /// Searches backwards through a file for a specified /// pattern, starting at a specified offset. /// /// /// A object containing a pattern /// to search for in the current instance. /// /// /// A value specifying at what /// seek position to start searching. /// /// /// A object specifying a pattern /// that the searched for pattern must appear after. If this /// pattern is found first, -1 is returned. /// /// /// A value containing the index at which /// the value was found. If not found, -1 is returned. /// /// /// Searching for is not yet /// implemented. /// /// /// is . /// long RFind (ByteVector pattern, long startPosition, ByteVector after) { if (pattern == null) throw new ArgumentNullException (nameof (pattern)); Mode = AccessMode.Read; if (pattern.Count > buffer_size) return -1; // The position in the file that the current buffer // starts at. ByteVector buffer; // These variables are used to keep track of a partial // match that happens at the end of a buffer. /* int previous_partial_match = -1; int after_previous_partial_match = -1; */ // Save the location of the current read pointer. We // will restore the position using Seek() before all // returns. long original_position = file_stream.Position; // Start the search at the offset. long buffer_offset = Length - startPosition; int read_size = buffer_size; read_size = (int)Math.Min (buffer_offset, buffer_size); buffer_offset -= read_size; file_stream.Position = buffer_offset; // See the notes in find() for an explanation of this // algorithm. for (buffer = ReadBlock (read_size); buffer.Count > 0; buffer = ReadBlock (read_size)) { // TODO: (1) previous partial match // (2) pattern contained in current buffer long location = buffer.RFind (pattern); if (location >= 0) { file_stream.Position = original_position; return buffer_offset + location; } if (after != null && buffer.RFind (after) >= 0) { file_stream.Position = original_position; return -1; } read_size = (int)Math.Min (buffer_offset, buffer_size); buffer_offset -= read_size; if (read_size + pattern.Count > buffer_size) buffer_offset += pattern.Count; file_stream.Position = buffer_offset; } // Since we hit the end of the file, reset the status // before continuing. file_stream.Position = original_position; return -1; } /// /// Searches backwards through a file for a specified /// pattern, starting at a specified offset. /// /// /// A object containing a pattern /// to search for in the current instance. /// /// /// A value specifying at what /// seek position to start searching. /// /// /// A value containing the index at which /// the value was found. If not found, -1 is returned. /// /// /// is . /// public long RFind (ByteVector pattern, long startPosition) { return RFind (pattern, startPosition, null); } /// /// Searches backwards through a file for a specified /// pattern, starting at the end of the file. /// /// /// A object containing a pattern /// to search for in the current instance. /// /// /// A value containing the index at which /// the value was found. If not found, -1 is returned. /// /// /// is . /// public long RFind (ByteVector pattern) { return RFind (pattern, 0); } /// /// Inserts a specifed block of data into the file repesented /// by the current instance at a specified location, /// replacing a specified number of bytes. /// /// /// A object containing the data to /// insert into the file. /// /// /// A value specifying at which point to /// insert the data. /// /// /// A value specifying the number of /// bytes to replace. Typically this is the original size of /// the data block so that a new block will replace the old /// one. /// /// /// is . /// public void Insert (ByteVector data, long start, long replace) { if (data == null) throw new ArgumentNullException (nameof (data)); Insert (data, data.Count, start, replace); } /// /// Inserts a specified block of data into the file repesented /// by the current instance at a specified location. /// /// /// A object containing the data to /// insert into the file. /// /// /// A value specifying at which point to /// insert the data. /// /// /// This method inserts a new block of data into the file. To /// replace an existing block, ie. replacing an existing /// tag with a new one of different size, use . /// /// /// is . /// public void Insert (ByteVector data, long start) { Insert (data, start, 0); } /// /// Inserts a specified block-size into the file repesented /// by the current instance at a specified location. Former /// data at this location is not overwriten and may then /// contain random content. /// /// /// A value specifying the size in bytes /// of the block to be inserted (reserved). /// /// /// A value specifying at which point to /// insert the data. /// /// /// This method is usefull to reserve some space in the file. /// To insert or replace defined data blocks, use or /// /// public void Insert (long size, long start) { Insert (null, size, start, 0); } /// /// Removes a specified block of data from the file /// represented by the current instance. /// /// /// A value specifying at which point to /// remove data. /// /// /// A value specifying the number of /// bytes to remove. /// public void RemoveBlock (long start, long length) { if (length <= 0) return; Mode = AccessMode.Write; int buffer_length = buffer_size; long read_position = start + length; long write_position = start; ByteVector buffer = (byte)1; while (buffer.Count != 0) { file_stream.Position = read_position; buffer = ReadBlock (buffer_length); read_position += buffer.Count; file_stream.Position = write_position; WriteBlock (buffer); write_position += buffer.Count; } Truncate (write_position); } /// /// Seeks the read/write pointer to a specified offset in the /// current instance, relative to a specified origin. /// /// /// A value indicating the byte offset to /// seek to. /// /// /// A value specifying an /// origin to seek from. /// public void Seek (long offset, SeekOrigin origin) { if (Mode != AccessMode.Closed) file_stream.Seek (offset, origin); } /// /// Seeks the read/write pointer to a specified offset in the /// current instance, relative to the beginning of the file. /// /// /// A value indicating the byte offset to /// seek to. /// public void Seek (long offset) { Seek (offset, SeekOrigin.Begin); } #endregion #region Public Static Methods /// /// Creates a new instance of a subclass /// for a specified path, guessing the mime-type from the /// file's extension and using the average read style. /// /// /// A object specifying the file to /// read from and write to. /// /// /// A new instance of as read from the /// specified path. /// /// /// The file could not be read due to corruption. /// /// /// The file could not be read because the mime-type could /// not be resolved or the library does not support an /// internal feature of the file crucial to its reading. /// public static File Create (string path) { return Create (path, null, ReadStyle.Average); } /// /// Creates a new instance of a subclass /// for a specified file abstraction, guessing the mime-type /// from the file's extension and using the average read /// style. /// /// /// A object to use when /// reading to and writing from the current instance. /// /// /// A new instance of as read from the /// specified abstraction. /// /// /// The file could not be read due to corruption. /// /// /// The file could not be read because the mime-type could /// not be resolved or the library does not support an /// internal feature of the file crucial to its reading. /// public static File Create (IFileAbstraction abstraction) { return Create (abstraction, null, ReadStyle.Average); } /// /// Creates a new instance of a subclass /// for a specified path and read style, guessing the /// mime-type from the file's extension. /// /// /// A object specifying the file to /// read from and write to. /// /// /// A value specifying the level of /// detail to use when reading the media information from the /// new instance. /// /// /// A new instance of as read from the /// specified path. /// /// /// The file could not be read due to corruption. /// /// /// The file could not be read because the mime-type could /// not be resolved or the library does not support an /// internal feature of the file crucial to its reading. /// public static File Create (string path, ReadStyle propertiesStyle) { return Create (path, null, propertiesStyle); } /// /// Creates a new instance of a subclass /// for a specified file abstraction and read style, guessing /// the mime-type from the file's extension. /// /// /// A object to use when /// reading to and writing from the current instance. /// /// /// A value specifying the level of /// detail to use when reading the media information from the /// new instance. /// /// /// A new instance of as read from the /// specified abstraction. /// /// /// The file could not be read due to corruption. /// /// /// The file could not be read because the mime-type could /// not be resolved or the library does not support an /// internal feature of the file crucial to its reading. /// public static File Create (IFileAbstraction abstraction, ReadStyle propertiesStyle) { return Create (abstraction, null, propertiesStyle); } /// /// Creates a new instance of a subclass /// for a specified path, mime-type, and read style. /// /// /// A object specifying the file to /// read from and write to. /// /// /// A object containing the mime-type /// to use when selecting the appropriate class to use, or /// if the extension in is to be used. /// /// /// A value specifying the level of /// detail to use when reading the media information from the /// new instance. /// /// /// A new instance of as read from the /// specified path. /// /// /// The file could not be read due to corruption. /// /// /// The file could not be read because the mime-type could /// not be resolved or the library does not support an /// internal feature of the file crucial to its reading. /// public static File Create (string path, string mimetype, ReadStyle propertiesStyle) { return Create (new LocalFileAbstraction (path), mimetype, propertiesStyle); } /// /// Creates a new instance of a subclass /// for a specified file abstraction, mime-type, and read /// style. /// /// /// A object to use when /// reading to and writing from the current instance. /// /// /// A object containing the mime-type /// to use when selecting the appropriate class to use, or /// if the extension in is to be used. /// /// /// A value specifying the level of /// detail to use when reading the media information from the /// new instance. /// /// /// A new instance of as read from the /// specified abstraction. /// /// /// The file could not be read due to corruption. /// /// /// The file could not be read because the mime-type could /// not be resolved or the library does not support an /// internal feature of the file crucial to its reading. /// public static File Create (IFileAbstraction abstraction, string mimetype, ReadStyle propertiesStyle) { if (mimetype == null) { string ext = string.Empty; int index = abstraction.Name.LastIndexOf (".") + 1; if (index >= 1 && index < abstraction.Name.Length) ext = abstraction.Name.Substring (index, abstraction.Name.Length - index); mimetype = "taglib/" + ext.ToLower (CultureInfo.InvariantCulture); } foreach (var resolver in file_type_resolvers) { var file = resolver (abstraction, mimetype, propertiesStyle); if (file != null) return file; } if (!FileTypes.AvailableTypes.ContainsKey (mimetype)) throw new UnsupportedFormatException ( string.Format (CultureInfo.InvariantCulture, "{0} ({1})", abstraction.Name, mimetype)); var file_type = FileTypes.AvailableTypes[mimetype]; try { var file = (File)Activator.CreateInstance (file_type, new object[] { abstraction, propertiesStyle }); file.MimeType = mimetype; return file; } catch (System.Reflection.TargetInvocationException e) { PrepareExceptionForRethrow (e.InnerException); throw e.InnerException; } } /// /// Adds a to the class. The one added last gets run first. /// /// /// A delegate to add to the /// file type recognition stack. /// /// /// A adds support for /// recognizing a file type outside of the standard mime-type /// methods. /// public static void AddFileTypeResolver (FileTypeResolver resolver) { if (resolver != null) file_type_resolvers.Insert (0, resolver); } #endregion #region Private/Protected Methods /// /// Prepare to Save the file. Thismust be called at the begining /// of every File.Save() method. /// protected void PreSave () { // Check validity if (!Writeable) throw new InvalidOperationException ("File not writeable."); if (PossiblyCorrupt) throw new CorruptFileException ("Corrupted file cannot be saved."); // All the Lazy objects must be loaded before opening the file // in Write mode if (Tag?.Pictures != null) { foreach (var pic in Tag.Pictures) { if (pic is ILazy lazy) { lazy.Load (); } } } } /// /// Inserts a specified block into the file repesented /// by the current instance at a specified location. /// /// /// A object containing the data to /// insert into the file. if null, no data is writen to the /// file and the block is just inserted without overwriting the /// former data at the given location. /// /// /// A value specifying the size of the block /// to be inserted. /// /// /// A value specifying at which point to /// insert the data. /// /// /// A value specifying the number of /// bytes to replace. Typically this is the original size of /// the data block so that a new block will replace the old /// one. /// /// /// This method inserts a new block of data into the file. To /// replace an existing block, ie. replacing an existing /// tag with a new one of different size, use . /// void Insert (ByteVector data, long size, long start, long replace) { Mode = AccessMode.Write; if (size == replace) { if (data != null) { file_stream.Position = start; WriteBlock (data); } return; } else if (size < replace) { if (data != null) { file_stream.Position = start; WriteBlock (data); } RemoveBlock (start + size, replace - size); return; } // Woohoo! Faster (about 20%) than id3lib at last. I // had to get hardcore and avoid TagLib's high level API // for rendering just copying parts of the file that // don't contain tag data. // // Now I'll explain the steps in this ugliness: // First, make sure that we're working with a buffer // that is longer or equal than the *difference* in the tag sizes, // and that is a multiple of buffer_size. // We want to avoid overwriting parts that aren't yet in // memory, so this is necessary. int buffer_length = (int)(size - replace); int modulo = buffer_length % buffer_size; if (modulo != 0) buffer_length += buffer_size - modulo; // Set where to start the reading and writing. long read_position = start + replace; long write_position = start; byte[] buffer; byte[] about_to_overwrite; // This is basically a special case of the loop below. // Here we're just doing the same steps as below, but // since we aren't using the same buffer size -- instead // we're using the tag size -- this has to be handled as // a special case. We're also using File::writeBlock() // just for the tag. That's a bit slower than using char // *'s so, we're only doing it here. file_stream.Position = read_position; about_to_overwrite = ReadBlock (buffer_length).Data; read_position += buffer_length; if (data != null) { file_stream.Position = write_position; WriteBlock (data); } else if (start + size > Length) { file_stream.SetLength (start + size); } write_position += size; buffer = new byte[about_to_overwrite.Length]; Array.Copy (about_to_overwrite, 0, buffer, 0, about_to_overwrite.Length); // Ok, here's the main loop. We want to loop until the // read fails, which means that we hit the end of the // file. while (buffer_length != 0) { // Seek to the current read position and read // the data that we're about to overwrite. // Appropriately increment the readPosition. file_stream.Position = read_position; int bytes_read = file_stream.Read ( about_to_overwrite, 0, buffer_length < about_to_overwrite.Length ? buffer_length : about_to_overwrite.Length); read_position += buffer_length; // Seek to the write position and write our // buffer. Increment the writePosition. file_stream.Position = write_position; file_stream.Write (buffer, 0, buffer_length < buffer.Length ? buffer_length : buffer.Length); write_position += buffer_length; // Make the current buffer the data that we read // in the beginning. Array.Copy (about_to_overwrite, 0, buffer, 0, bytes_read); // Again, we need this for the last write. We // don't want to write garbage at the end of our // file, so we need to set the buffer size to // the amount that we actually read. buffer_length = bytes_read; } } /// /// Resized the current instance to a specified number of /// bytes. /// /// /// A value specifying the number of /// bytes to resize the file to. /// protected void Truncate (long length) { var old_mode = Mode; Mode = AccessMode.Write; file_stream.SetLength (length); Mode = old_mode; } /// /// Causes the original strack trace of the exception to be preserved when it is rethrown /// /// static void PrepareExceptionForRethrow (Exception ex) { var ctx = new StreamingContext (StreamingContextStates.CrossAppDomain); var mgr = new ObjectManager (null, ctx); var si = new SerializationInfo (ex.GetType (), new FormatterConverter ()); ex.GetObjectData (si, ctx); mgr.RegisterObject (ex, 1, si); // prepare for SetObjectData mgr.DoFixups (); // ObjectManager calls SetObjectData } #endregion #region Classes /// /// This class implements /// to provide support for accessing the local/standard file /// system. /// /// /// This class is used as the standard file abstraction /// throughout the library. /// public class LocalFileAbstraction : IFileAbstraction { /// /// Contains the name used to open the file. /// readonly string name; /// /// Constructs and initializes a new instance of /// for a /// specified path in the local file system. /// /// /// A object containing the /// path of the file to use in the new instance. /// /// /// is . /// public LocalFileAbstraction (string path) { if (path == null) throw new ArgumentNullException (nameof (path)); name = path; } /// /// Gets the path of the file represented by the /// current instance. /// /// /// A object containing the /// path of the file represented by the current /// instance. /// public string Name => name; /// /// Gets a new readable, seekable stream from the /// file represented by the current instance. /// /// /// A new to be used /// when reading the file represented by the current /// instance. /// public Stream ReadStream => System.IO.File.Open (Name, FileMode.Open, FileAccess.Read, FileShare.Read); /// /// Gets a new writable, seekable stream from the /// file represented by the current instance. /// /// /// A new to be used /// when writing to the file represented by the /// current instance. /// public Stream WriteStream => System.IO.File.Open (Name, FileMode.Open, FileAccess.ReadWrite); /// /// Closes a stream created by the current instance. /// /// /// A object /// created by the current instance. /// public void CloseStream (Stream stream) { if (stream == null) throw new ArgumentNullException (nameof (stream)); stream.Close (); } } #endregion #region Interfaces /// /// This interface provides abstracted access to a file. It /// premits access to non-standard file systems and data /// retrieval methods. /// /// /// To use a custom abstraction, use instead of when creating files. /// /// /// The following example uses Gnome VFS to open a file /// and read its title. /// using TagLib; ///using Gnome.Vfs; /// ///public class ReadTitle ///{ /// public static void Main (string [] args) /// { /// if (args.Length != 1) /// return; /// /// Gnome.Vfs.Vfs.Initialize (); /// /// try { /// TagLib.File file = TagLib.File.Create ( /// new VfsFileAbstraction (args [0])); /// System.Console.WriteLine (file.Tag.Title); /// } finally { /// Vfs.Shutdown() /// } /// } ///} /// ///public class VfsFileAbstraction : TagLib.File.IFileAbstraction ///{ /// private string name; /// /// public VfsFileAbstraction (string file) /// { /// name = file; /// } /// /// public string Name { /// get { return name; } /// } /// /// public System.IO.Stream ReadStream { /// get { return new VfsStream(Name, System.IO.FileMode.Open); } /// } /// /// public System.IO.Stream WriteStream { /// get { return new VfsStream(Name, System.IO.FileMode.Open); } /// } /// /// public void CloseStream (System.IO.Stream stream) /// { /// stream.Close (); /// } ///} /// import TagLib from "taglib-sharp.dll" ///import Gnome.Vfs from "gnome-vfs-sharp" /// ///class VfsFileAbstraction (TagLib.File.IFileAbstraction): /// /// _name as string /// /// def constructor(file as string): /// _name = file /// /// Name: /// get: /// return _name /// /// ReadStream: /// get: /// return VfsStream(_name, FileMode.Open) /// /// WriteStream: /// get: /// return VfsStream(_name, FileMode.Open) /// ///if len(argv) == 1: /// Vfs.Initialize() /// /// try: /// file as TagLib.File = TagLib.File.Create (VfsFileAbstraction (argv[0])) /// print file.Tag.Title /// ensure: /// Vfs.Shutdown() /// public interface IFileAbstraction { /// /// Gets the name or identifier used by the /// implementation. /// /// /// A object containing the /// name or identifier used by the implementation. /// /// /// This value would typically represent a path or /// URL to be used when identifying the file in the /// file system, but it could be any value /// as appropriate for the implementation. /// string Name { get; } /// /// Gets a readable, seekable stream for the file /// referenced by the current instance. /// /// /// A object to be /// used when reading a file. /// /// /// This property is typically used when creating /// constructing an instance of . /// Upon completion of the constructor, will be called to close /// the stream. If the stream is to be reused after /// this point, should be /// implemented in a way to keep it open. /// Stream ReadStream { get; } /// /// Gets a writable, seekable stream for the file /// referenced by the current instance. /// /// /// A object to be /// used when writing to a file. /// /// /// This property is typically used when saving a /// file with . Upon completion of /// the method, will be /// called to close the stream. If the stream is to /// be reused after this point, should be implemented in a /// way to keep it open. /// Stream WriteStream { get; } /// /// Closes a stream originating from the current /// instance. /// /// /// A object /// originating from the current instance. /// /// /// If the stream is to be used outside of the scope, /// of TagLib#, this method should perform no action. /// For example, a stream that was created outside of /// the current instance, or a stream that will /// subsequently be used to play the file. /// void CloseStream (Stream stream); } #endregion } }