1*8da7d805SAndreas Gohr<?php 2*8da7d805SAndreas Gohr 3*8da7d805SAndreas Gohr/** 4*8da7d805SAndreas Gohr * @noinspection SqlNoDataSourceInspection 5*8da7d805SAndreas Gohr * @noinspection SqlDialectInspection 6*8da7d805SAndreas Gohr * @noinspection PhpComposerExtensionStubsInspection 7*8da7d805SAndreas Gohr */ 8*8da7d805SAndreas Gohr 9*8da7d805SAndreas Gohrnamespace dokuwiki\plugin\sqlite; 10*8da7d805SAndreas Gohr 11*8da7d805SAndreas Gohr 12*8da7d805SAndreas Gohr/** 13*8da7d805SAndreas Gohr * Helpers to access a SQLite Database with automatic schema migration 14*8da7d805SAndreas Gohr */ 15*8da7d805SAndreas Gohrclass SQLiteDB 16*8da7d805SAndreas Gohr{ 17*8da7d805SAndreas Gohr const FILE_EXTENSION = '.sqlite3'; 18*8da7d805SAndreas Gohr 19*8da7d805SAndreas Gohr /** @var \PDO */ 20*8da7d805SAndreas Gohr protected $pdo; 21*8da7d805SAndreas Gohr 22*8da7d805SAndreas Gohr /** @var string */ 23*8da7d805SAndreas Gohr protected $schemadir; 24*8da7d805SAndreas Gohr 25*8da7d805SAndreas Gohr /** @var string */ 26*8da7d805SAndreas Gohr protected $dbname; 27*8da7d805SAndreas Gohr 28*8da7d805SAndreas Gohr /** @var \helper_plugin_sqlite 29*8da7d805SAndreas Gohr protected $helper; 30*8da7d805SAndreas Gohr 31*8da7d805SAndreas Gohr /** 32*8da7d805SAndreas Gohr * Constructor 33*8da7d805SAndreas Gohr * 34*8da7d805SAndreas Gohr * @param string $dbname Database name 35*8da7d805SAndreas Gohr * @param string $schemadir directory with schema migration files 36*8da7d805SAndreas Gohr * @param \helper_plugin_sqlite $sqlitehelper for backwards compatibility 37*8da7d805SAndreas Gohr * @throws \Exception 38*8da7d805SAndreas Gohr */ 39*8da7d805SAndreas Gohr public function __construct($dbname, $schemadir, $sqlitehelper = null) 40*8da7d805SAndreas Gohr { 41*8da7d805SAndreas Gohr if (!class_exists('pdo') || !in_array('sqlite', \PDO::getAvailableDrivers())) { 42*8da7d805SAndreas Gohr throw new \Exception('SQLite PDO driver not available'); 43*8da7d805SAndreas Gohr } 44*8da7d805SAndreas Gohr 45*8da7d805SAndreas Gohr // backwards compatibility, circular dependency 46*8da7d805SAndreas Gohr $this->helper = $sqlitehelper; 47*8da7d805SAndreas Gohr if($this->helper) $this->helper->setAdapter($this); 48*8da7d805SAndreas Gohr 49*8da7d805SAndreas Gohr $this->schemadir = $schemadir; 50*8da7d805SAndreas Gohr $this->dbname = $dbname; 51*8da7d805SAndreas Gohr $file = $this->getDbFile(); 52*8da7d805SAndreas Gohr 53*8da7d805SAndreas Gohr $this->pdo = new \PDO( 54*8da7d805SAndreas Gohr 'sqlite:' . $file, 55*8da7d805SAndreas Gohr null, 56*8da7d805SAndreas Gohr null, 57*8da7d805SAndreas Gohr [ 58*8da7d805SAndreas Gohr \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION 59*8da7d805SAndreas Gohr ] 60*8da7d805SAndreas Gohr ); 61*8da7d805SAndreas Gohr 62*8da7d805SAndreas Gohr if($schemadir !== '') { 63*8da7d805SAndreas Gohr // schema dir is empty, when accessing the DB from Admin interface instead of plugin context 64*8da7d805SAndreas Gohr $this->applyMigrations(); 65*8da7d805SAndreas Gohr } 66*8da7d805SAndreas Gohr Functions::register($this->pdo); 67*8da7d805SAndreas Gohr } 68*8da7d805SAndreas Gohr 69*8da7d805SAndreas Gohr 70*8da7d805SAndreas Gohr // region public API 71*8da7d805SAndreas Gohr 72*8da7d805SAndreas Gohr /** 73*8da7d805SAndreas Gohr * Direct access to the PDO object 74*8da7d805SAndreas Gohr * @return \PDO 75*8da7d805SAndreas Gohr */ 76*8da7d805SAndreas Gohr public function pdo() 77*8da7d805SAndreas Gohr { 78*8da7d805SAndreas Gohr return $this->pdo; 79*8da7d805SAndreas Gohr } 80*8da7d805SAndreas Gohr 81*8da7d805SAndreas Gohr /** 82*8da7d805SAndreas Gohr * Execute a statement and return it 83*8da7d805SAndreas Gohr * 84*8da7d805SAndreas Gohr * @param string $sql 85*8da7d805SAndreas Gohr * @param array $parameters 86*8da7d805SAndreas Gohr * @return \PDOStatement Be sure to close the cursor yourself 87*8da7d805SAndreas Gohr * @throws \PDOException 88*8da7d805SAndreas Gohr */ 89*8da7d805SAndreas Gohr public function query($sql, $parameters = []) 90*8da7d805SAndreas Gohr { 91*8da7d805SAndreas Gohr $stmt = $this->pdo->prepare($sql); 92*8da7d805SAndreas Gohr $stmt->execute($parameters); 93*8da7d805SAndreas Gohr return $stmt; 94*8da7d805SAndreas Gohr } 95*8da7d805SAndreas Gohr 96*8da7d805SAndreas Gohr /** 97*8da7d805SAndreas Gohr * Execute a statement and return metadata 98*8da7d805SAndreas Gohr * 99*8da7d805SAndreas Gohr * Returns the last insert ID on INSERTs or the number of affected rows 100*8da7d805SAndreas Gohr * 101*8da7d805SAndreas Gohr * @param string $sql 102*8da7d805SAndreas Gohr * @param array $parameters 103*8da7d805SAndreas Gohr * @return int 104*8da7d805SAndreas Gohr * @throws \PDOException 105*8da7d805SAndreas Gohr */ 106*8da7d805SAndreas Gohr public function exec($sql, $parameters = []) 107*8da7d805SAndreas Gohr { 108*8da7d805SAndreas Gohr $stmt = $this->pdo->prepare($sql); 109*8da7d805SAndreas Gohr $stmt->execute($parameters); 110*8da7d805SAndreas Gohr 111*8da7d805SAndreas Gohr $count = $stmt->rowCount(); 112*8da7d805SAndreas Gohr $stmt->closeCursor(); 113*8da7d805SAndreas Gohr if ($count && preg_match('/^INSERT /i', $sql)) { 114*8da7d805SAndreas Gohr return $this->queryValue('SELECT last_insert_rowid()'); 115*8da7d805SAndreas Gohr } 116*8da7d805SAndreas Gohr 117*8da7d805SAndreas Gohr return $count; 118*8da7d805SAndreas Gohr } 119*8da7d805SAndreas Gohr 120*8da7d805SAndreas Gohr /** 121*8da7d805SAndreas Gohr * Simple query abstraction 122*8da7d805SAndreas Gohr * 123*8da7d805SAndreas Gohr * Returns all data 124*8da7d805SAndreas Gohr * 125*8da7d805SAndreas Gohr * @param string $sql 126*8da7d805SAndreas Gohr * @param array $params 127*8da7d805SAndreas Gohr * @return array 128*8da7d805SAndreas Gohr * @throws \PDOException 129*8da7d805SAndreas Gohr */ 130*8da7d805SAndreas Gohr public function queryAll($sql, $params = []) 131*8da7d805SAndreas Gohr { 132*8da7d805SAndreas Gohr $stmt = $this->query($sql, $params); 133*8da7d805SAndreas Gohr $data = $stmt->fetchAll(\PDO::FETCH_ASSOC); 134*8da7d805SAndreas Gohr $stmt->closeCursor(); 135*8da7d805SAndreas Gohr return $data; 136*8da7d805SAndreas Gohr } 137*8da7d805SAndreas Gohr 138*8da7d805SAndreas Gohr /** 139*8da7d805SAndreas Gohr * Query one single row 140*8da7d805SAndreas Gohr * 141*8da7d805SAndreas Gohr * @param string $sql 142*8da7d805SAndreas Gohr * @param array $params 143*8da7d805SAndreas Gohr * @return array|null 144*8da7d805SAndreas Gohr * @throws \PDOException 145*8da7d805SAndreas Gohr */ 146*8da7d805SAndreas Gohr public function queryRecord($sql, $params = []) 147*8da7d805SAndreas Gohr { 148*8da7d805SAndreas Gohr $stmt = $this->query($sql, $params); 149*8da7d805SAndreas Gohr $row = $stmt->fetch(); 150*8da7d805SAndreas Gohr $stmt->closeCursor(); 151*8da7d805SAndreas Gohr if (is_array($row) && count($row)) return $row; 152*8da7d805SAndreas Gohr return null; 153*8da7d805SAndreas Gohr } 154*8da7d805SAndreas Gohr 155*8da7d805SAndreas Gohr /** 156*8da7d805SAndreas Gohr * Insert or replace the given data into the table 157*8da7d805SAndreas Gohr * 158*8da7d805SAndreas Gohr * @param string $table 159*8da7d805SAndreas Gohr * @param array $data 160*8da7d805SAndreas Gohr * @param bool $replace Conflict resolution, replace or ignore 161*8da7d805SAndreas Gohr * @throws \PDOException 162*8da7d805SAndreas Gohr */ 163*8da7d805SAndreas Gohr public function saveRecord($table, $data, $replace = true) 164*8da7d805SAndreas Gohr { 165*8da7d805SAndreas Gohr $columns = array_map(function ($column) { 166*8da7d805SAndreas Gohr return '"' . $column . '"'; 167*8da7d805SAndreas Gohr }, array_keys($data)); 168*8da7d805SAndreas Gohr $values = array_values($data); 169*8da7d805SAndreas Gohr $placeholders = array_pad([], count($columns), '?'); 170*8da7d805SAndreas Gohr 171*8da7d805SAndreas Gohr if ($replace) { 172*8da7d805SAndreas Gohr $command = 'REPLACE'; 173*8da7d805SAndreas Gohr } else { 174*8da7d805SAndreas Gohr $command = 'INSERT OR IGNORE'; 175*8da7d805SAndreas Gohr } 176*8da7d805SAndreas Gohr 177*8da7d805SAndreas Gohr /** @noinspection SqlResolve */ 178*8da7d805SAndreas Gohr $sql = $command . ' INTO "' . $table . '" (' . join(',', $columns) . ') VALUES (' . join(',', $placeholders) . ')'; 179*8da7d805SAndreas Gohr $stm = $this->pdo->prepare($sql); 180*8da7d805SAndreas Gohr $stm->execute($values); 181*8da7d805SAndreas Gohr $stm->closeCursor(); 182*8da7d805SAndreas Gohr } 183*8da7d805SAndreas Gohr 184*8da7d805SAndreas Gohr /** 185*8da7d805SAndreas Gohr * Execute a query that returns a single value 186*8da7d805SAndreas Gohr * 187*8da7d805SAndreas Gohr * @param string $sql 188*8da7d805SAndreas Gohr * @param array $params 189*8da7d805SAndreas Gohr * @return mixed|null 190*8da7d805SAndreas Gohr * @throws \PDOException 191*8da7d805SAndreas Gohr */ 192*8da7d805SAndreas Gohr public function queryValue($sql, $params = []) 193*8da7d805SAndreas Gohr { 194*8da7d805SAndreas Gohr $result = $this->queryAll($sql, $params); 195*8da7d805SAndreas Gohr if (is_array($result) && count($result)) return array_values($result[0])[0]; 196*8da7d805SAndreas Gohr return null; 197*8da7d805SAndreas Gohr } 198*8da7d805SAndreas Gohr 199*8da7d805SAndreas Gohr // endregion 200*8da7d805SAndreas Gohr 201*8da7d805SAndreas Gohr // region meta handling 202*8da7d805SAndreas Gohr 203*8da7d805SAndreas Gohr /** 204*8da7d805SAndreas Gohr * Get a config value from the opt table 205*8da7d805SAndreas Gohr * 206*8da7d805SAndreas Gohr * @param string $opt Config name 207*8da7d805SAndreas Gohr * @param mixed $default What to return if the value isn't set 208*8da7d805SAndreas Gohr * @return mixed 209*8da7d805SAndreas Gohr * @throws \PDOException 210*8da7d805SAndreas Gohr */ 211*8da7d805SAndreas Gohr public function getOpt($opt, $default = null) 212*8da7d805SAndreas Gohr { 213*8da7d805SAndreas Gohr $value = $this->queryValue("SELECT val FROM opts WHERE opt = ?", [$opt]); 214*8da7d805SAndreas Gohr if ($value === null) return $default; 215*8da7d805SAndreas Gohr return $value; 216*8da7d805SAndreas Gohr } 217*8da7d805SAndreas Gohr 218*8da7d805SAndreas Gohr /** 219*8da7d805SAndreas Gohr * Set a config value in the opt table 220*8da7d805SAndreas Gohr * 221*8da7d805SAndreas Gohr * @param $opt 222*8da7d805SAndreas Gohr * @param $value 223*8da7d805SAndreas Gohr * @throws \PDOException 224*8da7d805SAndreas Gohr */ 225*8da7d805SAndreas Gohr public function setOpt($opt, $value) 226*8da7d805SAndreas Gohr { 227*8da7d805SAndreas Gohr $this->exec('REPLACE INTO opts (opt,val) VALUES (?,?)', [$opt, $value]); 228*8da7d805SAndreas Gohr } 229*8da7d805SAndreas Gohr 230*8da7d805SAndreas Gohr /** 231*8da7d805SAndreas Gohr * @return string 232*8da7d805SAndreas Gohr */ 233*8da7d805SAndreas Gohr public function getDbName() 234*8da7d805SAndreas Gohr { 235*8da7d805SAndreas Gohr return $this->dbname; 236*8da7d805SAndreas Gohr } 237*8da7d805SAndreas Gohr 238*8da7d805SAndreas Gohr /** 239*8da7d805SAndreas Gohr * @return string 240*8da7d805SAndreas Gohr */ 241*8da7d805SAndreas Gohr public function getDbFile() 242*8da7d805SAndreas Gohr { 243*8da7d805SAndreas Gohr global $conf; 244*8da7d805SAndreas Gohr return $conf['metadir'] . '/' . $this->dbname . self::FILE_EXTENSION; 245*8da7d805SAndreas Gohr } 246*8da7d805SAndreas Gohr 247*8da7d805SAndreas Gohr /** 248*8da7d805SAndreas Gohr * Create a dump of the database and its contents 249*8da7d805SAndreas Gohr * 250*8da7d805SAndreas Gohr * @return string 251*8da7d805SAndreas Gohr * @throws \Exception 252*8da7d805SAndreas Gohr */ 253*8da7d805SAndreas Gohr public function dumpToFile($filename) 254*8da7d805SAndreas Gohr { 255*8da7d805SAndreas Gohr $fp = fopen($filename, 'w'); 256*8da7d805SAndreas Gohr if (!$fp) { 257*8da7d805SAndreas Gohr throw new \Exception('Could not open file ' . $filename . ' for writing'); 258*8da7d805SAndreas Gohr } 259*8da7d805SAndreas Gohr 260*8da7d805SAndreas Gohr 261*8da7d805SAndreas Gohr $tables = $this->queryAll('SELECT name,sql FROM sqlite_master WHERE type="table"'); 262*8da7d805SAndreas Gohr fwrite($fp, 'BEGIN TRANSACTION;' . "\n"); 263*8da7d805SAndreas Gohr 264*8da7d805SAndreas Gohr foreach ($tables as $table) { 265*8da7d805SAndreas Gohr fwrite($fp, $table['sql'] . ";\n"); // table definition 266*8da7d805SAndreas Gohr 267*8da7d805SAndreas Gohr // data as INSERT statements 268*8da7d805SAndreas Gohr $sql = "SELECT * FROM " . $table['name']; 269*8da7d805SAndreas Gohr $res = $this->query($sql); 270*8da7d805SAndreas Gohr while ($row = $res->fetch(\PDO::FETCH_ASSOC)) { 271*8da7d805SAndreas Gohr $line = 'INSERT INTO ' . $table['name'] . ' VALUES('; 272*8da7d805SAndreas Gohr foreach ($row as $no_entry => $entry) { 273*8da7d805SAndreas Gohr if ($no_entry !== 0) { 274*8da7d805SAndreas Gohr $line .= ','; 275*8da7d805SAndreas Gohr } 276*8da7d805SAndreas Gohr 277*8da7d805SAndreas Gohr if (is_null($entry)) { 278*8da7d805SAndreas Gohr $line .= 'NULL'; 279*8da7d805SAndreas Gohr } elseif (!is_numeric($entry)) { 280*8da7d805SAndreas Gohr $line .= $this->pdo->quote($entry); 281*8da7d805SAndreas Gohr } else { 282*8da7d805SAndreas Gohr // TODO depending on locale extra leading zeros 283*8da7d805SAndreas Gohr // are truncated e.g 1.300 (thousand three hunderd)-> 1.3 284*8da7d805SAndreas Gohr $line .= $entry; 285*8da7d805SAndreas Gohr } 286*8da7d805SAndreas Gohr } 287*8da7d805SAndreas Gohr $line .= ');' . "\n"; 288*8da7d805SAndreas Gohr fwrite($fp, $line); 289*8da7d805SAndreas Gohr } 290*8da7d805SAndreas Gohr $res->closeCursor(); 291*8da7d805SAndreas Gohr } 292*8da7d805SAndreas Gohr 293*8da7d805SAndreas Gohr // indexes 294*8da7d805SAndreas Gohr $indexes = $this->queryAll("SELECT name,sql FROM sqlite_master WHERE type='index'"); 295*8da7d805SAndreas Gohr foreach ($indexes as $index) { 296*8da7d805SAndreas Gohr fwrite($fp, $index['sql'] . ";\n"); 297*8da7d805SAndreas Gohr } 298*8da7d805SAndreas Gohr fwrite($fp, 'COMMIT;' . "\n"); 299*8da7d805SAndreas Gohr fclose($fp); 300*8da7d805SAndreas Gohr return $filename; 301*8da7d805SAndreas Gohr } 302*8da7d805SAndreas Gohr 303*8da7d805SAndreas Gohr // endregion 304*8da7d805SAndreas Gohr 305*8da7d805SAndreas Gohr // region migration handling 306*8da7d805SAndreas Gohr 307*8da7d805SAndreas Gohr /** 308*8da7d805SAndreas Gohr * Apply all pending migrations 309*8da7d805SAndreas Gohr * 310*8da7d805SAndreas Gohr * Each migration is executed in a transaction which is rolled back on failure 311*8da7d805SAndreas Gohr * Migrations can be files in the schema directory or event handlers 312*8da7d805SAndreas Gohr * 313*8da7d805SAndreas Gohr * @throws \Exception 314*8da7d805SAndreas Gohr */ 315*8da7d805SAndreas Gohr protected function applyMigrations() 316*8da7d805SAndreas Gohr { 317*8da7d805SAndreas Gohr $currentVersion = $this->currentDbVersion(); 318*8da7d805SAndreas Gohr $latestVersion = $this->latestDbVersion(); 319*8da7d805SAndreas Gohr 320*8da7d805SAndreas Gohr for ($newVersion = $currentVersion + 1; $newVersion <= $latestVersion; $newVersion++) { 321*8da7d805SAndreas Gohr $data = [ 322*8da7d805SAndreas Gohr 'dbname' => $this->dbname, 323*8da7d805SAndreas Gohr 'from' => $currentVersion, 324*8da7d805SAndreas Gohr 'to' => $newVersion, 325*8da7d805SAndreas Gohr 'file' => $this->getMigrationFile($newVersion), 326*8da7d805SAndreas Gohr 'sqlite' => $this->helper, 327*8da7d805SAndreas Gohr 'adapter' => $this, 328*8da7d805SAndreas Gohr ]; 329*8da7d805SAndreas Gohr $event = new \Doku_Event('PLUGIN_SQLITE_DATABASE_UPGRADE', $data); 330*8da7d805SAndreas Gohr 331*8da7d805SAndreas Gohr $this->pdo->beginTransaction(); 332*8da7d805SAndreas Gohr try { 333*8da7d805SAndreas Gohr if ($event->advise_before()) { 334*8da7d805SAndreas Gohr // standard migration file 335*8da7d805SAndreas Gohr $sql = file_get_contents($data['file']); 336*8da7d805SAndreas Gohr $this->exec($sql); 337*8da7d805SAndreas Gohr } else if (!$event->result) { 338*8da7d805SAndreas Gohr // advise before returned false, but the result was false 339*8da7d805SAndreas Gohr throw new \PDOException('Plugin event did not signal success'); 340*8da7d805SAndreas Gohr } 341*8da7d805SAndreas Gohr $this->setOpt('dbversion', $newVersion); 342*8da7d805SAndreas Gohr $this->pdo->commit(); 343*8da7d805SAndreas Gohr $event->advise_after(); 344*8da7d805SAndreas Gohr } catch (\Exception $e) { 345*8da7d805SAndreas Gohr // something went wrong, rollback 346*8da7d805SAndreas Gohr $this->pdo->rollBack(); 347*8da7d805SAndreas Gohr throw $e; 348*8da7d805SAndreas Gohr } 349*8da7d805SAndreas Gohr } 350*8da7d805SAndreas Gohr 351*8da7d805SAndreas Gohr // vacuum the database to free up unused space 352*8da7d805SAndreas Gohr $this->pdo->exec('VACUUM'); 353*8da7d805SAndreas Gohr } 354*8da7d805SAndreas Gohr 355*8da7d805SAndreas Gohr /** 356*8da7d805SAndreas Gohr * Read the current version from the opt table 357*8da7d805SAndreas Gohr * 358*8da7d805SAndreas Gohr * The opt table is created here if not found 359*8da7d805SAndreas Gohr * 360*8da7d805SAndreas Gohr * @return int 361*8da7d805SAndreas Gohr * @throws \PDOException 362*8da7d805SAndreas Gohr */ 363*8da7d805SAndreas Gohr protected function currentDbVersion() 364*8da7d805SAndreas Gohr { 365*8da7d805SAndreas Gohr try { 366*8da7d805SAndreas Gohr $version = $this->getOpt('dbversion', 0); 367*8da7d805SAndreas Gohr return (int)$version; 368*8da7d805SAndreas Gohr } catch (\PDOException $ignored) { 369*8da7d805SAndreas Gohr // add the opt table - if this fails too, let the exception bubble up 370*8da7d805SAndreas Gohr $sql = "CREATE TABLE IF NOT EXISTS opts (opt TEXT NOT NULL PRIMARY KEY, val NOT NULL DEFAULT '')"; 371*8da7d805SAndreas Gohr $this->exec($sql); 372*8da7d805SAndreas Gohr $this->setOpt('dbversion', 0); 373*8da7d805SAndreas Gohr return 0; 374*8da7d805SAndreas Gohr } 375*8da7d805SAndreas Gohr } 376*8da7d805SAndreas Gohr 377*8da7d805SAndreas Gohr /** 378*8da7d805SAndreas Gohr * Get the version this db should have 379*8da7d805SAndreas Gohr * 380*8da7d805SAndreas Gohr * @return int 381*8da7d805SAndreas Gohr * @throws \PDOException 382*8da7d805SAndreas Gohr */ 383*8da7d805SAndreas Gohr protected function latestDbVersion() 384*8da7d805SAndreas Gohr { 385*8da7d805SAndreas Gohr if (!file_exists($this->schemadir . '/latest.version')) { 386*8da7d805SAndreas Gohr throw new \PDOException('No latest.version in schema dir'); 387*8da7d805SAndreas Gohr } 388*8da7d805SAndreas Gohr return (int)trim(file_get_contents($this->schemadir . '/latest.version')); 389*8da7d805SAndreas Gohr } 390*8da7d805SAndreas Gohr 391*8da7d805SAndreas Gohr /** 392*8da7d805SAndreas Gohr * Get the migrartion file for the given version 393*8da7d805SAndreas Gohr * 394*8da7d805SAndreas Gohr * @param int $version 395*8da7d805SAndreas Gohr * @return string 396*8da7d805SAndreas Gohr */ 397*8da7d805SAndreas Gohr protected function getMigrationFile($version) 398*8da7d805SAndreas Gohr { 399*8da7d805SAndreas Gohr return sprintf($this->schemadir . '/update%04d.sql', $version); 400*8da7d805SAndreas Gohr } 401*8da7d805SAndreas Gohr // endregion 402*8da7d805SAndreas Gohr} 403