Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add queryAll to query a whole reg key #4

Merged
merged 2 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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