#region CPL License /* Nuclex Framework Copyright (C) 2002-2009 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. 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 IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.Collections.Generic; using System.Net.Sockets; using System.Text; using Nuclex.Networking; using Nuclex.Networking.Exceptions; namespace Nuclex.Audio.Metadata { /// /// Handles communication following the CDDB protocol /// internal class CddbProtocol : IDisposable { /// ID of the ISO-8859-1 code page private const int ISO_8859_1 = 28591; #region class CddbLineParser /// Parses lines from CDDB replies private class CddbLineParser : LineParser { /// Initializes a new CDDB protocol line parser public CddbLineParser() : base(16384) { } /// /// Called when the message is growing beyond the maximum message size /// /// /// An exception that will be thrown to indicate the too large message /// protected override Exception HandleMessageTooLarge() { return new BadResponseException("Server response is too large"); } /// /// Called when the message contains a carriage return without a line feed /// /// /// It is safe to throw an exception here. The exception will travel up in /// the call stack to the caller of the method. /// protected override void HandleLoneCarriageReturn() { } /// /// Called to scan the bytes of a potential line for invalid characters /// /// /// Array containing the bytes that to can for invalid characters /// /// Index in the array at which to begin reading /// Number of bytes from the array to scan /// /// /// This method is used to check for invalid characters even before a complete /// line has been received. It will be called with incomplete lines (for example, /// when the received data ends before a CR LF is encountered) to allow for early /// rejection of data containing characters not allowed by a protocol. /// /// /// It is safe to throw an exception here. The exception will travel up in /// the call stack to the caller of the method. /// /// protected override void VerifyPotentialLine(byte[] buffer, int start, int count) { } /// /// Called to transform a received series of bytes into a string /// /// Buffer containing the bytes to be transformed /// Index of the first byte to transform /// Number of bytes to transform into a string /// The string produced from the bytes in the specified buffer /// /// This method allows you to use your own encoding for transforming the bytes /// in a line into a string. Always called to transform an entire line in one /// piece, excluding the CR LF characters at the line's end. /// protected override string TransformToString(byte[] buffer, int start, int count) { return Encoding.ASCII.GetString(buffer, start, count); } } #endregion // class CddbLineParser /// Initializes a new CDDB protocol handler /// /// Socket through which the CDDB can be reached /// public CddbProtocol(Socket connectedSocket) { this.syncRoot = new object(); this.socket = connectedSocket; this.lineParser = new CddbLineParser(); this.buffer = new byte[256]; updateEncoding(); } /// Immediately releases all resources owned by the instance public void Dispose() { if(this.socket != null) { this.socket.Shutdown(SocketShutdown.Both); this.socket.Close(); this.socket = null; } } /// /// Whether UTF-8 encoding is used to convert CDDB data between binary and text /// public bool UseUtf8 { get { return this.useUtf8; } set { if(value != this.useUtf8) { this.useUtf8 = value; updateEncoding(); } } } /// /// Synchronization root that can be used to ensure only one request communicates /// with the CDDB server at a time /// public object SyncRoot { get { return this.syncRoot; } } /// Receives a single line from the CDDB server /// /// Timeout, in milliseconds, after which to stop waiting for incoming data /// /// The received line public string ReceiveLine(int timeoutMilliseconds) { enforceAlive(); // We might require any number of reads to complete a line if data is slowly // trickling into the socket for(; ; ) { // Try to extract a line from the data we received so far. If we had enough // data for a line, return it immediately. string line = this.lineParser.ParseLine(); if(line != null) { return line; } // If this point is reached, we have not yet received a complete line from // the socket. Read all data from the socket's buffer (or wait for data // to arrive if the socket's buffer is empty) this.socket.ReceiveTimeout = timeoutMilliseconds; int receivedByteCount = this.socket.Receive(this.buffer, SocketFlags.None); if(receivedByteCount == 0) { throw new InvalidOperationException("Socket has been closed"); } // Hand over the received data to the line parser. When the ParseLine() method // is called in the next loop cycle, the line parser will either decode // the line directly from the receive buffer or store the received data in // its own buffer to be able to complete the line with future incoming data. this.lineParser.SetReceivedData(this.buffer, 0, receivedByteCount); } } /// Sends a single line to the CDDB server /// Line that will be sent to the CDDB server /// /// Timeout, in milliseconds, after which to stop waiting for the data to reach /// the CDDB server /// public void SendLine(string line, int timeoutMilliseconds) { enforceAlive(); // Allocate an array large enough to hold at least the number of bytes the // line have when transformed into the target encoding int byteCount = this.encoding.GetMaxByteCount(line.Length); byte[] bytesToSend = new byte[byteCount + 2]; // + 2 for CR LF // Translate the line into a series bytes using the target encoding int actualByteCount = this.encoding.GetBytes( line, 0, line.Length, bytesToSend, 0 ); // Append a newline to the end of the line to terminate the command bytesToSend[actualByteCount] = 13; bytesToSend[actualByteCount + 1] = 10; // Reset the line parser for the reply (prevents the line parser from accumulating // the message size with each received line, eventually complaining that the received // message became too large) this.lineParser.Reset(); // Everything is prepared, transmit the encoded line this.socket.SendTimeout = timeoutMilliseconds; this.socket.Send(bytesToSend, actualByteCount + 2, SocketFlags.None); } /// Extracts the status code from a server response /// Server response line expected to contain a status code /// The status code as an integer public static int GetStatusCode(string line) { // If the line is shorter than 3 characters, it cannot contain a status code // and has to be considered invalid. if(line.Length < 3) { throw makeBadResponseException("response too short"); } // Make sure the first three characters are numeric. If they aren't, we know it's // not a valid status code number and can report this error right away. bool allNumeric = char.IsNumber(line, 0) && char.IsNumber(line, 1) && char.IsNumber(line, 2); if(!allNumeric) { throw makeBadResponseException("status code is missing"); } // There seems to be a number in the first three digits, parsing will likely // succeed. Return the status code to the caller as an integer. try { return Convert.ToInt32(line.Substring(0, 3)); } catch(Exception exception) { throw makeBadResponseException("cannot parse status code", exception); } } /// Constructs a new BadResponseException /// /// Detailed reason to show in the exception message in addition to the standard /// bad response message /// /// The newly constructed exception private static BadResponseException makeBadResponseException(string detailedReason) { return new BadResponseException( string.Format("Bad response from CDDB server ({0})", detailedReason) ); } /// Constructs a new BadResponseException /// /// Detailed reason to show in the exception message in addition to the standard /// bad response message /// /// /// Inner exception that caused the response to be considered as malformed /// /// The newly constructed exception private static BadResponseException makeBadResponseException( string detailedReason, Exception innerException ) { return new BadResponseException( string.Format("Bad response from CDDB server ({0})", detailedReason), innerException ); } /// Updates the text encoding used in the protocol private void updateEncoding() { if(this.useUtf8) { this.encoding = Encoding.UTF8; } else { this.encoding = Encoding.GetEncoding(ISO_8859_1); } } /// Makes sure the object is still in a live, non-disposed state private void enforceAlive() { if(this.socket == null) { throw new ObjectDisposedException( "CddbProtocol", "The object has already been disposed" ); } } /// Whether the protocol uses UTF-8 encoding for text /// /// Up to version 5 of CDDBP, ISO-8859-1 was used. This is the default encoding /// that will be set when UseUTF8 is false. From version 6 onwards, the encoding /// has been changed to UTF-8. /// private volatile bool useUtf8; /// Encoding used to transform strings sent and received via CDDBP private volatile Encoding encoding; /// /// Synchronization root used to ensure only one request communicates with /// the CDDB server at a time /// private volatile object syncRoot; /// Socket by which we're connected to the CDDB server private volatile Socket socket; /// /// Parser that chops binary server responses into one string per line /// private CddbLineParser lineParser; /// Receive buffer used to store incoming data from the socket private byte[] buffer; } } // namespace Nuclex.Audio.Metadata