├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.yml ├── .vscode ├── extensions.json └── settings.json ├── .yarnrc ├── LICENSE ├── README.md ├── benchmarks └── reg.ts ├── binding.gyp ├── docs ├── enumerate-values.md ├── index.md └── releases.md ├── jest.json ├── lib ├── index.ts └── registry.ts ├── package-lock.json ├── package.json ├── script ├── download-node-lib-win-arm64.ps1 └── upload.js ├── src └── main.cc ├── test └── registry-test.ts ├── tsconfig.json ├── vendor └── yarn-1.16.0.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: windows-2019 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [windows-2019] 19 | include: 20 | - os: windows-2019 21 | friendlyName: Windows 22 | timeout-minutes: 10 23 | steps: 24 | - uses: actions/checkout@v2 25 | with: 26 | submodules: recursive 27 | - name: Install Node.js 28 | uses: actions/setup-node@v2.1.2 29 | with: 30 | node-version: 18 31 | 32 | # This step can be removed as soon as official Windows arm64 builds are published: 33 | # https://github.com/nodejs/build/issues/2450#issuecomment-705853342 34 | - run: | 35 | $NodeVersion = (node --version) -replace '^.' 36 | $NodeFallbackVersion = "15.8.0" 37 | & .\script\download-node-lib-win-arm64.ps1 $NodeVersion $NodeFallbackVersion 38 | name: Install Windows arm64 node.lib 39 | 40 | - name: Install and build dependencies 41 | run: yarn 42 | - name: Lint 43 | run: yarn check-prettier 44 | - name: Build 45 | run: yarn build 46 | - name: Test 47 | run: yarn test 48 | - name: Prebuild x64 49 | run: npm run prebuild-napi-x64 50 | - name: Prebuild arm64 51 | run: npm run prebuild-napi-arm64 52 | - name: Prebuild x86 53 | run: npm run prebuild-napi-ia32 54 | - name: Publish 55 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 56 | run: yarn upload 57 | env: 58 | GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # generated TS files 61 | dist/ 62 | 63 | # prebuild native modules for publishing 64 | prebuilds/ 65 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ignore everything unrelated to the source code 2 | .vscode 3 | .prettierrc.yml 4 | benchmarks/ 5 | docs/ 6 | lib/ 7 | script/ 8 | test/ 9 | .gitattributes 10 | tsconfig.json 11 | .yarnrc 12 | vendor/ 13 | yarn-error.log 14 | *.tgz 15 | build 16 | jest.json 17 | .node-version 18 | .github 19 | dist/test 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .prettierrc.yml 3 | benchmarks/ 4 | .gitattributes 5 | .yarnrc 6 | vendor/ 7 | yarn-error.log 8 | build 9 | dist 10 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | trailingComma: es5 3 | semi: false 4 | proseWrap: always 5 | endOfLine: auto 6 | arrowParens: avoid 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "eg2.tslint", 4 | "samverschueren.final-newline", 5 | "DmitryDorofeev.empty-indent", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/dist": true, 6 | "**/out": true, 7 | ".awcache": true 8 | }, 9 | "files.exclude": { 10 | "**/.git": true, 11 | "**/.svn": true, 12 | "**/.hg": true, 13 | "**/.DS_Store": true, 14 | "**/node_modules": true, 15 | "**/build": true, 16 | "**/dist": true 17 | }, 18 | "editor.tabSize": 2, 19 | "prettier.semi": false, 20 | "prettier.singleQuote": true, 21 | "prettier.trailingComma": "es5", 22 | "editor.formatOnSave": true 23 | } 24 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | yarn-path "./vendor/yarn-1.16.0.js" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 GitHub Desktop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # registry-js 2 | 3 | ## A simple and opinionated library for working with the Windows registry 4 | 5 | ## Goals 6 | 7 | * zero dependencies 8 | * faster than `reg.exe` 9 | * implement based on usage - don't replicate the registry API 10 | * leverage TypeScript declarations wherever possible 11 | 12 | **Note:** This is currently in preview, with support for features that GitHub 13 | Desktop and Atom require. 14 | 15 | ## Install 16 | 17 | ```shellsession 18 | $ npm install --save registry-js 19 | # or 20 | $ yarn add registry-js 21 | ``` 22 | 23 | ## But Why? 24 | 25 | The current set of libraries for interacting with the registry have some 26 | limitations that meant we couldn't use it in GitHub Desktop: 27 | 28 | * [`windows-registry`](https://www.npmjs.com/package/windows-registry) depends 29 | on `ffi` at runtime, which caused issues with webpack-ing, and was missing 30 | APIs we needed. 31 | * [`winreg`](https://www.npmjs.com/package/winreg) depends on 32 | `reg.exe` which breaks as soon as you enable "Prevent access to registry 33 | editing tools" Group Policy rules (yes, even `QUERY` operations are caught by 34 | this). More details about this can be found in 35 | [desktop/desktop#3105](https://github.com/desktop/desktop/issues/3105). 36 | 37 | After exploring other options like invoking PowerShell - which was too slow - we 38 | decided to write our own little library to do the stuff we require by invoking 39 | the Win32 APIs directly. 40 | 41 | ## Documentation 42 | 43 | See the documentation under the 44 | [`docs`](https://github.com/desktop/registry-js/tree/master/docs) folder. 45 | 46 | ## Supported versions 47 | 48 | Each release of `registry-js` includes prebuilt binaries for the versions of 49 | Node and Electron that are actively supported by these projects. Please refer 50 | to the release documentation for [Node](https://github.com/nodejs/Release) and 51 | [Electron](https://electronjs.org/docs/tutorial/support) to see what is 52 | supported currently. 53 | 54 | ## Contributing 55 | 56 | Read the [Setup](https://github.com/desktop/registry-js/blob/master/docs/index.md#setup) 57 | section to ensure your development environment is setup for what you need. 58 | 59 | This project isn't about implementing a 1-1 replication of the Windows registry 60 | API, but implementing just enough for whatever usages there are in the wild. 61 | 62 | If you want to see something supported, open an issue to start a discussion 63 | about it. 64 | -------------------------------------------------------------------------------- /benchmarks/reg.ts: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark') 2 | import { spawn } from 'child_process' 3 | import { enumerateValues, HKEY } from '../dist/lib/index' 4 | 5 | const suite = new Benchmark.Suite() 6 | 7 | suite 8 | .add( 9 | 'reg.exe', 10 | function(deferred: any) { 11 | const proc = spawn( 12 | 'C:\\Windows\\System32\\reg.exe', 13 | ['QUERY', 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion'], 14 | { 15 | cwd: undefined, 16 | } 17 | ) 18 | proc.on('close', code => { 19 | deferred.resolve() 20 | }) 21 | }, 22 | { defer: true } 23 | ) 24 | .add('registry-js', function() { 25 | enumerateValues( 26 | HKEY.HKEY_LOCAL_MACHINE, 27 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion' 28 | ) 29 | }) 30 | .on('cycle', function(event: any) { 31 | console.log(String(event.target)) 32 | }) 33 | .run({ async: true }) 34 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'registry', 5 | 'cflags!': [ '-fno-exceptions' ], 6 | 'cflags_cc!': [ '-fno-exceptions' ], 7 | 'xcode_settings': { 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', 8 | 'CLANG_CXX_LIBRARY': 'libc++', 9 | 'MACOSX_DEPLOYMENT_TARGET': '10.7', 10 | }, 11 | 'msvs_settings': { 12 | 'VCCLCompilerTool': { 'ExceptionHandling': 1 }, 13 | }, 14 | 'include_dirs': [ 15 | ' { 89 | if (!nativeModule) { 90 | // this code is a no-op when the module is missing 91 | return [] 92 | } 93 | 94 | const hkey = mapToLong(key) 95 | 96 | const result: ReadonlyArray = nativeModule.readValues( 97 | hkey, 98 | subkey 99 | ) 100 | 101 | return result 102 | } 103 | 104 | export function enumerateValuesSafe( 105 | key: HKEY, 106 | subkey: string 107 | ): ReadonlyArray { 108 | try { 109 | return enumerateValues(key, subkey) 110 | } catch { 111 | return [] 112 | } 113 | } 114 | 115 | export function enumerateKeys( 116 | key: HKEY, 117 | subkey?: string | null 118 | ): ReadonlyArray { 119 | if (!nativeModule) { 120 | // this code is a no-op when the module is missing 121 | return [] 122 | } 123 | 124 | const hkey = mapToLong(key) 125 | 126 | const result: ReadonlyArray = nativeModule.enumKeys(hkey, subkey) 127 | 128 | return result 129 | } 130 | 131 | export function enumerateKeysSafe( 132 | key: HKEY, 133 | subkey?: string | null 134 | ): ReadonlyArray { 135 | try { 136 | return enumerateKeys(key, subkey) 137 | } catch { 138 | return [] 139 | } 140 | } 141 | 142 | export function createKey(key: HKEY, subkey: string): boolean { 143 | if (!nativeModule) { 144 | // this code is a no-op when the module is missing 145 | return false 146 | } 147 | const hkey = mapToLong(key) 148 | 149 | const result: boolean = nativeModule.createKey(hkey, subkey) 150 | 151 | return result 152 | } 153 | 154 | export function createKeySafe(key: HKEY, subkey: string): boolean { 155 | try { 156 | return createKey(key, subkey) 157 | } catch { 158 | return false 159 | } 160 | } 161 | 162 | export function setValue( 163 | key: HKEY, 164 | subkey: string, 165 | valueName: string, 166 | valueType: RegistryValueType, 167 | valueData: string 168 | ): boolean { 169 | if (!nativeModule) { 170 | // this code is a no-op when the module is missing 171 | return false 172 | } 173 | 174 | if ( 175 | valueType != RegistryValueType.REG_SZ && 176 | valueType != RegistryValueType.REG_EXPAND_SZ && 177 | valueType != RegistryValueType.REG_DWORD 178 | ) { 179 | // not implemented yet 180 | return false 181 | } 182 | const hkey = mapToLong(key) 183 | 184 | const result: boolean = nativeModule.setValue( 185 | hkey, 186 | subkey, 187 | valueName, 188 | valueType, 189 | valueData 190 | ) 191 | 192 | return result 193 | } 194 | 195 | export function setValueSafe( 196 | key: HKEY, 197 | subkey: string, 198 | valueName: string, 199 | valueType: RegistryValueType, 200 | valueData: string 201 | ): boolean { 202 | try { 203 | return setValue(key, subkey, valueName, valueType, valueData) 204 | } catch { 205 | return false 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "registry-js", 3 | "version": "1.16.1", 4 | "description": "A simple and opinionated library for working with the Windows registry", 5 | "main": "dist/lib/index.js", 6 | "typings": "dist/lib/index.d.ts", 7 | "scripts": { 8 | "install": "prebuild-install || node-gyp rebuild", 9 | "build": "tsc", 10 | "pretest": "yarn build", 11 | "test": "jest -c jest.json", 12 | "prepublishOnly": "yarn build && yarn test", 13 | "postpublish": "git push --follow-tags", 14 | "benchmark": "ts-node benchmarks/reg.ts", 15 | "prettier": "yarn prettier --write", 16 | "check-prettier": "prettier --check \"./**/*.{ts,tsx,js,json,jsx,scss,html,yaml,yml}\"", 17 | "prebuild-napi-x64": "prebuild -t 3 -r napi -a x64 --strip", 18 | "prebuild-napi-ia32": "prebuild -t 3 -r napi -a ia32 --strip", 19 | "prebuild-napi-arm64": "prebuild -t 3 -r napi -a arm64 --strip", 20 | "prebuild-all": "yarn prebuild-napi-x64 && yarn prebuild-napi-ia32 && yarn prebuild-napi-arm64", 21 | "upload": "node ./script/upload.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/desktop/registry-js.git" 26 | }, 27 | "keywords": [], 28 | "author": "", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/desktop/registry-js/issues" 32 | }, 33 | "homepage": "https://github.com/desktop/registry-js#readme", 34 | "devDependencies": { 35 | "@types/benchmark": "^1.0.31", 36 | "@types/jest": "^26.0.13", 37 | "@types/node": "^12.0.0", 38 | "benchmark": "^2.1.4", 39 | "jest": "^26.4.2", 40 | "node-abi": "^2.21.0", 41 | "node-gyp": "^11.2.0", 42 | "prebuild": "^10.0.1", 43 | "prettier": "^2.0.5", 44 | "ts-node": "^9.0.0", 45 | "typescript": "^3.9.0" 46 | }, 47 | "dependencies": { 48 | "node-addon-api": "^3.2.1", 49 | "prebuild-install": "^5.3.5" 50 | }, 51 | "binary": { 52 | "napi_versions": [ 53 | 3 54 | ] 55 | }, 56 | "config": { 57 | "runtime": "napi", 58 | "target": 3 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /script/download-node-lib-win-arm64.ps1: -------------------------------------------------------------------------------- 1 | # This script can be removed as soon as official Windows arm64 builds are published: 2 | # https://github.com/nodejs/build/issues/2450#issuecomment-705853342 3 | 4 | $nodeVersion = $args[0] 5 | $fallbackVersion = $args[1] 6 | 7 | If ($null -eq $nodeVersion -Or $null -eq $fallbackVersion) { 8 | Write-Error "No NodeJS version given as argument to this file. Run it like download-nodejs-win-arm64.ps1 NODE_VERSION NODE_FALLBACK_VERSION" 9 | exit 1 10 | } 11 | 12 | $url = "https://unofficial-builds.nodejs.org/download/release/v$nodeVersion/win-arm64/node.lib" 13 | $fallbackUrl = "https://unofficial-builds.nodejs.org/download/release/v$fallbackVersion/win-arm64/node.lib" 14 | 15 | # Always write to the $nodeVersion cache folder, even if we're using the fallbackVersion 16 | $cacheFolder = "$env:TEMP\prebuild\napi\$nodeVersion\arm64" 17 | 18 | If (!(Test-Path $cacheFolder)) { 19 | New-Item -ItemType Directory -Force -Path $cacheFolder 20 | } 21 | 22 | $output = "$cacheFolder\node.lib" 23 | $start_time = Get-Date 24 | 25 | Try { 26 | Invoke-WebRequest -Uri $url -OutFile $output 27 | $downloadedNodeVersion = $nodeVersion 28 | } Catch { 29 | If ($_.Exception.Response -And $_.Exception.Response.StatusCode -eq "NotFound") { 30 | Write-Output "No arm64 node.lib found for Node Windows $nodeVersion, trying fallback version $fallbackVersion..." 31 | Invoke-WebRequest -Uri $fallbackUrl -OutFile $output 32 | $downloadedNodeVersion = $fallbackVersion 33 | } 34 | } 35 | 36 | Write-Output "Downloaded arm64 NodeJS lib v$downloadedNodeVersion to $output in $((Get-Date).Subtract($start_time).Seconds) second(s)" 37 | -------------------------------------------------------------------------------- /script/upload.js: -------------------------------------------------------------------------------- 1 | // to ensure that env not in the CI server log 2 | 3 | const path = require('path') 4 | const { spawnSync } = require('child_process') 5 | 6 | spawnSync( 7 | path.join( 8 | __dirname, 9 | '../node_modules/.bin/prebuild' + 10 | (process.platform === 'win32' ? '.cmd' : '') 11 | ), 12 | ['--upload-all', process.env.GITHUB_AUTH_TOKEN], 13 | { stdio: 'inherit' } 14 | ) 15 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | // Windows.h strict mode 2 | #define STRICT 3 | #define UNICODE 4 | 5 | #include "napi.h" 6 | #include "uv.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | using namespace Napi; 15 | 16 | namespace { 17 | 18 | const DWORD MAX_VALUE_NAME = 16383; 19 | 20 | LPWSTR utf8ToWideChar(std::string utf8) { 21 | int wide_char_length = MultiByteToWideChar(CP_UTF8, 22 | 0, 23 | utf8.c_str(), 24 | -1, 25 | nullptr, 26 | 0); 27 | if (wide_char_length == 0) { 28 | return nullptr; 29 | } 30 | 31 | LPWSTR result = new WCHAR[wide_char_length]; 32 | if (MultiByteToWideChar(CP_UTF8, 33 | 0, 34 | utf8.c_str(), 35 | -1, 36 | result, 37 | wide_char_length) == 0) { 38 | delete[] result; 39 | return nullptr; 40 | } 41 | 42 | return result; 43 | } 44 | 45 | Napi::Object CreateEntry(const Napi::Env& env, LPWSTR name, const LPWSTR type, LPWSTR data, DWORD dataLengthBytes) 46 | { 47 | // NB: We must verify the data, since there's no guarantee that REG_SZ are stored with null terminators. 48 | 49 | // Test is ">= sizeof(wchar_t)" because otherwise 1/2 - 1 = -1 and things go kabloom: 50 | if (dataLengthBytes >= sizeof(wchar_t) && data[dataLengthBytes/sizeof(wchar_t) - 1] == L'\0') 51 | { 52 | // The string is (correctly) null-terminated. 53 | // Trim off the null terminator before handing it to NewFromTwoByte: 54 | dataLengthBytes -= sizeof(wchar_t); 55 | } 56 | 57 | // ... otherwise, it's not null-terminated, but we're passing the explicit length 58 | // to NewFromTwoByte anyway so we'll be fine (we won't over-read). 59 | 60 | auto obj = Napi::Object::New(env); 61 | obj.Set(Napi::String::New(env, "name"), Napi::String::New(env, (char16_t*)name)); 62 | obj.Set(Napi::String::New(env, "type"), Napi::String::New(env, (char16_t*)type)); 63 | obj.Set(Napi::String::New(env, "data"), Napi::String::New(env, (char16_t*)data, dataLengthBytes/sizeof(wchar_t))); 64 | return obj; 65 | } 66 | 67 | Napi::Object CreateEntry(const Napi::Env& env, LPWSTR name, const LPWSTR type, DWORD data) 68 | { 69 | auto obj = Napi::Object::New(env); 70 | obj.Set(Napi::String::New(env, "name"), Napi::String::New(env, (char16_t*)name)); 71 | obj.Set(Napi::String::New(env, "type"), Napi::String::New(env, (char16_t*)type)); 72 | obj.Set(Napi::String::New(env, "data"), Napi::Number::New(env, static_cast(data))); 73 | return obj; 74 | } 75 | 76 | Napi::Array EnumerateValues(const Napi::Env& env, HKEY hCurrentKey) { 77 | DWORD cValues, cchMaxValue, cbMaxValueData; 78 | 79 | auto retCode = RegQueryInfoKey( 80 | hCurrentKey, 81 | nullptr, // classname (not needed) 82 | nullptr, // classname length (not needed) 83 | nullptr, // reserved 84 | nullptr, // can ignore subkey values 85 | nullptr, 86 | nullptr, 87 | &cValues, // number of values for key 88 | &cchMaxValue, // longest value name 89 | &cbMaxValueData, // longest value data 90 | nullptr, // can ignore these values 91 | nullptr); 92 | 93 | if (retCode != ERROR_SUCCESS) 94 | { 95 | char errorMessage[49]; // 38 for message + 10 for int + 1 for nul 96 | sprintf_s(errorMessage, "RegQueryInfoKey failed - exit code: '%d'", retCode); 97 | Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); 98 | return Napi::Array::New(env, 0); 99 | } 100 | 101 | auto results = Napi::Array::New(env, cValues); 102 | 103 | auto buffer = std::make_unique(cbMaxValueData); 104 | for (DWORD i = 0; i < cValues; i++) 105 | { 106 | auto cchValue = MAX_VALUE_NAME; 107 | WCHAR achValue[MAX_VALUE_NAME]; 108 | achValue[0] = '\0'; 109 | 110 | DWORD lpType; 111 | DWORD cbData = cbMaxValueData; 112 | 113 | auto retCode = RegEnumValue( 114 | hCurrentKey, 115 | i, 116 | achValue, 117 | &cchValue, 118 | nullptr, 119 | &lpType, 120 | buffer.get(), 121 | &cbData); 122 | 123 | if (retCode == ERROR_SUCCESS) 124 | { 125 | if (lpType == REG_SZ) 126 | { 127 | auto text = reinterpret_cast(buffer.get()); 128 | auto obj = CreateEntry(env, achValue, (LPWSTR)L"REG_SZ", text, cbData); 129 | results.Set(i, obj); 130 | } 131 | else if (lpType == REG_EXPAND_SZ) 132 | { 133 | auto text = reinterpret_cast(buffer.get()); 134 | auto obj = CreateEntry(env, achValue, (LPWSTR)L"REG_EXPAND_SZ", text, cbData); 135 | results.Set(i, obj); 136 | } 137 | else if (lpType == REG_DWORD) 138 | { 139 | assert(cbData == sizeof(DWORD)); 140 | results.Set(i, CreateEntry(env, achValue, (LPWSTR)L"REG_DWORD", *reinterpret_cast(buffer.get()))); 141 | } 142 | } 143 | else if (retCode == ERROR_NO_MORE_ITEMS) 144 | { 145 | // no more items found, time to wrap up 146 | break; 147 | } 148 | else 149 | { 150 | char errorMessage[50]; // 39 for message + 10 for int + 1 for nul 151 | sprintf_s(errorMessage, "RegEnumValue returned an error code: '%d'", retCode); 152 | Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); 153 | return Napi::Array::New(env, 0); 154 | } 155 | } 156 | 157 | return results; 158 | } 159 | 160 | Napi::Value ReadValues(const Napi::CallbackInfo& info) 161 | { 162 | const Napi::Env& env = info.Env(); 163 | 164 | if (info.Length() < 2) 165 | { 166 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 167 | return env.Undefined(); 168 | } 169 | 170 | if (!info[0].IsNumber()) 171 | { 172 | Napi::TypeError::New(env, "A number was expected for the first argument, but wasn't received.").ThrowAsJavaScriptException(); 173 | return env.Undefined(); 174 | } 175 | 176 | if (!info[1].IsString()) 177 | { 178 | Napi::TypeError::New(env, "A string was expected for the second argument, but wasn't received.").ThrowAsJavaScriptException(); 179 | return env.Undefined(); 180 | } 181 | 182 | auto first = reinterpret_cast(info[0].As().Int64Value()); 183 | 184 | std::string subkeyArg = info[1].As(); 185 | auto subkey = utf8ToWideChar(subkeyArg); 186 | 187 | if (subkey == nullptr) 188 | { 189 | Napi::TypeError::New(env, "A string was expected for the second argument, but could not be parsed.").ThrowAsJavaScriptException(); 190 | return env.Undefined(); 191 | } 192 | 193 | HKEY hCurrentKey; 194 | LONG openKey = RegOpenKeyEx( 195 | first, 196 | subkey, 197 | 0, 198 | KEY_READ | KEY_WOW64_64KEY, 199 | &hCurrentKey); 200 | 201 | if (openKey == ERROR_FILE_NOT_FOUND) 202 | { 203 | // the key does not exist, just return an empty array for now 204 | return Napi::Array::New(env, 0); 205 | } 206 | else if (openKey == ERROR_SUCCESS) 207 | { 208 | Napi::Array results = EnumerateValues(env, hCurrentKey); 209 | RegCloseKey(hCurrentKey); 210 | return results; 211 | } 212 | else 213 | { 214 | char errorMessage[46]; // 35 for message + 10 for int + 1 for nul 215 | sprintf_s(errorMessage, "RegOpenKeyEx failed - exit code: '%d'", openKey); 216 | Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException(); 217 | return env.Undefined(); 218 | } 219 | } 220 | 221 | Napi::Value EnumKeys(const Napi::CallbackInfo& info) { 222 | const Napi::Env& env = info.Env(); 223 | 224 | auto argCount = info.Length(); 225 | if (argCount != 1 && argCount != 2) 226 | { 227 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 228 | return env.Undefined(); 229 | } 230 | 231 | if (!info[0].IsNumber()) 232 | { 233 | Napi::TypeError::New(env, "A number was expected for the first argument, but wasn't received.").ThrowAsJavaScriptException(); 234 | return env.Undefined(); 235 | } 236 | 237 | auto first = reinterpret_cast(info[0].As().Int64Value()); 238 | 239 | HKEY hCurrentKey = first; 240 | if (argCount == 2 && !info[1].IsNull() && !info[1].IsUndefined()) 241 | { 242 | if (!info[1].IsString()) 243 | { 244 | Napi::TypeError::New(env, "A string was expected for the second argument, but wasn't received.").ThrowAsJavaScriptException(); 245 | return env.Undefined(); 246 | } 247 | std::string subkeyArg = info[1].As(); 248 | auto subkey = utf8ToWideChar(subkeyArg); 249 | if (subkey == nullptr) 250 | { 251 | Napi::TypeError::New(env, "A string was expected for the second argument, but could not be parsed.").ThrowAsJavaScriptException(); 252 | return env.Undefined(); 253 | } 254 | 255 | auto openKey = RegOpenKeyEx( 256 | first, 257 | subkey, 258 | 0, 259 | KEY_READ | KEY_WOW64_64KEY, 260 | &hCurrentKey); 261 | if (openKey != ERROR_SUCCESS) 262 | { 263 | // FIXME: the key does not exist, just return an empty array for now 264 | return Napi::Array::New(env, 0); 265 | } 266 | } 267 | 268 | auto results = Napi::Array::New(env, 0); 269 | WCHAR name[MAX_VALUE_NAME]; 270 | for (int i = 0;; i++) 271 | { 272 | DWORD nameLen = MAX_VALUE_NAME; 273 | auto ret = RegEnumKeyEx(hCurrentKey, i, name, &nameLen, nullptr, nullptr, nullptr, nullptr); 274 | if (ret == ERROR_SUCCESS) 275 | { 276 | results.Set(i, Napi::String::New(env, (char16_t*)name)); 277 | continue; 278 | } 279 | break; // FIXME: We should do better error handling here 280 | } 281 | if (hCurrentKey != first) 282 | RegCloseKey(hCurrentKey); 283 | return results; 284 | } 285 | 286 | Napi::Value CreateKey(const Napi::CallbackInfo& info) 287 | { 288 | const Napi::Env& env = info.Env(); 289 | 290 | auto argCount = info.Length(); 291 | if (argCount != 2) 292 | { 293 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 294 | return env.Undefined(); 295 | } 296 | 297 | if (!info[0].IsNumber()) 298 | { 299 | Napi::TypeError::New(env, "A number was expected for the first argument, but wasn't received.").ThrowAsJavaScriptException(); 300 | return env.Undefined(); 301 | } 302 | 303 | if (!info[1].IsString()) 304 | { 305 | Napi::TypeError::New(env, "A string was expected for the second argument, but wasn't received.").ThrowAsJavaScriptException(); 306 | return env.Undefined(); 307 | } 308 | 309 | auto first = reinterpret_cast(info[0].As().Int64Value()); 310 | 311 | HKEY hCurrentKey = first; 312 | if (!info[1].IsNull() && !info[1].IsUndefined()) 313 | { 314 | std::string subkeyArg = info[1].As(); 315 | auto subKey = utf8ToWideChar(subkeyArg); 316 | if (subKey == nullptr) 317 | { 318 | Napi::TypeError::New(env, "A string was expected for the second argument, but could not be parsed.").ThrowAsJavaScriptException(); 319 | return env.Undefined(); 320 | } 321 | auto newKey = RegCreateKeyEx( 322 | first, 323 | subKey, 324 | 0, 325 | nullptr, 326 | REG_OPTION_NON_VOLATILE, 327 | KEY_READ | KEY_WOW64_64KEY, 328 | nullptr, 329 | &hCurrentKey, 330 | nullptr); 331 | if (newKey != ERROR_SUCCESS) 332 | { 333 | // FIXME: the key does not exist, just return false for now 334 | return Napi::Boolean::New(env, false); 335 | } 336 | } 337 | 338 | if (hCurrentKey != first) 339 | RegCloseKey(hCurrentKey); 340 | 341 | return Napi::Boolean::New(env, true); 342 | } 343 | 344 | Napi::Value SetValue(const Napi::CallbackInfo& info) 345 | { 346 | const Napi::Env& env = info.Env(); 347 | 348 | auto argCount = info.Length(); 349 | if (argCount != 5) 350 | { 351 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); 352 | return env.Undefined(); 353 | } 354 | 355 | if (!info[0].IsNumber()) 356 | { 357 | Napi::TypeError::New(env, "A number was expected for the first argument, but wasn't received.").ThrowAsJavaScriptException(); 358 | return env.Undefined(); 359 | } 360 | 361 | if (!info[1].IsString()) 362 | { 363 | Napi::TypeError::New(env, "A string was expected for the second argument, but wasn't received.").ThrowAsJavaScriptException(); 364 | return env.Undefined(); 365 | } 366 | 367 | if (!info[2].IsString()) 368 | { 369 | Napi::TypeError::New(env, "A string was expected for the third argument, but wasn't received.").ThrowAsJavaScriptException(); 370 | return env.Undefined(); 371 | } 372 | 373 | if (!info[3].IsString()) 374 | { 375 | Napi::TypeError::New(env, "A string was expected for the fourth argument, but wasn't received.").ThrowAsJavaScriptException(); 376 | return env.Undefined(); 377 | } 378 | 379 | if (!info[4].IsString()) 380 | { 381 | Napi::TypeError::New(env, "A string was expected for the fifth argument, but wasn't received.").ThrowAsJavaScriptException(); 382 | return env.Undefined(); 383 | } 384 | 385 | auto first = reinterpret_cast(info[0].As().Int64Value()); 386 | 387 | HKEY hCurrentKey = first; 388 | std::string subkeyArg = info[1].As(); 389 | auto subkey = utf8ToWideChar(subkeyArg); 390 | if (subkey == nullptr) 391 | { 392 | Napi::TypeError::New(env, "A string was expected for the second argument, but could not be parsed.").ThrowAsJavaScriptException(); 393 | return env.Undefined(); 394 | } 395 | 396 | std::string nameArg = info[2].As(); 397 | auto valueName = utf8ToWideChar(nameArg); 398 | if (valueName == nullptr) 399 | { 400 | Napi::TypeError::New(env, "A string was expected for the third argument, but could not be parsed.").ThrowAsJavaScriptException(); 401 | return env.Undefined(); 402 | } 403 | 404 | std::string typeArg = info[3].As(); 405 | auto valueType = utf8ToWideChar(typeArg); 406 | if (valueType == nullptr) 407 | { 408 | Napi::TypeError::New(env, "A string was expected for the fourth argument, but could not be parsed.").ThrowAsJavaScriptException(); 409 | return env.Undefined(); 410 | } 411 | 412 | HKEY hOpenKey; 413 | LONG openKey = RegOpenKeyEx( 414 | first, 415 | subkey, 416 | 0, 417 | KEY_WRITE | KEY_WOW64_64KEY, 418 | &hOpenKey); 419 | 420 | if (openKey == ERROR_FILE_NOT_FOUND) 421 | { 422 | Napi::TypeError::New(env, "RegOpenKeyEx : cannot find the registrykey, error_code : ERROR_FILE_NOT_FOUND").ThrowAsJavaScriptException(); 423 | return env.Undefined(); 424 | } 425 | else if (openKey == ERROR_SUCCESS) 426 | { 427 | long setValue = ERROR_INVALID_HANDLE; 428 | 429 | if (wcscmp(valueType, L"REG_SZ") == 0 || wcscmp(valueType, L"REG_EXPAND_SZ") == 0) 430 | { 431 | std::string typeArg = info[4].As(); 432 | auto valueData = utf8ToWideChar(typeArg); 433 | if (valueData == nullptr) 434 | { 435 | Napi::TypeError::New(env, "A string was expected for the fifth argument, but could not be parsed.").ThrowAsJavaScriptException(); 436 | return env.Undefined(); 437 | } 438 | int datalength = static_cast(wcslen(valueData) * sizeof(valueData[0])); 439 | DWORD regType = wcscmp(valueType, L"REG_SZ") == 0 ? REG_SZ : REG_EXPAND_SZ; 440 | setValue = RegSetValueEx( 441 | hOpenKey, 442 | valueName, 443 | 0, 444 | regType, 445 | (const BYTE *)valueData, 446 | datalength); 447 | } 448 | else if (wcscmp(valueType, L"REG_DWORD") == 0) 449 | { 450 | uint32_t dwordData = info[4].ToNumber().Uint32Value(); 451 | DWORD valueData = static_cast(dwordData); 452 | 453 | setValue = RegSetValueEx( 454 | hOpenKey, 455 | valueName, 456 | 0, 457 | REG_DWORD, 458 | (const BYTE *)&valueData, 459 | sizeof(valueData)); 460 | } 461 | else 462 | { 463 | char errorMessage[255]; 464 | sprintf_s(errorMessage, "RegSetValueEx Unmanaged type : '%ls'", valueType); 465 | Napi::TypeError::New(env, errorMessage).ThrowAsJavaScriptException(); 466 | return env.Undefined(); 467 | } 468 | 469 | if (setValue != ERROR_SUCCESS) 470 | { 471 | // FIXME: the key does not exist, just return false for now 472 | return Napi::Boolean::New(env, false); 473 | } 474 | RegCloseKey(hOpenKey); 475 | return Napi::Boolean::New(env, true); 476 | } 477 | else 478 | { 479 | char errorMessage[46]; // 35 for message + 10 for int + 1 for nul 480 | sprintf_s(errorMessage, "RegOpenKeyEx failed - exit code: '%d'", openKey); 481 | Napi::TypeError::New(env, errorMessage).ThrowAsJavaScriptException(); 482 | return env.Undefined(); 483 | } 484 | } 485 | 486 | Napi::Object Init(Napi::Env env, Napi::Object exports) 487 | { 488 | exports.Set(Napi::String::New(env, "readValues"), Napi::Function::New(env, ReadValues)); 489 | exports.Set(Napi::String::New(env, "enumKeys"), Napi::Function::New(env, EnumKeys)); 490 | exports.Set(Napi::String::New(env, "createKey"), Napi::Function::New(env, CreateKey)); 491 | exports.Set(Napi::String::New(env, "setValue"), Napi::Function::New(env, SetValue)); 492 | 493 | return exports; 494 | } 495 | } 496 | 497 | #if NODE_MAJOR_VERSION >= 10 498 | NAN_MODULE_WORKER_ENABLED(registryNativeModule, Init) 499 | #else 500 | NODE_API_MODULE(registryNativeModule, Init); 501 | #endif 502 | -------------------------------------------------------------------------------- /test/registry-test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | enumerateValues, 3 | enumerateKeys, 4 | setValue, 5 | createKey, 6 | HKEY, 7 | RegistryValueType, 8 | } from '../lib/index' 9 | 10 | if (process.platform === 'win32') { 11 | describe('enumerateValue', () => { 12 | it('can read strings from the registry', () => { 13 | const values = enumerateValues( 14 | HKEY.HKEY_LOCAL_MACHINE, 15 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion' 16 | ) 17 | 18 | const programFilesDir = values.find(v => v.name == 'ProgramFilesDir') 19 | expect(programFilesDir!.type).toBe('REG_SZ') 20 | expect(programFilesDir!.data).toContain(':\\Program Files') 21 | 22 | const programFilesPath = values.find(v => v.name == 'ProgramFilesPath') 23 | expect(programFilesPath!.type).toBe('REG_EXPAND_SZ') 24 | expect(programFilesPath!.data).toBe('%ProgramFiles%') 25 | }) 26 | 27 | it('can read numbers from the registry', () => { 28 | const values = enumerateValues( 29 | HKEY.HKEY_LOCAL_MACHINE, 30 | 'SOFTWARE\\Microsoft\\Windows\\Windows Error Reporting' 31 | ) 32 | 33 | const serviceTimeout = values.find(v => v.name == 'ServiceTimeout') 34 | expect(serviceTimeout!.type).toBe('REG_DWORD') 35 | expect(serviceTimeout!.data).toBe(60000) 36 | }) 37 | 38 | it('can read values from HKCU', () => { 39 | const values = enumerateValues( 40 | HKEY.HKEY_CURRENT_USER, 41 | 'SOFTWARE\\Microsoft\\Windows\\DWM' 42 | ) 43 | 44 | const composition = values.find(v => v.name == 'Composition') 45 | expect(composition!.type).toBe('REG_DWORD') 46 | expect(composition!.data).toBe(1) 47 | }) 48 | 49 | it('returns empty array when key is missing', () => { 50 | const values = enumerateValues(HKEY.HKEY_LOCAL_MACHINE, 'blahblahblah') 51 | 52 | expect(values).toEqual([]) 53 | }) 54 | }) 55 | 56 | describe('enumerateKeys', () => { 57 | it('can enumerate key names from the registry (No subkey)', () => { 58 | const values = enumerateKeys(HKEY.HKEY_LOCAL_MACHINE, null) 59 | 60 | expect(values.indexOf('HARDWARE')).toBeGreaterThanOrEqual(0) 61 | expect(values.indexOf('SOFTWARE')).toBeGreaterThanOrEqual(0) 62 | expect(values.indexOf('SYSTEM')).toBeGreaterThanOrEqual(0) 63 | }) 64 | it('can enumerate key names from the registry', () => { 65 | const values = enumerateKeys(HKEY.HKEY_LOCAL_MACHINE, 'SOFTWARE') 66 | 67 | expect(values.indexOf('Classes')).toBeGreaterThanOrEqual(0) 68 | expect(values.indexOf('Microsoft')).toBeGreaterThanOrEqual(0) 69 | }) 70 | }) 71 | 72 | describe('setValue', () => { 73 | it("can't set not implemented value for a registry key", () => { 74 | const result = setValue( 75 | HKEY.HKEY_CURRENT_USER, 76 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion', 77 | 'ValueTest', 78 | RegistryValueType.REG_MULTI_SZ, 79 | 'Value' 80 | ) 81 | expect(result).toBeFalsy() 82 | }) 83 | 84 | it('can set DWORD value for a registry key', () => { 85 | let result = false 86 | try { 87 | result = setValue( 88 | HKEY.HKEY_CURRENT_USER, 89 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion', 90 | 'ValueTestDword', 91 | RegistryValueType.REG_DWORD, 92 | '1' 93 | ) 94 | } catch (e) { 95 | console.log(e) 96 | } 97 | expect(result).toBeTruthy() 98 | 99 | const values = enumerateValues( 100 | HKEY.HKEY_CURRENT_USER, 101 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion' 102 | ) 103 | 104 | const programFilesDir = values.find(v => v.name == 'ValueTestDword') 105 | expect(programFilesDir!.type).toBe('REG_DWORD') 106 | expect(programFilesDir!.data).toBe(1) 107 | }) 108 | 109 | it('can set REG_SZ value for a registry key', () => { 110 | let result = false 111 | try { 112 | result = setValue( 113 | HKEY.HKEY_CURRENT_USER, 114 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion', 115 | 'ValueTestSz', 116 | RegistryValueType.REG_SZ, 117 | 'Value 123 ! test@test.com (456)' 118 | ) 119 | } catch (e) { 120 | console.log(e) 121 | } 122 | expect(result).toBeTruthy() 123 | 124 | const values = enumerateValues( 125 | HKEY.HKEY_CURRENT_USER, 126 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion' 127 | ) 128 | 129 | const programFilesDir = values.find(v => v.name == 'ValueTestSz') 130 | expect(programFilesDir!.type).toBe('REG_SZ') 131 | expect(programFilesDir!.data).toBe('Value 123 ! test@test.com (456)') 132 | }) 133 | 134 | it('can set REG_EXPAND_SZ value for a registry key', () => { 135 | let result = false 136 | try { 137 | result = setValue( 138 | HKEY.HKEY_CURRENT_USER, 139 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion', 140 | 'ValueTestExpandSz', 141 | RegistryValueType.REG_EXPAND_SZ, 142 | 'Value 123 ! test@test.com (456);%NVM_HOME%;%NVM_SYMLINK%' 143 | ) 144 | } catch (e) { 145 | console.log(e) 146 | } 147 | expect(result).toBeTruthy() 148 | 149 | const values = enumerateValues( 150 | HKEY.HKEY_CURRENT_USER, 151 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion' 152 | ) 153 | 154 | const value = values.find(v => v.name == 'ValueTestExpandSz') 155 | expect(value!.type).toBe('REG_EXPAND_SZ') 156 | expect(value!.data).toBe( 157 | 'Value 123 ! test@test.com (456);%NVM_HOME%;%NVM_SYMLINK%' 158 | ) 159 | }) 160 | }) 161 | 162 | describe('createKey', () => { 163 | it('can create a registry key', () => { 164 | const result = createKey( 165 | HKEY.HKEY_CURRENT_USER, 166 | 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\UnitTests' 167 | ) 168 | expect(result).toBeTruthy() 169 | }) 170 | 171 | it('can create a registry key in WOW6432Node', () => { 172 | const result = createKey( 173 | HKEY.HKEY_CURRENT_USER, 174 | 'SOFTWARE\\WOW6432Node\\UnitTests' 175 | ) 176 | expect(result).toBeTruthy() 177 | }) 178 | }) 179 | } 180 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "noImplicitReturns": true, 8 | "noImplicitThis": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noUnusedLocals": true, 11 | "strict": true, 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "declaration": true, 15 | "types": ["jest", "node"], 16 | "typeRoots": ["./node_modules/@types"] 17 | }, 18 | "exclude": ["node_modules", "build"], 19 | "include": ["lib/**/*.ts", "test/**/*.ts"], 20 | "compileOnSave": false 21 | } 22 | --------------------------------------------------------------------------------