Skip to content

Commit

Permalink
path: add path.glob
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Apr 9, 2023
1 parent 284e6ac commit 9ae0a18
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 0 deletions.
4 changes: 4 additions & 0 deletions lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const {
StringPrototypeToLowerCase,
} = primordials;

const { glob } = internalBinding('path');

const {
CHAR_UPPERCASE_A,
CHAR_LOWERCASE_A,
Expand Down Expand Up @@ -1064,6 +1066,7 @@ const win32 = {

return ret;
},
glob,

sep: '\\',
delimiter: ';',
Expand Down Expand Up @@ -1530,6 +1533,7 @@ const posix = {

return ret;
},
glob,

sep: '/',
delimiter: ':',
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
'src/node_metadata.cc',
'src/node_options.cc',
'src/node_os.cc',
'src/node_path.cc',
'src/node_perf.cc',
'src/node_platform.cc',
'src/node_postmortem_metadata.cc',
Expand Down Expand Up @@ -225,6 +226,7 @@
'src/node_object_wrap.h',
'src/node_options.h',
'src/node_options-inl.h',
'src/node_path.h',
'src/node_perf.h',
'src/node_perf_common.h',
'src/node_platform.h',
Expand Down
1 change: 1 addition & 0 deletions src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
V(mksnapshot) \
V(options) \
V(os) \
V(path) \
V(performance) \
V(permission) \
V(pipe_wrap) \
Expand Down
120 changes: 120 additions & 0 deletions src/node_path.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#include "node_path.h"
#include "env-inl.h"
#include "node_errors.h"
#include "node_external_reference.h"

namespace node {

namespace path {
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Local;
using v8::Object;
using v8::Value;

// extracted from
// https://github.com/torvalds/linux/blob/cdc9718d5e590d6905361800b938b93f2b66818e/lib/glob.c
bool glob(char const* pat, char const* str) {
/*
* Backtrack to previous * on mismatch and retry starting one
* character later in the string. Because * matches all characters
* (no exception for /), it can be easily proved that there's
* never a need to backtrack multiple levels.
*/
char const* back_pat = nullptr;
char const* back_str = nullptr;

/*
* Loop over each token (character or class) in pat, matching
* it against the remaining unmatched tail of str. Return false
* on mismatch, or true after matching the trailing nul bytes.
*/
for (;;) {
unsigned char c = *str++;
unsigned char d = *pat++;

switch (d) {
case '?': /* Wildcard: anything but nul */
if (c == '\0') return false;
break;
case '*': /* Any-length wildcard */
if (*pat == '\0') /* Optimize trailing * case */
return true;
back_pat = pat;
back_str = --str; /* Allow zero-length match */
break;
case '[': { /* Character class */
bool match = false, inverted = (*pat == '!');
char const* cls = pat + inverted;
unsigned char a = *cls++;

/*
* Iterate over each span in the character class.
* A span is either a single character a, or a
* range a-b. The first span may begin with ']'.
*/
do {
unsigned char b = a;

if (a == '\0') /* Malformed */
goto literal;

if (cls[0] == '-' && cls[1] != ']') {
b = cls[1];

if (b == '\0') goto literal;

cls += 2;
/* Any special action if a > b? */
}
match |= (a <= c && c <= b);
} while ((a = *cls++) != ']');

if (match == inverted) goto backtrack;
pat = cls;
} break;
case '\\':
d = *pat++;
[[fallthrough]];
default: /* Literal character */
literal:
if (c == d) {
if (d == '\0') return true;
break;
}
backtrack:
if (c == '\0' || !back_pat) return false; /* No point continuing */
/* Try again from last *, one character later in str. */
pat = back_pat;
str = ++back_str;
break;
}
}
}
void glob(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 2);
CHECK(args[0]->IsString());
CHECK(args[1]->IsString());

std::string pattern = Utf8Value(env->isolate(), args[0]).ToString();
std::string str = Utf8Value(env->isolate(), args[1]).ToString();
args.GetReturnValue().Set(glob(pattern.c_str(), str.c_str()));
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
SetMethod(context, target, "glob", glob);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(glob);
}
} // namespace path

} // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(path, node::path::Initialize)
NODE_BINDING_EXTERNAL_REFERENCE(path, node::path::RegisterExternalReferences)
17 changes: 17 additions & 0 deletions src/node_path.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#ifndef SRC_NODE_PATH_H_
#define SRC_NODE_PATH_H_

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include "base_object.h"
#include "node_snapshotable.h"
#include "v8.h"

namespace node {

namespace path {} // namespace path
} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#endif // SRC_NODE_PATH_H_
88 changes: 88 additions & 0 deletions test/parallel/test-path-glob.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import '../common/index.mjs';
import { describe, it } from 'node:test';
import * as assert from 'node:assert';
import * as path from 'node:path';


// https://github.com/torvalds/linux/blob/cdc9718d5e590d6905361800b938b93f2b66818e/lib/globtest.c
const patterns = [
{ expected: true, pattern: 'a', name: 'a' },
{ expected: false, pattern: 'a', name: 'b' },
{ expected: false, pattern: 'a', name: 'aa' },
{ expected: false, pattern: 'a', name: '' },
{ expected: true, pattern: '', name: '' },
{ expected: false, pattern: '', name: 'a' },
/* Simple character class tests */
{ expected: true, pattern: '[a]', name: 'a' },
{ expected: false, pattern: '[a]', name: 'b' },
{ expected: false, pattern: '[!a]', name: 'a' },
{ expected: true, pattern: '[!a]', name: 'b' },
{ expected: true, pattern: '[ab]', name: 'a' },
{ expected: true, pattern: '[ab]', name: 'b' },
{ expected: false, pattern: '[ab]', name: 'c' },
{ expected: true, pattern: '[!ab]', name: 'c' },
{ expected: true, pattern: '[a-c]', name: 'b' },
{ expected: false, pattern: '[a-c]', name: 'd' },
/* Corner cases in character class parsing */
{ expected: true, pattern: '[a-c-e-g]', name: '-' },
{ expected: false, pattern: '[a-c-e-g]', name: 'd' },
{ expected: true, pattern: '[a-c-e-g]', name: 'f' },
{ expected: true, pattern: '[]a-ceg-ik[]', name: 'a' },
{ expected: true, pattern: '[]a-ceg-ik[]', name: ']' },
{ expected: true, pattern: '[]a-ceg-ik[]', name: '[' },
{ expected: true, pattern: '[]a-ceg-ik[]', name: 'h' },
{ expected: false, pattern: '[]a-ceg-ik[]', name: 'f' },
{ expected: false, pattern: '[!]a-ceg-ik[]', name: 'h' },
{ expected: false, pattern: '[!]a-ceg-ik[]', name: ']' },
{ expected: true, pattern: '[!]a-ceg-ik[]', name: 'f' },
/* Simple wild cards */
{ expected: true, pattern: '?', name: 'a' },
{ expected: false, pattern: '?', name: 'aa' },
{ expected: false, pattern: '??', name: 'a' },
{ expected: true, pattern: '?x?', name: 'axb' },
{ expected: false, pattern: '?x?', name: 'abx' },
{ expected: false, pattern: '?x?', name: 'xab' },
/* Asterisk wild cards (backtracking) */
{ expected: false, pattern: '*??', name: 'a' },
{ expected: true, pattern: '*??', name: 'ab' },
{ expected: true, pattern: '*??', name: 'abc' },
{ expected: true, pattern: '*??', name: 'abcd' },
{ expected: false, pattern: '??*', name: 'a' },
{ expected: true, pattern: '??*', name: 'ab' },
{ expected: true, pattern: '??*', name: 'abc' },
{ expected: true, pattern: '??*', name: 'abcd' },
{ expected: false, pattern: '?*?', name: 'a' },
{ expected: true, pattern: '?*?', name: 'ab' },
{ expected: true, pattern: '?*?', name: 'abc' },
{ expected: true, pattern: '?*?', name: 'abcd' },
{ expected: true, pattern: '*b', name: 'b' },
{ expected: true, pattern: '*b', name: 'ab' },
{ expected: false, pattern: '*b', name: 'ba' },
{ expected: true, pattern: '*b', name: 'bb' },
{ expected: true, pattern: '*b', name: 'abb' },
{ expected: true, pattern: '*b', name: 'bab' },
{ expected: true, pattern: '*bc', name: 'abbc' },
{ expected: true, pattern: '*bc', name: 'bc' },
{ expected: true, pattern: '*bc', name: 'bbc' },
{ expected: true, pattern: '*bc', name: 'bcbc' },
/* Multiple asterisks (complex backtracking) */
{ expected: true, pattern: '*ac*', name: 'abacadaeafag' },
{ expected: true, pattern: '*ac*ae*ag*', name: 'abacadaeafag' },
{ expected: true, pattern: '*a*b*[bc]*[ef]*g*', name: 'abacadaeafag' },
{ expected: false, pattern: '*a*b*[ef]*[cd]*g*', name: 'abacadaeafag' },
{ expected: true, pattern: '*abcd*', name: 'abcabcabcabcdefg' },
{ expected: true, pattern: '*ab*cd*', name: 'abcabcabcabcdefg' },
{ expected: true, pattern: '*abcd*abcdef*', name: 'abcabcdabcdeabcdefg' },
{ expected: false, pattern: '*abcd*', name: 'abcabcabcabcefg' },
{ expected: false, pattern: '*ab*cd*', name: 'abcabcabcabcefg' },
];

describe('path.glob', () => {
for (const { expected, pattern, name } of patterns) {
it(`pattern "${pattern}" should ${expected ? '' : 'not '}match "${name}"`, () => {
assert.strictEqual(path.glob(pattern, name), expected);
});
}

// TODO: error handling, invalid parameters
});

0 comments on commit 9ae0a18

Please sign in to comment.