Skip to content

Commit 207625e

Browse files
committed
path: add path.glob
1 parent 284e6ac commit 207625e

File tree

7 files changed

+247
-0
lines changed

7 files changed

+247
-0
lines changed

lib/path.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const {
3131
StringPrototypeToLowerCase,
3232
} = primordials;
3333

34+
const { glob: _glob } = internalBinding('path');
35+
3436
const {
3537
CHAR_UPPERCASE_A,
3638
CHAR_LOWERCASE_A,
@@ -153,6 +155,12 @@ function _format(sep, pathObject) {
153155
return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`;
154156
}
155157

158+
function glob(pattern, name) {
159+
validateString(pattern, 'pattern');
160+
validateString(name, 'name');
161+
return _glob(pattern, name);
162+
}
163+
156164
const win32 = {
157165
/**
158166
* path.resolve([from ...], to)
@@ -1064,6 +1072,7 @@ const win32 = {
10641072

10651073
return ret;
10661074
},
1075+
glob,
10671076

10681077
sep: '\\',
10691078
delimiter: ';',
@@ -1530,6 +1539,7 @@ const posix = {
15301539

15311540
return ret;
15321541
},
1542+
glob,
15331543

15341544
sep: '/',
15351545
delimiter: ':',

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
'src/node_metadata.cc',
107107
'src/node_options.cc',
108108
'src/node_os.cc',
109+
'src/node_path.cc',
109110
'src/node_perf.cc',
110111
'src/node_platform.cc',
111112
'src/node_postmortem_metadata.cc',
@@ -225,6 +226,7 @@
225226
'src/node_object_wrap.h',
226227
'src/node_options.h',
227228
'src/node_options-inl.h',
229+
'src/node_path.h',
228230
'src/node_perf.h',
229231
'src/node_perf_common.h',
230232
'src/node_platform.h',

src/node_binding.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
V(mksnapshot) \
5353
V(options) \
5454
V(os) \
55+
V(path) \
5556
V(performance) \
5657
V(permission) \
5758
V(pipe_wrap) \

src/node_external_reference.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class ExternalReferenceRegistry {
9090
V(module_wrap) \
9191
V(options) \
9292
V(os) \
93+
V(path) \
9394
V(performance) \
9495
V(permission) \
9596
V(process_methods) \

src/node_path.cc

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#include "node_path.h"
2+
#include "env-inl.h"
3+
#include "node_errors.h"
4+
#include "node_external_reference.h"
5+
6+
namespace node {
7+
8+
namespace path {
9+
using v8::Context;
10+
using v8::FunctionCallbackInfo;
11+
using v8::Local;
12+
using v8::Object;
13+
using v8::Value;
14+
15+
// extracted from
16+
// https://github.com/torvalds/linux/blob/cdc9718d5e590d6905361800b938b93f2b66818e/lib/glob.c
17+
bool glob(char const* pat, char const* str) {
18+
/*
19+
* Backtrack to previous * on mismatch and retry starting one
20+
* character later in the string. Because * matches all characters
21+
* (no exception for /), it can be easily proved that there's
22+
* never a need to backtrack multiple levels.
23+
*/
24+
char const* back_pat = nullptr;
25+
char const* back_str = nullptr;
26+
27+
/*
28+
* Loop over each token (character or class) in pat, matching
29+
* it against the remaining unmatched tail of str. Return false
30+
* on mismatch, or true after matching the trailing nul bytes.
31+
*/
32+
for (;;) {
33+
unsigned char c = *str++;
34+
unsigned char d = *pat++;
35+
36+
switch (d) {
37+
case '?': /* Wildcard: anything but nul */
38+
if (c == '\0') return false;
39+
break;
40+
case '*': /* Any-length wildcard */
41+
if (*pat == '\0') /* Optimize trailing * case */
42+
return true;
43+
back_pat = pat;
44+
back_str = --str; /* Allow zero-length match */
45+
break;
46+
case '[': { /* Character class */
47+
bool match = false, inverted = (*pat == '!');
48+
char const* cls = pat + inverted;
49+
unsigned char a = *cls++;
50+
51+
/*
52+
* Iterate over each span in the character class.
53+
* A span is either a single character a, or a
54+
* range a-b. The first span may begin with ']'.
55+
*/
56+
do {
57+
unsigned char b = a;
58+
59+
if (a == '\0') /* Malformed */
60+
goto literal;
61+
62+
if (cls[0] == '-' && cls[1] != ']') {
63+
b = cls[1];
64+
65+
if (b == '\0') goto literal;
66+
67+
cls += 2;
68+
/* Any special action if a > b? */
69+
}
70+
match |= (a <= c && c <= b);
71+
} while ((a = *cls++) != ']');
72+
73+
if (match == inverted) goto backtrack;
74+
pat = cls;
75+
} break;
76+
case '\\':
77+
d = *pat++;
78+
[[fallthrough]];
79+
default: /* Literal character */
80+
literal:
81+
if (c == d) {
82+
if (d == '\0') return true;
83+
break;
84+
}
85+
backtrack:
86+
if (c == '\0' || !back_pat) return false; /* No point continuing */
87+
/* Try again from last *, one character later in str. */
88+
pat = back_pat;
89+
str = ++back_str;
90+
break;
91+
}
92+
}
93+
}
94+
void glob(const FunctionCallbackInfo<Value>& args) {
95+
Environment* env = Environment::GetCurrent(args);
96+
CHECK_GE(args.Length(), 2);
97+
CHECK(args[0]->IsString());
98+
CHECK(args[1]->IsString());
99+
100+
std::string pattern = Utf8Value(env->isolate(), args[0]).ToString();
101+
std::string str = Utf8Value(env->isolate(), args[1]).ToString();
102+
args.GetReturnValue().Set(glob(pattern.c_str(), str.c_str()));
103+
}
104+
105+
void Initialize(Local<Object> target,
106+
Local<Value> unused,
107+
Local<Context> context,
108+
void* priv) {
109+
SetMethod(context, target, "glob", glob);
110+
}
111+
112+
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
113+
registry->Register(glob);
114+
}
115+
} // namespace path
116+
117+
} // namespace node
118+
119+
NODE_BINDING_CONTEXT_AWARE_INTERNAL(path, node::path::Initialize)
120+
NODE_BINDING_EXTERNAL_REFERENCE(path, node::path::RegisterExternalReferences)

src/node_path.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#ifndef SRC_NODE_PATH_H_
2+
#define SRC_NODE_PATH_H_
3+
4+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
5+
6+
#include "base_object.h"
7+
#include "node_snapshotable.h"
8+
#include "v8.h"
9+
10+
namespace node {
11+
12+
namespace path {} // namespace path
13+
} // namespace node
14+
15+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
16+
17+
#endif // SRC_NODE_PATH_H_

test/parallel/test-path-glob.mjs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import '../common/index.mjs';
2+
import { describe, it } from 'node:test';
3+
import * as assert from 'node:assert';
4+
import * as path from 'node:path';
5+
6+
7+
// https://github.com/torvalds/linux/blob/cdc9718d5e590d6905361800b938b93f2b66818e/lib/globtest.c
8+
const patterns = [
9+
{ expected: true, pattern: 'a', name: 'a' },
10+
{ expected: false, pattern: 'a', name: 'b' },
11+
{ expected: false, pattern: 'a', name: 'aa' },
12+
{ expected: false, pattern: 'a', name: '' },
13+
{ expected: true, pattern: '', name: '' },
14+
{ expected: false, pattern: '', name: 'a' },
15+
/* Simple character class tests */
16+
{ expected: true, pattern: '[a]', name: 'a' },
17+
{ expected: false, pattern: '[a]', name: 'b' },
18+
{ expected: false, pattern: '[!a]', name: 'a' },
19+
{ expected: true, pattern: '[!a]', name: 'b' },
20+
{ expected: true, pattern: '[ab]', name: 'a' },
21+
{ expected: true, pattern: '[ab]', name: 'b' },
22+
{ expected: false, pattern: '[ab]', name: 'c' },
23+
{ expected: true, pattern: '[!ab]', name: 'c' },
24+
{ expected: true, pattern: '[a-c]', name: 'b' },
25+
{ expected: false, pattern: '[a-c]', name: 'd' },
26+
/* Corner cases in character class parsing */
27+
{ expected: true, pattern: '[a-c-e-g]', name: '-' },
28+
{ expected: false, pattern: '[a-c-e-g]', name: 'd' },
29+
{ expected: true, pattern: '[a-c-e-g]', name: 'f' },
30+
{ expected: true, pattern: '[]a-ceg-ik[]', name: 'a' },
31+
{ expected: true, pattern: '[]a-ceg-ik[]', name: ']' },
32+
{ expected: true, pattern: '[]a-ceg-ik[]', name: '[' },
33+
{ expected: true, pattern: '[]a-ceg-ik[]', name: 'h' },
34+
{ expected: false, pattern: '[]a-ceg-ik[]', name: 'f' },
35+
{ expected: false, pattern: '[!]a-ceg-ik[]', name: 'h' },
36+
{ expected: false, pattern: '[!]a-ceg-ik[]', name: ']' },
37+
{ expected: true, pattern: '[!]a-ceg-ik[]', name: 'f' },
38+
/* Simple wild cards */
39+
{ expected: true, pattern: '?', name: 'a' },
40+
{ expected: false, pattern: '?', name: 'aa' },
41+
{ expected: false, pattern: '??', name: 'a' },
42+
{ expected: true, pattern: '?x?', name: 'axb' },
43+
{ expected: false, pattern: '?x?', name: 'abx' },
44+
{ expected: false, pattern: '?x?', name: 'xab' },
45+
/* Asterisk wild cards (backtracking) */
46+
{ expected: false, pattern: '*??', name: 'a' },
47+
{ expected: true, pattern: '*??', name: 'ab' },
48+
{ expected: true, pattern: '*??', name: 'abc' },
49+
{ expected: true, pattern: '*??', name: 'abcd' },
50+
{ expected: false, pattern: '??*', name: 'a' },
51+
{ expected: true, pattern: '??*', name: 'ab' },
52+
{ expected: true, pattern: '??*', name: 'abc' },
53+
{ expected: true, pattern: '??*', name: 'abcd' },
54+
{ expected: false, pattern: '?*?', name: 'a' },
55+
{ expected: true, pattern: '?*?', name: 'ab' },
56+
{ expected: true, pattern: '?*?', name: 'abc' },
57+
{ expected: true, pattern: '?*?', name: 'abcd' },
58+
{ expected: true, pattern: '*b', name: 'b' },
59+
{ expected: true, pattern: '*b', name: 'ab' },
60+
{ expected: false, pattern: '*b', name: 'ba' },
61+
{ expected: true, pattern: '*b', name: 'bb' },
62+
{ expected: true, pattern: '*b', name: 'abb' },
63+
{ expected: true, pattern: '*b', name: 'bab' },
64+
{ expected: true, pattern: '*bc', name: 'abbc' },
65+
{ expected: true, pattern: '*bc', name: 'bc' },
66+
{ expected: true, pattern: '*bc', name: 'bbc' },
67+
{ expected: true, pattern: '*bc', name: 'bcbc' },
68+
/* Multiple asterisks (complex backtracking) */
69+
{ expected: true, pattern: '*ac*', name: 'abacadaeafag' },
70+
{ expected: true, pattern: '*ac*ae*ag*', name: 'abacadaeafag' },
71+
{ expected: true, pattern: '*a*b*[bc]*[ef]*g*', name: 'abacadaeafag' },
72+
{ expected: false, pattern: '*a*b*[ef]*[cd]*g*', name: 'abacadaeafag' },
73+
{ expected: true, pattern: '*abcd*', name: 'abcabcabcabcdefg' },
74+
{ expected: true, pattern: '*ab*cd*', name: 'abcabcabcabcdefg' },
75+
{ expected: true, pattern: '*abcd*abcdef*', name: 'abcabcdabcdeabcdefg' },
76+
{ expected: false, pattern: '*abcd*', name: 'abcabcabcabcefg' },
77+
{ expected: false, pattern: '*ab*cd*', name: 'abcabcabcabcefg' },
78+
];
79+
80+
const invalid = [null, undefined, 1, Number.MAX_SAFE_INTEGER, true, false, Symbol(), {}, [], () => {}];
81+
82+
describe('path.glob', () => {
83+
for (const { expected, pattern, name } of patterns) {
84+
it(`pattern "${pattern}" should ${expected ? '' : 'not '}match "${name}"`, () => {
85+
assert.strictEqual(path.glob(pattern, name), expected);
86+
});
87+
}
88+
89+
for (const x of invalid) {
90+
const name = typeof x === 'symbol' ? 'Symnol()' : x;
91+
it(`${name} should throw as a parameter`, () => {
92+
assert.throws(() => path.glob(x, ''), { code: 'ERR_INVALID_ARG_TYPE' });
93+
assert.throws(() => path.glob('', x), { code: 'ERR_INVALID_ARG_TYPE' });
94+
});
95+
}
96+
});

0 commit comments

Comments
 (0)