From e92bfcf7f7727d2443b67c493e80218fc73f17ef Mon Sep 17 00:00:00 2001
From: flashwave <me@flash.moe>
Date: Wed, 23 Dec 2020 01:44:45 +0000
Subject: [PATCH] import

---
 .gitattributes                            |   1 +
 .gitignore                                |   1 +
 lib/FWIF/FWIF.php                         | 162 ++++++++++++++++++++++
 lib/FWIF/FWIFDecodeStream.php             |  31 +++++
 lib/FWIF/FWIFException.php                |   4 +
 lib/FWIF/FWIFSerializable.php             |   6 +
 lib/FWIF/FWIFUnsupportedTypeException.php |   4 +
 patchouli.txt                             |  28 ++++
 public/index.php                          |  55 ++++++++
 src/Dummy/DummyPackage.php                |  55 ++++++++
 src/Http/HttpRequest.php                  |  82 +++++++++++
 src/IPackage.php                          |  10 ++
 src/Patchouli.php                         |  18 +++
 src/SingleInstance.php                    |  28 ++++
 src/Version.php                           |  19 +++
 startup.php                               |  21 +++
 16 files changed, 525 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 .gitignore
 create mode 100644 lib/FWIF/FWIF.php
 create mode 100644 lib/FWIF/FWIFDecodeStream.php
 create mode 100644 lib/FWIF/FWIFException.php
 create mode 100644 lib/FWIF/FWIFSerializable.php
 create mode 100644 lib/FWIF/FWIFUnsupportedTypeException.php
 create mode 100644 patchouli.txt
 create mode 100644 public/index.php
 create mode 100644 src/Dummy/DummyPackage.php
 create mode 100644 src/Http/HttpRequest.php
 create mode 100644 src/IPackage.php
 create mode 100644 src/Patchouli.php
 create mode 100644 src/SingleInstance.php
 create mode 100644 src/Version.php
 create mode 100644 startup.php

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