This commit is contained in:
flash 2020-12-23 01:44:45 +00:00
commit e92bfcf7f7
16 changed files with 525 additions and 0 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.debug

162
lib/FWIF/FWIF.php Normal file
View file

@ -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;
}
}

View file

@ -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++]);
}
}

View file

@ -0,0 +1,4 @@
<?php
namespace FWIF;
class FWIFException extends \Exception {}

View file

@ -0,0 +1,6 @@
<?php
namespace FWIF;
interface FWIFSerializable {
function fwifSerialize();
}

View file

@ -0,0 +1,4 @@
<?php
namespace FWIF;
class FWIFUnsupportedTypeException extends FWIFException {}

28
patchouli.txt Normal file
View file

@ -0,0 +1,28 @@
                        
                    _,, -ンヘ_.::::
                           _ :::::::`ヽ/
                     ノ /  ..::::;;::::::.... ヽ\:::::..::: ̄ ̄l
               '::::〈::  _r、::::::::::: |::::::: l|、 ̄ト、/
              -‐─〉 ::::::-、__〉 l゙ー' ノ、: ;: l::::ヽj゙、 〉
             / /|ヽ_〉 /  |! l l ̄ l (_ ハ/ ∨
            ∧ノ_ノ! l,,_|_l |l ! |_,,,_ト l l   |
               __〉、ハ〈こ>リV゙´ !ヽ ヽ、
          , へヽ;;;;;ヾミヽ、⊥_   l    /_ノ>-─┐、
          〉   l;;;;l;;;;`ヾ三、ミ`ヽー/ 二三‐二二ニ⊥、\
          ⌒T´::l;;;;l;;;;;;;'';;ヽ、;;;`ヽ〉//二 -─..''.. ̄ ̄;//  '' ー-、
         ,,, -─'''>、.l;;;;l;;;;;;;;;;;;;;;;;;;;;ヾニィ´;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;//\_  、
      ,   /く ィュ|::l;;;;l;;;;;;;;;;;;;;;;;;;;;;;;;;|;;|:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;// ノ  `> ヾ ヽ
 __〈∠l _/| ::ノ.::l;;;;l;;;;;;;;;;;;;;;;;l;;;;∩;;;;;;;r─-、;;;;//'' ィ、 |   |、 |
´         ̄ Y .:::::l;;;;l;;;;;;;;;;;;;;;;;ト、ヽ〉 ヽノ |´:::::r‐ノ;;//  ''  | l
       ............... | ::::::;;;;:ー-.、;;;;;;;;    l::::_l/ 〈;;;;//l >| ̄丿 ノ!/
           :::::.. :: ̄ >、;;;;;;;|    〉! 、  `ヾ、ヽ:ヽ_ 〈
           ::::::::: ̄ ̄へ  `ヽイ:::::_ノヽ ゞ    !:ノ::::::.  ヽ_ _
..                _::二-''/ ̄ハ:::`ーr-、 `´ィ:::.....    〉:::::::::::..   ̄\:::`ヽ
:::.      ,, ''´.. ̄:::::,::-::::::::ノ:::::ヽ::::::|:::l:::..:::::::::─- .:::|::.::::::::::      ヽ::::
::::::;;: ''゙´  ::::::::::::: ::::::::;ィ´: :: ::::::::::!:::::::::::::....::: l:::::.::::::        ::
    /::::::: .::::: l:: :::::::::::::/:l ..:::: ̄ ̄`ー‐''´:::::: _
     //´ └-/  / .::::     ..::-─/   .....:::::::::::::.   ::    :: ̄`ヽ
  .::. : /     /  /..:::::/   _ 二 -─/    ノー-、::::::::::::..        `ヽ::::::::::.
 .::::::._|    /   _/      ::::::::/    /  .::::- 、:::...       :::::::..
::::::      !:: l::  :. .   ..::::::::::/   ..::::/  .:::::::!:::::ト、 `ヽ;:..        \ー-、
      |::/ ::::7 :: :::...::::::::::::: 〈  ::: /: .::::::::/::   :.        ヽ:::::

55
public/index.php Normal file
View file

@ -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\',\' 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."}';

View file

@ -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();
}
}

82
src/Http/HttpRequest.php Normal file
View file

@ -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);
}
}

10
src/IPackage.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace Patchouli;
use FWIF\FWIFSerializable;
interface IPackage extends FWIFSerializable {
function getName(): string;
function getVersion(): Version;
function getDependencies(): array;
}

18
src/Patchouli.php Normal file
View file

@ -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;
}
}

28
src/SingleInstance.php Normal file
View file

@ -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);
}
}

19
src/Version.php Normal file
View file

@ -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';
}
}

21
startup.php Normal file
View file

@ -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';
});