Skip to content

Commit 38aa64c

Browse files
authored
Merge pull request #1958 from RedisInsight/be/bugfix/RI-4390-fix_redis_glob
#RI-4390 fix glob pattern to match redis glob-style
2 parents f22327d + 8392159 commit 38aa64c

File tree

7 files changed

+133
-21
lines changed

7 files changed

+133
-21
lines changed

redisinsight/api/src/modules/browser/services/hash-business/hash-business.service.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
NotFoundException,
77
} from '@nestjs/common';
88
import { chunk, flatMap, isNull } from 'lodash';
9-
import * as isGlob from 'is-glob';
10-
import { catchAclError, catchTransactionError, unescapeGlob } from 'src/utils';
9+
import {
10+
catchAclError, catchTransactionError, isRedisGlob, unescapeRedisGlob,
11+
} from 'src/utils';
1112
import ERROR_MESSAGES from 'src/constants/error-messages';
1213
import { RedisErrorCodes } from 'src/constants';
1314
import config from 'src/utils/config';
@@ -105,8 +106,8 @@ export class HashBusinessService {
105106
new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),
106107
);
107108
}
108-
if (dto.match && !isGlob(dto.match, { strict: false })) {
109-
const field = unescapeGlob(dto.match);
109+
if (dto.match && !isRedisGlob(dto.match)) {
110+
const field = unescapeRedisGlob(dto.match);
110111
result.nextCursor = 0;
111112
const value = await this.browserTool.execCommand(
112113
clientMetadata,

redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { toNumber, omit, isNull } from 'lodash';
2-
import * as isGlob from 'is-glob';
32
import config from 'src/utils/config';
4-
import { unescapeGlob } from 'src/utils';
3+
import { isRedisGlob, unescapeRedisGlob } from 'src/utils';
54
import {
65
BrowserToolClusterService,
76
} from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service';
@@ -46,8 +45,8 @@ export class ClusterStrategy extends AbstractStrategy {
4645
const settings = await this.settingsService.getAppSettings('1');
4746
await this.calculateNodesTotalKeys(nodes);
4847

49-
if (!isGlob(match, { strict: false })) {
50-
const keyName = Buffer.from(unescapeGlob(match));
48+
if (!isRedisGlob(match)) {
49+
const keyName = Buffer.from(unescapeRedisGlob(match));
5150
nodes.forEach((node) => {
5251
// eslint-disable-next-line no-param-reassign
5352
node.cursor = 0;

redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import * as isGlob from 'is-glob';
21
import { isNull } from 'lodash';
32
import config from 'src/utils/config';
4-
import { unescapeGlob } from 'src/utils';
3+
import { isRedisGlob, unescapeRedisGlob } from 'src/utils';
54
import {
65
GetKeyInfoResponse,
76
GetKeysWithDetailsResponse,
@@ -47,8 +46,8 @@ export class StandaloneStrategy extends AbstractStrategy {
4746

4847
node.total = await getTotal(client);
4948

50-
if (!isGlob(match, { strict: false })) {
51-
const keyName = Buffer.from(unescapeGlob(match));
49+
if (!isRedisGlob(match)) {
50+
const keyName = Buffer.from(unescapeRedisGlob(match));
5251
node.cursor = 0;
5352
node.scanned = isNull(node.total) ? 1 : node.total;
5453
node.keys = await this.getKeysInfo(client, [keyName]);

redisinsight/api/src/modules/browser/services/set-business/set-business.service.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import {
55
Logger,
66
NotFoundException,
77
} from '@nestjs/common';
8-
import * as isGlob from 'is-glob';
98
import { RedisErrorCodes } from 'src/constants';
109
import ERROR_MESSAGES from 'src/constants/error-messages';
1110
import config from 'src/utils/config';
12-
import { catchAclError, catchTransactionError, unescapeGlob } from 'src/utils';
11+
import {
12+
catchAclError, catchTransactionError, isRedisGlob, unescapeRedisGlob,
13+
} from 'src/utils';
1314
import { ReplyError } from 'src/models';
1415
import { ClientMetadata } from 'src/common/models';
1516
import {
@@ -101,8 +102,8 @@ export class SetBusinessService {
101102
new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),
102103
);
103104
}
104-
if (dto.match && !isGlob(dto.match, { strict: false })) {
105-
const member = unescapeGlob(dto.match);
105+
if (dto.match && !isRedisGlob(dto.match)) {
106+
const member = unescapeRedisGlob(dto.match);
106107
result.nextCursor = 0;
107108
const memberIsExist = await this.browserTool.execCommand(
108109
clientMetadata,

redisinsight/api/src/modules/browser/services/z-set-business/z-set-business.service.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {
66
NotFoundException,
77
} from '@nestjs/common';
88
import { isNull, isNaN } from 'lodash';
9-
import * as isGlob from 'is-glob';
109
import config from 'src/utils/config';
11-
import { catchAclError, catchTransactionError, unescapeGlob } from 'src/utils';
10+
import {
11+
catchAclError, catchTransactionError, isRedisGlob, unescapeRedisGlob,
12+
} from 'src/utils';
1213
import {
1314
AddMembersToZSetDto,
1415
CreateZSetWithExpireDto,
@@ -269,8 +270,8 @@ export class ZSetBusinessService {
269270
new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),
270271
);
271272
}
272-
if (dto.match && !isGlob(dto.match, { strict: false })) {
273-
const member = unescapeGlob(dto.match);
273+
if (dto.match && !isRedisGlob(dto.match)) {
274+
const member = unescapeRedisGlob(dto.match);
274275
result.nextCursor = 0;
275276
const score = await this.browserTool.execCommand(
276277
clientMetadata,

redisinsight/api/src/utils/glob-pattern-helper.spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { unescapeGlob } from 'src/utils/glob-pattern-helper';
1+
import { unescapeGlob, isRedisGlob } from 'src/utils/glob-pattern-helper';
22

33
const unescapeGlobTests = [
44
{ input: 'h?llo', output: 'h?llo' },
@@ -30,3 +30,41 @@ describe('unescapeGlob', () => {
3030
});
3131
});
3232
});
33+
34+
describe('isRedisGlob', () => {
35+
const testCases: [string, boolean][] = [
36+
['?ello', true],
37+
['??llo', true],
38+
['\\?\\?llo', false],
39+
['\\??llo', true],
40+
['?\\?llo', true],
41+
['\\\\\\\\?\\?llo', true],
42+
['h?llo', true],
43+
['h\\?llo', false],
44+
['h\\\\?llo', true],
45+
['h\\\\\\?llo', false],
46+
['h????llo', true],
47+
['h\\????llo', true],
48+
['h????ll?o', true],
49+
['h*llo', true],
50+
['h**llo', true],
51+
['h***ll*o', true],
52+
['\\*ello', false],
53+
['\\\\*ello', true],
54+
['\\\\\\*ello', false],
55+
['h[ae]llo', true],
56+
['h[^e]llo', true],
57+
['h[a-b]llo', true],
58+
['h[a-b\\]llo', false],
59+
['h[a-b\\\\]llo', true],
60+
['h\\[a-b\\\\]llo', false],
61+
['h\\\\[a-b\\\\]llo', true],
62+
['h\\\\[a-b\\]llo', false],
63+
];
64+
65+
testCases.forEach((tc) => {
66+
it(`should return ${tc[1]} for input: ${tc[0]}`, () => {
67+
expect(isRedisGlob(tc[0])).toEqual(tc[1]);
68+
});
69+
});
70+
});

redisinsight/api/src/utils/glob-pattern-helper.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,76 @@ export const unescapeGlob = (value: string): string => {
1111

1212
return result.replace(/\\{2}/g, '\\');
1313
};
14+
15+
const REDIS_GLOB_SPEC_CHAR = ['?', '*', '[', ']'];
16+
export const unescapeRedisGlob = (value: string): string => {
17+
let result = value;
18+
19+
REDIS_GLOB_SPEC_CHAR.forEach((char: string) => {
20+
const regex = new RegExp('\\'.repeat(3) + char, 'g');
21+
result = result.replace(regex, char);
22+
});
23+
24+
return result.replace(/\\{2}/g, '\\');
25+
};
26+
27+
/**
28+
* Determines if any character on specific position is escaped
29+
* @param str
30+
* @param pos
31+
*/
32+
export const isEscaped = (str: string, pos: number) => {
33+
let currPos = pos;
34+
while (currPos > 0 && str[currPos - 1] === '\\') {
35+
currPos -= 1;
36+
}
37+
38+
const escCount = pos - currPos;
39+
40+
return escCount && (escCount % 2) > 0;
41+
};
42+
43+
/**
44+
* Find first position of unescaped char
45+
* @param char
46+
* @param str
47+
* @param startPosition
48+
*/
49+
const findUnescapedCharPosition = (char: string, str: string, startPosition = 0) => {
50+
let pos = str.indexOf(char, startPosition);
51+
while (pos >= 0) {
52+
if (!isEscaped(str, pos)) {
53+
return pos;
54+
}
55+
56+
pos = str.indexOf(char, pos + 1);
57+
}
58+
59+
return pos;
60+
};
61+
62+
/**
63+
* Check if string has at least one unescaped char or sequence of unescaped chars in proper order
64+
* Supported only 1-char and 2-chars conditions for now
65+
* @param char
66+
* @param str
67+
* @param startPosition
68+
*/
69+
const hasUnescapedChar = (char: string, str: string, startPosition = 0) => {
70+
if (char.length === 1) {
71+
return findUnescapedCharPosition(char, str, startPosition) >= 0;
72+
}
73+
74+
if (char.length === 2) {
75+
const firstCharPos = findUnescapedCharPosition(char[0], str, startPosition);
76+
if (firstCharPos >= 0) {
77+
return findUnescapedCharPosition(char[1], str, firstCharPos) >= 0;
78+
}
79+
}
80+
81+
return false;
82+
};
83+
84+
export const isRedisGlob = (str: string) => hasUnescapedChar('?', str)
85+
|| hasUnescapedChar('*', str)
86+
|| hasUnescapedChar('[]', str);

0 commit comments

Comments
 (0)