web-dev-qa-db-ja.com

パスがNode.jsの別のサブディレクトリかどうかを確認する

私は MQTTハンドラー に取り組んでいます。イベントリスナーがある親ディレクトリごとにイベントを発行したいと思います。例えば:

利用可能な以下のMQTTパスがあり、サブスクリプトがある場合–これらのパスにはイベントリスナーがあります–

  • test
  • replyer/request
  • test/replyer/request

そして誰かがトピックtest/replyer/request/@issuer、2つのイベントがエミットされるはずです:testtest/replyer/request

どのパスも可能であり、使用可能な有効なイベントのリストがない場合、パスが別のパスの親であるかどうかのみを確認する必要があります。これを正規表現で実行できますか?もしそうなら、それはどのように見えますか?よりシンプルで効率的なソリューションはありますか?

20
jsdario

2017年後半の回答

ES6の場合

_const isChildOf = (child, parent) => {
  if (child === parent) return false
  const parentTokens = parent.split('/').filter(i => i.length)
  return parentTokens.every((t, i) => child.split('/')[i] === t)
}
_

node.jsで作業していて、クロスプラットフォームにしたい場合は、pathモジュールを含め、split('/')split(path.sep)に置き換えます


使い方:

したがって、ディレクトリ(_home/etc/subdirectory_など)が別のディレクトリ(_home/etc_など)のサブディレクトリであるかどうかを確認する必要があります。

仮説のchildパスとparentパスの両方を取得し、splitを使用して配列に変換します。

_['home', 'etc', 'subdirectory'], ['home', 'etc']
_

次に、parent配列内のすべてのトークンを反復処理し、ES6の.every()を使用して、child配列内の相対位置に対して1つずつ確認します。

親のすべてが子のすべてと一致し、それらがまったく同じディレクトリであることが除外されていることがわかっている場合(_child !== parent_を使用)、答えが得られます。

5
Dom Vinyard

Node自体が作業を行います。

const path = require('path');
const relative = path.relative(parent, dir);
return relative && !relative.startsWith('..') && !path.isAbsolute(relative);

正規化も行います。

const path = require('path');

const tests = [
  ['/foo', '/foo'],
  ['/foo', '/bar'],
  ['/foo', '/foobar'],
  ['/foo', '/foo/bar'],
  ['/foo', '/foo/../bar'],
  ['/foo', '/foo/./bar'],
  ['/bar/../foo', '/foo/bar'],
  ['/foo', './bar'],
  ['C:\\Foo', 'C:\\Foo\\Bar'],
  ['C:\\Foo', 'C:\\Bar'],
  ['C:\\Foo', 'D:\\Foo\\Bar'],
];

tests.forEach(([parent, dir]) => {
    const relative = path.relative(parent, dir);
    const isSubdir = relative && !relative.startsWith('..') && !path.isAbsolute(relative);
    console.log(`[${parent}, ${dir}] => ${isSubdir} (${relative})`);
});

Windowsのドライブ間でも動作します。

[/foo, /foo] => false ()
[/foo, /bar] => false (..\bar)
[/foo, /foobar] => false (..\foobar)
[/foo, /foo/bar] => true (bar)
[/foo, /foo/../bar] => false (..\bar)
[/foo, /foo/./bar] => true (bar)
[/bar/../foo, /foo/bar] => true (bar)
[/foo, ./bar] => false (..\Users\kozhevnikov\Desktop\bar)
[C:\Foo, C:\Foo\Bar] => true (Bar)
[C:\Foo, C:\Bar] => false (..\Bar)
[C:\Foo, D:\Foo\Bar] => false (D:\Foo\Bar)
39

これは本当に古い質問ですが、ノードの組み込み path.relative を使用して、これに対する本当に簡単な解決策を思いつきました。子が親の内側にある場合、子からへの相対パスは常に..で始まります。シンプル。

import { relative } from 'path';

function isSubDirectory(parent, child) {
  return relative(child, parent).startsWith('..');
}
3
Lenny

正規表現でそれを行うことはそれを回避する1つの方法です(イベントリスナーを持つすべてのパスについて、公開されたトピックがそのパスで始まっているかどうかを確認してください)。 URL、公開されたトピックを分解する方が効率的かもしれません。

このようなものもおそらく読みやすいでしょう:

Edit@ huaoguo は間違いなく正しい、indexOf === 0は本当に必要なすべてです!

let paths = [
  'test',
  'replyer/request',
  'test/replyer/request'
]

let topic = 'test/replyer/request/@issuer'

let respondingPaths = (paths, topic) => paths.filter(path => topic.indexOf(path) === 0)

console.log(respondingPaths(paths, topic)) // ['test', 'test/replyer/request']
1

失敗を防ぐために必要なことがいくつかあります。

  • ファイルシステムのパスを解決する必要がありますか? (私はそう思う)
  • あるディレクトリに別のディレクトリが含まれているかどうかを確認すると、シンボリックリンクが機能するはずです。

私は、ファイルシステムのパスを可能な限り解決しようとする一方で、存在するかどうかに関係なくパスを許可するソリューションを考え出しました:

  • OSのパスセパレーターでパスを分割します
  • ファイルシステム内のこれらのパスコンポーネントをできるだけ多く解決する
  • 解決できなかった残りのコンポーネントを追加する
  • 親と子の間の相対パスが_.. + path.sep_で始まらず、_.._ではない場合、親パスには子パスが含まれます

これはすべて機能し、存在しないパスコンポーネントはディレクトリとファイルのみを使用して作成されると想定します(シンボリックリンクなし)。たとえば、スクリプトがホワイトリストに登録されたパスにのみ書き込む必要があり、信頼できない(ユーザー指定の)ファイル名を受け入れるとします。 PHPのmkdir と_$recursive = true_を使用してサブディレクトリを作成し、 この例 と同様に、1つの手順でディレクトリ構造を作成できます。

以下はコードです(Stack OverflowがNode.jsをサポートするまで実行できません)。重要な関数はresolveFileSystemPath()pathContains()です:

_const kWin32 = false;

const fs = require('fs');
const path = kWin32 ? require('path').win32 : require('path');

////////// functions //////////

// resolves (possibly nonexistent) path in filesystem, assuming that any missing components would be files or directories (not symlinks)
function resolveFileSystemPath(thePath) {
        let remainders = [];

        for (
                let parts = path.normalize(thePath).split(path.sep); // handle any combination of "/" or "\" path separators
                parts.length > 0;
                remainders.unshift(parts.pop())
        ) {
                try {
                        thePath =
                                fs.realpathSync(parts.join('/')) + // fs expects "/" for cross-platform compatibility
                                (remainders.length ? path.sep + remainders.join(path.sep) : ''); // if all attempts fail, then path remains unchanged

                        break;
                } catch (e) {}
        }

        return path.normalize(thePath);
}

// returns true if parentPath contains childPath, assuming that any missing components would be files or directories (not symlinks)
function pathContains(parentPath, childPath, resolveFileSystemPaths = true) {
        if (resolveFileSystemPaths) {
                parentPath = resolveFileSystemPath(parentPath);
                childPath = resolveFileSystemPath(childPath);
        }

        const relativePath = path.relative(parentPath, childPath);

        return !relativePath.startsWith('..' + path.sep) && relativePath != '..';
}

////////// file/directory/symlink creation //////////

console.log('directory contents:');

console.log();

try {
        fs.mkdirSync('parent');
} catch (e) {} // suppress error if already exists

fs.writeFileSync('parent/child.txt', 'Hello, world!');

try {
        fs.mkdirSync('outside');
} catch (e) {} // suppress error if already exists

try {
        fs.symlinkSync(path.relative('parent', 'outside'), 'parent/child-symlink');
} catch (e) {} // suppress error if already exists

fs.readdirSync('.').forEach(file => {
        const stat = fs.lstatSync(file);

        console.log(
                stat.isFile()
                        ? 'file'
                        : stat.isDirectory() ? 'dir ' : stat.isSymbolicLink() ? 'link' : '    ',
                file
        );
});
fs.readdirSync('parent').forEach(file => {
        file = 'parent/' + file;

        const stat = fs.lstatSync(file);

        console.log(
                stat.isFile()
                        ? 'file'
                        : stat.isDirectory() ? 'dir ' : stat.isSymbolicLink() ? 'link' : '    ',
                file
        );
});

////////// tests //////////

console.log();

console.log(
        "path.resolve('parent/child.txt'):       ",
        path.resolve('parent/child.txt')
);
console.log(
        "fs.realpathSync('parent/child.txt'):    ",
        fs.realpathSync('parent/child.txt')
);
console.log(
        "path.resolve('parent/child-symlink'):   ",
        path.resolve('parent/child-symlink')
);
console.log(
        "fs.realpathSync('parent/child-symlink'):",
        fs.realpathSync('parent/child-symlink')
);

console.log();

console.log(
        'parent contains .:                                ',
        pathContains('parent', '.', true)
);
console.log(
        'parent contains ..:                               ',
        pathContains('parent', '..', true)
);
console.log(
        'parent contains parent:                           ',
        pathContains('parent', 'parent', true)
);
console.log(
        'parent contains parent/.:                         ',
        pathContains('parent', 'parent/.', true)
);
console.log(
        'parent contains parent/..:                        ',
        pathContains('parent', 'parent/..', true)
);
console.log(
        'parent contains parent/child.txt (unresolved):    ',
        pathContains('parent', 'parent/child.txt', false)
);
console.log(
        'parent contains parent/child.txt (resolved):      ',
        pathContains('parent', 'parent/child.txt', true)
);
console.log(
        'parent contains parent/child-symlink (unresolved):',
        pathContains('parent', 'parent/child-symlink', false)
);
console.log(
        'parent contains parent/child-symlink (resolved):  ',
        pathContains('parent', 'parent/child-symlink', true)
);_

出力:

_directory contents:

file .bash_logout
file .bashrc
file .profile
file config.json
dir  node_modules
dir  outside
dir  parent
link parent/child-symlink
file parent/child.txt

path.resolve('parent/child.txt'):        /home/runner/parent/child.txt
fs.realpathSync('parent/child.txt'):     /home/runner/parent/child.txt
path.resolve('parent/child-symlink'):    /home/runner/parent/child-symlink
fs.realpathSync('parent/child-symlink'): /home/runner/outside

parent contains .:                                 false
parent contains ..:                                false
parent contains parent:                            true
parent contains parent/.:                          true
parent contains parent/..:                         false
parent contains parent/child.txt (unresolved):     true
parent contains parent/child.txt (resolved):       true
parent contains parent/child-symlink (unresolved): true
parent contains parent/child-symlink (resolved):   false
_

ライブの例: https://repl.it/repls/LawngreenWorriedGreyware

出力の最後の行は重要な行であり、解決されたファイルシステムパスが正しい結果につながる方法を示しています(その上の未解決の結果とは異なります)。

ファイルシステムの読み取り/書き込みを特定のディレクトリに制限することはセキュリティにとって非常に重要なので、Node.jsがこの機能を組み込みに組み込むことを望みます。これはネイティブのWindowsボックスでテストしていませんので、_kWin32_フラグが機能しているかどうかをお知らせください。時間の許す限り、私はこの答えを整理するように努めます。

0
Zack Morris

@ dom-vinyardのアイデアは良いですが、コードが正しく機能していません。たとえば、次の入力では:

isChildOf('/x/y', '/x') //false

私はここに自分のバージョンを書きました:

function isParentOf(child, parent) {
  const childTokens = child.split('/').filter(i => i.length);
  const parentTokens = parent.split('/').filter(i => i.length);

  if (parentTokens.length > childTokens.length || childTokens.length === parentTokens.length) {
    return false;
  }

  return childTokens
    .slice(0, parentTokens.length)
    .every((childToken, index) => parentTokens[index] === childToken);
}
0

ここでは、indexOfを使用する別のソリューション(または文字列を比較することで機能します)。
以下の関数では、複数のパス区切り文字をサポートするためにindexOfを使用しませんでした。チェックすることはできますが、セパレーターが1つであることが確かな場合は、indexOfを問題なく使用できます。
トリックは、パスがセパレータで終わるかどうかをチェックするかifそのようなセパレータをそれに追加するだけではありません。その場合、子パスに完全パスではない部分文字列があっても問題はありません。 [/this/isme_manおよび/this/isme](indexOf(もちろんfalseの場合)を単純に使用する場合、1つ目は2つ目の子ですが、このようなトリックを使用する場合は[/this/isme/および/this/isme_man/]と同じindexOfを使用して比較すると、問題はありません。
orEqual(子または等しい)でのチェックを許可するオプションがあることにも注意してください。これは3番目のオプションパラメーターです。

以下のコードを確認してください。

const PATH_SEPA = ['\\', '/'];

function isPathChildOf(path, parentPath, orEqual) {
    path = path.trim();
    parentPath = parentPath.trim();

    // trick: making sure the paths end with a separator
    let lastChar_path = path[path.length - 1];
    let lastChar_parentPath = path[parentPath.length - 1];
    if (lastChar_parentPath !== '\\' && lastChar_parentPath !== '/') parentPath += '/';
    if (lastChar_path !== '\\' && lastChar_path !== '/') path += '/';

    if (!orEqual && parentPath.length >= path.length) return false; // parent path should be smaller in characters then the child path (and they should be all the same from the start , if they differ in one char then they are not related)

    for (let i = 0; i < parentPath.length; i++) {
        // if both are not separators, then we compare (if one is separator, the other is not, the are different, then it return false, if they are both no separators, then it come down to comparaison, if they are same nothing happen, if they are different it return false)
        if (!(isPathSeparator(parentPath[i]) && isPathSeparator(path[i])) && parentPath[i] !== path[i]) {
            return false;
        }
    }
    return true;
}

function isPathSeparator(chr) {
    for (let i = 0; i < PATH_SEPA.length; i++) {
        if (chr === PATH_SEPA[i]) return true;
    }
    return false;
}

ここでテスト例:

let path = '/ok/this/is/the/path';
let parentPath = '/ok/this/is';
let parentPath2 = '/ok/this/is/';
let parentPath3 = '/notok/this/is/different';

console.log("/ok/this/is/the/path' is child of /ok/this/is => " + isPathChildOf(path, parentPath));
console.log("/ok/this/is/the/path' is child of /ok/this/is/=> " + isPathChildOf(path, parentPath2));
console.log("/ok/this/is/' is child of /ok/this/is/ => " + isPathChildOf(parentPath2, parentPath2));
console.log("/ok/this/is/the/path' is child of /notok/this/is/different => " + isPathChildOf(path, parentPath3));

// test number 2:

console.log('test number 2 : ');
console.log("=============================");

let pthParent = '/look/at/this/path';
let pth = '/look/at/this/patholabi/hola'; // in normal use of indexof it will return true (know too we didn't use indexof just to support the different path separators, otherwise we would have used indexof in our function)

//expected result is false
console.log(`${pth}  is a child of ${pthParent}  ===>  ${isPathChildOf(pth, pthParent)}`);


let pthParent2 = '/look/at/this/path';
let pth2 = '/look/at/this/path/hola'; 

//expected result is true
console.log(`${pth2}  is a child of ${pthParent2}  ===>  ${isPathChildOf(pth2, pthParent2)}`);


let pthParent3 = '/look/at/this/path';
let pth3 = '/look/at/this/pathholabi'; 

//expected result is false
console.log(`${pth3}  is a child of ${pthParent3}  ===>  ${isPathChildOf(pth3, pthParent3)}`);

// test 3: equality
console.log('\ntest 3 : equality');
console.log("==========================");

let pParent =  "/this/is/same/Path";
let p =  "/this\\is/same/Path/";

console.log(`${p} is child of  ${pParent}   ====> ${isPathChildOf(p, pParent, true)}`);

最後の例では、関数を使用して両方が子か等しいかを確認する方法を示しています(これは非常に少数です)。

また、splitメソッドの別の実装(正規表現エンジンを使用せずに複数のセパレーターを使用した分割メソッド)を含む、2つの関連するgithubリポジトリを確認できます。また、このメソッドもいくつか説明します(コメントを確認してください)コード内):

0
Mohamed Allal

Dom Vinyardのコードに基づく&改善:

const path = require('path');

function isAncestorDir(papa, child) {
    const papaDirs = papa.split(path.sep).filter(dir => dir!=='');
    const childDirs = child.split(path.sep).filter(dir => dir!=='');

    return papaDirs.every((dir, i) => childDirs[i] === dir);
}

結果:

assert(isAncestorDir('/path/to/parent', '/path/to/parent/and/child')===true);
assert(isAncestorDir('/path/to/parent', '/path/to')===false);
assert(isAncestorDir('/path/to/parent', '/path/to/parent')===true);
0
brillout