commit e92bfcf7f7727d2443b67c493e80218fc73f17ef Author: flashwave <me@flash.moe> Date: Wed Dec 23 01:44:45 2020 +0000 import diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd76df5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.debug diff --git a/lib/FWIF/FWIF.php b/lib/FWIF/FWIF.php new file mode 100644 index 0000000..bb97148 --- /dev/null +++ b/lib/FWIF/FWIF.php @@ -0,0 +1,162 @@ +<?php +namespace FWIF; + +class FWIF { + public const CONTENT_TYPE = 'text/plain; charset=us-ascii'; // TODO: come up with a mime type + + public const TYPE_NULL = 0; // NULL, no data + public const TYPE_INTEGER = 0x01; // LEB128, implicit length + public const TYPE_FLOAT = 0x02; // double precision IEEE 754, fixed length of 8 bytes + public const TYPE_STRING = 0x03; // UTF-8 string, terminated with TYPE_TRAILER + public const TYPE_ARRAY = 0x04; // List of values, terminated with TYPE_TRAILER + public const TYPE_OBJECT = 0x05; // List of values with ASCII names, terminated with TYPE_TRAILER + public const TYPE_BUFFER = 0x06; // Buffer with binary data, prefixed with a LEB128 length + public const TYPE_DATE = 0x07; // A gregorian year, month and day, fixed length of * bytes + public const TYPE_DATETIME = 0x08; // A gregorian year, month and day as well as an hour, minute and seconds component, fixed length of * bytes + public const TYPE_PERIOD = 0x09; // A time period, fixed length of * bytes + public const TYPE_TRAILER = 0xFF; // Termination byte + + private const CODECS = [ + self::TYPE_NULL => 'Null', + self::TYPE_INTEGER => 'Integer', + self::TYPE_FLOAT => 'Float', + self::TYPE_STRING => 'String', + self::TYPE_ARRAY => 'Array', + self::TYPE_OBJECT => 'Object', + ]; + + private static function isAssocArray($array): bool { + if(!is_array($array) || $array === []) + return false; + return array_keys($array) !== range(0, count($array) - 1); + } + + private static function detectType($data): int { + if(is_null($data)) + return self::TYPE_NULL; + if(is_int($data)) + return self::TYPE_INTEGER; + if(is_float($data)) + return self::TYPE_FLOAT; + if(is_string($data)) // Should this check if a string is valid UTF-8 and swap over to TYPE_BUFFER? + return self::TYPE_STRING; + if(is_object($data) || self::isAssocArray($data)) + return self::TYPE_OBJECT; + if(is_array($data)) + return self::TYPE_ARRAY; + throw new FWIFUnsupportedTypeException(gettype($data)); + } + + public static function encode($data): string { + if($data instanceof FWIFSerializable) + $data = $data->fwifSerialize(); + $type = self::detectType($data); + return chr($type) . self::{'encode' . self::CODECS[$type]}($data); + } + + public static function decode(string $data) { + return self::decodeInternal(new FWIFDecodeStream($data)); + } + + private static function decodeInternal(FWIFDecodeStream $data) { + $type = $data->readByte(); + if(!array_key_exists($type, self::CODECS)) { + $hexType = dechex($type); $hexPos = dechex($data->getPosition()); + throw new FWIFUnsupportedTypeException("Unsupported type {$type} (0x{$hexType}) at position {$data->getPosition()} (0x{$hexPos})"); + } + return self::{'decode' . self::CODECS[$type]}($data); + } + + private static function encodeNull($data): string { return ''; } + private static function decodeNull(FWIFDecodeStream $data) { return null; } + + private static function encodeInteger(int $number): string { + $packed = ''; $more = 1; $negative = $number < 0; $size = PHP_INT_SIZE * 8; + while($more) { + $byte = $number & 0x7F; + $number >>= 7; + if($negative) + $number |= (~0 << ($size - 7)); + if((!$number && !($byte & 0x40)) || ($number === -1 && ($byte & 0x40))) + $more = 0; + else + $byte |= 0x80; + $packed .= chr($byte); + } + return $packed; + } + private static function decodeInteger(FWIFDecodeStream $data): int { + $number = 0; $shift = 0; $o = 0; $size = PHP_INT_SIZE * 8; + do { + $byte = $data->readByte(); + $number |= ($byte & 0x7F) << $shift; + $shift += 7; + } while($byte & 0x80); + if(($shift < $size) && ($byte & 0x40)) + $number |= (~0 << $shift); + return $number; + } + + private static function encodeFloat(float $number): string { + return pack('E', $number); + } + private static function decodeFloat(FWIFDecodeStream $data): float { + $packed = ''; for($i = 0; $i < 8; ++$i) $packed .= chr($data->readByte()); + return unpack('E', $packed)[1]; + } + + private static function encodeString(string $string): string { + $packed = ''; $string = unpack('C*', mb_convert_encoding($string, 'utf-8')); + foreach($string as $char) + $packed .= chr($char); + return $packed . chr(self::TYPE_TRAILER); + } + private static function decodeAsciiString(FWIFDecodeStream $data): string { + $string = ''; + for(;;) { + $byte = $data->readByte(); + if($byte === self::TYPE_TRAILER) + break; + $string .= chr($byte); + } + return $string; + } + private static function decodeString(FWIFDecodeStream $data): string { // This should decode based on the utf-8 spec rather than just + return mb_convert_encoding(self::decodeAsciiString($data), 'utf-8'); // grabbing the FF terminated string representation. + } + + private static function encodeArray(array $array): string { + $packed = ''; + foreach($array as $value) + $packed .= self::encode($value); + return $packed . chr(self::TYPE_TRAILER); + } + private static function decodeArray(FWIFDecodeStream $data): array { + $array = []; + for(;;) { + if($data->readByte() === self::TYPE_TRAILER) + break; + $data->stepBack(); + $array[] = self::decodeInternal($data); + } + return $array; + } + + private static function encodeObject($object): string { + $packed = ''; $array = (array)$object; + foreach($array as $name => $value) + $packed .= $name . chr(self::TYPE_TRAILER) . self::encode($value); + return $packed . chr(self::TYPE_TRAILER); + } + private static function decodeObject(FWIFDecodeStream $data): object { + $array = []; + for(;;) { + if($data->readByte() === self::TYPE_TRAILER) + break; + $data->stepBack(); + $name = self::decodeAsciiString($data); + $array[$name] = self::decodeInternal($data); + } + return (object)$array; + } +} diff --git a/lib/FWIF/FWIFDecodeStream.php b/lib/FWIF/FWIFDecodeStream.php new file mode 100644 index 0000000..3608976 --- /dev/null +++ b/lib/FWIF/FWIFDecodeStream.php @@ -0,0 +1,31 @@ +<?php +namespace FWIF; + +class FWIFDecodeStream { + private string $body; + private int $length; + private int $position = 0; + + public function __construct(string $body) { + $this->body = $body; + $this->length = strlen($body); + } + + public function getLength(): int { + return $this->length; + } + + public function getPosition(): int { + return $this->position; + } + + public function stepBack(): void { + $this->position = max(0, $this->position - 1); + } + + public function readByte(): int { + if($this->position + 1 >= $this->length) + return 0xFF; + return ord($this->body[$this->position++]); + } +} diff --git a/lib/FWIF/FWIFException.php b/lib/FWIF/FWIFException.php new file mode 100644 index 0000000..4f0c487 --- /dev/null +++ b/lib/FWIF/FWIFException.php @@ -0,0 +1,4 @@ +<?php +namespace FWIF; + +class FWIFException extends \Exception {} diff --git a/lib/FWIF/FWIFSerializable.php b/lib/FWIF/FWIFSerializable.php new file mode 100644 index 0000000..cbd562e --- /dev/null +++ b/lib/FWIF/FWIFSerializable.php @@ -0,0 +1,6 @@ +<?php +namespace FWIF; + +interface FWIFSerializable { + function fwifSerialize(); +} diff --git a/lib/FWIF/FWIFUnsupportedTypeException.php b/lib/FWIF/FWIFUnsupportedTypeException.php new file mode 100644 index 0000000..f0da66b --- /dev/null +++ b/lib/FWIF/FWIFUnsupportedTypeException.php @@ -0,0 +1,4 @@ +<?php +namespace FWIF; + +class FWIFUnsupportedTypeException extends FWIFException {} diff --git a/patchouli.txt b/patchouli.txt new file mode 100644 index 0000000..7fa6004 --- /dev/null +++ b/patchouli.txt @@ -0,0 +1,28 @@ + /\ /\ + _,, -‐=ンヘノ\_.::::\ + // _ :::::::`ヽ/ + ノ / ..::::;;::::::.... ヽ\:::::..:::∨ ̄ ̄`l + r‐'::::〈::ノ _r、:::::::::::l |::::::: l|、 ̄ト、/ + r-‐─〉 ::::::r‐-、__〉 l\゙ー' ノ、: ;: l::::ヽj゙、 〉 + // /|ヽ_〉 / l l |! l l ̄ l (_ ハ/ ∨ + \/∧ノ_ノ! l l,,_|_l |l ! |_,,,_ト l l | + _ノノ\_〉、ハ〈こ>リV゙´匕j 〉 !ヽ ヽ、 + , へヽ;;;;;`ヾミ`ヽ、⊥_ l /_ノ>-─┐、 + 〉 l;;;;l;\;;;`ヾ三、ミ`ヽー/ 二三‐二二ニ⊥、\ + ⌒T´::l;;;;l;;;;;;;`'';;ヽ、;;;`ヽ〉//二 -─..''.. ̄ ̄;// `'' ー-、 + ,,, -─'''>、.l;;;;l;;;;;;;;;;;;;;;;;;;`;;ヾニィ´;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;//\_ 、 \ + /, /く ィュ|::l;;;;l;;;;;;;;;;;;;;;;;;;;;;;;;;|;;|:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;// ノ `> ヾ ヽ + __〈∠l _//| `::ノ.::l;;;;l;;;;;;;;;;;;;;;;;l\〈\;;;;∩;;;;;;;r─-、;;;;//'' ィ、 | l |、 | +´  ̄ `Y .:::::l;;;;l;;;;;;;;;;;;;;;;;ト、ヽ〉 ヽノ |´∨:::::r‐ノ;;// \r'' | l ∨ + ............... | ::::::l;;;;:ー-.、;;;;;;;;\ l::::_ノl/ 〈;;;;//l >| ̄丿 ノ!/ + :::::\.. :: ̄ >、;;\;;;;;|\ 〉! 、 `ヾ、ヽ:ヽ_ 〈 + ::::::::: ̄ ̄へ `ヽイ:::::_>、ノノヽ ゞ !:ノ::::::. \ヽ_ _ +.. _::二-‐''/ ̄ハ:::`ーr-、 `´ィ:::..... 〉:::::::::::..  ̄\:::`ヽ +:::. ,, ‐''´/.. ̄:::::,::-‐:/:::::::ノ:::::ヽ::::::|:::l:::..:::::::::─-ノ .:::|::.:::::::::: ヽ:::: +::::::;;: ‐''゙´ /:::::::::::::/ ::/::::::;ィ´: :: ::::::::::!::::\:::::::::..../::: l:::::.:::::: :: +/ /ノ7:::::::/ /.:::::/ l:: :::::::::::::/:l ..:::: ̄ ̄`ー‐''´::::::\ _ + //´ └-/ / .::::/ ..::-─/ \ .....:::::::::::::. :: \:: ̄`ヽ + .::. : / / /..:::::/ _ 二 -─/ ノー-、::::::::::::.. `ヽ::::::::::. + .::::::._| / \_/ ::::::::/ / .::::\- 、:::... \:::::::.. +::::::/ !:: l\:: :. . ..::::::::::/ ..::::/ .:::::::!:::::ト、 `ヽ;:.. \ー-、 +/ |::/ ::::7 :: :::...::::::::::::: 〈 ::: /: .::::::::/::/ \ :. ヽ::::: \ No newline at end of file diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..79ad9ef --- /dev/null +++ b/public/index.php @@ -0,0 +1,55 @@ +<?php +namespace Patchouli; + +use FWIF\FWIF; + +require_once __DIR__ . '/../startup.php'; + +$request = Http\HttpRequest::create(); + +header('Content-Type: ' . FWIF::CONTENT_TYPE); + +if($request->match('GET', '/packages')) { + $tags = explode(';', (string)$request->getQueryParam('tags', FILTER_SANITIZE_STRING)); + $packages = empty($tags) ? Patchouli::getPackages() : Patchouli::getPackagesWithTags($tags); + + $encoded = FWIF::encode($packages); + echo strlen($encoded) . ' ' . $encoded; + + echo "\r\n\r\n--------------------\r\n\r\n"; + + $jsonEncoded = json_encode($packages); + echo strlen($jsonEncoded) . ' ' . $jsonEncoded; + + echo "\r\n\r\n--------------------\r\n\r\n"; + + $hexdump = bin2hex($encoded); $hexdumpSect = 8; $hexdumpSize = 32; + for($i = 0; $i < strlen($hexdump) / $hexdumpSize; ++$i) { + $line = substr($hexdump, $i * $hexdumpSize, $hexdumpSize); + echo str_pad(dechex($i * $hexdumpSize), 4, '0', STR_PAD_LEFT) . ' '; + for($j = 0; $j < strlen($line) / $hexdumpSect; ++$j) + echo substr($line, $j * $hexdumpSect, $hexdumpSect) . ' '; + echo "\r\n"; + } + + echo "\r\n--------------------\r\n\r\n"; + + var_dump([(object)$packages[0]->fwifSerialize()]); + + echo "\r\n--------------------\r\n\r\n"; + + $decoded = FWIF::decode($encoded); + var_dump($decoded); + return; +} + +if($request->match('GET', '/')) { + header('Content-Type: text/html; charset=utf-8'); + echo '<!doctype html><title>Patchouli</title><pre style="font-family:IPAMonaPGothic,\'IPA モナー Pゴシック\',Monapo,Mona,\'MS PGothic\',\'MS Pゴシック\',sans-serif;font-size:16px;line-height:18px;">'; + readfile(PAT_ROOT . '/patchouli.txt'); + echo '</pre>'; + return; +} + +http_response_code(404); +echo '{"code":404,"message":"Path not found."}'; diff --git a/src/Dummy/DummyPackage.php b/src/Dummy/DummyPackage.php new file mode 100644 index 0000000..fcdbf17 --- /dev/null +++ b/src/Dummy/DummyPackage.php @@ -0,0 +1,55 @@ +<?php +namespace Patchouli\Dummy; + +use Patchouli\IPackage; +use Patchouli\Version; + +class DummyPackage implements IPackage, \JsonSerializable { + public function getId(): string { + return 'package-id'; + } + + public function getName(): string { + return 'Human Readable Name'; + } + + public function getVersion(): Version { + return new Version; + } + + public function getDependencies(): array { + return []; + } + + public function fwifSerialize(): array { + $data = [ + 'null' => null, + 'zero' => 0, + 'u8' => 0x42, + 'u16' => 0x4344, + 'u24' => 0x454647, + 'u32' => 0x58596061, + 'u40' => 0x6263646566, + 'u48' => 0x676869707172, + 'u56' => 0x73747576777879, + 'u64' => 0x7481828384858687, + 'neg32' => -12345678, + 'neg64' => -1234567890987654, + 'float' => 12345.6789, + 'array' => ['e', 'a', 0x55], + 'object' => new \stdClass, + 'misaka' => '御坂 美琴', + 'id' => $this->getId(), + 'name' => $this->getName(), + 'version' => $this->getVersion(), + 'deps' => [], + ]; + foreach($this->getDependencies() as $dependency) + $data['deps'][] = $dependency->getName(); + return $data; + } + + public function jsonSerialize() { + return $this->fwifSerialize(); + } +} diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php new file mode 100644 index 0000000..e3d46b7 --- /dev/null +++ b/src/Http/HttpRequest.php @@ -0,0 +1,82 @@ +<?php +namespace Patchouli\Http; + +class HttpRequest { + private string $method; + private string $path; + + private array $serverParams; + private array $queryParams; + private array $postParams; + + public function __construct(array $server, array $query, array $post) { + $this->method = $server['REQUEST_METHOD']; + $this->path = '/' . trim(parse_url($server['REQUEST_URI'], PHP_URL_PATH), '/'); + $this->serverParams = $server; + $this->queryParams = $query; + $this->postParams = $post; + } + + public static function create(): self { + return new static($_SERVER, $_GET, $_POST); + } + + public function getMethod(): string { + return $this->method; + } + public function isMethod(string|array $method): bool { + if(is_string($method)) + return $this->method === $method; + return in_array($this->method, $method); + } + + public function getPath(): string { + return $this->path; + } + public function isPath(string $path): bool { + return $this->path === $path; + } + public function matchPath(string $regex, ?array &$args = null): bool { + if(!preg_match($regex, $this->path, $matches)) + return false; + $args = array_slice($matches, 1); + return true; + } + + public function match(string|array $method, string $path, ?array &$args = null): bool { + if(!$this->isMethod($method)) + return false; + if($path[0] === '/') + return $this->isPath($path); + return $this->matchPath($path, $args); + } + + public function getHeader($name): string { + $name = strtr(strtoupper($name), '-', '_'); + if($name === 'CONTENT_LENGTH' || $name === 'CONTENT_LENGTH') + return $this->serverParams[$name]; + return $this->serverParams['HTTP_' . $name] ?? ''; + } + + public function getServerParam(string $name): string { + return $this->serverParams[$name] ?? ''; + } + + public function getBody(): string { + return file_get_contents('php://input'); + } + + public function getQueryParams(): array { + return $this->queryParams; + } + public function getQueryParam(string $name, int $filter = FILTER_DEFAULT, mixed $options = null): mixed { + return filter_var($this->queryParams[$name] ?? null, $filter, $options); + } + + public function getPostParams(): array { + return $this->postParams; + } + public function getPostParam(string $name, int $filter = FILTER_DEFAULT, mixed $options = null): mixed { + return filter_var($this->postParams[$name] ?? null, $filter, $options); + } +} diff --git a/src/IPackage.php b/src/IPackage.php new file mode 100644 index 0000000..340966d --- /dev/null +++ b/src/IPackage.php @@ -0,0 +1,10 @@ +<?php +namespace Patchouli; + +use FWIF\FWIFSerializable; + +interface IPackage extends FWIFSerializable { + function getName(): string; + function getVersion(): Version; + function getDependencies(): array; +} diff --git a/src/Patchouli.php b/src/Patchouli.php new file mode 100644 index 0000000..3627901 --- /dev/null +++ b/src/Patchouli.php @@ -0,0 +1,18 @@ +<?php +namespace Patchouli; + +use Patchouli\Dummy\DummyPackage; + +class Patchouli extends SingleInstance { + public function _getPackages(): array { + return [new DummyPackage]; + } + + public function _getPackagesWithTags(array $tags): array { + return [new DummyPackage]; + } + + public function _getPackage(string $packageName): IPackage { + return new DummyPackage; + } +} diff --git a/src/SingleInstance.php b/src/SingleInstance.php new file mode 100644 index 0000000..84ca037 --- /dev/null +++ b/src/SingleInstance.php @@ -0,0 +1,28 @@ +<?php +namespace Patchouli; + +abstract class SingleInstance { + private static $instance = null; + + public static function getInstance(): self { + if(self::$instance === null) + self::$instance = new static; + return self::$instance; + } + + public function __call(string $name, array $args) { + if($name[0] === '_') { + trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR); + return; + } + return $this->{'_' . $name}(...$args); + } + + public static function __callStatic(string $name, array $args) { + if($name[0] === '_') { + trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR); + return; + } + return self::getInstance()->{'_' . $name}(...$args); + } +} diff --git a/src/Version.php b/src/Version.php new file mode 100644 index 0000000..bf0caed --- /dev/null +++ b/src/Version.php @@ -0,0 +1,19 @@ +<?php +namespace Patchouli; + +use FWIF\FWIFSerializable; +use JsonSerializable; + +class Version implements FWIFSerializable, JsonSerializable { + public function fwifSerialize(): string { + return (string)$this; + } + + public function jsonSerialize(): string { + return (string)$this; + } + + public function __toString(): string { + return '1.0.0'; + } +} diff --git a/startup.php b/startup.php new file mode 100644 index 0000000..edd9e5c --- /dev/null +++ b/startup.php @@ -0,0 +1,21 @@ +<?php +namespace Patchouli; + +define('PAT_STARTUP', microtime(true)); +define('PAT_ROOT', __DIR__); +define('PAT_DEBUG', is_file(PAT_ROOT . '/.debug')); +define('PAT_PUB', PAT_ROOT . '/public'); // WWW visible +define('PAT_SRC', PAT_ROOT . '/src'); // Patchouli namespace +define('PAT_LIB', PAT_ROOT . '/lib'); // Other unresolved namespaces + +ini_set('display_errors', PAT_DEBUG ? 'on' : 'off'); +error_reporting(PAT_DEBUG ? -1 : 0); + +set_include_path(PAT_SRC . PATH_SEPARATOR . PAT_LIB . PATH_SEPARATOR . get_include_path()); +spl_autoload_register(function(string $className) { + $parts = explode('\\', trim($className, '\\'), 2); + if($parts[0] === __NAMESPACE__) + require_once PAT_SRC . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $parts[1]) . '.php'; + else + require_once PAT_LIB . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; +});