Page MenuHomePhabricator

D11444.id27775.diff
No OneTemporary

D11444.id27775.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -399,6 +399,7 @@
'phutil_escape_uri' => 'markup/render.php',
'phutil_escape_uri_path_component' => 'markup/render.php',
'phutil_exit' => 'utils/utils.php',
+ 'phutil_fnmatch' => 'utils/utils.php',
'phutil_format_bytes' => 'utils/viewutils.php',
'phutil_format_relative_time' => 'utils/viewutils.php',
'phutil_format_relative_time_detailed' => 'utils/viewutils.php',
diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php
--- a/src/utils/__tests__/PhutilUtilsTestCase.php
+++ b/src/utils/__tests__/PhutilUtilsTestCase.php
@@ -674,4 +674,107 @@
phutil_var_export(new PhutilTestPhobject()));
}
+ public function testFnmatch() {
+ $cases = array(
+ '' => array(
+ array(''),
+ array('.', '/'),
+ ),
+ '*' => array(
+ array('file'),
+ array('dir/', '/dir'),
+ ),
+ '**' => array(
+ array('file', 'dir/', '/dir', 'dir/subdir/file'),
+ array(),
+ ),
+ '**/file' => array(
+ array('file', 'dir/file', 'dir/subdir/file', 'dir/subdir/subdir/file'),
+ array('file/', 'file/dir'),
+ ),
+ 'file.*' => array(
+ array('file.php', 'file.a', 'file.'),
+ array('files.php', 'file.php/blah'),
+ ),
+ 'fo?' => array(
+ array('foo', 'fot'),
+ array('fooo', 'ffoo', 'fo/', 'foo/'),
+ ),
+ 'fo{o,t}' => array(
+ array('foo', 'fot'),
+ array('fob', 'fo/', 'foo/'),
+ ),
+ 'fo{o,\\,}' => array(
+ array('foo', 'fo,'),
+ array('foo/', 'fo,/'),
+ ),
+ 'fo{o,\\\\}' => array(
+ array('foo', 'fo\\'),
+ array('foo/', 'fo\\/'),
+ ),
+ '/foo' => array(
+ array('/foo'),
+ array('foo', '/foo/'),
+ ),
+
+ // Tests for various `fnmatch` flags.
+ '*.txt' => array(
+ array(
+ 'file.txt',
+
+ // FNM_PERIOD
+ '.secret-file.txt',
+ ),
+ array(
+ // FNM_PATHNAME
+ 'dir/file.txt',
+
+ // FNM_CASEFOLD
+ 'file.TXT',
+ ),
+ '\\*.txt' => array(
+ array(
+ // FNM_NOESCAPE
+ '*.txt',
+ ),
+ array(
+ 'file.txt',
+ ),
+ ),
+ ),
+ );
+
+ $invalid = array(
+ '{',
+ 'asdf\\',
+ );
+
+ foreach ($cases as $input => $expect) {
+ list($matches, $no_matches) = $expect;
+
+ foreach ($matches as $match) {
+ $this->assertTrue(
+ phutil_fnmatch($input, $match),
+ pht('Expecting "%s" to match "%s".', $input, $match));
+ }
+
+ foreach ($no_matches as $no_match) {
+ $this->assertFalse(
+ phutil_fnmatch($input, $no_match),
+ pht('Expecting "%s" not to match "%s".', $input, $no_match));
+ }
+ }
+
+ foreach ($invalid as $input) {
+ $caught = null;
+ try {
+ phutil_fnmatch($input, '');
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertTrue($caught instanceof InvalidArgumentException);
+ }
+ }
+
}
diff --git a/src/utils/utils.php b/src/utils/utils.php
--- a/src/utils/utils.php
+++ b/src/utils/utils.php
@@ -1184,3 +1184,73 @@
// Let PHP handle everything else.
return var_export($var, true);
}
+
+
+/**
+ * An improved version of `fnmatch`.
+ *
+ * @param string A glob pattern.
+ * @param string A path.
+ * @return bool
+ */
+function phutil_fnmatch($glob, $path) {
+ // Modify the glob to allow `**/` to match files in the root directory.
+ $glob = preg_replace('@(?:(?<!\\\\)\\*){2}/@', '{,*/,**/}', $glob);
+
+ $escaping = false;
+ $in_curlies = 0;
+ $regex = '';
+
+ for ($i = 0; $i < strlen($glob); $i++) {
+ $char = $glob[$i];
+ $next_char = ($i < strlen($glob) - 1) ? $glob[$i + 1] : null;
+
+ $escape = array('$', '(', ')', '+', '.', '^', '|');
+ $mapping = array();
+
+ if ($escaping) {
+ $escape[] = '*';
+ $escape[] = '?';
+ $escape[] = '{';
+ } else {
+ $mapping['*'] = $next_char === '*' ? '.*' : '[^/]*';
+ $mapping['?'] = '[^/]';
+ $mapping['{'] = '(';
+
+ if ($in_curlies) {
+ $mapping[','] = '|';
+ $mapping['}'] = ')';
+ }
+ }
+
+ if (in_array($char, $escape)) {
+ $regex .= "\\{$char}";
+ } else if ($replacement = idx($mapping, $char, false)) {
+ $regex .= $replacement;
+ } else if ($char === '\\') {
+ if ($escaping) {
+ $regex .= '\\\\';
+ }
+
+ $escaping = !$escaping;
+ continue;
+ } else {
+ $regex .= $char;
+ }
+
+ if ($char === '{' && !$escaping) {
+ ++$in_curlies;
+ } else if ($char === '}' && $in_curlies && !$escaping) {
+ --$in_curlies;
+ }
+
+ $escaping = false;
+ }
+
+ if ($in_curlies || $escaping) {
+ throw new InvalidArgumentException(pht('Invalid glob pattern.'));
+ }
+
+ $regex = '(\A'.$regex.'\z)';
+ return (bool)preg_match($regex, $path);
+}

File Metadata

Mime Type
text/plain
Expires
Wed, May 8, 10:59 PM (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6273403
Default Alt Text
D11444.id27775.diff (5 KB)

Event Timeline