<?php
/*
 * Copyright 2008-2010 GuardTime AS
 *
 * This file is part of the GuardTime PHP SDK.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @package tsp
 */

/**
 * Data hash object used as seed when creating and verifying timestamps.
 *
 * To calculate the hash sum of some data, first create a data hash object and then
 * add data to it by using the update methods. Do this until all the data has been
 * fed to the hash calculator. You can then retrieve the resulting hash value using
 * getHashedMessage() method but this closes the hash object and you can't add more
 * data after that.
 *
 * Example 1 - new hash:
 *
 * <code>
 * $stream = fopen('http://www.guardtime.com/data.txt', 'r');
 *
 * $hash = new GTDataHash(GTHashAlgorithm::getByName('DEFAULT'));
 * $hash->update(array(0, 1, 2));
 * $hash->updateFile('data.txt');
 * $hahs->updateStream($stream);
 * $hash->close();
 *
 * print_r($hash->getHashedMessage());
 * </code>
 *
 * Example 2 - hash with existing hash value:
 *
 * <code>
 * $bytes = array(
 *  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
 *  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1
 * );
 *
 * $hash = new GTDataHash(GTHashAlgorithm::getByName('SHA256'), $bytes);
 * </code>
 *
 * Example 3 - hash from dataImprint:
 *
 * <code>
 * $dataImprint = array(
 *  GTHashAlgorithm::getByName('SHA256')->getGtid(),
 *  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
 *  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1
 * );
 *
 * $hash = GTDataHash::getInstance($dataImprint);
 * </code>
 *
 * @package tsp
 */
class GTDataHash {

    private $hashContext;
    private $hashAlgorithm;
    private $hashedMessage;

    /**
     * Builds a new hash object with the given hashAlgorithm and hashedMessage.
     *
     * When hashedMessage is null the GTDataHash will be open and it will be
     * possible to add more data using update methods.
     *
     * When hashedMessage is specified the GTDataHash will be closed and it's
     * not possible to add more data using update methods.
     *
     *
     * @throws GTException
     * @param  GTHashAlgorithm $hashAlgorithm hash algorithm to use
     * @param  array $hashedMessage hash bytes
     */
    public function __construct($hashAlgorithm, $hashedMessage = null) {

        if (empty($hashAlgorithm)) {
            throw new GTException("parameter hashAlgorithm must not be empty");
        }

        if (!($hashAlgorithm instanceof GTHashAlgorithm)) {
            throw new GTException("parameter hashAlgorithm must be an instance of GTHashAlgorithm");
        }

        $this->hashAlgorithm = $hashAlgorithm;

        if ($hashedMessage == null) {

            $this->hashContext = hash_init($this->hashAlgorithm->getName());

            if (!is_resource($this->hashContext)) {
                throw new GTException("Unable to init GTDataHash with algorithm: {$this->hashAlgorithm->getName()}");
            }

        } else {

            if (!is_array($hashedMessage)) {
                throw new GTException("hash must be an array of bytes");
            }

            if (count($hashedMessage) != $this->hashAlgorithm->getLength()) {
                throw new GTException("hash length does not match with that defined in hash algorithm");
            }

            $this->hashedMessage = $hashedMessage;

        }

    }

    /**
     * Updates this hash calculator with the given bytes.
     *
     * @throws GTException
     * @param  $bytes byte array to feed to this hash calculator
     * @return void
     */
    public function update($bytes) {

        if ($this->isClosed()) {
            throw new GTException("hash calculator already closed");
        }

        if (empty($bytes)) {
            throw new GTException("parameter bytes must not be empty");
        }

        $result = hash_update($this->hashContext, GTUtil::fromByteArray($bytes));

        if (!$result) {
            throw new GTException("hash update failed with unknown error");
        }
    }

    /**
     * Updates this hash calculator with the bytes from the given file.
     *
     * @throws GTException
     * @param  string $file file to feed to this hash calculator
     * @return void
     */
    public function updateFile($file) {

        if ($this->isClosed()) {
            throw new GTException("hash calculator already closed");
        }

        if (empty($file)) {
            throw new GTException("parameter file must not be empty");
        }

        if (!is_file($file)) {
            throw new GTException("file must be a file");
        }

        if (!is_readable($file)) {
            throw new GTException("file must be readable");
        }

        if (filesize($file) == 0) {
            throw new GTException("file must not be 0 bytes");
        }

        $result = hash_update_file($this->hashContext, $file);

        if (!$result) {
            throw new GTException("hash updateFile failed with unknown error");
        }

    }

    /**
     * Updates this hash calculator with the bytes from the given stream.
     *
     * @throws GTException
     * @param  $handle handle of the stream to feed to this hash calculator
     * @return void
     */
    public function updateStream($handle) {

        if ($this->isClosed()) {
            throw new GTException("hash calculator already closed");
        }

        if (empty($handle)) {
            throw new GTException("parameter handle must not be empty");
        }

        if (!is_resource($handle)) {
            throw new GTException("parameter handle must be a resource");
        }

        $result = hash_update_stream($this->hashContext, $handle);

        if (!$result) {
            throw new GTException("hash updateStream failed with unknown error");
        }

    }

    /**
     * Explicitly closes this hash calculator.
     *
     * After the hash calculator is closed all calls to update methods will throw an exception.
     *
     * @return GTDataHash
     */
    public function close() {

        if (!$this->isClosed()) {

            $this->hashedMessage = GTUtil::toByteArray(hash_final($this->hashContext, true));
            $this->hashContext = null;

        }

        return $this;
    }

    /**
     * Checks if this hash calculator is closed.
     *
     * @return bool true if this hash calculator is closed.
     */
    public function isClosed() {
        return $this->hashContext == null;
    }

    /**
     * Gets the GTHashAlgorithm used by this hash calculator.
     *
     * @return GTHashAlgorithm the currently used algorithm
     */
    public function getHashAlgorithm() {
        return $this->hashAlgorithm;
    }

    /**
     * Gets the current hash bytes.
     *
     * This method will also close the hash calculator when called. After closing
     * all update() methods will throw an exception.
     *
     * @return array
     */
    public function getHashedMessage() {

        if ($this->isClosed()) {
            $this->close();
        }

        return $this->hashedMessage;
    }

    /**
     * Gets the current hash data imprint.
     *
     * This method will also close the hash calculator when called. After closing
     * all update() methods will throw an exception.
     *
     * Data imprint is an array of bytes consisting of GTHashAlgorithm->getGtid() + hash bytes
     *
     * @return array the data imprint
     */
    public function getDataImprint() {

        if (!$this->isClosed()) {
            $this->close();
        }

        $result = array($this->hashAlgorithm->getGtid());

        foreach ($this->hashedMessage as $byte) {
            array_push($result, $byte);
        }

        return $result;
    }

    /**
     * Gets a GTDataHash instance from a $dataImprint.
     *
     * DataImprint is an array of bytes consisting of GTHashAlgorithm->getGtid() and hash bytes.
     *
     * <code>
     * $dataImprint = array(
     *   GTHashAlgorithm::getByName('SHA256')->getGtid(),
     *   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
     *   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1
     * );
     * </code>
     *
     * @static
     * @throws GTException
     * @param  array $dataImprint dataImprint bytes
     * @return GTDataHash
     */
    public static function getInstance($dataImprint) {

        if (empty($dataImprint)) {
            throw new GTException("parameter dataImprint must not be empty");
        }

        if (!is_array($dataImprint)) {
            throw new GTException("parameter dataImprint must be an array of bytes");
        }

        if (count($dataImprint) < 2) {
            throw new GTException("parameter dataImprint must be at least 2 bytes long");
        }

        $hashAlgorithm = GTHashAlgorithm::getByGtid($dataImprint[0]);
        $hashedMessage = array_slice($dataImprint, 1);

        return new GTDataHash($hashAlgorithm, $hashedMessage);
    }

}

?>
