Last active
May 6, 2022 02:40
-
-
Save dixyes/feec7e0794da940bd9e6497434b59c83 to your computer and use it in GitHub Desktop.
php FFI registry utilities
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Win32 api utitlities class | |
* needs to be initialized with W32api::init() | |
*/ | |
final class W32api | |
{ | |
private const BASIC_CDEFS = <<<'CDEF' | |
typedef uint16_t WORD, *PWORD, *LPWORD; | |
typedef uint32_t DWORD, *PDWORD, *LPDWORD; | |
typedef uint64_t SIZE_T; | |
typedef uint8_t BYTE, *LPBYTE; | |
typedef long LONG; | |
typedef unsigned long ULONG; | |
typedef unsigned int UINT; | |
typedef short SHORT; | |
typedef long LSTATUS, LRESULT; | |
typedef uint16_t LANGID; | |
typedef DWORD ACCESS_MASK; | |
typedef ACCESS_MASK REGSAM; | |
typedef void* LPVOID; | |
typedef void* HANDLE; | |
typedef HANDLE* PHANDLE; | |
typedef HANDLE HKEY, *PHKEY; | |
typedef int32_t BOOL, *LPBOOL; | |
typedef int16_t wchar_t; | |
typedef const wchar_t *LPCWSTR; | |
typedef wchar_t WCHAR, *LPWSTR; | |
typedef const char *LPCSTR; | |
typedef char CHAR, *LPSTR; | |
CDEF; | |
public const KERNEL32_CDEFS = <<<'CDEF' | |
int MultiByteToWideChar( | |
UINT CodePage, | |
DWORD dwFlags, | |
char *lpMultiByteStr, | |
int cbMultiByte, | |
LPWSTR lpWideCharStr, | |
int cchWideChar | |
); | |
int WideCharToMultiByte( | |
UINT CodePage, | |
DWORD dwFlags, | |
LPCWSTR lpWideCharStr, | |
int cchWideChar, | |
LPSTR lpMultiByteStr, | |
int cbMultiByte, | |
LPCSTR lpDefaultChar, | |
LPBOOL lpUsedDefaultChar | |
); | |
BOOL CloseHandle( | |
HANDLE hObject | |
); | |
LSTATUS RegCreateKeyExA( | |
HKEY hKey, | |
LPCSTR lpSubKey, | |
DWORD Reserved, | |
LPSTR lpClass, | |
DWORD dwOptions, | |
REGSAM samDesired, | |
const /*LPSECURITY_ATTRIBUTES*/ void* lpSecurityAttributes, | |
PHKEY phkResult, | |
LPDWORD lpdwDisposition | |
); | |
LSTATUS RegOpenKeyExA( | |
HKEY hKey, | |
LPCSTR lpSubKey, | |
DWORD ulOptions, | |
REGSAM samDesired, | |
PHKEY phkResult | |
); | |
LSTATUS RegCloseKey( | |
HKEY hKey | |
); | |
LSTATUS RegQueryValueExA( | |
HKEY hKey, | |
LPCSTR lpValueName, | |
LPDWORD lpReserved, | |
LPDWORD lpType, | |
LPBYTE lpData, | |
LPDWORD lpcbData | |
); | |
LSTATUS RegSetValueExA( | |
HKEY hKey, | |
LPCSTR lpValueName, | |
DWORD Reserved, | |
DWORD dwType, | |
LPVOID lpData, | |
DWORD cbData | |
); | |
LSTATUS RegDeleteTreeA( | |
HKEY hKey, | |
LPCSTR lpSubKey | |
); | |
LSTATUS RegEnumKeyExA( | |
HKEY hKey, | |
DWORD dwIndex, | |
LPSTR lpName, | |
LPDWORD lpcchName, | |
LPDWORD lpReserved, | |
LPSTR lpClass, | |
LPDWORD lpcchClass, | |
LPVOID lpftLastWriteTime | |
); | |
LSTATUS RegEnumValueA( | |
HKEY hKey, | |
DWORD dwIndex, | |
LPSTR lpValueName, | |
LPDWORD lpcchValueName, | |
LPDWORD lpReserved, | |
LPDWORD lpType, | |
char* lpData, | |
LPDWORD lpcbData | |
); | |
DWORD GetLastError(); | |
CDEF; | |
/** @removable */ | |
private function __construct() | |
{ | |
throw new Exception('this utility class should not be instance'); | |
} | |
/** | |
* make ffi objects by cdef with basic cdefs | |
* | |
* @param string $cdef cdef used | |
* @param string $dll dll to find symbols | |
* @return \FFI | |
*/ | |
public static function make(string $cdef, string $dll): FFI | |
{ | |
return \FFI::cdef(self::BASIC_CDEFS . $cdef, $dll); | |
} | |
/** | |
* kernel32 wrapper | |
* | |
* @var Kernel32API | |
*/ | |
public static \FFI $kernel32; | |
/** | |
* HKEY_CLASSES_ROOT handle | |
*/ | |
public static \FFI\CData $hkcr; | |
/** | |
* HKEY_CURRENT_USER handle | |
*/ | |
public static \FFI\CData $hkcu; | |
/** | |
* HKEY_LOCAL_MACHINE handle | |
*/ | |
public static \FFI\CData $hklm; | |
/** | |
* HKEY_USERS handle | |
*/ | |
public static \FFI\CData $hku; | |
/** | |
* HKEY_CURRENT_CONFIG handle | |
*/ | |
public static \FFI\CData $hkcc; | |
public static function init(): void | |
{ | |
/* @phpstan-ignore-next-line */ | |
self::$kernel32 = self::make(self::KERNEL32_CDEFS, 'kernel32.dll'); | |
foreach ([ | |
"hkcr" => "\x00\x00\x00\x80" /* 0x80000000 HKEY_CLASSES_ROOT */, | |
"hkcu" => "\x01\x00\x00\x80" /* 0x80000001 HKEY_CURRENT_USER */, | |
"hklm" => "\x02\x00\x00\x80" /* 0x80000002 HKEY_LOCAL_MACHINE */, | |
"hku" => "\x03\x00\x00\x80" /* 0x80000003 HKEY_USERS */, | |
"hkcc" => "\x04\x00\x00\x80" /* 0x80000004 HKEY_CURRENT_CONFIG */, | |
] as $name => $data) { | |
/* @phpstan-ignore-next-line */ | |
$hkey = self::$kernel32->new('HKEY', false, true); | |
/* @removable */ | |
if ($hkey === null) { | |
throw new Exception('failed create ffi object'); | |
} | |
\FFI::memcpy(\FFI::addr($hkey), $data, 4); | |
static::${$name} = $hkey; | |
} | |
} | |
} | |
class RegistryValue extends stdClass implements Stringable | |
{ | |
const REG_SZ = 1; | |
const REG_EXPAND_SZ = 2; | |
const REG_BINARY = 3; | |
const REG_MULTI_SZ = 7; | |
const REG_DWORD = 4; | |
const REG_QWORD = 11; | |
public function __construct( | |
public ?int $type, | |
public string|int|bool|array $data, | |
) { | |
if ($this->type === null) { | |
switch (true) { | |
case is_bool($data): | |
case is_int($data): | |
$this->type = static::REG_DWORD; | |
break; | |
case is_string($data): | |
$this->type = static::REG_SZ; | |
break; | |
case is_array($data): | |
$this->type = static::REG_MULTI_SZ; | |
break; | |
default: | |
throw new Exception('not implemented'); | |
} | |
} | |
// validate | |
switch ($this->type) { | |
case static::REG_SZ: | |
case static::REG_EXPAND_SZ: | |
case static::REG_BINARY: | |
if (!is_string($this->data)) { | |
goto typeerror; | |
} | |
break; | |
case static::REG_MULTI_SZ: | |
if (!is_array($this->data)) { | |
goto typeerror; | |
} | |
foreach ($this->data as $v) { | |
if (!is_string($v)) { | |
throw new Exception('bad multi sz'); | |
} | |
} | |
break; | |
case static::REG_DWORD: | |
case static::REG_QWORD: | |
switch (true) { | |
case is_int($this->data): | |
break; | |
case is_bool($this->data): | |
$this->data = $this->data ? 1 : 0; | |
break; | |
case is_string($this->data): | |
if ($this->type === static::REG_QWORD && strlen($this->data) === 8) { | |
break; | |
} | |
throw new Exception("bad dword/qword value \"{$this->data}\""); | |
default: | |
goto typeerror; | |
} | |
break; | |
default: | |
throw new Exception('not implemented'); | |
} | |
return; | |
typeerror: | |
if ($data instanceof \FFI\CData) { | |
return; | |
} | |
throw new TypeError("cannot use {$this->data} for type {$this->type}"); | |
} | |
/** | |
* make cdata for setting reg value | |
* | |
* @return array{0: \FFI\CData|str, 1: int} | |
*/ | |
public function makeCData(): array | |
{ | |
if ($this->data instanceof \FFI\CData) { | |
return [$this->data, \FFI::sizeof($this->data)]; | |
} | |
switch ($this->type) { | |
case static::REG_SZ: | |
case static::REG_EXPAND_SZ: | |
case static::REG_BINARY: | |
$cvalue = $this->data; | |
$clen = strlen($this->data); | |
break; | |
case static::REG_MULTI_SZ: | |
$data = array_map(fn ($str) => $str . "\0", $this->data); | |
$cvalue = implode('', $data) . "\0"; | |
$clen = strlen($cvalue); | |
break; | |
case static::REG_DWORD: | |
$clen = 4; | |
$cvalue = \FFI::new('char[4]'); | |
$cintvalue = \FFI::new('uint32_t'); | |
$cintvalue->cdata = $this->data; | |
\FFI::memcpy($cvalue, $cintvalue, 4); | |
break; | |
case static::REG_QWORD: | |
$clen = 8; | |
if (is_string($this->data)) { | |
$cvalue = \FFI::new('char[8]'); | |
\FFI::memcpy($cvalue, $this->data, 8); | |
break; | |
} | |
$cvalue = \FFI::new('char[8]'); | |
$cintvalue = \FFI::new('uint64_t'); | |
$cintvalue->cdata = $this->data; | |
\FFI::memcpy($cvalue, $cintvalue, 8); | |
break; | |
default: | |
throw new Exception('not implemented'); | |
} | |
return [$cvalue, $clen]; | |
} | |
public function toNumber(): int | |
{ | |
switch ($this->type) { | |
case static::REG_DWORD: | |
if ($this->data instanceof \FFI\CData) { | |
return $this->data->cdata; | |
} | |
return $this->data; | |
case static::REG_QWORD: | |
if ($this->data instanceof \FFI\CData || is_string($this->data)) { | |
$uint64 = \FFI::new('uint64_t'); | |
\FFI::memcpy($uint64, $this->data, 8); | |
return $uint64->cdata; | |
} | |
return $this->data; | |
default: | |
throw new Exception('not implemented'); | |
} | |
} | |
public function __toString(): string | |
{ | |
$data = $this->data; | |
switch ($this->type) { | |
case static::REG_DWORD: | |
case static::REG_QWORD: | |
$data = $this->toNumber(); | |
break; | |
case static::REG_BINARY: | |
case static::REG_EXPAND_SZ: | |
case static::REG_SZ: | |
if ($data instanceof \FFI\CData) { | |
$data = \FFI::string($data); | |
} | |
break; | |
default: | |
throw new Exception('not implemented'); | |
} | |
return (string)$data; | |
} | |
public static function typeToName(int $type): string | |
{ | |
// I want enum... | |
switch ($type) { | |
case static::REG_SZ: | |
return 'REG_SZ'; | |
case static::REG_EXPAND_SZ: | |
return 'REG_EXPAND_SZ'; | |
case static::REG_BINARY: | |
return 'REG_BINARY'; | |
case static::REG_MULTI_SZ: | |
return 'REG_MULTI_SZ'; | |
case static::REG_DWORD: | |
return 'REG_DWORD'; | |
case static::REG_QWORD: | |
return 'REG_QWORD'; | |
default: | |
throw new Exception('not implemented'); | |
} | |
} | |
} | |
/** | |
* registry key class, use arrayaccess apis to curd values | |
* | |
* @implements \ArrayAccess<string|null,int|string|null> | |
*/ | |
class RegistryKey implements ArrayAccess, IteratorAggregate | |
{ | |
/** | |
* readonly permission | |
*/ | |
public const KEY_READ = 0x20019; | |
/** | |
* readable and writable permission | |
*/ | |
public const KEY_WRITE = 0x20006; | |
final private function __construct( | |
private FFI\CData $hkey, | |
private string $key, | |
private int $permission, | |
) { | |
} | |
public function __destruct() | |
{ | |
W32api::$kernel32->RegCloseKey($this->hkey); | |
\FFI::free(\FFI::addr($this->hkey)); | |
} | |
public function __toString(): string | |
{ | |
return 'RegistryKey<' . $this->key . '>'; | |
} | |
public function __debugInfo() | |
{ | |
/** | |
* ffi may try to read memory address which handle value refered to, then segfault | |
*/ | |
return ['hkey' => '<redacted>', 'key' => $this->key]; | |
} | |
/** | |
* create a registry key, can also open existing key | |
* | |
* @param string $key key name | |
* @param null|FFI\CData|self $root root key, default is HKLM | |
* @param int|null $permission permission, default is rw | |
* @return static|null created/opened regkey | |
*/ | |
public static function create(string $key, null|\FFI\CData|self $root = null, ?int $permission = null): ?static | |
{ | |
$hkey = W32api::$kernel32->new('HKEY', false, true); | |
/* @removable */ | |
if ($hkey === null) { | |
throw new Exception('failed create ffi object'); | |
} | |
$hkey_root = $root instanceof \FFI\CData ? $root : ($root ? $root->hkey : W32api::$hklm); | |
$full_key = $root instanceof static ? static::joinPath($root->key, $key) : $key; | |
$permission = $permission ?? static::KEY_WRITE; | |
$ret = W32api::$kernel32->RegCreateKeyExA( | |
$hkey_root, // hkey | |
$key, // subkey | |
0, // reserved | |
null, // class | |
0, // options | |
$permission, // sam | |
null, // sec attr | |
\FFI::addr($hkey), // pointer to hkey | |
null // dispos | |
); | |
if ($ret !== 0) { | |
\FFI::free(\FFI::addr($hkey)); | |
throw new Exception("failed create key \"{$full_key}\": {$ret}"); | |
} | |
return new static($hkey, $key, $permission); | |
} | |
/** | |
* open exist key | |
* | |
* @param string $key key path | |
* @param null|FFI\CData|self $root root key, default is HKLM | |
* @param int|null $permission permission, default is ro | |
*/ | |
public static function open(string $key, null|\FFI\CData|self $root = null, ?int $permission = null): ?static | |
{ | |
$hkey = W32api::$kernel32->new('HKEY', false, true); | |
/* @removable */ | |
if ($hkey === null) { | |
throw new Exception('failed create ffi object'); | |
} | |
$hkey_root = $root instanceof \FFI\CData ? $root : ($root ? $root->hkey : W32api::$hklm); | |
$full_key = $root instanceof static ? static::joinPath($root->key, $key) : $key; | |
$permission = $permission ?? static::KEY_READ; | |
$ret = W32api::$kernel32->RegOpenKeyExA( | |
$hkey_root, // hkey | |
$key, // subkey | |
0, // options | |
$permission, // sam | |
\FFI::addr($hkey) // pointer to hkey | |
); | |
if ($ret !== 0) { | |
\FFI::free(\FFI::addr($hkey)); | |
if ($ret/* ERROR_FILE_NOT_FOUND */ !== 2) { | |
throw new Exception("failed open key \"{$full_key}\": {$ret}"); | |
} | |
return null; | |
} | |
return new static($hkey, $full_key, $permission); | |
} | |
/** | |
* create sub key | |
* | |
* @param string $key key path | |
* @param int|null $permission permission, default is rw | |
*/ | |
public function createSubKey(string $key, ?int $permission = null): ?static | |
{ | |
return static::create($key, $this->hkey, $permission); | |
} | |
/** | |
* open sub exist key | |
* | |
* @param string $key key path | |
* @param int|null $permission permission, default is ro | |
*/ | |
public function openSubKey(string $key, ?int $permission = null): ?static | |
{ | |
return static::open($key, $this->hkey, $permission); | |
} | |
/** | |
* join reg path | |
*/ | |
public static function joinPath(string $a, string $b): string | |
{ | |
return rtrim($a, '\\') . '\\' . ltrim($b, '\\'); | |
} | |
/** | |
* enum keys under a key | |
* | |
* @return Iterable<string> | |
*/ | |
public function enumrateSubKeys(): Iterable | |
{ | |
$keyName = \FFI::new('char[4096]'); | |
$keyNameLen = W32API::$kernel32->new('DWORD', false, true); | |
$ret = 0; | |
$index = 0; | |
while ($ret != 259/*ERROR_NO_MORE_ITEMS*/) { | |
$keyNameLen->cdata = \FFI::sizeof($keyName); | |
$ret = W32api::$kernel32->RegEnumKeyExA( | |
$this->hkey, // hkey | |
$index++, // index | |
$keyName, // out: key name | |
\FFI::addr($keyNameLen), // in, out: key name size | |
null, | |
null, // out: key class | |
null, // in, out: size of key class | |
null, // in, out: last written | |
); | |
if ($ret == 259/*ERROR_NO_MORE_ITEMS*/) { | |
break; | |
} | |
if ($ret !== 0) { | |
\FFI::free($keyNameLen); | |
throw new Exception("failed enumurate key \"{$this->key}\" values: {$ret}"); | |
} | |
$name = \FFI::string($keyName, $keyNameLen->cdata); | |
yield $name; | |
} | |
\FFI::free($keyNameLen); | |
} | |
/** | |
* prepare buffer cdata from RegEnumValueA or RegQueryValueExA | |
*/ | |
private static function prepareCData(int $type, int $size): \FFI\CData | |
{ | |
switch ($type) { | |
case RegistryValue::REG_SZ: | |
case RegistryValue::REG_EXPAND_SZ: | |
case RegistryValue::REG_BINARY: | |
case RegistryValue::REG_MULTI_SZ: | |
$cval = \FFI::new("char[{$size}]", false, true); | |
break; | |
case RegistryValue::REG_DWORD: | |
case RegistryValue::REG_QWORD: | |
$cval = \FFI::new('uint64_t[1]', false, true); | |
break; | |
default: | |
throw new Exception('not implemented type ' . $type); | |
} | |
return $cval; | |
} | |
/** | |
* parse cdata from RegEnumValueA or RegQueryValueExA | |
*/ | |
private static function parseCData(int $type, \FFI\CData $cval, int $size, bool $useRegistryValue): mixed | |
{ | |
switch ($type) { | |
case RegistryValue::REG_SZ: | |
case RegistryValue::REG_EXPAND_SZ: | |
// omit endding '\0' | |
$size--; | |
case RegistryValue::REG_BINARY: | |
$retval = \FFI::string($cval, $size); | |
break; | |
case RegistryValue::REG_MULTI_SZ: | |
if ($size <= 1) { | |
$retval = []; | |
break; | |
} | |
$data = \FFI::string($cval, $size - 1); | |
$retval = explode("\0", $data); | |
array_pop($retval); | |
reset($retval); | |
break; | |
case RegistryValue::REG_DWORD: | |
$retval = $cval[0]; | |
break; | |
case RegistryValue::REG_QWORD: | |
return new RegistryValue(RegistryValue::REG_QWORD, \FFI::string($cval, 8)); | |
default: | |
throw new Exception('not implemented'); | |
} | |
if ($useRegistryValue) { | |
$retval = new RegistryValue($type, $retval); | |
} | |
return $retval; | |
} | |
/** | |
* enum values under a key | |
* | |
* @return Iterable<string, mixed> | |
* @phpstan-return ($useRegistryValue is true ? Iterable<string, RegistryValue> : Iterable<string, mixed>) | |
*/ | |
public function enumrateValues(bool $useRegistryValue = false): Iterable | |
{ | |
$valueName = \FFI::new("char[16384]"); | |
$valueNameLen = W32API::$kernel32->new('DWORD'); | |
$valueDataLen = W32API::$kernel32->new('DWORD'); | |
$valueType = W32API::$kernel32->new('DWORD'); | |
$ret = 0; | |
$index = 0; | |
while ($ret != 259/*ERROR_NO_MORE_ITEMS*/) { | |
// get data len | |
$valueNameLen->cdata = \FFI::sizeof($valueName); | |
$ret = W32api::$kernel32->RegEnumValueA( | |
$this->hkey, // hkey | |
$index, // index | |
$valueName, // out: value name | |
\FFI::addr($valueNameLen), // in, out: value name size | |
null, | |
\FFI::addr($valueType), // out: value type | |
null, // out: value data | |
\FFI::addr($valueDataLen), // in, out: size of value data | |
); | |
if ($ret == 259/*ERROR_NO_MORE_ITEMS*/) { | |
break; | |
} | |
if ($ret !== 0) { | |
throw new Exception("failed enumurate key \"{$this->key}\" size: {$ret}"); | |
} | |
// get data | |
$valueNameLen->cdata = \FFI::sizeof($valueName); | |
$valueData = static::prepareCData($valueType->cdata, $valueDataLen->cdata); | |
$ret = W32api::$kernel32->RegEnumValueA( | |
$this->hkey, // hkey | |
$index++, // index | |
$valueName, // out: value name | |
\FFI::addr($valueNameLen), // in, out: value name size | |
null, | |
\FFI::addr($valueType), // out: value type | |
\FFI::cast('char*', $valueData), // out: value data | |
\FFI::addr($valueDataLen), // in, out: size of value data | |
); | |
if ($ret == 259/*ERROR_NO_MORE_ITEMS*/) { | |
break; | |
} | |
if ($ret !== 0) { | |
throw new Exception("failed enumurate key \"{$this->key}\" values: {$ret}"); | |
} | |
if ($valueNameLen->cdata === 0) { | |
$name = null; | |
} else { | |
$name = \FFI::string($valueName, $valueNameLen->cdata); | |
} | |
$type = $valueType->cdata; | |
$data = static::parseCData($type, $valueData, $valueDataLen->cdata, $useRegistryValue); | |
yield $name => $data; | |
} | |
} | |
/** | |
* get value under a key | |
* | |
* @param string|null $valueName value name, null for "Default" value | |
*/ | |
public function getValue(?string $valueName, bool $useRegistryValue = false): mixed | |
{ | |
$retval = null; | |
try { | |
$type = W32api::$kernel32->new('DWORD', false, true); | |
/* @removable */ | |
if ($type === null) { | |
throw new Exception('failed create ffi object'); | |
} | |
$size = W32api::$kernel32->new('DWORD', false, true); | |
/* @removable */ | |
if ($size === null) { | |
throw new Exception('failed create ffi object'); | |
} | |
$ret = W32api::$kernel32->RegQueryValueExA($this->hkey, $valueName, null, \FFI::addr($type), null, \FFI::addr($size)); | |
if ($ret !== 0) { | |
if ($ret/* ERROR_FILE_NOT_FOUND */ !== 2) { | |
echo "failed read \"{$valueName}\" type and size: {$ret}" . PHP_EOL; | |
} | |
return null; | |
} | |
//var_dump($type->cdata, $size->cdata); | |
/** | |
* @var int $sizeInt | |
*/ | |
$sizeInt = $size->cdata; | |
$cval = static::prepareCData($type->cdata, $sizeInt); | |
/* @removable */ | |
if ($cval === null) { | |
throw new Exception('failed create ffi object'); | |
} | |
$ret = W32api::$kernel32->RegQueryValueExA($this->hkey, $valueName, null, null, \FFI::cast('uint8_t*', $cval), \FFI::addr($size)); | |
if ($ret !== 0) { | |
echo "failed read \"{$valueName}\" value: {$ret}" . PHP_EOL; | |
return null; | |
} | |
$retval = static::parseCData($type->cdata, $cval, $size->cdata, $useRegistryValue); | |
} finally { | |
if (isset($type)) { | |
\FFI::free($type); | |
} | |
if (isset($size)) { | |
\FFI::free($size); | |
} | |
if (isset($cval)) { | |
\FFI::free($cval); | |
} | |
} | |
return $retval; | |
} | |
/** | |
* deleta a reg key | |
* | |
* @param string $key key path | |
* @param CData|null $root root key, default is HKLM | |
* @return bool true is success, otherwise false | |
*/ | |
public static function delete(string $key, ?FFI\CData $root = null): bool | |
{ | |
$ret = W32api::$kernel32->RegDeleteTreeA($root ?? W32api::$hklm, $key); | |
if ($ret !== 0 && $ret/* ERROR_FILE_NOT_FOUND */ !== 2) { | |
throw new Exception("failed delete key \"{$key}\": {$ret}"); | |
} | |
return true; | |
} | |
public function offsetExists(mixed $key): bool | |
{ | |
if (!is_string($key)) { | |
throw new Exception("bad offset type: {$key}"); | |
} | |
return $this->offsetGet($key) !== null; | |
} | |
/** | |
* @param string $key | |
*/ | |
public function offsetGet(mixed $key): mixed | |
{ | |
return $this->getValue($key); | |
} | |
public function offsetSet(mixed $key, mixed $value): void | |
{ | |
if (!$value instanceof RegistryValue) { | |
$value = new RegistryValue(type: null, data: $value); | |
} | |
$type = $value->type; | |
[$cvalue, $clen] = $value->makeCData(); | |
$ret = W32api::$kernel32->RegSetValueExA($this->hkey, $key, 0, $type, $cvalue, $clen); | |
if ($ret !== 0) { | |
throw new Exception("failed set \"{$key}\" for {$this}: $ret"); | |
} | |
} | |
public function offsetUnset(mixed $offset): void | |
{ | |
throw new Exception('not implemented'); | |
} | |
public function getIterator(): Traversable | |
{ | |
return $this->enumrateValues(); | |
} | |
} | |
W32API::init(); | |
$ceshiKey = RegistryKey::create( | |
key: 'ceshi', | |
root: W32api::$hkcu, | |
permission: RegistryKey::KEY_WRITE | RegistryKey::KEY_READ | |
); | |
function myassert($thing, $msg = null) | |
{ | |
if (!$thing) { | |
throw new Exception('failed assert' . ($msg ? ': ' . $msg : '')); | |
} | |
} | |
function myassertSame($a, $b) | |
{ | |
if ($a !== $b) { | |
var_dump($a, $b); | |
myassert(false, "$a is not $b"); | |
} | |
} | |
function myassertNotSame($a, $b) | |
{ | |
if ($a === $b) { | |
var_dump($a, $b); | |
myassert(false, "$a is $b"); | |
} | |
} | |
function matchformat($str, $format) | |
{ | |
$matches = sscanf(trim($str), trim($format)); | |
foreach ($matches as $v) { | |
myassert($v !== null); | |
} | |
} | |
var_dump($ceshiKey); | |
ob_start(); | |
var_dump($ceshiKey); | |
$dumped = ob_get_clean(); | |
matchformat($dumped, ' | |
object(RegistryKey)#%d (%d) { | |
["hkey"]=> | |
string(%d) "<redacted>" | |
["key"]=> | |
string(%d) "ceshi" | |
}'); | |
print("$ceshiKey\n"); | |
myassertSame("$ceshiKey", "RegistryKey<ceshi>"); | |
$ceshiROKey = RegistryKey::open('ceshi', W32api::$hkcu); | |
myassert($ceshiROKey); | |
// tests setting value on "default" or a named value via literal values | |
foreach ([null, 'ceshiValue'] as $valueName) { | |
$ceshiKey[$valueName] = 0xcafebabe; | |
myassertSame($ceshiROKey[$valueName], 0xcafebabe); | |
$ceshiKey[$valueName] = 'cafebabe'; | |
myassertSame($ceshiROKey[$valueName], 'cafebabe'); | |
$ceshiKey[$valueName] = ['cafe', 'babe']; | |
myassertSame($ceshiROKey[$valueName], ['cafe', 'babe']); | |
$ceshiKey[$valueName] = false; | |
myassertSame($ceshiROKey[$valueName], 0); | |
$ceshiKey[$valueName] = true; | |
myassertSame($ceshiROKey[$valueName], 1); | |
} | |
// tests setting value on "default" or a named value via RegistryValue object | |
foreach ([null, 'ceshiValue'] as $valueName) { | |
// bool as dword | |
$ceshiKey[$valueName] = new RegistryValue(null, false); | |
myassertSame($ceshiKey[$valueName], 0); | |
myassertSame($ceshiROKey[$valueName], 0); | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_DWORD, false); | |
myassertSame($ceshiKey[$valueName], 0); | |
myassertSame($ceshiROKey[$valueName], 0); | |
$ceshiKey[$valueName] = new RegistryValue(null, true); | |
myassertSame($ceshiKey[$valueName], 1); | |
myassertSame($ceshiROKey[$valueName], 1); | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_DWORD, true); | |
myassertSame($ceshiKey[$valueName], 1); | |
myassertSame($ceshiROKey[$valueName], 1); | |
// guess dword | |
$ceshiKey[$valueName] = new RegistryValue(null, 0xcafebabe); | |
myassertSame($ceshiKey[$valueName], 0xcafebabe); | |
myassertSame($ceshiROKey[$valueName], 0xcafebabe); | |
// manually set dword | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_DWORD, 0xcafebabe); | |
myassertSame($ceshiKey[$valueName], 0xcafebabe); | |
myassertSame($ceshiROKey[$valueName], 0xcafebabe); | |
// manually set 32bit num int as qword, read as int | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_QWORD, 0xcafebabe); | |
myassert($ceshiROKey[$valueName] instanceof RegistryValue); | |
myassertSame($ceshiROKey[$valueName]->type, RegistryValue::REG_QWORD); | |
myassertSame($ceshiROKey[$valueName]->toNumber(), 0xcafebabe); | |
// manually set 63bit num int as qword, read as int | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_QWORD, 0x6463626134333231); | |
myassert($ceshiROKey[$valueName] instanceof RegistryValue); | |
myassertSame($ceshiROKey[$valueName]->type, RegistryValue::REG_QWORD); | |
myassertSame($ceshiROKey[$valueName]->toNumber(), 0x6463626134333231); | |
// manually set 64bit num int as qword, read as int | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_QWORD, -0x6463626134333231); | |
myassert($ceshiROKey[$valueName] instanceof RegistryValue); | |
myassertSame($ceshiROKey[$valueName]->type, RegistryValue::REG_QWORD); | |
myassertSame($ceshiROKey[$valueName]->toNumber(), -0x6463626134333231); | |
// manually set 8byte qword, read as int | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_QWORD, "\xef\xbe\xad\xde\xbe\xba\xfe\xca"); | |
myassert($ceshiROKey[$valueName] instanceof RegistryValue); | |
myassertSame($ceshiROKey[$valueName]->type, RegistryValue::REG_QWORD); | |
myassertSame($ceshiROKey[$valueName]->toNumber(), -0x3501454121524111); | |
// guess sz | |
$ceshiKey[$valueName] = new RegistryValue(null, 'cafebabe'); | |
myassertSame($ceshiROKey[$valueName], 'cafebabe'); | |
// manually set sz | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_SZ, 'cafebabe'); | |
myassertSame($ceshiROKey[$valueName], 'cafebabe'); | |
// manually set binary | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_SZ, "\0cafebabe\0\1\2"); | |
myassertSame($ceshiROKey[$valueName], "\0cafebabe\0\1\2"); | |
// manually set expandable | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_EXPAND_SZ, "cafebabe"); | |
myassertSame($ceshiROKey[$valueName], "cafebabe"); | |
// guess multi sz | |
$ceshiKey[$valueName] = new RegistryValue(null, ['cafe', 'babe']); | |
myassertSame($ceshiROKey[$valueName], ['cafe', 'babe']); | |
// manually set multi sz | |
$ceshiKey[$valueName] = new RegistryValue(RegistryValue::REG_MULTI_SZ, ['cafe', 'babe']); | |
myassertSame($ceshiROKey[$valueName], ['cafe', 'babe']); | |
} | |
$ceshiKey[null] = 'unsetted!!!'; | |
$ceshiKey['ceshiValue'] = false; | |
$ceshiKey['ceshiDWORD'] = 0x1234; | |
$ceshiKey['ceshiSZ'] = 'hello php'; | |
// emurate values by enumrateValues | |
$existValues = [ | |
null => new RegistryValue(RegistryValue::REG_SZ, 'unsetted!!!'), | |
'ceshiValue' => new RegistryValue(RegistryValue::REG_DWORD, 0), | |
'ceshiDWORD' => new RegistryValue(RegistryValue::REG_DWORD, 0x1234), | |
'ceshiSZ' => new RegistryValue(RegistryValue::REG_SZ, 'hello php'), | |
]; | |
foreach ($ceshiKey->enumrateValues(true) as $name => $value) { | |
$valueAssertion = $existValues[$name]; | |
myassertSame($valueAssertion->type, $value->type); | |
myassertSame("$valueAssertion", "$value"); | |
unset($existValues[$name]); | |
} | |
myassertSame($existValues, []); | |
// emurate values by iterator | |
$existValues = [ | |
null => new RegistryValue(RegistryValue::REG_SZ, 'unsetted!!!'), | |
'ceshiValue' => new RegistryValue(RegistryValue::REG_DWORD, 0), | |
'ceshiDWORD' => new RegistryValue(RegistryValue::REG_DWORD, 0x1234), | |
'ceshiSZ' => new RegistryValue(RegistryValue::REG_SZ, 'hello php'), | |
]; | |
foreach ($ceshiKey as $name => $value) { | |
$valueAssertion = $existValues[$name]; | |
myassertSame("$valueAssertion", "$value"); | |
unset($existValues[$name]); | |
} | |
myassertSame($existValues, []); | |
// TODO: remove value | |
//unset($ceshiKey['ceshiValue']); | |
//unset($ceshiKey['ceshiDWORD']); | |
//unset($ceshiKey['ceshiSZ']); | |
//myassertSame($ceshiROKey['ceshiValue'], null); | |
//myassertSame($ceshiROKey['ceshiDWORD'], null); | |
//myassertSame($ceshiROKey['ceshiSZ'], null); | |
// create new key | |
$subKey = $ceshiKey->createSubKey('subKey'); | |
$subKey2 = $ceshiKey->createSubKey('subKey2'); | |
// emurate keys | |
$existKeys = ['subKey', 'subKey2']; | |
foreach ($ceshiKey->enumrateSubKeys() as $keyName) { | |
$key = array_search($keyName, $existKeys); | |
myassertNotSame($key, false); | |
unset($existKeys[$key]); | |
} | |
myassertSame($existKeys, []); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment