Skip to content

Commit

Permalink
add queryAll to query a whole reg key (#4)
Browse files Browse the repository at this point in the history
* try fixing the auto bump bot

* add queryAll function
  • Loading branch information
hivivo committed Jul 4, 2023
1 parent 54f92fc commit 61f2e09
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 85 deletions.
9 changes: 8 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@
"sourceType": "module",
"warnOnUnsupportedTypeScriptVersion": true
},
"ignorePatterns": ["/.cache", "/.git", "/.husky", "/.yarn", "/dist"],
"ignorePatterns": [
"/.cache",
"/.git",
"/.husky",
"/.yarn",
"/dist",
"/coverage"
],
"settings": {
"import/resolver": {
"typescript": {
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
with:
token: ${{ secrets.BOT_TOKEN }}

- name: Install Node.js and Yarn
uses: actions/setup-node@v3
Expand All @@ -65,6 +67,8 @@ jobs:

- name: Bump version
uses: 'phips28/gh-action-bump-version@master'
env:
GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }}
with:
tag-prefix: 'v'

Expand Down
52 changes: 48 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Access Windows registry with REG command line tool
yarn add windows-reg
```

### To Read a Registry Key Value
### To Read a Single Registry Key Value

```typescript
import { query } from 'windows-reg';
Expand All @@ -25,14 +25,58 @@ The example output would be

```javascript
{
hive: 'HKEY_CURRENT_USER',
key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Run',
valueKey: 'OneDrive',
valueName: 'OneDrive',
valueType: 'REG_SZ',
value: '"C:\\Program Files\\Microsoft OneDrive\\OneDrive.exe" /background'
}
```

### To Read All Values under a Registry Key Recursively

```typescript
import { queryAll } from 'windows-reg';

console.log(
await queryAll(
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run',
true,
),
);
```

The example output would be

```javascript
{
'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run': [
{
valueName: 'OneDrive',
valueType: 'REG_SZ',
value: '"C:\\Program Files\\Microsoft OneDrive\\OneDrive.exe" /background'
},
{
valueName: 'Docker Desktop',
valueType: 'REG_SZ',
value: 'C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe -Autostart'
},
...
],
'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\ExampleSubKey': [
{
valueName: 'ExampleBinaryValue',
valueType: 'REG_BINARY',
value: <Buffer ad fa df ad fa df ad fa dc ae fa df ea e2 31 3f 23 23 e2 da fd af>
},
{
valueName: 'ExampleNumberValue',
valueType: 'REG_QWORD',
value: 1672226
},
...
],
}
```

## Developing

### Available Scripts
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"test": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js"
},
"devDependencies": {
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
Expand Down Expand Up @@ -71,7 +70,8 @@
"src/**/*.ts",
"src/**/*.mts",
"!src/**/*.d.ts",
"!src/**/*.d.mts"
"!src/**/*.d.mts",
"!src/TestContext.ts"
]
}
}
41 changes: 41 additions & 0 deletions src/TestContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { jest } from '@jest/globals';
import { ExecException } from 'child_process';

interface MockedCallbackArguments {
stdout?: string;
stderr?: string;
exception?: ExecException;
}

export class TestContext {
readonly execMock: jest.Mock;
private mockedCommands: Record<string, MockedCallbackArguments>;
constructor() {
this.execMock = jest
.fn()
.mockImplementation(
(
command: string,
_options: unknown,
cb: (e: ExecException | null, so: string, se: string) => void,
) => {
if (this.mockedCommands[command]) {
cb(
this.mockedCommands[command].exception ?? null,
this.mockedCommands[command].stdout ?? '',
this.mockedCommands[command].stderr ?? '',
);
} else {
cb(new Error(`No mock for command: ${command}`), '', '');
}
},
);
this.mockedCommands = {};
jest.unstable_mockModule('node:child_process', () => ({
exec: this.execMock,
}));
}
mockCommand(command: string, args: MockedCallbackArguments) {
this.mockedCommands[command] = args;
}
}
24 changes: 23 additions & 1 deletion src/command.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildCommand } from './command.js';
import { describe, expect, it } from '@jest/globals';
import { RegCommand, buildCommand } from './command.js';

describe('buildCommand', () => {
it('allows to build command without any argument', () => {
Expand All @@ -21,3 +22,24 @@ describe('buildCommand', () => {
);
});
});

describe('RegCommand', () => {
it('builds REG query command', () => {
const key = 'mockKey';
const valueName = 'mockValueName';
const result = RegCommand.query(key, valueName);
expect(result).toMatch(new RegExp(`reg.*${key}.*${valueName}`, 'iu'));
});

it('builds REG query all command', () => {
const key = 'mockKey';

const resultNonRecursively = RegCommand.queryAll(key, false);
expect(resultNonRecursively).toMatch(new RegExp(`reg.*${key}`, 'iu'));
expect(resultNonRecursively).not.toMatch(/\/s/iu);

const resultRecursively = RegCommand.queryAll(key, true);
expect(resultRecursively).toMatch(new RegExp(`reg.*${key}`, 'iu'));
expect(resultRecursively).toMatch(/\/s/iu);
});
});
7 changes: 5 additions & 2 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ export function buildCommand(main: string, args?: string[]) {
}

export const RegCommand = {
query: (key: string, valueKey: string) =>
buildCommand('REG', ['QUERY', key, '/v', valueKey]),
query: (key: string, valueName: string) =>
buildCommand('REG', ['QUERY', key, '/v', valueName]),
queryAll: (key: string, recursively: boolean) =>
buildCommand('REG', ['QUERY', key, ...(recursively ? ['/s'] : [])]),

delete: (key: string) => buildCommand('REG', ['DELETE', key, '/f']),
export: (key: string, file: string) =>
buildCommand('REG', ['EXPORT', key, file]),
Expand Down
41 changes: 2 additions & 39 deletions src/execution.test.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,5 @@
import { jest } from '@jest/globals';
import { ExecException } from 'child_process';

interface MockedCallbackArguments {
stdout?: string;
stderr?: string;
exception?: ExecException;
}

class TestContext {
readonly execMock: jest.Mock;
private mockedCommands: Record<string, MockedCallbackArguments>;
constructor() {
this.execMock = jest
.fn()
.mockImplementation(
(
command: string,
_options: unknown,
cb: (e: ExecException | null, so: string, se: string) => void,
) => {
if (this.mockedCommands[command]) {
cb(
this.mockedCommands[command].exception ?? null,
this.mockedCommands[command].stdout ?? '',
this.mockedCommands[command].stderr ?? '',
);
}
},
);
this.mockedCommands = {};
jest.unstable_mockModule('node:child_process', () => ({
exec: this.execMock,
}));
}
mockCommand(command: string, args: MockedCallbackArguments) {
this.mockedCommands[command] = args;
}
}
import { describe, expect, it } from '@jest/globals';
import { TestContext } from './TestContext.js';

describe('execute', () => {
const tc = new TestContext();
Expand Down
93 changes: 93 additions & 0 deletions src/registry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, it } from '@jest/globals';
import { TestContext } from './TestContext.js';

describe('query', () => {
const tc = new TestContext();

it('sends a REG command to query a specific value name under a reg key', async () => {
const { RegistryValueType, query } = await import('./registry.js');

const regKey = 'HKEY_CURRENT_USER\\Software\\WindowsReg';
const valueName = 'Test';
const testCommand = `REG "QUERY" "${regKey}" "/v" "${valueName}"`;
const valueType = RegistryValueType.String;
const value = 'Test Value';
tc.mockCommand(testCommand, {
stdout: `${regKey}\r\n ${valueName} ${valueType} ${value}`,
});

const actualResult = await query(regKey, valueName);
expect(tc.execMock).toHaveBeenCalledWith(
testCommand,
expect.anything(),
expect.anything(),
);
expect(actualResult.valueName).toBe(valueName);
expect(actualResult.valueType).toBe(valueType);
expect(actualResult.value).toBe(value);
});

it('sends a REG command to query all the values under a reg key', async () => {
const { queryAll } = await import('./registry.js');
const regKey = 'HKEY_CURRENT_USER\\Software\\WindowsReg';

await queryAll(regKey);

expect(tc.execMock).toHaveBeenCalledWith(
`REG "QUERY" "${regKey}"`,
expect.anything(),
expect.anything(),
);

await queryAll(regKey, true);

expect(tc.execMock).toHaveBeenCalledWith(
`REG "QUERY" "${regKey}" "/s"`,
expect.anything(),
expect.anything(),
);
});

it('returns null if the execution fails or the reg key does not exist', async () => {
const { query, queryAll } = await import('./registry.js');
const regKey = 'HKEY_CURRENT_USER\\Software\\NotExisting';

expect(await queryAll(regKey)).toBe(null);
expect(await query(regKey, 'anything')).toBe(null);
});

it('parses different value types when query a reg key', async () => {
const { RegistryValueType, queryAll } = await import('./registry.js');

const regKey = 'HKEY_CURRENT_USER\\Software\\WindowsReg';
const testCommand = `REG "QUERY" "${regKey}"`;
tc.mockCommand(testCommand, {
stdout: `${regKey}\r
NumberValue ${RegistryValueType.Number32} 123\r
LongNumberValue ${RegistryValueType.Number64} 12345\r
StringValue ${RegistryValueType.String} hello abc 123\r
StringValue2 ${RegistryValueType.ExpandableString} this is expandable string\r
MultiStrings ${RegistryValueType.MultipleStrings} line 1\\0line 2\\0line last\r
Binary ${RegistryValueType.Binary} 68656c6c6f`,
});

const actualResult = await queryAll(regKey);
expect(Object.keys(actualResult)).toContain(regKey);

const [
numberValue,
longNumberValue,
stringValue,
expString,
multiStrings,
binaryValue,
] = actualResult[regKey];
expect(numberValue.value).toBe(123);
expect(longNumberValue.value).toBe(12345);
expect(stringValue.value).toBe('hello abc 123');
expect(expString.value).toBe('this is expandable string');
expect(multiStrings.value).toHaveLength(3);
expect(multiStrings.value[2]).toBe('line last');
expect((binaryValue.value as Buffer).toString('ascii')).toBe('hello');
});
});
Loading

0 comments on commit 61f2e09

Please sign in to comment.