package com.hanhuy.keepassj;
/*
  KeePass Password Safe - The Open-Source Password Manager
  Copyright (C) 2003-2014 Dominik Reichl <dominik.reichl@t-online.de>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program 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 General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.io.BaseEncoding;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;

import java.io.*;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;


	/// <summary>
	/// Serialization to KeePass KDBX files.
	/// </summary>
	public class KdbxFile
	{
		/// <summary>
		/// File identifier, first 32-bit value.
		/// </summary>
		static final int FileSignature1 = 0x9AA2D903;

		/// <summary>
		/// File identifier, second 32-bit value.
		/// </summary>
		static final int FileSignature2 = 0xB54BFB67;

		/// <summary>
		/// File version of files saved by the current <c>KdbxFile</c> class.
		/// KeePass 2.07 has version 1.01, 2.08 has 1.02, 2.09 has 2.00,
		/// 2.10 has 2.02, 2.11 has 2.04, 2.15 has 3.00, 2.20 has 3.01.
		/// The first 2 bytes are critical (i.e. loading will fail, if the
		/// file version is too high), the last 2 bytes are informational.
		/// </summary>
		private final static int FileVersion32 = 0x00030001;

		private final static int FileVersionCriticalMask = 0xFFFF0000;

		// KeePass 1.x signature
		final static int FileSignatureOld1 = 0x9AA2D903;
		final static int FileSignatureOld2 = 0xB54BFB65;
		// KeePass 2.x pre-release (alpha and beta) signature
		final static int FileSignaturePreRelease1 = 0x9AA2D903;
		final static int FileSignaturePreRelease2 = 0xB54BFB66;

		private final static String ElemDocNode = "KeePassFile";
		private final static String ElemMeta = "Meta";
		private final static String ElemRoot = "Root";
		private final static String ElemGroup = "Group";
		private final static String ElemEntry = "Entry";

		private final static String ElemGenerator = "Generator";
		private final static String ElemHeaderHash = "HeaderHash";
		private final static String ElemDbName = "DatabaseName";
		private final static String ElemDbNameChanged = "DatabaseNameChanged";
		private final static String ElemDbDesc = "DatabaseDescription";
		private final static String ElemDbDescChanged = "DatabaseDescriptionChanged";
		private final static String ElemDbDefaultUser = "DefaultUserName";
		private final static String ElemDbDefaultUserChanged = "DefaultUserNameChanged";
		private final static String ElemDbMntncHistoryDays = "MaintenanceHistoryDays";
		private final static String ElemDbColor = "Color";
		private final static String ElemDbKeyChanged = "MasterKeyChanged";
		private final static String ElemDbKeyChangeRec = "MasterKeyChangeRec";
		private final static String ElemDbKeyChangeForce = "MasterKeyChangeForce";
		private final static String ElemRecycleBinEnabled = "RecycleBinEnabled";
		private final static String ElemRecycleBinUuid = "RecycleBinUUID";
		private final static String ElemRecycleBinChanged = "RecycleBinChanged";
		private final static String ElemEntryTemplatesGroup = "EntryTemplatesGroup";
		private final static String ElemEntryTemplatesGroupChanged = "EntryTemplatesGroupChanged";
		private final static String ElemHistoryMaxItems = "HistoryMaxItems";
		private final static String ElemHistoryMaxSize = "HistoryMaxSize";
		private final static String ElemLastSelectedGroup = "LastSelectedGroup";
		private final static String ElemLastTopVisibleGroup = "LastTopVisibleGroup";

		private final static String ElemMemoryProt = "MemoryProtection";
		private final static String ElemProtTitle = "ProtectTitle";
		private final static String ElemProtUserName = "ProtectUserName";
		private final static String ElemProtPassword = "ProtectPassword";
		private final static String ElemProtUrl = "ProtectURL";
		private final static String ElemProtNotes = "ProtectNotes";
		// private final static String ElemProtAutoHide = "AutoEnableVisualHiding";

		private final static String ElemCustomIcons = "CustomIcons";
		private final static String ElemCustomIconItem = "Icon";
		private final static String ElemCustomIconItemID = "UUID";
		private final static String ElemCustomIconItemData = "Data";

		private final static String ElemAutoType = "AutoType";
		private final static String ElemHistory = "History";

		private final static String ElemName = "Name";
		private final static String ElemNotes = "Notes";
		private final static String ElemUuid = "UUID";
		private final static String ElemIcon = "IconID";
		private final static String ElemCustomIconID = "CustomIconUUID";
		private final static String ElemFgColor = "ForegroundColor";
		private final static String ElemBgColor = "BackgroundColor";
		private final static String ElemOverrideUrl = "OverrideURL";
		private final static String ElemTimes = "Times";
		private final static String ElemTags = "Tags";

		private final static String ElemCreationTime = "CreationTime";
		private final static String ElemLastModTime = "LastModificationTime";
		private final static String ElemLastAccessTime = "LastAccessTime";
		private final static String ElemExpiryTime = "ExpiryTime";
		private final static String ElemExpires = "Expires";
		private final static String ElemUsageCount = "UsageCount";
		private final static String ElemLocationChanged = "LocationChanged";

		private final static String ElemGroupDefaultAutoTypeSeq = "DefaultAutoTypeSequence";
		private final static String ElemEnableAutoType = "EnableAutoType";
		private final static String ElemEnableSearching = "EnableSearching";

		private final static String ElemString = "String";
		private final static String ElemBinary = "Binary";
		private final static String ElemKey = "Key";
		private final static String ElemValue = "Value";

		private final static String ElemAutoTypeEnabled = "Enabled";
		private final static String ElemAutoTypeObfuscation = "DataTransferObfuscation";
		private final static String ElemAutoTypeDefaultSeq = "DefaultSequence";
		private final static String ElemAutoTypeItem = "Association";
		private final static String ElemWindow = "Window";
		private final static String ElemKeystrokeSequence = "KeystrokeSequence";

		private final static String ElemBinaries = "Binaries";

		private final static String AttrId = "ID";
		private final static String AttrRef = "Ref";
		private final static String AttrProtected = "Protected";
		private final static String AttrProtectedInMemPlainXml = "ProtectInMemory";
		private final static String AttrCompressed = "Compressed";

		private final static String ElemIsExpanded = "IsExpanded";
		private final static String ElemLastTopVisibleEntry = "LastTopVisibleEntry";

		private final static String ElemDeletedObjects = "DeletedObjects";
		private final static String ElemDeletedObject = "DeletedObject";
		private final static String ElemDeletionTime = "DeletionTime";

		private final static String ValFalse = "False";
		private final static String ValTrue = "True";

		private final static String ElemCustomData = "CustomData";
		private final static String ElemStringDictExItem = "Item";

		private PwDatabase m_pwDatabase; // Not null, see constructor

		private XmlSerializer m_xmlWriter = null;
		private CryptoRandomStream m_randomStream = null;
		private KdbxFormat m_format = KdbxFormat.Default;
		private IStatusLogger m_slLogger = null;

		private byte[] m_pbMasterSeed = null;
		private byte[] m_pbTransformSeed = null;
		private byte[] m_pbEncryptionIV = null;
		private byte[] m_pbProtectedStreamKey = null;
		private byte[] m_pbStreamStartBytes = null;

		// ArcFourVariant only for compatibility; KeePass will default to a
		// different (more secure) algorithm when *writing* databases
		private CrsAlgorithm m_craInnerRandomStream = CrsAlgorithm.ArcFourVariant;

		private Map<String, ProtectedBinary> m_dictBinPool =
			new HashMap<String, ProtectedBinary>();

		private byte[] m_pbHashOfHeader = null;
		private byte[] m_pbHashOfFileOnDisk = null;

		private final Date m_dtNow = new Date(); // Cache current time

		private final static int NeutralLanguageOffset = 0x100000; // 2^20, see 32-bit Unicode specs
		private final static int NeutralLanguageIDSec = 0x7DC5C; // See 32-bit Unicode specs
		private final static int NeutralLanguageID = NeutralLanguageOffset + NeutralLanguageIDSec;
		private static boolean m_bLocalizedNames = false;

		private enum KdbxHeaderFieldID
		{
			EndOfHeader,
			Comment,
			CipherID ,
			CompressionFlags,
			MasterSeed,
			TransformSeed,
			TransformRounds,
			EncryptionIV,
			ProtectedStreamKey,
			StreamStartBytes,
			InnerRandomStreamID
		}

		public byte[] getHashOfFileOnDisk()
		{
			return m_pbHashOfFileOnDisk;
		}

		private boolean m_bRepairMode = false;
		public boolean getRepairMode()
		{
			return m_bRepairMode;
		}
        public void setRepairMode(boolean value) { m_bRepairMode = value; }

		private String m_strDetachBins = null;
		/// <summary>
		/// Detach binaries when opening a file. If this isn't <c>null</c>,
		/// all binaries are saved to the specified path and are removed
		/// from the database.
		/// </summary>
		public String getDetachBinaries()
		{
			return m_strDetachBins;
		}
        public void setDetachBinaries(String value) { m_strDetachBins = value; }

		/// <summary>
		/// Default constructor.
		/// </summary>
		/// <param name="pwDataStore">The <c>PwDatabase</c> instance that the
		/// class will load file data into or use to create a KDBX file.</param>
		public KdbxFile(PwDatabase pwDataStore)
		{
			assert pwDataStore != null;
			if(pwDataStore == null) throw new IllegalArgumentException("pwDataStore");

			m_pwDatabase = pwDataStore;
		}

		/// <summary>
		/// Call this once to determine the current localization settings.
		/// </summary>
		public static void DetermineLanguageId()
		{
			// Test if localized names should be used. If localized names are used,
			// the m_bLocalizedNames value must be set to true. By default, localized
			// names should be used! (Otherwise characters could be corrupted
			// because of different code pages).
				int uTest = 0;
				for(char ch : PwDatabase.getLocalizedAppName().toCharArray())
					uTest = uTest * 5 + ch;

				m_bLocalizedNames = (uTest != NeutralLanguageID);
		}

		private void BinPoolBuild(PwGroup pgDataSource)
		{
			m_dictBinPool = new HashMap<String, ProtectedBinary>();

			if(pgDataSource == null) { assert false; return; }

			EntryHandler eh = new EntryHandler() { public boolean delegate(PwEntry pe)
			{
				for(PwEntry peHistory : pe.getHistory())
				{
					BinPoolAdd(peHistory.getBinaries());
				}

				BinPoolAdd(pe.getBinaries());
				return true;
			}};

			pgDataSource.TraverseTree(TraversalMethod.PreOrder, null, eh);
		}

		private void BinPoolAdd(ProtectedBinaryDictionary dict)
		{
			for(Map.Entry<String, ProtectedBinary> kvp : dict)
			{
				BinPoolAdd(kvp.getValue());
			}
		}

		private void BinPoolAdd(ProtectedBinary pb)
		{
			if(pb == null) { assert false; return; }

			if(BinPoolFind(pb) != null) return; // Exists already

			m_dictBinPool.put(String.valueOf(m_dictBinPool.size()), pb);
		}

		private String BinPoolFind(ProtectedBinary pb)
		{
			if(pb == null) { assert false; return null; }

			for(Map.Entry<String, ProtectedBinary> kvp : m_dictBinPool.entrySet())
			{
				if(pb.Equals(kvp.getValue())) return kvp.getKey();
			}

			return null;
		}

		private ProtectedBinary BinPoolGet(String strKey)
		{
			if(strKey == null) { assert false; return null; }

            return m_dictBinPool.get(strKey);
		}

		private static void SaveBinary(String strName, ProtectedBinary pb,
			String strSaveDir) throws IOException
		{
			if(pb == null) { assert false; return; }

			if(Strings.isNullOrEmpty(strName)) strName = "File.bin";

			String strPath;
			int iTry = 1;
			do
			{
				strPath = UrlUtil.EnsureTerminatingSeparator(strSaveDir, false);

				String strExt = UrlUtil.GetExtension(strName);
				String strDesc = UrlUtil.StripExtension(strName);

				strPath += strDesc;
				if(iTry > 1)
					strPath += " (" + String.valueOf(iTry) +
						")";

				if(!Strings.isNullOrEmpty(strExt)) strPath += "." + strExt;

				++iTry;
			}
			while(new File(strPath).exists());

			FileOutputStream fs = new FileOutputStream(strPath, false);
            try {
                byte[] pbData = pb.ReadData();
                fs.write(pbData, 0, pbData.length);
            } finally {
                fs.close();
            }
        }

        // KdbxFile.Read.cs
            /// <summary>
            /// Serialization to KeePass KDBX files.
            /// </summary>
            /// <summary>
            /// Load a KDB file from a file.
            /// </summary>
            /// <param name="strFilePath">File to load.</param>
            /// <param name="kdbFormat">Format specifier.</param>
            /// <param name="slLogger">Status logger (optional).</param>
            public void Load(String strFilePath, KdbxFormat kdbFormat, IStatusLogger slLogger)
            throws IOException
            {
                IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFilePath);
                Load(IOConnection.OpenRead(ioc), kdbFormat, slLogger);
            }

            /// <summary>
            /// Load a KDB file from a stream.
            /// </summary>
            /// <param name="sSource">Stream to read the data from. Must contain
            /// a KDBX stream.</param>
            /// <param name="kdbFormat">Format specifier.</param>
            /// <param name="slLogger">Status logger (optional).</param>
            public void Load(InputStream sSource, KdbxFormat kdbFormat, IStatusLogger slLogger)
            throws IOException
            {
                assert sSource != null;
                if(sSource == null) throw new IllegalArgumentException("sSource");

                m_format = kdbFormat;
                m_slLogger = slLogger;

                HashingInputStreamEx hashedStream = new HashingInputStreamEx(sSource);

                Charset encNoBom = StrUtil.Utf8;
                try
                {
                    BinaryReaderEx br = null;
                    BinaryReaderEx brDecrypted = null;
                    InputStream readerStream = null;

                    if(kdbFormat == KdbxFormat.Default)
                    {
                        br = new BinaryReaderEx(hashedStream, encNoBom, "file corrupted");
                        ReadHeader(br);

                        InputStream sDecrypted = AttachStreamDecryptor(hashedStream);
                        if((sDecrypted == null) || (sDecrypted == hashedStream))
                            throw new SecurityException("stream decryption failure");

                        brDecrypted = new BinaryReaderEx(sDecrypted, encNoBom, "file corrupted");
                        byte[] pbStoredStartBytes = brDecrypted.ReadBytes(32);

                        if((m_pbStreamStartBytes == null) || (m_pbStreamStartBytes.length != 32))
                            throw new KdbxFileFormatException("stream start bytes");

                        for(int iStart = 0; iStart < 32; ++iStart)
                        {
                            if(pbStoredStartBytes[iStart] != m_pbStreamStartBytes[iStart])
                                throw new InvalidCompositeKeyException(
                                        BaseEncoding.base16().encode(pbStoredStartBytes) +
                                                " != " +
                                                BaseEncoding.base16().encode(m_pbStreamStartBytes)
                                );
                        }

                        InputStream sHashed = new HashedBlockStream.Input(sDecrypted,
                                !m_bRepairMode);

                        if(m_pwDatabase.getCompression() == PwCompressionAlgorithm.GZip)
                            readerStream = new GZIPInputStream(sHashed);
                        else readerStream = sHashed;
                    }
                    else if(kdbFormat == KdbxFormat.PlainXml)
                        readerStream = hashedStream;
                    else { assert false; throw new KdbxFileFormatException("KdbFormat"); }

                    if(kdbFormat != KdbxFormat.PlainXml) // Is an encrypted format
                    {
                        if(m_pbProtectedStreamKey == null)
                        {
                            assert false;
                            throw new SecurityException("Invalid protected stream key!");
                        }

                        m_randomStream = new CryptoRandomStream(m_craInnerRandomStream,
                                m_pbProtectedStreamKey);
                    }
                    else m_randomStream = null; // No random stream for plain-text files

                    ReadXmlStreamed(readerStream, hashedStream);
                    // ReadXmlDom(readerStream);

                    readerStream.close();
                    // GC.KeepAlive(br);
                    // GC.KeepAlive(brDecrypted);
                }
                catch(Exception e) // Thrown on invalid padding
                {
                    throw new KdbxFileFormatException(e);
                }
                finally {
                    try {
                        CommonCleanUpRead(sSource, hashedStream);
                    } catch (AssertionError e) {
                    }
                }
            }

            private void CommonCleanUpRead(InputStream sSource, HashingInputStreamEx hashedStream)
                    throws IOException
            {
                hashedStream.close();
                m_pbHashOfFileOnDisk = hashedStream.getHash();

                sSource.close();

                // Reset memory protection settings (to always use reasonable
                // defaults)
                m_pwDatabase.setMemoryProtection(new MemoryProtectionConfig());

                // Remove old backups (this call is required here in order to apply
                // the default history maintenance settings for people upgrading from
                // KeePass <= 2.14 to >= 2.15; also it ensures history integrity in
                // case a different application has created the KDBX file and ignored
                // the history maintenance settings)
                m_pwDatabase.MaintainBackups(); // Don't mark database as modified

                // Expand the root group, such that in case the user accidently
                // collapses the root group he can simply reopen the database
                PwGroup pgRoot = m_pwDatabase.getRootGroup();
                if(pgRoot != null) pgRoot.setExpanded(true);
                else { assert(false); }

                m_pbHashOfHeader = null;
            }

            private void ReadHeader(BinaryReaderEx br)
                    throws IOException
            {
                ByteArrayOutputStream msHeader = new ByteArrayOutputStream();
                assert br.getCopyDataTo() == null;
                br.setCopyDataTo(msHeader);

                byte[] pbSig1 = br.ReadBytes(4);
                int uSig1 = MemUtil.BytesToUInt32(pbSig1);
                byte[] pbSig2 = br.ReadBytes(4);
                int uSig2 = MemUtil.BytesToUInt32(pbSig2);

                if((uSig1 == FileSignatureOld1) && (uSig2 == FileSignatureOld2))
                    throw new OldFormatException(PwDefs.ShortProductName + " 1.x",
                    OldFormatException.OldFormatType.KeePass1x);

                if((uSig1 == FileSignature1) && (uSig2 == FileSignature2)) { }
                else if((uSig1 == FileSignaturePreRelease1) && (uSig2 ==
                        FileSignaturePreRelease2)) { }
                else throw new KdbxFileFormatException("invalid signature");

                byte[] pb = br.ReadBytes(4);
                int uVersion = MemUtil.BytesToUInt32(pb);
                if((uVersion & FileVersionCriticalMask) > (FileVersion32 & FileVersionCriticalMask))
                    throw new KdbxFileFormatException("Unsupported file version" +
                            "\n" + "new version required");

                while(true)
                {
                    if(!ReadHeaderField(br))
                        break;
                }

                br.setCopyDataTo(null);
                byte[] pbHeader = msHeader.toByteArray();
                msHeader.close();
                m_pbHashOfHeader = Digests.sha256(pbHeader);
            }

            private boolean ReadHeaderField(BinaryReaderEx brSource)
                    throws IOException
            {
                assert brSource != null;
                if(brSource == null) throw new IllegalArgumentException("brSource");

                byte btFieldID = brSource.ReadByte();
                short uSize = MemUtil.BytesToUInt16(brSource.ReadBytes(2));

                byte[] pbData = null;
                if(uSize > 0)
                {
                    String strPrevExcpText = brSource.getReadExceptionText();
                    brSource.setReadExceptionText("file header end early");

                    pbData = brSource.ReadBytes(uSize);

                    brSource.setReadExceptionText(strPrevExcpText);
                }

                boolean bResult = true;
                KdbxHeaderFieldID kdbID = KdbxHeaderFieldID.values()[btFieldID];
                switch(kdbID)
                {
                    case EndOfHeader:
                        bResult = false; // Returning false indicates end of header
                        break;

                    case CipherID:
                        SetCipher(pbData);
                        break;

                    case CompressionFlags:
                        SetCompressionFlags(pbData);
                        break;

                    case MasterSeed:
                        m_pbMasterSeed = pbData;
                        CryptoRandom.getInstance().AddEntropy(pbData);
                        break;

                    case TransformSeed:
                        m_pbTransformSeed = pbData;
                        CryptoRandom.getInstance().AddEntropy(pbData);
                        break;

                    case TransformRounds:
                        m_pwDatabase.setKeyEncryptionRounds(MemUtil.BytesToUInt64(pbData));
                        break;

                    case EncryptionIV:
                        m_pbEncryptionIV = pbData;
                        break;

                    case ProtectedStreamKey:
                        m_pbProtectedStreamKey = pbData;
                        CryptoRandom.getInstance().AddEntropy(pbData);
                        break;

                    case StreamStartBytes:
                        m_pbStreamStartBytes = pbData;
                        break;

                    case InnerRandomStreamID:
                        SetInnerRandomStreamID(pbData);
                        break;

                    default:
                        assert false;
                        if(m_slLogger != null)
                            m_slLogger.SetText("unknown header id" + ": " +
                            kdbID + "!", LogStatusType.Warning);
                        break;
                }

                return bResult;
            }

            private void SetCipher(byte[] pbID)
            {
                if((pbID == null) || (pbID.length != 16))
                    throw new KdbxFileFormatException("unknown cipher");

                m_pwDatabase.setDataCipherUuid(new PwUuid(pbID));
            }

            private void SetCompressionFlags(byte[] pbFlags)
            {
                int nID = (int)MemUtil.BytesToUInt32(pbFlags);
                if((nID < 0) || (nID >= PwCompressionAlgorithm.Count.ordinal()))
                    throw new KdbxFileFormatException("unknown file compression type");

                m_pwDatabase.setCompression(PwCompressionAlgorithm.values()[nID]);
            }

            private void SetInnerRandomStreamID(byte[] pbID)
            {
                int uID = MemUtil.BytesToUInt32(pbID);
                if(uID >= CrsAlgorithm.Count.ordinal())
                    throw new KdbxFileFormatException("unknown cipher");

                m_craInnerRandomStream = CrsAlgorithm.values()[uID];
            }

            private InputStream AttachStreamDecryptor(InputStream s)
            {
                ByteArrayOutputStream ms = new ByteArrayOutputStream();

                assert m_pbMasterSeed.length == 32;
                if(m_pbMasterSeed.length != 32)
                    throw new KdbxFileFormatException("master seed length invalid");
                ms.write(m_pbMasterSeed, 0, 32);

                byte[] pKey32 = m_pwDatabase.getMasterKey().GenerateKey32(m_pbTransformSeed,
                        m_pwDatabase.getKeyEncryptionRounds()).ReadData();
                if((pKey32 == null) || (pKey32.length != 32))
                    throw new SecurityException("invalid composite key");
                ms.write(pKey32, 0, 32);

                byte[] aesKey = Digests.sha256(ms.toByteArray());

                Arrays.fill(pKey32, 0, 32, (byte)0);

                if((aesKey == null) || (aesKey.length != 32))
                    throw new SecurityException("final key creation failed");

                ICipherEngine iEngine = CipherPool.getGlobalPool().GetCipher(m_pwDatabase.getDataCipherUuid());
                if(iEngine == null) throw new SecurityException("Unknown file cipher");
                return iEngine.DecryptStream(s, aesKey, m_pbEncryptionIV);
            }

            @Deprecated
            public static List<PwEntry> ReadEntries(PwDatabase pwDatabase, InputStream msData)
                    throws IOException
            {
                return ReadEntries(msData);
            }

            /// <summary>
            /// Read entries from a stream.
            /// </summary>
            /// <param name="msData">Input stream to read the entries from.</param>
            /// <returns>Extracted entries.</returns>
            public static List<PwEntry> ReadEntries(InputStream msData) throws IOException
            {
			/* KdbxFile f = new KdbxFile(pwDatabase);
			f.m_format = KdbxFormat.PlainXml;

			XmlDocument doc = new XmlDocument();
			doc.Load(msData);

			XmlElement el = doc.DocumentElement;
			if(el.Name != ElemRoot) throw new FormatException();

			List<PwEntry> vEntries = new List<PwEntry>();

			for(XmlNode xmlChild : el.ChildNodes)
			{
				if(xmlChild.Name == ElemEntry)
				{
					PwEntry pe = f.ReadEntry(xmlChild);
					pe.Uuid = new PwUuid(true);

					for(PwEntry peHistory : pe.History)
						peHistory.Uuid = pe.Uuid;

					vEntries.Add(pe);
				}
				else { assert false; }
			}

			return vEntries; */

                PwDatabase pd = new PwDatabase();
                KdbxFile f = new KdbxFile(pd);
                f.Load(msData, KdbxFormat.PlainXml, null);

                List<PwEntry> vEntries = new ArrayList<PwEntry>();
                for(PwEntry pe : pd.getRootGroup().getEntries())
                {
                    pe.SetUuid(new PwUuid(true), true);
                    vEntries.add(pe);
                }

                return vEntries;
            }

        // KdbxFile.Read.Streamed.cs
    private enum KdbContext
    {
        Null,
        KeePassFile,
        Meta,
        Root,
        MemoryProtection,
        CustomIcons,
        CustomIcon,
        Binaries,
        CustomData,
        CustomDataItem,
        RootDeletedObjects,
        DeletedObject,
        Group,
        GroupTimes,
        Entry,
        EntryTimes,
        EntryString,
        EntryBinary,
        EntryAutoType,
        EntryAutoTypeItem,
        EntryHistory
    }

    private boolean m_bReadNextNode = true;
    private Stack<PwGroup> m_ctxGroups = new Stack<PwGroup>();
    private PwGroup m_ctxGroup = null;
    private PwEntry m_ctxEntry = null;
    private String m_ctxStringName = null;
    private ProtectedString m_ctxStringValue = null;
    private String m_ctxBinaryName = null;
    private ProtectedBinary m_ctxBinaryValue = null;
    private String m_ctxATName = null;
    private String m_ctxATSeq = null;
    private boolean m_bEntryInHistory = false;
    private PwEntry m_ctxHistoryBase = null;
    private PwDeletedObject m_ctxDeletedObject = null;
    private PwUuid m_uuidCustomIconID = PwUuid.Zero;
    private byte[] m_pbCustomIconData = null;
    private String m_strCustomDataKey = null;
    private String m_strCustomDataValue = null;

    private void ReadXmlStreamed(InputStream readerStream, InputStream sParentStream) throws IOException, XmlPullParserException {
        ReadDocumentStreamed(CreateXmlReader(readerStream), sParentStream);
    }

        /*
    static XmlReaderSettings CreateStdXmlReaderSettings() {
        {
            XmlReaderSettings xrs = new XmlReaderSettings();

            xrs.CloseInput = true;
            xrs.IgnoreComments = true;
            xrs.IgnoreProcessingInstructions = true;
            xrs.IgnoreWhitespace = true;

            xrs.ProhibitDtd = true;
            xrs.ValidationType = ValidationType.None;

            return xrs;
        }
        */


    private static XmlPullParser CreateXmlReader(InputStream readerStream)
    {
        try {
            XmlPullParserFactory fact = XmlPullParserFactory.newInstance();
            fact.setFeature(XmlPullParser.FEATURE_VALIDATION, false);
//            XmlReaderSettings xrs = CreateStdXmlReaderSettings();
            XmlPullParser xr = fact.newPullParser();
            xr.setInput(readerStream, "utf-8");
            return xr;
        } catch (Exception e) { throw new IllegalStateException(e); }
    }

    private void ReadDocumentStreamed(XmlPullParser xr, InputStream sParentStream)
            throws IOException, XmlPullParserException
    {
        assert xr != null;
        if(xr == null) throw new IllegalArgumentException("xr");

        m_ctxGroups.clear();
        m_dictBinPool = new HashMap<String, ProtectedBinary>();

        KdbContext ctx = KdbContext.Null;

        int uTagCounter = 0;

        boolean bSupportsStatus = (m_slLogger != null);
        long lStreamLength = 1;
        /*
        try
        {
            sParentStream.Position.ToString(); // Test Position support
            lStreamLength = sParentStream.Length;
        }
        catch(Exception e) { bSupportsStatus = false; }
        */
        if(lStreamLength <= 0) { assert false; lStreamLength = 1; }

        m_bReadNextNode = true;

        while(true)
        {
            if(m_bReadNextNode)
            {
                if (xr.next() == XmlPullParser.END_DOCUMENT) break;
            }
            else m_bReadNextNode = true;

            switch(xr.getEventType())
            {
                case XmlPullParser.START_TAG:
                    ctx = ReadXmlElement(ctx, xr);
                    if (xr.getEventType() == XmlPullParser.START_TAG &&
                            xr.isEmptyElementTag()) xr.next(); // skip empty END_TAG
                    break;

                case XmlPullParser.END_TAG:
                    ctx = EndXmlElement(ctx, xr);
                    break;

                case XmlPullParser.DOCDECL:
                    break; // Ignore

                default:
//                    assert false;
                    break;
            }

            ++uTagCounter;
            /*
            if(((uTagCounter % 256) == 0) && bSupportsStatus)
            {
                assert lStreamLength == sParentStream.Length;
                int uPct = (int)((sParentStream.Position * 100) /
                        lStreamLength);

                // Clip percent value in case the stream reports incorrect
                // position/length values (M120413)
                if(uPct > 100) { assert false; uPct = 100; }

                m_slLogger.SetProgress(uPct);
            }
            */
        }

        assert ctx == KdbContext.Null;
        if(ctx != KdbContext.Null) throw new KdbxFileFormatException("non null context");

        assert m_ctxGroups.size() == 0;
        if(m_ctxGroups.size() != 0) throw new KdbxFileFormatException("nonzero groups");
    }

    private KdbContext ReadXmlElement(KdbContext ctx, XmlPullParser xr) throws XmlPullParserException, IOException {
        switch(ctx)
        {
            case Null:
                if(Objects.equal(xr.getName(), ElemDocNode))
                    return SwitchContext(ctx, KdbContext.KeePassFile, xr);
                else ReadUnknown(xr);
                break;

            case KeePassFile:
                if(xr.getName().equals(ElemMeta))
                    return SwitchContext(ctx, KdbContext.Meta, xr);
                else if(xr.getName().equals(ElemRoot))
                    return SwitchContext(ctx, KdbContext.Root, xr);
                else ReadUnknown(xr);
                break;

            case Meta:
                if(xr.getName().equals(ElemGenerator))
                    ReadString(xr); // Ignore
                else if(xr.getName().equals(ElemHeaderHash))
                {
                    String strHash = ReadString(xr);
                    if(!Strings.isNullOrEmpty(strHash) && (m_pbHashOfHeader != null) &&
                            !m_bRepairMode)
                    {
                        byte[] pbHash = BaseEncoding.base64().decode(strHash);
                        if(!MemUtil.ArraysEqual(pbHash, m_pbHashOfHeader))
                            throw new KdbxFileFormatException("file corrupted");
                    }
                }
                else if(xr.getName().equals(ElemDbName))
                    m_pwDatabase.setName(ReadString(xr));
                else if(xr.getName().equals(ElemDbNameChanged))
                    m_pwDatabase.setNameChanged(ReadTime(xr));
                else if(xr.getName().equals(ElemDbDesc))
                    m_pwDatabase.setDescription(ReadString(xr));
                else if(xr.getName().equals(ElemDbDescChanged))
                    m_pwDatabase.setDescriptionChanged(ReadTime(xr));
                else if(xr.getName().equals(ElemDbDefaultUser))
                    m_pwDatabase.setDefaultUserName(ReadString(xr));
                else if(xr.getName().equals(ElemDbDefaultUserChanged))
                    m_pwDatabase.setDefaultUserNameChanged(ReadTime(xr));
                else if(xr.getName().equals(ElemDbMntncHistoryDays))
                    m_pwDatabase.setMaintenanceHistoryDays(ReadUInt(xr, 365));
                else if(xr.getName().equals(ElemDbColor))
                {
                    String strColor = ReadString(xr);
                    if(!Strings.isNullOrEmpty(strColor))
                        m_pwDatabase.setColor(ColorTranslator.FromHtml(strColor));
                }
                else if(xr.getName().equals(ElemDbKeyChanged))
                    m_pwDatabase.setMasterKeyChanged(ReadTime(xr));
                else if(xr.getName().equals(ElemDbKeyChangeRec))
                    m_pwDatabase.setMasterKeyChangeRec(ReadLong(xr, -1));
                else if(xr.getName().equals(ElemDbKeyChangeForce))
                    m_pwDatabase.setMasterKeyChangeForce(ReadLong(xr, -1));
                else if(xr.getName().equals(ElemMemoryProt))
                    return SwitchContext(ctx, KdbContext.MemoryProtection, xr);
                else if(xr.getName().equals(ElemCustomIcons))
                    return SwitchContext(ctx, KdbContext.CustomIcons, xr);
                else if(xr.getName().equals(ElemRecycleBinEnabled))
                    m_pwDatabase.setRecycleBinEnabled(ReadBool(xr, true));
                else if(xr.getName().equals(ElemRecycleBinUuid))
                    m_pwDatabase.setRecycleBinUuid(ReadUuid(xr));
                else if(xr.getName().equals(ElemRecycleBinChanged))
                    m_pwDatabase.setRecycleBinChanged(ReadTime(xr));
                else if(xr.getName().equals(ElemEntryTemplatesGroup))
                    m_pwDatabase.setEntryTemplatesGroup(ReadUuid(xr));
                else if(xr.getName().equals(ElemEntryTemplatesGroupChanged))
                    m_pwDatabase.setEntryTemplatesGroupChanged(ReadTime(xr));
                else if(xr.getName().equals(ElemHistoryMaxItems))
                    m_pwDatabase.setHistoryMaxItems(ReadInt(xr, -1));
                else if(xr.getName().equals(ElemHistoryMaxSize))
                    m_pwDatabase.setHistoryMaxSize(ReadLong(xr, -1));
                else if(xr.getName().equals(ElemLastSelectedGroup)) {
                    m_pwDatabase.setLastSelectedGroup(ReadUuid(xr));
                }
                else if(xr.getName().equals(ElemLastTopVisibleGroup))
                    m_pwDatabase.setLastTopVisibleGroup(ReadUuid(xr));
                else if(xr.getName().equals(ElemBinaries))
                    return SwitchContext(ctx, KdbContext.Binaries, xr);
                else if(xr.getName().equals(ElemCustomData))
                    return SwitchContext(ctx, KdbContext.CustomData, xr);
                else ReadUnknown(xr);
                break;

            case MemoryProtection:
                if(xr.getName().equals(ElemProtTitle))
                    m_pwDatabase.getMemoryProtection().ProtectTitle = ReadBool(xr, false);
                else if(xr.getName().equals(ElemProtUserName))
                    m_pwDatabase.getMemoryProtection().ProtectUserName = ReadBool(xr, false);
                else if(xr.getName().equals(ElemProtPassword))
                    m_pwDatabase.getMemoryProtection().ProtectPassword = ReadBool(xr, true);
                else if(xr.getName().equals(ElemProtUrl))
                    m_pwDatabase.getMemoryProtection().ProtectUrl = ReadBool(xr, false);
                else if(xr.getName().equals(ElemProtNotes))
                    m_pwDatabase.getMemoryProtection().ProtectNotes = ReadBool(xr, false);
                    // else if(xr.Name == ElemProtAutoHide)
                    //	m_pwDatabase.MemoryProtection.AutoEnableVisualHiding = ReadBool(xr, true);
                else ReadUnknown(xr);
                break;

            case CustomIcons:
                if(xr.getName().equals(ElemCustomIconItem))
                    return SwitchContext(ctx, KdbContext.CustomIcon, xr);
                else ReadUnknown(xr);
                break;

            case CustomIcon:
                if(xr.getName().equals(ElemCustomIconItemID))
                    m_uuidCustomIconID = ReadUuid(xr);
                else if(xr.getName().equals(ElemCustomIconItemData))
                {
                    String strData = ReadString(xr);
                    if(!Strings.isNullOrEmpty(strData))
                        m_pbCustomIconData = BaseEncoding.base64().decode(strData);
                    else { assert false; }
                }
                else ReadUnknown(xr);
                break;

            case Binaries:
                if(xr.getName().equals(ElemBinary))
                {
                    String strKey = xr.getAttributeValue(null, AttrId);
                    if(strKey != null)
                    {
                        ProtectedBinary pbData = ReadProtectedBinary(xr);

                        m_dictBinPool.put(strKey == null ? "" : strKey, pbData);
                    }
                    else ReadUnknown(xr);
                }
                else ReadUnknown(xr);
                break;

            case CustomData:
                if(xr.getName().equals(ElemStringDictExItem))
                    return SwitchContext(ctx, KdbContext.CustomDataItem, xr);
                else ReadUnknown(xr);
                break;

            case CustomDataItem:
                if(xr.getName().equals(ElemKey))
                    m_strCustomDataKey = ReadString(xr);
                else if(xr.getName().equals(ElemValue))
                    m_strCustomDataValue = ReadString(xr);
                else ReadUnknown(xr);
                break;

            case Root:
                if(xr.getName().equals(ElemGroup))
                {
                    assert m_ctxGroups.size() == 0;
                    if(m_ctxGroups.size() != 0) throw new KdbxFileFormatException("groups size not 0");

                    m_pwDatabase.setRootGroup(new PwGroup(false, false));
                    m_ctxGroups.push(m_pwDatabase.getRootGroup());
                    m_ctxGroup = m_ctxGroups.peek();

                    return SwitchContext(ctx, KdbContext.Group, xr);
                }
                else if(xr.getName().equals(ElemDeletedObjects))
                    return SwitchContext(ctx, KdbContext.RootDeletedObjects, xr);
                else ReadUnknown(xr);
                break;

            case Group:
                if(xr.getName().equals(ElemUuid))
                    m_ctxGroup.setUuid(ReadUuid(xr));
                else if(xr.getName().equals(ElemName))
                    m_ctxGroup.setName(ReadString(xr));
                else if(xr.getName().equals(ElemNotes))
                    m_ctxGroup.setNotes(ReadString(xr));
                else if(xr.getName().equals(ElemIcon))
                    m_ctxGroup.setIconId(PwIcon.values()[ReadInt(xr, PwIcon.Folder.ordinal())]);
                else if(xr.getName().equals(ElemCustomIconID))
                    m_ctxGroup.setCustomIconUuid(ReadUuid(xr));
                else if(xr.getName().equals(ElemTimes))
                    return SwitchContext(ctx, KdbContext.GroupTimes, xr);
                else if(xr.getName().equals(ElemIsExpanded))
                    m_ctxGroup.setExpanded(ReadBool(xr, true));
                else if(xr.getName().equals(ElemGroupDefaultAutoTypeSeq))
                    m_ctxGroup.setDefaultAutoTypeSequence(ReadString(xr));
                else if(xr.getName().equals(ElemEnableAutoType))
                    m_ctxGroup.setEnableAutoType(StrUtil.StringToBoolEx(ReadString(xr)));
                else if(xr.getName().equals(ElemEnableSearching))
                    m_ctxGroup.setEnableSearching(StrUtil.StringToBoolEx(ReadString(xr)));
                else if(xr.getName().equals(ElemLastTopVisibleEntry))
                    m_ctxGroup.setLastTopVisibleEntry(ReadUuid(xr));
                else if(xr.getName().equals(ElemGroup))
                {
                    m_ctxGroup = new PwGroup(false, false);
                    m_ctxGroups.peek().AddGroup(m_ctxGroup, true);

                    m_ctxGroups.push(m_ctxGroup);

                    return SwitchContext(ctx, KdbContext.Group, xr);
                }
                else {
                    if (xr.getName().equals(ElemEntry)) {
                        m_ctxEntry = new PwEntry(false, false);
                        m_ctxGroup.AddEntry(m_ctxEntry, true);

                        m_bEntryInHistory = false;
                        return SwitchContext(ctx, KdbContext.Entry, xr);
                    } else ReadUnknown(xr);
                }
                break;

            case Entry:
                if(xr.getName().equals(ElemUuid)) {
                    m_ctxEntry.setUuid(ReadUuid(xr));
                }
                else if(xr.getName().equals(ElemIcon))
                    m_ctxEntry.setIconId(PwIcon.values()[ReadInt(xr, PwIcon.Key.ordinal())]);
                else if(xr.getName().equals(ElemCustomIconID))
                    m_ctxEntry.setCustomIconUuid(ReadUuid(xr));
                else if(xr.getName().equals(ElemFgColor))
                {
                    String strColor = ReadString(xr);
                    if(!Strings.isNullOrEmpty(strColor))
                        m_ctxEntry.setForegroundColor(ColorTranslator.FromHtml(strColor));
                }
                else if(xr.getName().equals(ElemBgColor))
                {
                    String strColor = ReadString(xr);
                    if(!Strings.isNullOrEmpty(strColor))
                        m_ctxEntry.setBackgroundColor(ColorTranslator.FromHtml(strColor));
                }
                else if(xr.getName().equals(ElemOverrideUrl))
                    m_ctxEntry.setOverrideUrl(ReadString(xr));
                else if(xr.getName().equals(ElemTags))
                    m_ctxEntry.setTags(StrUtil.StringToTags(ReadString(xr)));
                else if(xr.getName().equals(ElemTimes))
                    return SwitchContext(ctx, KdbContext.EntryTimes, xr);
                else if(xr.getName().equals(ElemString))
                    return SwitchContext(ctx, KdbContext.EntryString, xr);
                else if(xr.getName().equals(ElemBinary))
                    return SwitchContext(ctx, KdbContext.EntryBinary, xr);
                else if(xr.getName().equals(ElemAutoType))
                    return SwitchContext(ctx, KdbContext.EntryAutoType, xr);
                else if(xr.getName().equals(ElemHistory))
                {
                    assert !m_bEntryInHistory;

                    if(!m_bEntryInHistory)
                    {
                        m_ctxHistoryBase = m_ctxEntry;
                        return SwitchContext(ctx, KdbContext.EntryHistory, xr);
                    }
                    else ReadUnknown(xr);
                }
                else ReadUnknown(xr);
                break;

            case GroupTimes:
            case EntryTimes:
                ITimeLogger tl = ((ctx == KdbContext.GroupTimes) ?
                        (ITimeLogger)m_ctxGroup : (ITimeLogger)m_ctxEntry);
                assert tl != null;

                if(xr.getName().equals(ElemCreationTime))
                    tl.setCreationTime(ReadTime(xr));
                else if(xr.getName().equals(ElemLastModTime))
                    tl.setLastModificationTime(ReadTime(xr));
                else if(xr.getName().equals(ElemLastAccessTime))
                    tl.setLastAccessTime(ReadTime(xr));
                else if(xr.getName().equals(ElemExpiryTime))
                    tl.setExpiryTime(ReadTime(xr));
                else if(xr.getName().equals(ElemExpires))
                    tl.setExpires(ReadBool(xr, false));
                else if(xr.getName().equals(ElemUsageCount))
                    tl.setUsageCount(ReadULong(xr, 0));
                else if(xr.getName().equals(ElemLocationChanged))
                    tl.setLocationChanged(ReadTime(xr));
                else ReadUnknown(xr);
                break;

            case EntryString:
                if(xr.getName().equals(ElemKey))
                    m_ctxStringName = ReadString(xr);
                else if(xr.getName().equals(ElemValue))
                    m_ctxStringValue = ReadProtectedString(xr);
                else ReadUnknown(xr);
                break;

            case EntryBinary:
                if(xr.getName().equals(ElemKey))
                    m_ctxBinaryName = ReadString(xr);
                else if(xr.getName().equals(ElemValue))
                    m_ctxBinaryValue = ReadProtectedBinary(xr);
                else ReadUnknown(xr);
                break;

            case EntryAutoType:
                if(xr.getName().equals(ElemAutoTypeEnabled))
                    m_ctxEntry.getAutoType().setEnabled(ReadBool(xr, true));
                else if(xr.getName().equals(ElemAutoTypeObfuscation))
                    m_ctxEntry.getAutoType().setObfuscationOptions(
                            AutoTypeObfuscationOptions.values()[ReadInt(xr, 0)]);
                else if(xr.getName().equals(ElemAutoTypeDefaultSeq))
                    m_ctxEntry.getAutoType().setDefaultSequence(ReadString(xr));
                else if(xr.getName().equals(ElemAutoTypeItem))
                    return SwitchContext(ctx, KdbContext.EntryAutoTypeItem, xr);
                else ReadUnknown(xr);
                break;

            case EntryAutoTypeItem:
                if(xr.getName().equals(ElemWindow))
                    m_ctxATName = ReadString(xr);
                else if(xr.getName().equals(ElemKeystrokeSequence))
                    m_ctxATSeq = ReadString(xr);
                else ReadUnknown(xr);
                break;

            case EntryHistory:
                if(xr.getName().equals(ElemEntry))
                {
                    m_ctxEntry = new PwEntry(false, false);
                    m_ctxHistoryBase.getHistory().Add(m_ctxEntry);

                    m_bEntryInHistory = true;
                    return SwitchContext(ctx, KdbContext.Entry, xr);
                }
                else ReadUnknown(xr);
                break;

            case RootDeletedObjects:
                if(xr.getName().equals(ElemDeletedObject))
                {
                    m_ctxDeletedObject = new PwDeletedObject();
                    m_pwDatabase.getDeletedObjects().Add(m_ctxDeletedObject);

                    return SwitchContext(ctx, KdbContext.DeletedObject, xr);
                }
                else ReadUnknown(xr);
                break;

            case DeletedObject:
                if(xr.getName().equals(ElemUuid))
                    m_ctxDeletedObject.setUuid(ReadUuid(xr));
                else if(xr.getName().equals(ElemDeletionTime))
                    m_ctxDeletedObject.setDeletionTime(ReadTime(xr));
                else ReadUnknown(xr);
                break;

            default:
                ReadUnknown(xr);
                break;
        }

        return ctx;
    }

    private KdbContext EndXmlElement(KdbContext ctx, XmlPullParser xr)
            throws XmlPullParserException, IOException
    {
        assert xr.getEventType() == XmlPullParser.END_TAG;

        if((ctx == KdbContext.KeePassFile) && (xr.getName().equals(ElemDocNode)))
            return KdbContext.Null;
        else if((ctx == KdbContext.Meta) && (xr.getName().equals(ElemMeta)))
            return KdbContext.KeePassFile;
        else if((ctx == KdbContext.Root) && (xr.getName().equals(ElemRoot)))
            return KdbContext.KeePassFile;
        else if((ctx == KdbContext.MemoryProtection) && (xr.getName().equals(ElemMemoryProt)))
            return KdbContext.Meta;
        else if((ctx == KdbContext.CustomIcons) && (xr.getName().equals(ElemCustomIcons)))
            return KdbContext.Meta;
        else if((ctx == KdbContext.CustomIcon) && (xr.getName().equals(ElemCustomIconItem)))
        {
            if(!m_uuidCustomIconID.Equals(PwUuid.Zero) &&
                    (m_pbCustomIconData != null))
                m_pwDatabase.getCustomIcons().add(new PwCustomIcon(
                        m_uuidCustomIconID, m_pbCustomIconData));
            else { assert false; }

            m_uuidCustomIconID = PwUuid.Zero;
            m_pbCustomIconData = null;

            return KdbContext.CustomIcons;
        }
        else if((ctx == KdbContext.Binaries) && (xr.getName().equals(ElemBinaries)))
            return KdbContext.Meta;
        else if((ctx == KdbContext.CustomData) && (xr.getName().equals(ElemCustomData)))
            return KdbContext.Meta;
        else if((ctx == KdbContext.CustomDataItem) && (xr.getName().equals(ElemStringDictExItem)))
        {
            if((m_strCustomDataKey != null) && (m_strCustomDataValue != null))
                m_pwDatabase.getCustomData().Set(m_strCustomDataKey, m_strCustomDataValue);
            else { assert false; }

            m_strCustomDataKey = null;
            m_strCustomDataValue = null;

            return KdbContext.CustomData;
        }
        else if((ctx == KdbContext.Group) && (xr.getName().equals(ElemGroup)))
        {
            if(PwUuid.Zero.Equals(m_ctxGroup.getUuid()))
                m_ctxGroup.setUuid(new PwUuid(true)); // No assert (import)

            m_ctxGroups.pop();

            if(m_ctxGroups.size() == 0)
            {
                m_ctxGroup = null;
                return KdbContext.Root;
            }
            else
            {
                m_ctxGroup = m_ctxGroups.peek();
                return KdbContext.Group;
            }
        }
        else if((ctx == KdbContext.GroupTimes) && (xr.getName().equals(ElemTimes)))
            return KdbContext.Group;
        else if((ctx == KdbContext.Entry) && (xr.getName().equals(ElemEntry)))
        {
            // Create new UUID if absent
            if(PwUuid.Zero.Equals(m_ctxEntry.getUuid()))
                m_ctxEntry.setUuid(new PwUuid(true)); // No assert (import)

            if(m_bEntryInHistory)
            {
                m_ctxEntry = m_ctxHistoryBase;
                return KdbContext.EntryHistory;
            }

            return KdbContext.Group;
        }
        else if((ctx == KdbContext.EntryTimes) && (xr.getName().equals(ElemTimes)))
            return KdbContext.Entry;
        else if((ctx == KdbContext.EntryString) && (xr.getName().equals(ElemString)))
        {
            m_ctxEntry.getStrings().Set(m_ctxStringName, m_ctxStringValue);
            m_ctxStringName = null;
            m_ctxStringValue = null;
            return KdbContext.Entry;
        }
        else if((ctx == KdbContext.EntryBinary) && (xr.getName().equals(ElemBinary)))
        {
            if(Strings.isNullOrEmpty(m_strDetachBins))
                m_ctxEntry.getBinaries().Set(m_ctxBinaryName, m_ctxBinaryValue);
            else
            {
                SaveBinary(m_ctxBinaryName, m_ctxBinaryValue, m_strDetachBins);

                m_ctxBinaryValue = null;
            }

            m_ctxBinaryName = null;
            m_ctxBinaryValue = null;
            return KdbContext.Entry;
        }
        else if((ctx == KdbContext.EntryAutoType) && (xr.getName().equals(ElemAutoType)))
            return KdbContext.Entry;
        else if((ctx == KdbContext.EntryAutoTypeItem) && (xr.getName().equals(ElemAutoTypeItem)))
        {
            AutoTypeAssociation atAssoc = new AutoTypeAssociation(m_ctxATName,
                    m_ctxATSeq);
            m_ctxEntry.getAutoType().Add(atAssoc);
            m_ctxATName = null;
            m_ctxATSeq = null;
            return KdbContext.EntryAutoType;
        }
        else if((ctx == KdbContext.EntryHistory) && (xr.getName().equals(ElemHistory)))
        {
            m_bEntryInHistory = false;
            return KdbContext.Entry;
        }
        else if((ctx == KdbContext.RootDeletedObjects) && (xr.getName().equals(ElemDeletedObjects)))
            return KdbContext.Root;
        else if((ctx == KdbContext.DeletedObject) && (xr.getName().equals(ElemDeletedObject)))
        {
            m_ctxDeletedObject = null;
            return KdbContext.RootDeletedObjects;
        }
        else
        {
            throw new KdbxFileFormatException("xml end tag failure: " + ctx + " => " + xr.getName());
        }
    }

    private String ReadString(XmlPullParser xr) throws XmlPullParserException, IOException
    {
        XorredBuffer xb = ProcessNode(xr);
        if(xb != null)
        {
            byte[] pb = xb.ReadPlainText();
            if(pb.length == 0) return "";
            return new String(pb, 0, pb.length, StrUtil.Utf8);
        }

        //m_bReadNextNode = false; // ReadElementString skips end tag
        return xr.nextText();
    }

    private String ReadStringRaw(XmlPullParser xr) throws XmlPullParserException, IOException
    {
        //m_bReadNextNode = false; // ReadElementString skips end tag
        return xr.nextText();
    }

    private boolean ReadBool(XmlPullParser xr, boolean bDefault)
            throws XmlPullParserException, IOException
    {
        String str = ReadString(xr);
        if(str.equals(ValTrue)) return true;
        else if(str.equals(ValFalse)) return false;

        assert false;
        return bDefault;
    }

    private PwUuid ReadUuid(XmlPullParser xr) throws XmlPullParserException, IOException
    {
        String str = ReadString(xr);
        if(Strings.isNullOrEmpty(str)) return PwUuid.Zero;
        return new PwUuid(BaseEncoding.base64().decode(str));
    }

    private int ReadInt(XmlPullParser xr, int nDefault)
            throws XmlPullParserException, IOException
    {
        String str = ReadString(xr);

        int[] n = new int[1];
        if(StrUtil.TryParseIntInvariant(str, n)) return n[0];

        // Backward compatibility
        if(StrUtil.TryParseInt(str, n)) return n[0];

        assert false;
        return nDefault;
    }

    private int ReadUInt(XmlPullParser xr, int uDefault) throws IOException, XmlPullParserException {
        String str = ReadString(xr);

        int[] u = new int[1];
        if(StrUtil.TryParseUIntInvariant(str, u)) return u[0];

        // Backward compatibility
        if(StrUtil.TryParseUInt(str, u)) return u[0];

        assert false;
        return uDefault;
    }

    private long ReadLong(XmlPullParser xr, long lDefault) throws IOException, XmlPullParserException {
        String str = ReadString(xr);

        long[] l = new long[1];
        if(StrUtil.TryParseLongInvariant(str, l)) return l[0];

        // Backward compatibility
        if(StrUtil.TryParseLong(str, l)) return l[0];

        assert false;
        return lDefault;
    }

    private long ReadULong(XmlPullParser xr, long uDefault) throws IOException, XmlPullParserException {
        return ReadLong(xr, uDefault);
    }

    private Date ReadTime(XmlPullParser xr) throws IOException, XmlPullParserException {
        String str = ReadString(xr);

        Date[] dt = new Date[1];
        if(TimeUtil.TryDeserializeUtc(str, dt)) return dt[0];

        assert false;
        return m_dtNow;
    }

    private ProtectedString ReadProtectedString(XmlPullParser xr) throws IOException, XmlPullParserException {
        XorredBuffer xb = ProcessNode(xr);
        if(xb != null) return new ProtectedString(true, xb);

        boolean bProtect = false;
        if(m_format == KdbxFormat.PlainXml)
        {
            String strProtect = xr.getAttributeValue(null, AttrProtectedInMemPlainXml);
            if(strProtect != null)
            {
                bProtect = ((strProtect != null) && (strProtect.equals(ValTrue)));
            }
        }

        ProtectedString ps = new ProtectedString(bProtect, ReadString(xr));
        return ps;
    }

    private ProtectedBinary ReadProtectedBinary(XmlPullParser xr) throws IOException, XmlPullParserException {
        String strRef = xr.getAttributeValue(null, AttrRef);
        if(strRef != null)
        {
			ProtectedBinary pb = BinPoolGet(strRef);
			// ignore change in 2.30
			// https://sourceforge.net/p/keepass/feature-requests/2023/
			if(pb != null) return pb;
			else { assert false; }
		}

        boolean bCompressed = false;

        bCompressed = ValTrue.equals(xr.getAttributeValue(null, AttrCompressed));

        XorredBuffer xb = ProcessNode(xr);
        if(xb != null)
        {
            assert !bCompressed; // See SubWriteValue(ProtectedBinary value)
            return new ProtectedBinary(true, xb);
        }

        String strValue = ReadString(xr);
        if(strValue.length() == 0) return new ProtectedBinary();

        byte[] pbData = BaseEncoding.base64().decode(strValue);
        if(bCompressed) pbData = MemUtil.Decompress(pbData);
        return new ProtectedBinary(false, pbData);
    }

    private void ReadUnknown(XmlPullParser xr) throws XmlPullParserException, IOException
    {
//        assert false; // Unknown node!

        if(xr.isEmptyElementTag()) return;

        String strUnknownName = xr.getName();
        ProcessNode(xr);

        int xrNodeType;
        while((xrNodeType = xr.next()) != XmlPullParser.END_DOCUMENT)
        {
            if(xrNodeType == XmlPullParser.END_TAG) break;
            if(xrNodeType != XmlPullParser.START_TAG) continue;

            ReadUnknown(xr);
        }

        assert xr.getName().equals(strUnknownName);
    }

    private XorredBuffer ProcessNode(XmlPullParser xr) throws IOException, XmlPullParserException {
        // assert xr.NodeType == XmlNodeType.Element;

        XorredBuffer xb = null;
        if(xr.getAttributeCount() > 0)
        {
                if(ValTrue.equals(xr.getAttributeValue(null, AttrProtected)))
                {
                    String strEncrypted = ReadStringRaw(xr);

                    byte[] pbEncrypted;
                    if(strEncrypted.length() > 0)
                        pbEncrypted = BaseEncoding.base64().decode(strEncrypted);
                    else pbEncrypted = new byte[0];

                    byte[] pbPad = m_randomStream.GetRandomBytes((int)pbEncrypted.length);

                    xb = new XorredBuffer(pbEncrypted, pbPad);
                }
        }

        return xb;
    }

    private static KdbContext SwitchContext(KdbContext ctxCurrent,
                                            KdbContext ctxNew, XmlPullParser xr) throws XmlPullParserException {
        if(xr.isEmptyElementTag()) return ctxCurrent;
        return ctxNew;
    }

        // KdbxFile.Write.cs
/// <summary>
/// Serialization to KeePass KDBX files.
/// </summary>
    // public void Save(String strFile, PwGroup pgDataSource, KdbxFormat format,
    //	IStatusLogger slLogger)
    // {
    //	boolean bMadeUnhidden = UrlUtil.UnhideFile(strFile);
    //
    //	IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFile);
    //	this.Save(IOConnection.OpenWrite(ioc), pgDataSource, format, slLogger);
    //
    //	if(bMadeUnhidden) UrlUtil.HideFile(strFile, true); // Hide again
    // }

    /// <summary>
    /// Save the contents of the current <c>PwDatabase</c> to a KDBX file.
    /// </summary>
    /// <param name="sSaveTo">Stream to write the KDBX file into.</param>
    /// <param name="pgDataSource">Group containing all groups and
    /// entries to write. If <c>null</c>, the complete database will
    /// be written.</param>
    /// <param name="format">Format of the file to create.</param>
    /// <param name="slLogger">Logger that recieves status information.</param>
    public void Save(OutputStream sSaveTo, PwGroup pgDataSource, KdbxFormat format,
                     IStatusLogger slLogger) throws IOException {
        assert sSaveTo != null;
        if(sSaveTo == null) throw new IllegalArgumentException("sSaveTo");

        m_format = format;
        m_slLogger = slLogger;

        HashingOutputStreamEx hashedStream = new HashingOutputStreamEx(sSaveTo);

        Charset encNoBom = StrUtil.Utf8;
        CryptoRandom cr = CryptoRandom.getInstance();

        try
        {
            m_pbMasterSeed = cr.GetRandomBytes(32);
            m_pbTransformSeed = cr.GetRandomBytes(32);
            m_pbEncryptionIV = cr.GetRandomBytes(16);

            m_pbProtectedStreamKey = cr.GetRandomBytes(32);
            m_craInnerRandomStream = CrsAlgorithm.Salsa20;
            m_randomStream = new CryptoRandomStream(m_craInnerRandomStream,
                    m_pbProtectedStreamKey);

            m_pbStreamStartBytes = cr.GetRandomBytes(32);

            OutputStream writerStream;
            if(m_format == KdbxFormat.Default)
            {
                WriteHeader(hashedStream); // Also flushes the stream

                OutputStream sEncrypted = AttachStreamEncryptor(hashedStream);
                if((sEncrypted == null) || (sEncrypted == hashedStream))
                    throw new SecurityException("failed to create crypto stream");

                sEncrypted.write(m_pbStreamStartBytes, 0, m_pbStreamStartBytes.length);

                OutputStream sHashed = new HashedBlockStream.Output(sEncrypted, 0);

                if(m_pwDatabase.getCompression() == PwCompressionAlgorithm.GZip)
                    writerStream = new GZIPOutputStream(sHashed);
                else
                    writerStream = sHashed;
            }
            else if(m_format == KdbxFormat.PlainXml)
                writerStream = hashedStream;
            else { assert false; throw new KdbxFileFormatException("KdbFormat"); }

            XmlPullParserFactory fact = XmlPullParserFactory.newInstance();
            m_xmlWriter = fact.newSerializer();
            m_xmlWriter.setOutput(writerStream, "utf-8");
//                    new XmlTextWriter(writerStream, encNoBom);
            WriteDocument(pgDataSource);

            m_xmlWriter.flush();
            writerStream.close();
        } catch (XmlPullParserException e) {
            throw new RuntimeException(e);
        } finally { CommonCleanUpWrite(sSaveTo, hashedStream); }
    }

    private void CommonCleanUpWrite(OutputStream sSaveTo, HashingOutputStreamEx hashedStream)
            throws IOException
    {
        hashedStream.close();
        m_pbHashOfFileOnDisk = hashedStream.getHash();

        sSaveTo.close();

        m_xmlWriter = null;
        m_pbHashOfHeader = null;
    }

    private void WriteHeader(OutputStream s)
            throws IOException
    {
        ByteArrayOutputStream ms = new ByteArrayOutputStream();

        MemUtil.Write(ms, MemUtil.UInt32ToBytes(FileSignature1));
        MemUtil.Write(ms, MemUtil.UInt32ToBytes(FileSignature2));
        MemUtil.Write(ms, MemUtil.UInt32ToBytes(FileVersion32));

        WriteHeaderField(ms, KdbxHeaderFieldID.CipherID,
                m_pwDatabase.getDataCipherUuid().getUuidBytes());

        int nCprID = m_pwDatabase.getCompression().ordinal();
        WriteHeaderField(ms, KdbxHeaderFieldID.CompressionFlags,
                MemUtil.UInt32ToBytes((int)nCprID));

        WriteHeaderField(ms, KdbxHeaderFieldID.MasterSeed, m_pbMasterSeed);
        WriteHeaderField(ms, KdbxHeaderFieldID.TransformSeed, m_pbTransformSeed);
        WriteHeaderField(ms, KdbxHeaderFieldID.TransformRounds,
                MemUtil.UInt64ToBytes(m_pwDatabase.getKeyEncryptionRounds()));
        WriteHeaderField(ms, KdbxHeaderFieldID.EncryptionIV, m_pbEncryptionIV);
        WriteHeaderField(ms, KdbxHeaderFieldID.ProtectedStreamKey, m_pbProtectedStreamKey);
        WriteHeaderField(ms, KdbxHeaderFieldID.StreamStartBytes, m_pbStreamStartBytes);

        int nIrsID = m_craInnerRandomStream.ordinal();
        WriteHeaderField(ms, KdbxHeaderFieldID.InnerRandomStreamID,
                MemUtil.UInt32ToBytes((int)nIrsID));

        WriteHeaderField(ms, KdbxHeaderFieldID.EndOfHeader, new byte[]{
                (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' });

        byte[] pbHeader = ms.toByteArray();
        ms.close();

        m_pbHashOfHeader = Digests.sha256(pbHeader);

        s.write(pbHeader, 0, pbHeader.length);
        s.flush();
    }

    private static void WriteHeaderField(OutputStream s, KdbxHeaderFieldID kdbID,
                                         byte[] pbData) throws IOException {
        s.write(kdbID.ordinal());

        if(pbData != null)
        {
            short uLength = (short)pbData.length;
            MemUtil.Write(s, MemUtil.UInt16ToBytes(uLength));

            if(uLength > 0) s.write(pbData, 0, pbData.length);
        }
        else MemUtil.Write(s, MemUtil.UInt16ToBytes((short)0));
    }

    private OutputStream AttachStreamEncryptor(OutputStream s) throws IOException
    {
        ByteArrayOutputStream ms = new ByteArrayOutputStream();

        assert m_pbMasterSeed != null;
        assert m_pbMasterSeed.length == 32;
        ms.write(m_pbMasterSeed, 0, 32);

        assert m_pwDatabase != null;
        assert m_pwDatabase.getMasterKey() != null;
        ProtectedBinary pbinKey = m_pwDatabase.getMasterKey().GenerateKey32(
                m_pbTransformSeed, m_pwDatabase.getKeyEncryptionRounds());
        assert pbinKey != null;
        if(pbinKey == null)
            throw new SecurityException("Invalid composite key");
        byte[] pKey32 = pbinKey.ReadData();
        if((pKey32 == null) || (pKey32.length != 32))
            throw new SecurityException("Invalid composite key");
        ms.write(pKey32, 0, 32);

        byte[] aesKey = Digests.sha256(ms.toByteArray());

        ms.close();
        Arrays.fill(pKey32, 0, 32, (byte)0);

        assert CipherPool.getGlobalPool() != null;
        ICipherEngine iEngine = CipherPool.getGlobalPool().GetCipher(m_pwDatabase.getDataCipherUuid());
        if(iEngine == null) throw new SecurityException("Unknown cipher");
        return iEngine.EncryptStream(s, aesKey, m_pbEncryptionIV);
    }

    private void WriteDocument(PwGroup pgDataSource) throws IOException {
        assert m_xmlWriter != null;
        if(m_xmlWriter == null) throw new UnsupportedOperationException();

        PwGroup pgRoot = (pgDataSource != null ? pgDataSource : m_pwDatabase.getRootGroup());

        final int[] uNumGroups = new int[1], uNumEntries = new int[1], uCurEntry = { 0 };
        pgRoot.GetCounts(true, uNumGroups, uNumEntries);

        BinPoolBuild(pgRoot);

//        m_xmlWriter.Formatting = Formatting.Indented;
//        m_xmlWriter.IndentChar = '\t';
//        m_xmlWriter.Indentation = 1;
// unsupported on android
//        m_xmlWriter.setProperty("http://xmlpull.org/v1/doc/properties.html#serializer-indentation", "\t");

        m_xmlWriter.startDocument("utf-8", true);
        m_xmlWriter.startTag(null, ElemDocNode);

        WriteMeta();

        m_xmlWriter.startTag(null, ElemRoot);
        StartGroup(pgRoot);

        final Stack<PwGroup> groupStack = new Stack<PwGroup>();
        groupStack.push(pgRoot);

        GroupHandler gh = new GroupHandler() { public boolean delegate(PwGroup pg) {
            assert pg != null;
            if (pg == null) throw new IllegalArgumentException("pg");

            while (true) {
                if (pg.getParentGroup().equals(groupStack.peek())) {
                    groupStack.push(pg);
                    try {
                        StartGroup(pg);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    break;
                } else {
                    groupStack.pop();
                    if (groupStack.size() <= 0) return false;

                    try {
                        EndGroup();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }

            return true;
        }};

        EntryHandler eh = new EntryHandler() { public boolean delegate(PwEntry pe) {
            assert pe != null;
            try {
                WriteEntry(pe, false);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            ++uCurEntry[0];
            if (m_slLogger != null)
                if (!m_slLogger.SetProgress((100 * uCurEntry[0]) / uNumEntries[0]))
                    return false;

            return true;
        }};

        if(!pgRoot.TraverseTree(TraversalMethod.PreOrder, gh, eh))
            throw new UnsupportedOperationException();

        while(groupStack.size() > 1)
        {
//            m_xmlWriter.WriteEndElement();
            EndGroup(); // FIXME TODO is this correct?
            groupStack.pop();
        }

        EndGroup();

        WriteList(ElemDeletedObjects, m_pwDatabase.getDeletedObjects());
        m_xmlWriter.endTag(null, ElemRoot); // Root

        m_xmlWriter.endTag(null, ElemDocNode); // ElemDocNode
        m_xmlWriter.endDocument();
    }

    private void WriteMeta() throws IOException {
        m_xmlWriter.startTag(null, ElemMeta);

        WriteObject(ElemGenerator, PwDatabase.getLocalizedAppName(), false); // Generator name

        if(m_pbHashOfHeader != null)
            WriteObject(ElemHeaderHash, BaseEncoding.base64().encode(
                    m_pbHashOfHeader), false);

        WriteObject(ElemDbName, m_pwDatabase.getName(), true);
        WriteObject(ElemDbNameChanged, m_pwDatabase.getNameChanged());
        WriteObject(ElemDbDesc, m_pwDatabase.getDescription(), true);
        WriteObject(ElemDbDescChanged, m_pwDatabase.getDescriptionChanged());
        WriteObject(ElemDbDefaultUser, m_pwDatabase.getDefaultUserName(), true);
        WriteObject(ElemDbDefaultUserChanged, m_pwDatabase.getDefaultUserNameChanged());
        WriteObject(ElemDbMntncHistoryDays, m_pwDatabase.getMaintenanceHistoryDays());
        WriteObject(ElemDbColor, StrUtil.ColorToUnnamedHtml(m_pwDatabase.getColor(), true), false);
        WriteObject(ElemDbKeyChanged, m_pwDatabase.getMasterKeyChanged());
        WriteObject(ElemDbKeyChangeRec, m_pwDatabase.getMasterKeyChangeRec());
        WriteObject(ElemDbKeyChangeForce, m_pwDatabase.getMasterKeyChangeForce());

        WriteList(ElemMemoryProt, m_pwDatabase.getMemoryProtection());

        WriteCustomIconList();

        WriteObject(ElemRecycleBinEnabled, m_pwDatabase.isRecycleBinEnabled());
        WriteObject(ElemRecycleBinUuid, m_pwDatabase.getRecycleBinUuid());
        WriteObject(ElemRecycleBinChanged, m_pwDatabase.getRecycleBinChanged());
        WriteObject(ElemEntryTemplatesGroup, m_pwDatabase.getEntryTemplatesGroup());
        WriteObject(ElemEntryTemplatesGroupChanged, m_pwDatabase.getEntryTemplatesGroupChanged());
        WriteObject(ElemHistoryMaxItems, m_pwDatabase.getHistoryMaxItems());
        WriteObject(ElemHistoryMaxSize, m_pwDatabase.getHistoryMaxSize());

        WriteObject(ElemLastSelectedGroup, m_pwDatabase.getLastSelectedGroup());
        WriteObject(ElemLastTopVisibleGroup, m_pwDatabase.getLastTopVisibleGroup());

        WriteBinPool();
        WriteList(ElemCustomData, m_pwDatabase.getCustomData());

        m_xmlWriter.endTag(null, ElemMeta);
    }

    private void StartGroup(PwGroup pg) throws IOException {
        m_xmlWriter.startTag(null, ElemGroup);
        WriteObject(ElemUuid, pg.getUuid());
        WriteObject(ElemName, pg.getName(), true);
        WriteObject(ElemNotes, pg.getNotes(), true);
        WriteObject(ElemIcon, pg.getIconId().ordinal());

        if(!pg.getCustomIconUuid().Equals(PwUuid.Zero))
            WriteObject(ElemCustomIconID, pg.getCustomIconUuid());

        WriteList(ElemTimes, pg);
        WriteObject(ElemIsExpanded, pg.isExpanded());
        WriteObject(ElemGroupDefaultAutoTypeSeq, pg.getDefaultAutoTypeSequence(), true);
        WriteObject(ElemEnableAutoType, StrUtil.BoolToStringEx(pg.getEnableAutoType()), false);
        WriteObject(ElemEnableSearching, StrUtil.BoolToStringEx(pg.getEnableSearching()), false);
        WriteObject(ElemLastTopVisibleEntry, pg.getLastTopVisibleEntry());
    }

    private void EndGroup() throws IOException {
        m_xmlWriter.endTag(null, ElemGroup); // Close group element
    }

    private void WriteEntry(PwEntry pe, boolean bIsHistory) throws IOException {
        assert pe != null; if(pe == null) throw new IllegalArgumentException("pe");

        m_xmlWriter.startTag(null, ElemEntry);

        WriteObject(ElemUuid, pe.getUuid());
        WriteObject(ElemIcon, pe.getIconId().ordinal());

        if(!pe.getCustomIconUuid().Equals(PwUuid.Zero))
            WriteObject(ElemCustomIconID, pe.getCustomIconUuid());

        WriteObject(ElemFgColor, StrUtil.ColorToUnnamedHtml(pe.getForegroundColor(), true), false);
        WriteObject(ElemBgColor, StrUtil.ColorToUnnamedHtml(pe.getBackgroundColor(), true), false);
        WriteObject(ElemOverrideUrl, pe.getOverrideUrl(), true);
        WriteObject(ElemTags, StrUtil.TagsToString(pe.getTags(), false), true);

        WriteList(ElemTimes, pe);

        WriteList(pe.getStrings(), true);
        WriteList(pe.getBinaries());
        WriteList(ElemAutoType, pe.getAutoType());

        if(!bIsHistory) WriteList(ElemHistory, pe.getHistory(), true);
        else { assert pe.getHistory().getUCount() == 0; }

        m_xmlWriter.endTag(null, ElemEntry);
    }

    private void WriteList(ProtectedStringDictionary dictStrings, boolean bEntryStrings) throws IOException {
        assert dictStrings != null;
        if(dictStrings == null) throw new IllegalArgumentException("dictStrings");

        for(Map.Entry<String, ProtectedString> kvp : dictStrings)
        WriteObject(kvp.getKey(), kvp.getValue(), bEntryStrings);
    }

    private void WriteList(ProtectedBinaryDictionary dictBinaries) throws IOException {
        assert dictBinaries != null;
        if(dictBinaries == null) throw new IllegalArgumentException("dictBinaries");

        for(Map.Entry<String, ProtectedBinary> kvp : dictBinaries)
        WriteObject(kvp.getKey(), kvp.getValue(), true);
    }

    private void WriteList(String name, AutoTypeConfig cfgAutoType) throws IOException {
        assert name != null;
        assert cfgAutoType != null;
        if(cfgAutoType == null) throw new IllegalArgumentException("cfgAutoType");

        m_xmlWriter.startTag(null, name);

        WriteObject(ElemAutoTypeEnabled, cfgAutoType.isEnabled());
        WriteObject(ElemAutoTypeObfuscation, cfgAutoType.getObfuscationOptions().ordinal());

        if(cfgAutoType.getDefaultSequence().length() > 0)
            WriteObject(ElemAutoTypeDefaultSeq, cfgAutoType.getDefaultSequence(), true);

        for(AutoTypeAssociation a : cfgAutoType.getAssociations())
        WriteObject(ElemAutoTypeItem, ElemWindow, ElemKeystrokeSequence,
                Maps.immutableEntry(a.getWindowName(), a.getSequence()));

        m_xmlWriter.endTag(null, name);
    }

    private void WriteList(String name, ITimeLogger times) throws IOException {
        assert name != null;
        assert times != null; if(times == null) throw new IllegalArgumentException("times");

        m_xmlWriter.startTag(null, name);

        WriteObject(ElemCreationTime, times.getCreationTime());
        WriteObject(ElemLastModTime, times.getLastModificationTime());
        WriteObject(ElemLastAccessTime, times.getLastAccessTime());
        WriteObject(ElemExpiryTime, times.getExpiryTime());
        WriteObject(ElemExpires, times.getExpires());
        WriteObject(ElemUsageCount, times.getUsageCount());
        WriteObject(ElemLocationChanged, times.getLocationChanged());

        m_xmlWriter.endTag(null, name); // Name
    }

    private void WriteList(String name, PwObjectList<PwEntry> value, boolean bIsHistory) throws IOException {
        assert name != null;
        assert value != null; if(value == null) throw new IllegalArgumentException("value");

        m_xmlWriter.startTag(null, name);

        for(PwEntry pe : value)
        WriteEntry(pe, bIsHistory);

        m_xmlWriter.endTag(null, name);
    }

    private void WriteList(String name, PwObjectList<PwDeletedObject> value) throws IOException {
        assert name != null;
        assert value != null; if(value == null) throw new IllegalArgumentException("value");

        m_xmlWriter.startTag(null, name);

        for(PwDeletedObject pdo : value)
        WriteObject(ElemDeletedObject, pdo);

        m_xmlWriter.endTag(null, name);
    }

    private void WriteList(String name, MemoryProtectionConfig value) throws IOException {
        assert name != null;
        assert value != null;

        m_xmlWriter.startTag(null, name);

        WriteObject(ElemProtTitle, value.ProtectTitle);
        WriteObject(ElemProtUserName, value.ProtectUserName);
        WriteObject(ElemProtPassword, value.ProtectPassword);
        WriteObject(ElemProtUrl, value.ProtectUrl);
        WriteObject(ElemProtNotes, value.ProtectNotes);
        // WriteObject(ElemProtAutoHide, value.AutoEnableVisualHiding);

        m_xmlWriter.endTag(null, name);
    }

    private void WriteList(String name, StringDictionaryEx value) throws IOException {
        assert name != null;
        assert value != null; if(value == null) throw new IllegalArgumentException("value");

        m_xmlWriter.startTag(null, name);

        for(Map.Entry<String, String> kvp : value)
        WriteObject(ElemStringDictExItem, ElemKey, ElemValue, kvp);

        m_xmlWriter.endTag(null, name);
    }

    private void WriteCustomIconList() throws IOException {
        if(m_pwDatabase.getCustomIcons().size() == 0) return;

        m_xmlWriter.startTag(null, ElemCustomIcons);

        for(PwCustomIcon pwci : m_pwDatabase.getCustomIcons())
        {
            m_xmlWriter.startTag(null, ElemCustomIconItem);

            WriteObject(ElemCustomIconItemID, pwci.getUuid());

            String strData = BaseEncoding.base64().encode(pwci.getImageDataPng());
            WriteObject(ElemCustomIconItemData, strData, false);

            m_xmlWriter.endTag(null, ElemCustomIconItem);
        }

        m_xmlWriter.endTag(null, ElemCustomIcons);
    }

    private void WriteObject(String name, String value,
                             boolean bFilterValueXmlChars) throws IOException {
        assert name != null;
        assert value != null;

        m_xmlWriter.startTag(null, name);

        if(bFilterValueXmlChars)
            m_xmlWriter.text(StrUtil.SafeXmlString(value));
        else m_xmlWriter.text(value);

        m_xmlWriter.endTag(null, name);
    }

    private void WriteObject(String name, boolean value) throws IOException {
        assert name != null;

        WriteObject(name, value ? ValTrue : ValFalse, false);
    }

    private void WriteObject(String name, PwUuid value) throws IOException {
        assert name != null;
        assert value != null; if(value == null) throw new IllegalArgumentException("value");

        WriteObject(name, BaseEncoding.base64().encode(value.getUuidBytes()), false);
    }

    private void WriteObject(String name, int value) throws IOException {
        assert name != null;

        m_xmlWriter.startTag(null, name);
        m_xmlWriter.text(String.valueOf(value));
        m_xmlWriter.endTag(null, name);
    }

    private void WriteObject(String name, long value) throws IOException {
        assert name != null;

        m_xmlWriter.startTag(null, name);
        m_xmlWriter.text(String.valueOf(value));
        m_xmlWriter.endTag(null, name);
    }

    private void WriteObject(String name, Date value) throws IOException {
        assert name != null;

        WriteObject(name, TimeUtil.SerializeUtc(value), false);
    }

    private void WriteObject(String name, String strKeyName,
                             String strValueName, Map.Entry<String, String> kvp) throws IOException {
        m_xmlWriter.startTag(null, name);

        m_xmlWriter.startTag(null, strKeyName);
        m_xmlWriter.text(StrUtil.SafeXmlString(kvp.getKey()));
        m_xmlWriter.endTag(null, strKeyName);
        m_xmlWriter.startTag(null, strValueName);
        m_xmlWriter.text(StrUtil.SafeXmlString(kvp.getValue()));
        m_xmlWriter.endTag(null, strValueName);

        m_xmlWriter.endTag(null, name);
    }

    private void WriteObject(String name, ProtectedString value, boolean bIsEntryString) throws IOException {
        assert name != null;
        assert value != null; if(value == null) throw new IllegalArgumentException("value");

        m_xmlWriter.startTag(null, ElemString);
        m_xmlWriter.startTag(null, ElemKey);
        m_xmlWriter.text(StrUtil.SafeXmlString(name));
        m_xmlWriter.endTag(null, ElemKey);
        m_xmlWriter.startTag(null, ElemValue);

        boolean bProtected = value.isProtected();
        if(bIsEntryString)
        {
            // Adjust memory protection setting (which might be different
            // from the database default, e.g. due to an import which
            // didn't specify the correct setting)
            if(PwDefs.TitleField.equals(name))
                bProtected = m_pwDatabase.getMemoryProtection().ProtectTitle;
            else if(PwDefs.UserNameField.equals(name))
                bProtected = m_pwDatabase.getMemoryProtection().ProtectUserName;
            else if(PwDefs.PasswordField.equals(name))
                bProtected = m_pwDatabase.getMemoryProtection().ProtectPassword;
            else if(PwDefs.UrlField.equals(name))
                bProtected = m_pwDatabase.getMemoryProtection().ProtectUrl;
            else if(PwDefs.NotesField.equals(name))
                bProtected = m_pwDatabase.getMemoryProtection().ProtectNotes;
        }

        if(bProtected && (m_format != KdbxFormat.PlainXml))
        {
            m_xmlWriter.attribute(null, AttrProtected, ValTrue);

            byte[] pbEncoded = value.ReadXorredString(m_randomStream);
            if(pbEncoded.length > 0)
                m_xmlWriter.text(BaseEncoding.base64().encode(pbEncoded, 0, pbEncoded.length));
        }
        else
        {
            String strValue = value.ReadString();

            // If names should be localized, we need to apply the language-dependent
            // String transformation here. By default, language-dependent conversions
            // should be applied, otherwise characters could be rendered incorrectly
            // (code page problems).
            if(m_bLocalizedNames)
            {
                StringBuilder sb = new StringBuilder();
                for(char ch : strValue.toCharArray())
                {
                    char chMapped = ch;

                    // Symbols and surrogates must be moved into the correct code
                    // page area
                    /* TODO FIXME is this necessary in java???
                    if(Character.getType(ch) == Character.OTHER_SYMBOL || Character.isSurrogate(ch))
                    {
                        System.Globalization.UnicodeCategory cat =
                                CharUnicodeInfo.GetUnicodeCategory(ch);
                        // Map character to correct position in code page
                        chMapped = (char)((int)cat * 32 + ch);
                    }
                    else if(Character.isISOControl(ch))
                    {
                        if(ch >= 256) // Control character in high ANSI code page
                        {
                            // Some of the control characters map to corresponding ones
                            // in the low ANSI range (up to 255) when calling
                            // ToLower on them with invariant culture (see
                            // http://lists.ximian.com/pipermail/mono-patches/2002-February/086106.html )
                            chMapped = Character.toLowerCase(ch);
                        }
                    }
                    */

                    sb.append(chMapped);
                }

                strValue = sb.toString(); // Correct String for current code page
            }

            if((m_format == KdbxFormat.PlainXml) && bProtected)
                m_xmlWriter.attribute(null, AttrProtectedInMemPlainXml, ValTrue);

            m_xmlWriter.text(StrUtil.SafeXmlString(strValue));
        }

        m_xmlWriter.endTag(null, ElemValue); // ElemValue
        m_xmlWriter.endTag(null, ElemString); // ElemString
    }

    private void WriteObject(String name, ProtectedBinary value, boolean bAllowRef) throws IOException {
        assert name != null;
        assert value != null; if(value == null) throw new IllegalArgumentException("value");

        m_xmlWriter.startTag(null, ElemBinary);
        m_xmlWriter.startTag(null, ElemKey);
        m_xmlWriter.text(StrUtil.SafeXmlString(name));
        m_xmlWriter.endTag(null, ElemKey);
        m_xmlWriter.startTag(null, ElemValue);

        String strRef = (bAllowRef ? BinPoolFind(value) : null);
        if(strRef != null)
        {
            m_xmlWriter.attribute(null, AttrRef, strRef);
        }
        else SubWriteValue(value);

        m_xmlWriter.endTag(null, ElemValue); // ElemValue
        m_xmlWriter.endTag(null, ElemBinary); // ElemBinary
    }

    private void SubWriteValue(ProtectedBinary value) throws IOException {
        if(value.isProtected() && (m_format != KdbxFormat.PlainXml))
        {
            m_xmlWriter.attribute(null, AttrProtected, ValTrue);

            byte[] pbEncoded = value.ReadXorredData(m_randomStream);
            if(pbEncoded.length > 0)
                m_xmlWriter.text(BaseEncoding.base64().encode(pbEncoded, 0, pbEncoded.length));
        }
        else
        {
            if(m_pwDatabase.getCompression() == PwCompressionAlgorithm.GZip)
            {
                m_xmlWriter.attribute(null, AttrCompressed, ValTrue);

                byte[] pbRaw = value.ReadData();
                byte[] pbCmp = MemUtil.Compress(pbRaw);
                m_xmlWriter.text(BaseEncoding.base64().encode(pbCmp, 0, pbCmp.length));
            }
            else
            {
                byte[] pbRaw = value.ReadData();
                m_xmlWriter.text(BaseEncoding.base64().encode(pbRaw, 0, pbRaw.length));
            }
        }
    }

    private void WriteObject(String name, PwDeletedObject value) throws IOException {
        assert name != null;
        assert value != null; if(value == null) throw new IllegalArgumentException("value");

        m_xmlWriter.startTag(null, name);
        WriteObject(ElemUuid, value.getUuid());
        WriteObject(ElemDeletionTime, value.getDeletionTime());
        m_xmlWriter.endTag(null, name);
    }

    private void WriteBinPool() throws IOException {
        m_xmlWriter.startTag(null, ElemBinaries);

        for(Map.Entry<String, ProtectedBinary> kvp : m_dictBinPool.entrySet())
        {
            m_xmlWriter.startTag(null, ElemBinary);
            m_xmlWriter.attribute(null, AttrId, kvp.getKey());
            SubWriteValue(kvp.getValue());
            m_xmlWriter.endTag(null, ElemBinary);
        }

        m_xmlWriter.endTag(null, ElemBinaries);
    }

    @Deprecated
    public static boolean WriteEntries(OutputStream msOutput, PwDatabase pwDatabase,
                                    PwEntry[] vEntries) throws IOException {
        return WriteEntries(msOutput, vEntries);
    }

    /// <summary>
    /// Write entries to a stream.
    /// </summary>
    /// <param name="msOutput">Output stream to which the entries will be written.</param>
    /// <param name="vEntries">Entries to serialize.</param>
    /// <returns>Returns <c>true</c>, if the entries were written successfully
    /// to the stream.</returns>
    public static boolean WriteEntries(OutputStream msOutput, PwEntry[] vEntries) throws IOException {
			/* KdbxFile f = new KdbxFile(pwDatabase);
			f.m_format = KdbxFormat.PlainXml;

			XmlTextWriter xtw = null;
			try { xtw = new XmlTextWriter(msOutput, StrUtil.Utf8); }
			catch(Exception) { assert false; return false; }
			if(xtw == null) { assert false; return false; }

			f.m_xmlWriter = xtw;

			xtw.Formatting = Formatting.Indented;
			xtw.IndentChar = '\t';
			xtw.Indentation = 1;

			xtw.WriteStartDocument(true);
			xtw.WriteStartElement(ElemRoot);

			for(PwEntry pe : vEntries)
				f.WriteEntry(pe, false);

			xtw.WriteEndElement();
			xtw.WriteEndDocument();

			xtw.Flush();
			xtw.Close();
			return true; */

        PwDatabase pd = new PwDatabase();
        pd.New(new IOConnectionInfo(), new CompositeKey());

        for(PwEntry peCopy : vEntries)
        pd.getRootGroup().AddEntry(peCopy.CloneDeep(), true);

        KdbxFile f = new KdbxFile(pd);
        f.Save(msOutput, null, KdbxFormat.PlainXml, null);
        return true;
    }
}
