<?php

namespace WP_Defender\Extra;
/**
 * Binary-to-text PHP Utilities
 *
 * @package     binary-to-text-php
 * @link        https://github.com/ademarre/binary-to-text-php
 * @author      Andre DeMarre
 * @copyright   2009-2013 Andre DeMarre
 * @license     http://opensource.org/licenses/MIT  MIT
 */

/**
 * Class for binary-to-text encoding with a base of 2^n
 *
 * The Base2n class is for binary-to-text conversion. It employs a
 * generalization of the algorithms used by many encoding schemes that
 * use a fixed number of bits to encode each character. In other words,
 * the base is a power of 2.
 *
 * Earlier versions of this class were named
 * FixedBitNotation and FixedBitEncoding.
 *
 * @package binary-to-text-php
 */
class Base2n {
	protected $_chars;
	protected $_bitsPerCharacter;
	protected $_radix;
	protected $_rightPadFinalBits;
	protected $_padFinalGroup;
	protected $_padCharacter;
	protected $_caseSensitive;
	protected $_charmap;

	/**
	 * Constructor
	 *
	 * @param integer $bitsPerCharacter Bits to use for each encoded character
	 * @param string $chars Base character alphabet
	 * @param boolean $caseSensitive To decode in a case-sensitive manner
	 * @param boolean $rightPadFinalBits How to encode last character
	 * @param boolean $padFinalGroup Add padding to end of encoded output
	 * @param string $padCharacter Character to use for padding
	 *
	 * @throws  InvalidArgumentException    for incompatible parameters
	 */
	public function __construct(
		$bitsPerCharacter,
		$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_',
		$caseSensitive = true, $rightPadFinalBits = false,
		$padFinalGroup = false, $padCharacter = '='
	) {
		// Ensure validity of $chars
		if ( ! is_string( $chars ) || ( $charLength = strlen( $chars ) ) < 2 ) {
			throw new InvalidArgumentException( '$chars must be a string of at least two characters' );
		}

		// Ensure validity of $padCharacter
		if ( $padFinalGroup ) {
			if ( ! is_string( $padCharacter ) || ! isset( $padCharacter[0] ) ) {
				throw new InvalidArgumentException( '$padCharacter must be a string of one character' );
			}

			if ( $caseSensitive ) {
				$padCharFound = strpos( $chars, $padCharacter[0] );
			} else {
				$padCharFound = stripos( $chars, $padCharacter[0] );
			}

			if ( $padCharFound !== false ) {
				throw new InvalidArgumentException( '$padCharacter can not be a member of $chars' );
			}
		}

		// Ensure validity of $bitsPerCharacter
		if ( ! is_int( $bitsPerCharacter ) ) {
			throw new InvalidArgumentException( '$bitsPerCharacter must be an integer' );
		}

		if ( $bitsPerCharacter < 1 ) {
			// $bitsPerCharacter must be at least 1
			throw new InvalidArgumentException( '$bitsPerCharacter can not be less than 1' );

		} elseif ( $charLength < 1 << $bitsPerCharacter ) {
			// Character length of $chars is too small for $bitsPerCharacter
			// Find greatest acceptable value of $bitsPerCharacter
			$bitsPerCharacter = 1;
			$radix            = 2;

			while ( $charLength >= ( $radix <<= 1 ) && $bitsPerCharacter < 8 ) {
				$bitsPerCharacter ++;
			}

			$radix >>= 1;
			throw new InvalidArgumentException(
				'$bitsPerCharacter can not be more than ' . $bitsPerCharacter
				. ' given $chars length of ' . $charLength
				. ' (max radix ' . $radix . ')' );

		} elseif ( $bitsPerCharacter > 8 ) {
			// $bitsPerCharacter must not be greater than 8
			throw new InvalidArgumentException( '$bitsPerCharacter can not be greater than 8' );

		} else {
			$radix = 1 << $bitsPerCharacter;
		}

		$this->_chars             = $chars;
		$this->_bitsPerCharacter  = $bitsPerCharacter;
		$this->_radix             = $radix;
		$this->_rightPadFinalBits = $rightPadFinalBits;
		$this->_padFinalGroup     = $padFinalGroup;
		$this->_padCharacter      = $padCharacter[0];
		$this->_caseSensitive     = $caseSensitive;
	}

	/**
	 * Encode a string
	 *
	 * @param string $rawString Binary data to encode
	 *
	 * @return  string
	 */
	public function encode( $rawString ) {
		// Unpack string into an array of bytes
		$bytes     = unpack( 'C*', $rawString );
		$byteCount = count( $bytes );

		$encodedString = '';
		$byte          = array_shift( $bytes );
		$bitsRead      = 0;
		$oldBits       = 0;

		$chars             = $this->_chars;
		$bitsPerCharacter  = $this->_bitsPerCharacter;
		$rightPadFinalBits = $this->_rightPadFinalBits;
		$padFinalGroup     = $this->_padFinalGroup;
		$padCharacter      = $this->_padCharacter;

		$charsPerByte  = 8 / $bitsPerCharacter;
		$encodedLength = $byteCount * $charsPerByte;

		// Generate encoded output; each loop produces one encoded character
		for ( $c = 0; $c < $encodedLength; $c ++ ) {

			// Get the bits needed for this encoded character
			if ( $bitsRead + $bitsPerCharacter > 8 ) {
				// Not enough bits remain in this byte for the current character
				// Save the remaining bits before getting the next byte
				$oldBitCount = 8 - $bitsRead;
				$oldBits     = $byte ^ ( $byte >> $oldBitCount << $oldBitCount );
				$newBitCount = $bitsPerCharacter - $oldBitCount;

				if ( ! $bytes ) {
					// Last bits; match final character and exit loop
					if ( $rightPadFinalBits ) {
						$oldBits <<= $newBitCount;
					}
					$encodedString .= $chars[ $oldBits ];

					if ( $padFinalGroup ) {
						// Array of the lowest common multiples of $bitsPerCharacter and 8, divided by 8
						$lcmMap        = array( 1 => 1, 2 => 1, 3 => 3, 4 => 1, 5 => 5, 6 => 3, 7 => 7, 8 => 1 );
						$bytesPerGroup = $lcmMap[ $bitsPerCharacter ];
						$pads          = $bytesPerGroup * $charsPerByte - ceil( ( strlen( $rawString ) % $bytesPerGroup ) * $charsPerByte );
						$encodedString .= str_repeat( $padCharacter, $pads );
					}

					break;
				}

				// Get next byte
				$byte     = array_shift( $bytes );
				$bitsRead = 0;

			} else {
				$oldBitCount = 0;
				$newBitCount = $bitsPerCharacter;
			}

			// Read only the needed bits from this byte
			$bits     = $byte >> 8 - ( $bitsRead + ( $newBitCount ) );
			$bits     ^= $bits >> $newBitCount << $newBitCount;
			$bitsRead += $newBitCount;

			if ( $oldBitCount ) {
				// Bits come from seperate bytes, add $oldBits to $bits
				$bits = ( $oldBits << $newBitCount ) | $bits;
			}

			$encodedString .= $chars[ $bits ];
		}

		return $encodedString;
	}

	/**
	 * Decode a string
	 *
	 * @param string $encodedString Data to decode
	 * @param boolean $strict Returns NULL if $encodedString contains an undecodable character
	 *
	 * @return  string
	 */
	public function decode( $encodedString, $strict = false ) {
		if ( ! $encodedString || ! is_string( $encodedString ) ) {
			// Empty string, nothing to decode
			return '';
		}

		$chars             = $this->_chars;
		$bitsPerCharacter  = $this->_bitsPerCharacter;
		$radix             = $this->_radix;
		$rightPadFinalBits = $this->_rightPadFinalBits;
		$padFinalGroup     = $this->_padFinalGroup;
		$padCharacter      = $this->_padCharacter;
		$caseSensitive     = $this->_caseSensitive;

		// Get index of encoded characters
		if ( $this->_charmap ) {
			$charmap = $this->_charmap;

		} else {
			$charmap = array();

			for ( $i = 0; $i < $radix; $i ++ ) {
				$charmap[ $chars[ $i ] ] = $i;
			}

			$this->_charmap = $charmap;
		}

		// The last encoded character is $encodedString[$lastNotatedIndex]
		$lastNotatedIndex = strlen( $encodedString ) - 1;

		// Remove trailing padding characters
		if ( $padFinalGroup ) {
			while ( $encodedString[ $lastNotatedIndex ] === $padCharacter ) {
				$encodedString = substr( $encodedString, 0, $lastNotatedIndex );
				$lastNotatedIndex --;
			}
		}

		$rawString   = '';
		$byte        = 0;
		$bitsWritten = 0;

		// Convert each encoded character to a series of unencoded bits
		for ( $c = 0; $c <= $lastNotatedIndex; $c ++ ) {

			if ( ! $caseSensitive && ! isset( $charmap[ $encodedString[ $c ] ] ) ) {
				// Encoded character was not found; try other case
				if ( isset( $charmap[ $cUpper = strtoupper( $encodedString[ $c ] ) ] ) ) {
					$charmap[ $encodedString[ $c ] ] = $charmap[ $cUpper ];

				} elseif ( isset( $charmap[ $cLower = strtolower( $encodedString[ $c ] ) ] ) ) {
					$charmap[ $encodedString[ $c ] ] = $charmap[ $cLower ];
				}
			}

			if ( isset( $charmap[ $encodedString[ $c ] ] ) ) {
				$bitsNeeded     = 8 - $bitsWritten;
				$unusedBitCount = $bitsPerCharacter - $bitsNeeded;

				// Get the new bits ready
				if ( $bitsNeeded > $bitsPerCharacter ) {
					// New bits aren't enough to complete a byte; shift them left into position
					$newBits     = $charmap[ $encodedString[ $c ] ] << $bitsNeeded - $bitsPerCharacter;
					$bitsWritten += $bitsPerCharacter;

				} elseif ( $c !== $lastNotatedIndex || $rightPadFinalBits ) {
					// Zero or more too many bits to complete a byte; shift right
					$newBits     = $charmap[ $encodedString[ $c ] ] >> $unusedBitCount;
					$bitsWritten = 8; //$bitsWritten += $bitsNeeded;

				} else {
					// Final bits don't need to be shifted
					$newBits     = $charmap[ $encodedString[ $c ] ];
					$bitsWritten = 8;
				}

				$byte |= $newBits;

				if ( $bitsWritten === 8 || $c === $lastNotatedIndex ) {
					// Byte is ready to be written
					$rawString .= pack( 'C', $byte );

					if ( $c !== $lastNotatedIndex ) {
						// Start the next byte
						$bitsWritten = $unusedBitCount;
						$byte        = ( $charmap[ $encodedString[ $c ] ] ^ ( $newBits << $unusedBitCount ) ) << 8 - $bitsWritten;
					}
				}

			} elseif ( $strict ) {
				// Unable to decode character; abort
				return null;
			}
		}

		return $rawString;
	}
}