// md5.h -- hash algorithm data product described in IETF RFC 1321

// Copyright (C) 2010 Kenneth Laskoski

/** @file md5.h
    @brief hash algorithm data product described in IETF RFC 1321
    @author Copyright (C) 2010 Kenneth Laskoski
*/

#ifndef KL_MD5_H
#define KL_MD5_H 

#include <array>
#include <istream>
#include <ostream>
#include <sstream>

#include <stdexcept>

#include "iostate.h"

namespace kashmir {
namespace md5 {

/** @class md5_t
    @brief This class implements the data product of the hash algorithm described in
    
    - IETF RFC 1321 - available at http://tools.ietf.org/html/rfc1321

    This documents the code below.
*/

// an MD5 is a string of 16 octets (128 bits)
// we use an unpacked representation, value_type may be larger than 8 bits,
// in which case every input operation must assert data[i] < 256 for i < 16
// note that even char may be more than 8 bits in some particular platform

typedef unsigned char value_type;
typedef std::size_t size_type;

const size_type size = 16, string_size = 32;

class md5_t
{
    typedef std::array<value_type, size> data_type;
    data_type data;

public:
    // we keep data uninitialized to stress concreteness
    md5_t() {}
    ~md5_t() {}

    // trivial copy and assignment
    md5_t(const md5_t& rhs) : data(rhs.data) {}
    md5_t& operator=(const md5_t& rhs)
    {
        data = rhs.data;
        return *this;
    }

    // OK, now we bow to convenience
    // initialization from C string
    explicit md5_t(const char* string)
    {
        std::stringstream stream(string);
        get(stream);
    }

    // test for nil value
    bool is_nil() const
    {
        for (size_type i = 0; i < size; ++i)
            if (data[i])
                return false;
        return true;
    }

    // safe bool idiom
    typedef data_type md5_t::*bool_type; 
    operator bool_type() const
    {
        return is_nil() ? 0 : &md5_t::data;
    }

    // only equality makes sense here
    bool operator==(const md5_t& rhs) const
    {
        return data == rhs.data;
    }

    // stream operators
    template<class char_t, class char_traits>
    std::basic_ostream<char_t, char_traits>& put(std::basic_ostream<char_t, char_traits>& os) const;

    template<class char_t, class char_traits>
    std::basic_istream<char_t, char_traits>& get(std::basic_istream<char_t, char_traits>& is);
};

// only equality makes sense here
inline bool operator!=(const md5_t& lhs, const md5_t& rhs) { return !(lhs == rhs); }

template<class char_t, class char_traits>
std::basic_ostream<char_t, char_traits>& md5_t::put(std::basic_ostream<char_t, char_traits>& os) const
{
    if (!os.good())
        return os;

    const typename std::basic_ostream<char_t, char_traits>::sentry ok(os);
    if (ok)
    {
        ios_flags_saver flags(os);
        basic_ios_fill_saver<char_t, char_traits> fill(os);

        const std::streamsize width = os.width(0);
        const std::streamsize mysize = string_size;

        // right padding
        if (flags.value() & (std::ios_base::right | std::ios_base::internal))
            for (std::streamsize i = width; i > mysize; --i)
                os << fill.value();

        os << std::hex;
        os.fill(os.widen('0'));

        for (size_t i = 0; i < size; ++i)
        {
            os.width(2);
            os << static_cast<unsigned>(data[i]);
        }

        // left padding
        if (flags.value() & std::ios_base::left)
            for (std::streamsize i = width; i > mysize; --i)
                os << fill.value();
    }

    return os;
}

template<class char_t, class char_traits>
std::basic_istream<char_t, char_traits>& md5_t::get(std::basic_istream<char_t, char_traits>& is)
{
    if (!is.good())
        return is;

    const typename std::basic_istream<char_t, char_traits>::sentry ok(is);
    if (ok)
    {
        char_t hexdigits[16];
        char_t* const npos = hexdigits+16;

        typedef std::ctype<char_t> facet_t;
        const facet_t& facet = std::use_facet<facet_t>(is.getloc());

        const char* tmp = "0123456789abcdef";
        facet.widen(tmp, tmp+16, hexdigits);

        char_t c;
        char_t* f;
        for (size_t i = 0; i < size; ++i)
        {
            is >> c;
            c = facet.tolower(c);

            f = std::find(hexdigits, npos, c);
            if (f == npos)
            {
                is.setstate(std::ios_base::failbit);
                break;
            }

            data[i] = static_cast<value_type>(std::distance(hexdigits, f));

            is >> c;
            c = facet.tolower(c);

            f = std::find(hexdigits, npos, c);
            if (f == npos)
            {
                is.setstate(std::ios_base::failbit);
                break;
            }

            data[i] <<= 4;
            data[i] |= static_cast<value_type>(std::distance(hexdigits, f));
        }

        if (!is)
            throw std::runtime_error("failed to extract valid md5 from stream.");
    }

    return is;
}

template<class char_t, class char_traits>
inline std::basic_ostream<char_t, char_traits>& operator<<(std::basic_ostream<char_t, char_traits>& os, const md5_t& md5)
{
    return md5.put(os);
}

template<class char_t, class char_traits>
inline std::basic_istream<char_t, char_traits>& operator>>(std::basic_istream<char_t, char_traits>& is, md5_t& md5)
{
    return md5.get(is);
}

} // namespace kashmir::md5

using md5::md5_t;

} // namespace kashmir

#endif
