view marccore.php @ 4:c642254dc9ee draft default tip

Fixed transfer chain generation and construction of empty updates, some small improvements in tools
author Ivo Smits <Ivo@UCIS.nl>
date Sat, 22 Nov 2014 18:18:52 +0100
parents 5c8c4fa95803
children
line wrap: on
line source

<?php
if (!function_exists('hex2bin')) { function hex2bin($h) { return pack("H*", $h); } }

class MARCUpdate implements ArrayAccess {
	private $_version, $_key, $_serial, $_label, $_extensions, $_value = FALSE, $_valueoffset = -1, $_message, $_tags = array();
	public function __get($name) {
		switch (strtolower($name)) {
			case 'version': return $this->_version;
			case 'key': return $this->_key;
			case 'serial': return $this->_serial;
			case 'label': return $this->_label;
			case 'expiration': return (isset($this->_extensions[4]) && strlen($this->_extensions[4]) >= 4) ? marc_decode_int32be($this->_extensions[4]) : NULL;
			case 'transfer': return isset($this->_extensions[1]) ? $this->_extensions[1] : NULL;
			case 'transferchain': return isset($this->_extensions[5]) ? $this->_extensions[5] : NULL;
			case 'value':
				if ($this->_value === FALSE) $this->_value = self::DecodeValue($this->_message, $this->_valueoffset);
				return $this->_value;
			case 'updatemessage': return $this->_message;
			case 'tags': return $this->_tags; break;
			default: throw new Exception('Property '.$name.' does not exist');
		}
	}
	public function __set($name, $value) {
		switch (strtolower($name)) {
			case 'version': case 'key': case 'serial': case 'label': case 'value': case 'expiration': case 'transfer': case 'updatemessage': case 'tags': throw new Exception('Property '.$name.' is read only');
			default: throw new Exception('Property '.$name.' does not exist or is read-only');
		}
	}
	public function __isset($name) {
		switch (strtolower($name)) {
			case 'version': case 'key': case 'serial': case 'label': case 'value': case 'updatemessage': case 'tags': return TRUE;
			case 'expiration': return isset($this->_extensions[4]);
			case 'transfer': return isset($this->_extensions[1]);
			case 'transferchain': return isset($this->_extensions[5]);
			default: return FALSE;
		}
	}
	public function __unset($name) {
		switch (strtolower($name)) {
			case 'version': case 'key': case 'serial': case 'label': case 'value': case 'expiration': case 'transfer': case 'updatemessage': case 'tags': throw new Exception('Property '.$name.' is read only');
			default: throw new Exception('Property '.$name.' does not exist');
		}
	}
	public function offsetSet($offset, $value) { $this->__set($offset, $value); }
	public function offsetExists($offset) { return $this->__isset($offset); }
	public function offsetUnset($offset) { $this->__unset($offset); }
	public function offsetGet($offset) { return $this->__get($offset); }

	private function __construct() { }
	public static function Decode($data) {
		$upd = new MARCUpdate();
		$upd->_message = $data;
		if (strlen($data) < 1 + NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES) return FALSE;
		$upd->_version = ord($data[0]);
		if ($upd->_version != 2) return FALSE;
		$upd->_key = substr($data, 1, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES);
		$i = NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES + 64 + 1;
		$l = strlen($data);
		if (!$l || $l < $i+4+1+1) return FALSE;
		$upd->_serial = marc_decode_int32be($data, $i); $i += 4;
		$labellen = ord($data[$i++]);
		if ($l < $i+$labellen+1) return FALSE;
		$upd->_label = substr($data, $i, $labellen); $i += $labellen;
		$upd->_extensions = array();
		for ($numext = ord($data[$i++]); $numext > 0; $numext--) {
			if ($l < $i+1+2) return FALSE;
			$extid = ord($data[$i++]);
			$extlen = marc_decode_int16be($data, $i); $i += 2;
			if ($l < $i + $extlen) return FALSE;
			$upd->_extensions[$extid] = substr($data, $i, $extlen);
			$i += $extlen;
		}
		$upd->_valueoffset = $i;
		return $upd;
	}
	public function Verify() {
		$data = $this->_message;
		if (strlen($data) < 1 + NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES) return FALSE;
		$version = ord($data[0]);
		if ($version != 2) return FALSE;
		$key = substr($data, 1, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES);
		$data = substr($data, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES + 1);
		$data = nacl_crypto_sign_ed25519_open($data, $key);
		return $data !== NULL && $data !== FALSE;
	}
	public function Create($upd, $seckey, $current = NULL) {
		if (strlen($seckey) == 32) nacl_crypto_sign_ed25519_keypair($seckey, $seckey);
		if (strlen($seckey) < 64) throw new Exception('Signing key is not valid');
		if (!isset($upd['key'])) $upd['key'] = substr($seckey, 32, 32);
		if ($upd['key'] != substr($seckey, 32, 32)) throw new Exception('Resource key is not valid');
		if (!isset($upd['label'])) throw new Exception('Resource label not set');
		$upd['label'] = (string)$upd['label'];
		if (strlen($upd['label']) > 255) throw new Exception('Resource label too long');
		if (!isset($upd['serial'])) $upd['serial'] = time();
		if ($current) {
			if ($upd['serial'] <= $current['serial']) $upd['serial'] = $current['serial'] + 1;
			if (!self::CanImport($upd, $current)) throw new Exception('Can not update resource');
		}
		if (isset($upd['transfer']) && (strlen($upd['transfer']) != 0 && strlen($upd['transfer']) != NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES)) throw new Exception('Transfer recipient key is not valid');
		if ($current) {
			unset($upd['transferchain']);
			if (isset($current['transferchain']) && ($chain = self::Decode($current['transferchain'])) && $chain->Verify() && ($current['key'] == $upd['key'] || ($chain->key == $current['key'] && $chain->serial == $current['serial']))) {
				$chain = $chain;
			} elseif (isset($current['updatemessage']) && $current['key'] != $upd['key']) {
				$chain = $current;
			} else {
				$chain = NULL;
			}
			while ($chain && $chain->key == $upd['key']) $chain = isset($chain->transferchain) ? self::Decode($chain->transferchain) : NULL;
			if ($chain && $chain->Verify() && $chain->serial >= time() - 365*24*60*60) $upd['transferchain'] = $chain->updatemessage;
		}
		if (isset($upd['transfer']) && isset($upd['value']) && !is_null($upd['value'])) {
			$chain = array('label' => $upd['label'], 'serial' => $upd['serial'], 'key' => $upd['key'], 'transfer' => $upd['transfer']);
			if (isset($upd['expiration'])) $chain['expiration'] = $upd['expiration'];
			if (isset($upd['transferchain'])) $chain['transferchain'] = $upd['transferchain'];
			$chain = self::Create($chain, $seckey);
			if ($chain && strlen($chain->updatemessage) <= 0xffff) $upd['transferchain'] = $chain->updatemessage;
		}
		$data = marc_encode_int32be($upd['serial']);
		$data .= chr(strlen($upd['label'])).$upd['label'];
		$value = array();
		if (isset($upd->_extensions)) foreach ($upd->_extensions as $identifier => $item) $value[$identifier] = $item;
		if (isset($upd['transfer'])) $value[1] = $upd['transfer'];
		if (isset($upd['expiration'])) $value[4] = marc_encode_int32be($upd['expiration']);
		if (isset($upd['transferchain'])) $value[5] = $upd['transferchain'];
		if (count($value) > 0xff) throw new Exception('Too many extensions used');
		$data .= chr(count($value));
		foreach ($value as $identifier => $item) {
			$item = (string)$item;
			if (strlen($item) > 0xffff) throw new Exception('Extension data too big');
			$data .= chr($identifier).marc_encode_int16be(strlen($item)).$item;
		}
		$data .= self::EncodeValue(isset($upd['value']) ? $upd['value'] : NULL);
		$data = nacl_crypto_sign_ed25519($data, $seckey);
		if (!strlen($data)) throw new Exception('Failed to sign data');
		if (!strlen(nacl_crypto_sign_ed25519_open($data, $upd['key']))) throw new Exception('Key pair is not valid');
		$data = chr(2).$upd['key'].$data;
		return self::Decode($data);
	}
	public function ToArray() {
		$arr = array('version' => $this->version, 'key' => $this->key, 'serial' => $this->serial, 'label' => $this->label, 'value' => $this->value);
		if (isset($this->expiration)) $arr['expiration'] = $this->expiration;
		if (isset($this->transfer)) $arr['transfer'] = $this->transfer;
		if (isset($this->transferchain)) $arr['transferchain'] = $this->transferchain;
		return $arr;
	}

	private static function EncodeValue($data) {
		if (is_null($data)) {
			return chr(0);
		} else if (is_scalar($data)) {
			return chr(1).(string)$data;
		} else if (is_array($data)) {
			$iscollection = TRUE;
			foreach ($data as $key => $value) if (!is_int($key) || $key < 0) $iscollection = FALSE;
			$ret = $iscollection ? chr(2) : chr(3);
			foreach ($data as $key => $value) {
				if (!$iscollection) {
					if (strlen($key) > 0xff) throw new Exception('Dictionary key is too long');
					$ret .= chr(strlen($key)).$key;
				}
				$bytes = self::EncodeValue($value);
				$ret .= marc_encode_int32be(strlen($bytes)).$bytes;
			}
			return $ret;
		} else {
			throw new Exception('Unable to encode type '.gettype($data));
		}
	}
	private static function DecodeValue($data, $offset = 0, $length = -1) {
		if ($length == -1) $length = strlen($data) - $offset;
		if ($length < 0) throw new Exception('Truncated');
		if ($length == 0) return NULL;
		$type = ord($data[$offset]); $offset++; $length--;
		switch ($type) {
			case 0: return NULL;
			case 1: return substr($data, $offset, $length);
			case 2:
				$value = array();
				while ($length > 0) {
					if (4 > $length) throw new Exception('Truncated');
					$len = marc_decode_int32be($data, $offset); $offset += 4; $length -= 4;
					if ($len > $length) throw new Exception('Truncated');
					$value[] = self::DecodeValue($data, $offset, $len); $offset += $len; $length -= $len;
				}
				return $value;
			case 3:
				$value = array();
				while ($length > 0) {
					if (1 > $length) throw new Exception('Truncated');
					$len = ord($data[$offset]); $offset++; $length--;
					if ($len + 4 > $length) throw new Exception('Truncated');
					$key = substr($data, $offset, $len); $offset += $len; $length -= $len;
					$len = marc_decode_int32be($data, $offset); $offset += 4; $length -= 4;
					if ($len > $length) throw new Exception('Truncated');
					$value[$key] = self::DecodeValue($data, $offset, $len); $offset += $len; $length -= $len;
				}
				return $value;
			default: throw new Exception('Unsupported type code '.$type);
		}
	}
	public static function CanImport($nw, $cu = NULL) {
		if (!$nw || !isset($nw['label'])) return FALSE;
		if ($nw['serial'] > time() + 7*24*60*60) return FALSE;
		if ($cu) {
			if ($nw['label'] != $cu['label']) return FALSE;
			if ($cu['serial'] >= $nw['serial']) return FALSE;
			if ($cu['key'] == $nw['key']) return TRUE;
			if (isset($cu['expiration']) && $cu['expiration'] < time()) return TRUE;
			if ($cu['serial'] < time() - 365*24*60*60) return TRUE;
			if (isset($cu['transfer']) && (!strlen($cu['transfer']) || $cu['transfer'] == $nw['key'])) return TRUE;
			if (isset($nw['transferchain']) && ($chain = MARCUpdate::Decode($nw['transferchain'])) && $chain->Verify() && self::CanImport($nw, $chain) && self::CanImport($chain, $cu)) return TRUE;
			return FALSE;
		} else {
			if ($nw['serial'] < time() - 365*24*60*60) return FALSE;
			if (isset($nw['expiration']) && $nw['expiration'] < time()) return FALSE;
			return TRUE;
		}
	}
}
abstract class MARCDatabase {
	private $_importResourceFilterCallbacks = array(), $_importResourceCallbacks = array();
	public abstract function GetResource($label);
	protected abstract function ImportInternal($resource);
	public abstract function GetResources($labelprefix = NULL, $mints = NULL);
	public abstract function DeleteResource($label);
	public function Import($resource, $force = FALSE) {
		if (is_string($resource)) $resource = MARCUpdate::Decode($resource);
		if (!$resource) return FALSE;
		$current = $this->GetResource($resource['label']);
		if (!$force && !$this->CanImportResource($resource, $current)) return FALSE;
		if (!$resource->Verify()) return FALSE;
		if (!$this->ImportInternal($resource)) return FALSE;
		$this->ResourceImported($resource);
		return $resource;
	}
	protected function CanImportResource($resource, $current) {
		foreach ($this->_importResourceFilterCallbacks as $callback) if (!call_user_func($callback, $this, $resource, $current)) return FALSE;
		if (!MARCUpdate::CanImport($resource, $current)) return FALSE;
		return TRUE;
	}
	protected function ResourceImported($resource) {
		foreach ($this->_importResourceCallbacks as $callback) call_user_func($callback, $this, $resource);
	}
	public function RegisterResourceFilterCallback($callback) {
		$this->_importResourceFilterCallbacks[] = $callback;
	}
	public function RegisterResourceImportCallback($callback) {
		$this->_importResourceCallbacks[] = $callback;
	}
	public function UpdateResource($resource, $seckey, $force = FALSE) {
		if (is_a($resource, 'MARCUpdate')) $resource = $resource->ToArray();
		if (!$force) unset($resource['serial'], $resource['key']);
		$res = MARCUpdate::Create($resource, $seckey, $force ? NULL : $this->GetResource($resource['label']));
		if (!$res) return FALSE;
		return $this->Import($res, $force);
	}
	public function SyncHTTP($server, $options = array()) {
		$log = isset($options['log']) ? $options['log'] : TRUE;
		$result = array();
		$method = isset($options['method']) ? $options['method'] : 'PUT';
		if ($method != 'POST' && $method != 'PUT' && $method != 'GET') throw new Exception('Unsupported method '.$method);
		$result['exported'] = 0;
		$post = NULL;
		if ($method != 'GET' && (!isset($options['noexport']) || !$options['noexport'])) {
			$post = $method == 'POST' ? array() : '';
			$updates = $this->GetResources(NULL, isset($options['exporttimestamp']) ? $options['exporttimestamp'] : NULL);
			foreach ($updates as $update) {
				if (!is_string($update)) $update = $update['updatemessage'];
				if ($method == 'PUT') {
					$post .= marc_encode_int32be(strlen($update)).$update;
				} else {
					$post[] = 'update[]='.urlencode($update);
				}
				$result['exported']++;
			}
			$updates = NULL;
			if ($method == 'POST') $post = implode('&', $post);
		}
		if ($log) echo "Sending ".strlen($post)." bytes... ";
		$result['bytessent'] = strlen($post);
		$ch = curl_init();
		$timestamp = isset($options['importtimestamp']) ? intval($options['importtimestamp']) : 0;
		curl_setopt($ch, CURLOPT_URL, $server.'?version=3&get='.$timestamp);
		if ($method == 'PUT') curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
		else if ($method == 'POST') curl_setopt($ch, CURLOPT_POST, TRUE);
		if ($post != NULL) curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
		$post = NULL;
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
		curl_setopt($ch, CURLOPT_TIMEOUT, 60);
		$response = curl_exec($ch);
		if ($response === FALSE) throw new Exception('HTTP request failed');
		if ($log) echo "received ".strlen($response)." bytes.\n";
		$result['bytesreceived'] = strlen($response);
		if (($httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE)) != 200) throw new Exception('Received unexpected HTTP code '.$httpcode.' - '.(strlen($response)?var_export($response,TRUE):''));
		if (($contenttype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE)) != 'application/octet-stream') throw new Exception('Received unexpected content type '.$contenttype.' - '.(strlen($response)?var_export($response,TRUE):''));
		curl_close($ch);
		$i = 0;
		if ($i + 2 > strlen($response)) throw new Exception('Data has been truncated');
		$version = ord($response[$i++]);
		if ($version != 3) throw new Exception('Unsupported protocol version '.$version);
		for ($numext = ord($response[$i++]); $numext > 0; $numext--) {
			if ($i + 3 > strlen($response)) throw new Exception('Truncated');
			$extid = ord($response[$i++]);
			$extlen = marc_decode_int16be($response, $i); $i += 2;
			if ($i + $extlen > strlen($response)) throw new Exception('Truncated');
			switch ($extid) {
				case 2:
					if ($extlen >= 4 * 3) {
						$result['remotereceived'] = marc_decode_int32be($response, $i + 0);
						$result['remoteimported'] = marc_decode_int32be($response, $i + 4);
						$result['remoteexported'] = marc_decode_int32be($response, $i + 8);
					}
					break;
				case 3: if ($extlen >= 4) $result['importtimestamp'] = marc_decode_int32be($response, $i); break;
			}
			$i += $extlen;
		}
		if ($log) echo "Exported ".$result['exported']." updates of which ".(isset($result['remoteimported']) ? $result['remoteimported'] : 'none')." were imported.\n";
		$result['updates'] = array();
		$result['imported'] = 0;
		$result['updatesreceived'] = 0;
		while ($i < strlen($response)) {
			if ($i + 4 > strlen($response)) throw new Exception('Truncated');
			$len = marc_decode_int32be($response, $i); $i += 4;
			if ($i + $len > strlen($response)) throw new Exception('Truncated');
			$result['updatesreceived']++;
			$value = substr($response, $i, $len); $i += $len;
			$res = MARCUpdate::Decode($value);
			$result['updates'][] = $res;
			if (!$this->Import($value)) continue;
			$result['imported']++;
		}
		if ($log) echo "Imported ".$result['imported']." out of ".$result['updatesreceived']." updates.\n";
		return $result;
	}
}
class MARCDatabaseFlatFile extends MARCDatabase {
	private $dbfile = NULL;
	private $resources = array();
	private $changed = FALSE;
	public function GetResource($label) {
		if (!isset($this->resources[$label])) return NULL;
		return $this->resources[$label]['update'];
	}
	public function DeleteResource($label) {
		if (!isset($this->resources[$label])) return FALSE;
		unset($this->resources[$label]);
		$this->changed = TRUE;
		return TRUE;
	}
	protected function ImportInternal($update) {
		$this->resources[$update['label']] = array('timestamp' => time(), 'update' => $update);
		$this->changed = TRUE;
		return TRUE;
	}
	public function GetResources($labelprefix = NULL, $mints = NULL) {
		return new MARCDatabaseFlatFileFilteredResourceIterator(new ArrayIterator($this->resources), $labelprefix, $mints);
	}
	public function __construct($filename = FALSE) {
		if ($filename) $this->Open($filename);
	}
	private function OpenFile($filename) {
		$this->Close();
		$this->dbfile = fopen($filename, 'c+');
		if (!$this->dbfile) throw new Exception('Could not open database file');
		if (!flock($this->dbfile, LOCK_EX)) throw new Exception('Could not lock database file');
	}
	public function Open($filename) {
		$this->OpenFile($filename);
		$this->resources = array();
		$this->changed = FALSE;
		rewind($this->dbfile);
		while (true) {
			$data = fread($this->dbfile, 8);
			if (strlen($data) == 0) break;
			if (strlen($data) != 8) throw new Exception('Database truncated');
			$ts = marc_decode_int32be($data, 0);
			$len = marc_decode_int32be($data, 4);
			$data = fread($this->dbfile, $len);
			if (strlen($data) != $len) throw new Exception('Database truncated');
			$res = MARCUpdate::Decode($data);
			if (!$res) continue;
			$this->resources[$res['label']] = array('timestamp' => $ts, 'update' => $res);
		}
	}
	public function Save() {
		if (!$this->dbfile) throw new Exception('No database file is open');
		rewind($this->dbfile);
		ftruncate($this->dbfile, 0);
		foreach ($this->resources as $res) {
			fwrite($this->dbfile, marc_encode_int32be($res['timestamp']));
			$u = (string)$res['update']['updatemessage'];
			fwrite($this->dbfile, marc_encode_int32be(strlen($u)));
			fwrite($this->dbfile, $u);
		}
		$this->changed = FALSE;
	}
	public function SaveAs($filename) {
		$this->OpenFile($filename);
		$this->Save();
	}
	public function Close() {
		if ($this->dbfile) {
			flock($this->dbfile, LOCK_UN);
			fclose($this->dbfile);
			$this->dbfile = NULL;
		}
	}
	public function IsChanged() {
		return $this->changed;
	}
}
class MARCDatabaseFlatFileFilteredResourceIterator implements Iterator {
	private $source, $labelprefixlength, $labelprefix, $mints;
	public function __construct($source, $labelprefix, $mints) {
		$this->source = $source;
		$this->labelprefixlength = is_null($labelprefix) ? 0 : strlen($labelprefix);
		$this->labelprefix = $labelprefix;
		$this->mints = intval($mints);
	}
	public function current() { $r = $this->source->current(); return $r ? $r['update'] : NULL; }
	public function key() { return $this->source->key(); }
	public function valid() { return $this->source->valid(); }
	public function next() { $this->source->next(); $this->findnext(); }
	public function rewind() { $this->source->rewind(); $this->findnext(); }
	private function findnext() {
		while ($this->source->valid()) {
			$c = $this->source->current();
			if ($c['timestamp'] >= $this->mints && (!$this->labelprefixlength || substr($c['update']['label'], 0, $this->labelprefixlength) == $this->labelprefix)) break;
			$this->source->next();
		}
	}
}
class MARCDatabaseSQLite extends MARCDatabase {
	private $pdo = NULL;
	protected function DBPrepareStatement($query, $args = NULL) {
		$stmt = $this->pdo->prepare($query);
		if (!is_array($args) && !is_null($args)) $args = array($args);
		$stmt->execute($args);
		return $stmt;
	}
	private function DBUpdate($query, $args = NULL) {
		$stmt = $this->DBPrepareStatement($query, $args);
		$cnt = $stmt->rowCount();
		$stmt->closeCursor();
		return $cnt;
	}
	public function __construct($filename) {
		$this->pdo = new PDO('sqlite:'.$filename);
		$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		$this->DBUpdate('CREATE TABLE IF NOT EXISTS `resources` (`timestamp` INTEGER, `label` BLOB PRIMARY KEY, `update` BLOB)');
	}
	public function GetResource($label) {
		$stmt = $this->DBPrepareStatement('SELECT `update` FROM `resources` WHERE `label` = ?', $label);
		$res = $stmt->fetchColumn();
		$stmt->closeCursor();
		if ($res === FALSE) return NULL;
		return MARCUpdate::Decode($res);
	}
	public function DeleteResource($label) {
		return $this->DBUpdate('DELETE FROM `resources` WHERE `label` = ?', $label) != 0;
	}
	protected function ImportInternal($update) {
		$args = array('d' => $update['updatemessage'], 't' => time(), 'l' => $update['label']);
		if ($this->DBUpdate('UPDATE `resources` SET `update` = :d, `timestamp` = :t WHERE `label` = :l', $args) == 0)
			$this->DBUpdate('INSERT OR IGNORE INTO `resources` (`label`, `update`, `timestamp`) VALUES (:l, :d, :t)', $args);
		return TRUE;
	}
	public function GetResources($labelprefix = NULL, $mints = NULL) {
		$labelprefix = is_null($labelprefix) ? '' : (string)$labelprefix;
		return $this->GetResourcesFromQuery('SELECT `update` FROM `resources` WHERE `timestamp` >= ? AND SUBSTR(`label`, 1, ?) = ? ORDER BY `label`', array(intval($mints), strlen($labelprefix), $labelprefix));
	}
	public function GetResourcesFromQuery($query, $args = NULL) {
		return $this->GetResourcesFromStatement($this->DBPrepareStatement($query, $args));
	}
	public function GetResourcesFromStatement($statement) {
		return new MARCDatabaseSQLiteResourceIterator($statement);
	}
	public function IsChanged() {
		return FALSE;
	}
	public function Close() {
	}
}
class MARCDatabaseSQLiteResourceIterator implements Iterator {
	private $source, $current = FALSE;
	public function __construct($source) { $this->source = $source; }
	public function current() { return $this->current === FALSE ? NULL : MARCUpdate::Decode($this->current); }
	public function key() { return NULL; }
	public function valid() { return $this->current !== FALSE; }
	public function next() { $this->current = $this->source->fetchColumn(); if ($this->current === FALSE) $this->source->closeCursor(); }
	public function rewind() { $this->next(); }
}
class MARCDatabaseDBA extends MARCDatabase {
	private $db = NULL;
	public function __construct($path, $mode = 'cd', $handler = 'qdbm') {
		$this->db = dba_open($path, $mode, $handler);
		if ($this->db === FALSE) throw new Exception('Could not open database');
	}
	public function GetResource($label) {
		$r = dba_fetch($label, $this->db);
		if ($r === FALSE) return NULL;
		return MARCUpdate::Decode($r);
	}
	public function DeleteResource($label) {
		return dba_delete($label, $this->db);
	}
	protected function ImportInternal($update) {
		return dba_replace($update['label'], $update['updatemessage'], $this->db);
	}
	public function GetResources($labelprefix = NULL, $mints = NULL) {
		return new MARCDatabaseDBAFilteredResourceIterator($this->db, $labelprefix, $mints);
	}
	public function Save() {
		dba_sync($this->db);
	}
	public function Close() {
		dba_close($this->db);
	}
	public function IsChanged() {
		return FALSE;
	}
}
class MARCDatabaseDBAFilteredResourceIterator implements Iterator {
	private $db, $currentkey = FALSE, $labelprefixlength, $labelprefix, $mints;
	public function __construct($db, $labelprefix, $mints) {
		$this->db = $db;
		$this->labelprefixlength = is_null($labelprefix) ? 0 : strlen($labelprefix);
		$this->labelprefix = $labelprefix;
		$this->mints = intval($mints);
	}
	public function current() { return MARCUpdate::Decode(dba_fetch($this->currentkey, $this->db)); }
	public function key() { return $this->currentkey; }
	public function valid() { return strlen($this->currentkey); }
	public function next() { $this->currentkey = dba_nextkey($this->db); $this->findnext(); }
	public function rewind() { $this->currentkey = dba_firstkey($this->db); $this->findnext(); }
	private function findnext() { while ($this->currentkey !== FALSE && ($this->labelprefixlength && substr($this->currentkey, 0, $this->labelprefixlength) != $this->labelprefix)) $this->currentkey = dba_nextkey($this->db); }
}

function marc_decode_int32be($data, $i = 0) { return (ord($data[$i]) << 24) | (ord($data[$i+1]) << 16) | (ord($data[$i+2]) << 8) | ord($data[$i+3]); }
function marc_decode_int16be($data, $i = 0) { return (ord($data[$i+0]) << 8) | ord($data[$i+1]); }
function marc_encode_int32be($v) { return pack("N", $v); }
function marc_encode_int16be($v) { return pack("n", $v); }