/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections; using System.Diagnostics; using System.IO; using System.Text; using JetBrains.DataStructures; using JetBrains.Omea.Base; namespace JetBrains.Omea.InstantMessaging.Miranda { internal interface IMirandaDB { IMirandaContact UserContact { get; } IEnumerable Contacts { get; } int ContactCount { get; } int FileSize { get; } int SlackSpace { get; } void Close(); } internal interface IMirandaObject { int NextOffset { get; } } internal interface IMirandaContact { int Offset { get; } int LastEventOffset { get; } IEnumerable ContactSettings { get; } IEnumerable Events { get; } bool DatabaseClosed { get; } } internal interface IMirandaContactSettings { string ModuleName { get; } IDictionary Settings { get; } } internal interface IMirandaEvent { string ModuleName { get; } int EventType { get; } DateTime Timestamp { get; } int Flags { get; } string EventData { get; } } internal class MirandaDatabaseCorruptedException: Exception { public MirandaDatabaseCorruptedException( string message ) : base( message ) { } } /** * A Miranda database. */ internal class MirandaDB: IMirandaDB { private int _contactCount; private int _ofsFileEnd; private int _slackSpace; private int _ofsFirstContact; private int _ofsUser; private int _ofsFirstModuleName; private IntHashTable _moduleHash; private FileStream _dbStream; private BinaryReader _dbReader; private const int SIGNATURE_MODULE = 0x4DDECADE; internal MirandaDB( string fileName ) { _dbStream = new FileStream( fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite ); _dbReader = new BinaryReader( _dbStream, Encoding.Default ); char[] signature = _dbReader.ReadChars( 16 ); if ( signature.Length < 16 ) throw new MirandaDatabaseCorruptedException( "DB signature is too short" ); string signatureStr = new string( signature, 0, 14 ); if ( signatureStr != "Miranda ICQ DB" ) throw new MirandaDatabaseCorruptedException( "Incorrect DB signature" ); int version = _dbReader.ReadInt32(); if ( version != 0x0700 ) throw new MirandaDatabaseCorruptedException( "Unsupported DB version" ); _ofsFileEnd = _dbReader.ReadInt32(); _slackSpace = _dbReader.ReadInt32(); _contactCount = _dbReader.ReadInt32(); _ofsFirstContact = _dbReader.ReadInt32(); _ofsUser = _dbReader.ReadInt32(); _ofsFirstModuleName = _dbReader.ReadInt32(); } private void LoadModules() { _moduleHash = new IntHashTable(); _dbStream.Position = _ofsFirstModuleName; while ( true ) { int ofs = (int) _dbStream.Position; int signature = _dbReader.ReadInt32(); if ( signature != SIGNATURE_MODULE ) throw new MirandaDatabaseCorruptedException( "Database corrupted: invalid module signature" ); int ofsNext = _dbReader.ReadInt32(); int cbName = _dbReader.ReadByte(); if( cbName > 0 ) { char[] nameChars = _dbReader.ReadChars( cbName ); _moduleHash [ofs] = new string( nameChars ); } if ( ofsNext == 0 ) break; _dbStream.Position = ofsNext; } } public void Close() { _dbReader.Close(); } public IMirandaContact UserContact { get { return new MirandaContact( this, _dbReader, _ofsUser ); } } public IEnumerable Contacts { get { return new MirandaContactEnumerator( this, _dbReader, _ofsFirstContact ); } } public int ContactCount { get { return _contactCount; } } public int FileSize { get { return _ofsFileEnd; } } public int SlackSpace { get { return _slackSpace; } } internal string GetModuleName( int offset ) { if ( _moduleHash == null ) { LoadModules(); } string moduleName = (string) _moduleHash [offset]; if ( moduleName == null ) { throw new MirandaDatabaseCorruptedException( "Unknown module at offset" + offset ); } return moduleName; } } /** * A generic enumerator for objects in the Miranda database. */ internal abstract class MirandaEnumerator: IEnumerable, IEnumerator { protected MirandaDB _db; protected BinaryReader _reader; private int _ofsFirst; protected int _ofsCurrent; private IMirandaObject _currentObject; internal MirandaEnumerator( MirandaDB db, BinaryReader reader, int ofsFirst ) { _db = db; _reader = reader; _ofsFirst = ofsFirst; _ofsCurrent = 0; _currentObject = null; } protected abstract IMirandaObject LoadCurrentObject(); public IEnumerator GetEnumerator() { return this; } public void Reset() { _ofsCurrent = 0; } public object Current { get { return _currentObject; } } public bool MoveNext() { if ( _ofsCurrent == 0 ) { if ( _ofsFirst == 0 ) return false; _ofsCurrent = _ofsFirst; } else { if ( _currentObject.NextOffset == 0 ) return false; _ofsCurrent = _currentObject.NextOffset; } _currentObject = LoadCurrentObject(); return true; } } /** * An enumerator for the contacts in the Miranda DB. */ internal class MirandaContactEnumerator: MirandaEnumerator { internal MirandaContactEnumerator( MirandaDB db, BinaryReader reader, int ofsFirst ) : base( db, reader, ofsFirst ) {} protected override IMirandaObject LoadCurrentObject() { return new MirandaContact( _db, _reader, _ofsCurrent ); } } /** * A contact in the Miranda database. */ internal class MirandaContact: IMirandaObject, IMirandaContact { private MirandaDB _db; private BinaryReader _reader; private int _ofs; private int _ofsNext; private int _ofsFirstSettings; private int _eventCount; private int _ofsFirstEvent; private int _ofsLastEvent; private int _ofsFirstUnreadEvent; private int _timestampFirstUnread; private const int SIGNATURE_CONTACT = 0x43DECADE; internal MirandaContact( MirandaDB db, BinaryReader reader, int ofs ) { Debug.Assert( ofs != 0 ); _db = db; _reader = reader; _ofs = ofs; reader.BaseStream.Position = ofs; int signature = _reader.ReadInt32(); if ( signature != SIGNATURE_CONTACT ) throw new MirandaDatabaseCorruptedException( "Database corrupted: invalid contact signature" ); _ofsNext = reader.ReadInt32(); _ofsFirstSettings = reader.ReadInt32(); _eventCount = reader.ReadInt32(); _ofsFirstEvent = reader.ReadInt32(); _ofsLastEvent = reader.ReadInt32(); _ofsFirstUnreadEvent = reader.ReadInt32(); _timestampFirstUnread = reader.ReadInt32(); } public bool DatabaseClosed { get { return _reader.BaseStream == null; } } public IEnumerable ContactSettings { get { return new ContactSettingsEnumerator( _db, _reader, _ofsFirstSettings ); } } public IEnumerable Events { get { return new MirandaEventEnumerator( _db, _reader, _ofsFirstEvent ); } } public int Offset { get { return _ofs; } } public int NextOffset { get { return _ofsNext; } } public int LastEventOffset { get { return _ofsLastEvent; } } } /** * An enumerator for the contact settings in the Miranda DB. */ internal class ContactSettingsEnumerator: MirandaEnumerator { internal ContactSettingsEnumerator( MirandaDB db, BinaryReader reader, int ofsFirst ) : base( db, reader, ofsFirst ) {} protected override IMirandaObject LoadCurrentObject() { return new MirandaContactSettings( _db, _reader, _ofsCurrent ); } } /** * A set of settings from one module for a contact in the Miranda DB. */ internal class MirandaContactSettings: IMirandaObject, IMirandaContactSettings { private BinaryReader _reader; private int _ofsNext; private string _moduleName; private int _cbBlob; private long _ofsData; private Hashtable _settings; private const int SIGNATURE_CONTACT_SETTINGS = 0x53DECADE; internal MirandaContactSettings( MirandaDB db, BinaryReader reader, int ofs ) { Guard.NullArgument( db, "db" ); Guard.NullArgument( reader, "reader" ); if ( reader.BaseStream == null ) { throw new InvalidOperationException( "Trying to load contact settings after Miranda DB has been closed" ); } _reader = reader; reader.BaseStream.Position = ofs; int signature = _reader.ReadInt32(); if ( signature != SIGNATURE_CONTACT_SETTINGS ) throw new MirandaDatabaseCorruptedException( "Database corrupted: invalid contact settings signature" ); _ofsNext = reader.ReadInt32(); int ofsModuleName = reader.ReadInt32(); _moduleName = db.GetModuleName( ofsModuleName ); _cbBlob = reader.ReadInt32(); _ofsData = reader.BaseStream.Position; } public int NextOffset { get { return _ofsNext; } } public string ModuleName { get { return _moduleName; } } public IDictionary Settings { get { if ( _settings == null ) LoadSettings(); return _settings; } } private void LoadSettings() { _settings = new Hashtable( new CaseInsensitiveHashCodeProvider(), new CaseInsensitiveComparer() ); _reader.BaseStream.Position = _ofsData; while( _reader.PeekChar() != 0 ) { MirandaSetting setting = new MirandaSetting( _reader ); if ( setting.Value != null && !_settings.ContainsKey( setting.Name ) ) // ignore deleted and duplicate settings { _settings.Add( setting.Name, setting.Value ); } } } } /// /// A single contact setting in the Miranda DB. /// internal class MirandaSetting { private string _name; private object _value; private const int DBVT_DELETED = 0; private const int DBVT_BYTE = 1; private const int DBVT_WORD = 2; private const int DBVT_DWORD = 4; private const int DBVT_ASCIIZ = 255; private const int DBVT_BLOB = 254; internal MirandaSetting( BinaryReader reader ) { int cbName = reader.ReadByte(); _name = new string( reader.ReadChars( cbName ) ); int dataValueType = reader.ReadByte(); switch( dataValueType ) { case DBVT_DELETED: _value = null; break; case DBVT_BYTE: _value = reader.ReadByte(); break; case DBVT_WORD: _value = reader.ReadUInt16(); break; case DBVT_DWORD: _value = reader.ReadInt32(); break; case DBVT_BLOB: { int cbBlob = reader.ReadUInt16(); byte[] blobBytes = new byte [cbBlob]; reader.Read( blobBytes, 0, cbBlob ); _value = blobBytes; break; } case DBVT_ASCIIZ: { int cbString = reader.ReadUInt16(); _value = new string( reader.ReadChars( cbString ) ); break; } default: throw new MirandaDatabaseCorruptedException( "Unknown or unsupported value type " + dataValueType ); } } public string Name { get { return _name; } } public object Value { get { return _value; } } } /** * An enumerator for the events in the Miranda DB. */ internal class MirandaEventEnumerator: MirandaEnumerator { internal MirandaEventEnumerator( MirandaDB db, BinaryReader reader, int ofsFirst ) : base( db, reader, ofsFirst ) {} protected override IMirandaObject LoadCurrentObject() { return new MirandaEvent( _db, _reader, _ofsCurrent ); } } /** * An event in the Miranda DB. */ internal class MirandaEvent: IMirandaObject, IMirandaEvent { private const int SIGNATURE_EVENT = 0x45DECADE; private int _ofsNext; private string _moduleName; private DateTime _timestamp; private int _flags; private int _eventType; private string _eventData; internal MirandaEvent( MirandaDB db, BinaryReader reader, int ofs ) { reader.BaseStream.Position = ofs; int signature = reader.ReadInt32(); if ( signature != SIGNATURE_EVENT ) throw new MirandaDatabaseCorruptedException( "Database corrupted: invalid event signature" ); reader.ReadInt32(); // skip _ofsPrev _ofsNext = reader.ReadInt32(); int ofsModuleName = reader.ReadInt32(); _moduleName = db.GetModuleName( ofsModuleName ); int timestamp = reader.ReadInt32(); _timestamp = new DateTime( 1970, 1, 1 ).AddDays( (double ) timestamp / ( 24 * 60 * 60 ) ); _flags = reader.ReadInt16(); _eventType = reader.ReadInt32(); int cbBlob = reader.ReadInt32(); if ( cbBlob == 0 ) _eventData = ""; else { byte[] eventData = reader.ReadBytes( cbBlob ); int nullIndex = Array.IndexOf( eventData, (byte) 0 ); if ( _eventType == 0 && nullIndex < cbBlob-3 ) { _eventData = Encoding.Unicode.GetString( eventData, nullIndex+1, cbBlob-nullIndex-3 ); } else { _eventData = Encoding.Default.GetString( eventData, 0, cbBlob-1 ); } } } public int NextOffset { get { return _ofsNext; } } public string ModuleName { get { return _moduleName; } } public int EventType { get { return _eventType; } } public string EventData { get { return _eventData; } } public int Flags { get { return _flags; } } public DateTime Timestamp { get { return _timestamp; } } } }