/* Copyright (c) Citrix Systems, Inc. 
 * All rights reserved. 
 * 
 * Redistribution and use in source and binary forms, 
 * with or without modification, are permitted provided 
 * that the following conditions are met: 
 * 
 * *   Redistributions of source code must retain the above 
 *     copyright notice, this list of conditions and the 
 *     following disclaimer. 
 * *   Redistributions in binary form must reproduce the above 
 *     copyright notice, this list of conditions and the 
 *     following disclaimer in the documentation and/or other 
 *     materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
 * SUCH DAMAGE.
 */

using System;
using System.IO;
using System.Xml;
using System.Collections;
using System.Text;
using System.Security.Cryptography;

/* Thrown if we fail to verify a block (ie sha1) checksum */
public class BlockChecksumFailed : ApplicationException
{
    private string block;
    private string recomputed;
    private string original;

    public BlockChecksumFailed(string block, string recomputed, string original)
    {
        this.block = block;
        this.recomputed = recomputed;
        this.original = original;
    }

    public override string ToString()
    {
        return "Failed to verify the block checksum: block = " + block + "; recomputed = " + recomputed + "; original = " + original;
    }
}

public class Export
{
    public static bool verbose_debugging = false;
    public static void debug(string x)
    {
        if (verbose_debugging)
            Console.WriteLine(x);
    }

    private readonly SHA1 sha = new SHA1CryptoServiceProvider();

    private static char nibble(int x)
    {
        if (x < 10)
            return (char)((int)'0' + x);
        else
            return (char)((int)'a' - 10 + x);
    }

    private static string hex(byte x)
    {
        char low = nibble((int)x & 0xf);
        char high = nibble(((int)x >> 4) & 0xf);
        char[] chars = { high, low };
        return new String(chars);
    }

    private string checksum(byte[] data)
    {
        byte[] result = sha.ComputeHash(data);
        /* convert to a hex string for comparison */
        string x = "";
        for (int i = 0; i < result.Length; i++)
            x = x + hex(result[i]);
        return x;
    }

    private static Hashtable parse_checksum_table(string checksum_xml)
    {
        Hashtable table = new Hashtable();

        XmlDocument xmlDoc = new System.Xml.XmlDocument();
        xmlDoc.LoadXml(checksum_xml);
        XmlNodeList members = xmlDoc.GetElementsByTagName("member");
        string name;
        string value;
        foreach (XmlNode member in members)
        {
            name = ""; value = "";
            foreach (XmlNode child in member.ChildNodes)
            {
                XmlNode v = child.FirstChild;
                if (child.Name.Equals("name"))
                    name = v.Value;
                if (child.Name.Equals("value"))
                    value = v.Value;
            }
            debug(String.Format("adding {0} = {1}", name, value));
            table.Add(name, value);
        }
        return table;
    }

    private static void compare_tables(Hashtable recomputed, Hashtable original)
    {
        foreach (DictionaryEntry x in recomputed)
        {
            string ours = (string)x.Value;
            string theirs = (string)original[x.Key];
            if (!ours.Equals(theirs))
            {
                throw new BlockChecksumFailed((string)x.Key, ours, theirs);
            }
            else
            {
                debug(String.Format("{0} hash OK", (string)x.Key));
            }
        }
    }

    private static string string_of_byte_array(byte[] payload)
    {
        Decoder decoder = Encoding.UTF8.GetDecoder();
        char[] chars = new char[decoder.GetCharCount(payload, 0, (int)payload.Length)];
        decoder.GetChars(payload, 0, (int)payload.Length, chars, 0);
        return new string(chars);
    }

    public delegate void verifyCallback(uint read);
    public delegate bool cancellingCallback();

    /* 'input' is the source of the export data, if 'output' is not null then
       a perfect copy should be echoed there. */
    public void verify(Stream input, Stream output, cancellingCallback cancelling)
    {
        verify(input, output, cancelling, null);
    }

    public void verify(Stream input, Stream output, cancellingCallback cancelling, verifyCallback callback)
    {
        Hashtable recomputed_checksums = new Hashtable();
        Hashtable original_checksums = null;
        try
        {
            while (!cancelling())
            {
                Header x = null;
                byte[] one = IO.unmarshal_n(input, Header.length);
                if (callback != null) callback(Header.length);
                if (output != null) output.Write(one, 0, one.Length);

                if (Header.all_zeroes(one))
                {
                    byte[] two = IO.unmarshal_n(input, Header.length);
                    if (callback != null) callback(Header.length);
                    if (output != null) output.Write(two, 0, two.Length);
                    if (Header.all_zeroes(two))
                        throw new EndOfArchive();
                    x = new Header(two);
                }
                else
                {
                    x = new Header(one);
                }

                debug(x.ToString());
                byte[] payload = IO.unmarshal_n(input, x.file_size);
                if (callback != null) callback(x.file_size);
                if (output != null) output.Write(payload, 0, payload.Length);

                if (x.file_name.Equals("ova.xml"))
                {
                    debug("skipping ova.xml");
                }
                else if (x.file_name.EndsWith(".checksum"))
                {
                    string csum = string_of_byte_array(payload);
                    string base_name = x.file_name.Substring(0, x.file_name.Length - 9);
                    string ours = (string)recomputed_checksums[base_name];
                    if (!ours.Equals(csum))
                        throw new BlockChecksumFailed(base_name, ours, csum);
                    debug(String.Format("{0} hash OK", base_name));
                }
                else if (x.file_name.Equals("checksum.xml"))
                {
                    string xml = string_of_byte_array(payload);
                    original_checksums = parse_checksum_table(xml);
                }
                else
                {
                    string csum = checksum(payload);
                    debug(String.Format("   has checksum: {0}", csum));
                    recomputed_checksums.Add(x.file_name, csum);
                }
                byte[] padding = IO.unmarshal_n(input, x.paddingLength());
                if (callback != null) callback(x.paddingLength());
                if (output != null) output.Write(padding, 0, padding.Length);
            }

        }
        catch (EndOfArchive)
        {
            debug("EOF");
            if(original_checksums != null)
                compare_tables(recomputed_checksums, original_checksums);
        }
    }
}
