如何从 Perl 创建然后使用长 Windows 路径?

发布于 2024-08-10 22:36:55 字数 2406 浏览 4 评论 0原文

我有一个构建过程的一部分,它在 Windows 中创建了一条长得可怕的路径。这不是我的错。它有几个目录深,并且没有一个目录名异常长;它们的长度和数量足以超过 MAX_PATH(260 个字符)。我在这些名称中没有使用除 ASCII 之外的任何内容。

最大的问题是爆炸发生在 Module::Build 的内部深处在 dist 目标期间,尽管我认为构建系统并不重要,因为它们会创建相同的目录。

使用 File::Path 创建这些过长的目录之一会失败:

 use File::Path qw( make_path );

 make_path( 'C:\\.....' ); # fails if path is over 260 chars

同样,一旦绝对路径超过 MAX_PATH,手动构建每个目录级别就会失败。

这不是新的,也不是 Perl 的错,微软在 命名文件、路径和命名空间。他们的修复建议在任何路径前面添加 \\?\ 来访问 Unicode 文件名 API。然而,这似乎并不是 Perl 脚本的完整修复,因为它仍然失败:

 use File::Path qw( make_path );

 make_path( '\\\\?\\C:\\.....' );  # still fails if path is over MAX_PATH, works otherwise

这可能是因为 make_path 将其参数分开,然后一次一层地遍历目录,因此\\?\ 仅适用于顶层,位于 MAX_PATH 内。

我挖出了一份 ActiveState 的错误报告,表明还有其他问题需要修复要获取 Unicode 文件名,Jan Dubois 在 回复:Windows 2K/XP 上的“长”文件名,尽管我不确定它是否适用(并且非常旧)。 perlrun 提到这是 -C 开关的工作,但显然那部分被放弃了。 Perl RT 队列有一个更新的错误60888:Win32:支持完整文件名中包含 unicode(使用宽系统调用)

Miyakawa 指出一些 Unicode 文件名问题Win32API::File 没有特别提及长路径。然而,Win32API::File CPAN 论坛条目似乎只表明恐惧,这会导致愤怒,这会导致仇恨,等等。 Perlmonks 帖子中有一个示例 如何在 Windows 中统计具有 Unicode (UTF16-LE) 文件名的文件?。看来 Win32::CreateDirectory 就是答案,下次我靠近 Windows 机器时我会尝试一下。

然后,假设我可以创建长路径。现在我必须教 Module::Build,也许还有其他东西来处理它。如果Win32::GetANSIPathName()按照它所说的去做,那么使用monkeypatches可能会立即变得容易。

I have part of a build process that creates a hideously long paths in Windows. It's not my fault. It's several directories deep, and none of the directory names are abnormally long; they're just long and numerous enough to make it over MAX_PATH (260 chars). I'm not using anything other than ASCII in these names.

The big problem is that the blow-up happens deep in the guts of Module::Build during the dist target, although I figure the build system doesn't matter because they'd make the same directories.

Creating one of these overly-long directories with File::Path fails:

 use File::Path qw( make_path );

 make_path( 'C:\\.....' ); # fails if path is over 260 chars

Similarly, constructing each directory level by hand fails once the absolute path would go over MAX_PATH.

This isn't new, isn't Perl's fault, and Microsoft documents it in Naming Files, Paths, and Namespaces. Their fix suggests adding the \\?\ in front of any path to access the Unicode filename API. However, that doesn't seem to be the full fix for a Perl script because it still fails:

 use File::Path qw( make_path );

 make_path( '\\\\?\\C:\\.....' );  # still fails if path is over MAX_PATH, works otherwise

This might be because make_path pulls apart its argument and then goes through the directories one level at a time, so \\?\ only applies to the top-level, which is within MAX_PATH.

I dug up a bug report to ActiveState that suggests there's something else I need to fix up to get to the Unicode filenames, and Jan Dubois gives a bit more details in Re: "long" filenames on Windows 2K/XP, although I'm not sure it applies (and is extremely old). perlrun mentions that this use to be the job of the -C switch, but apparently that part was abandoned. The perl RT queue has a more recent bug 60888: Win32: support full unicode in filenames (use Wide-system calls).

Miyagawa notes some Unicode filename issues and Win32API::File without specifically mentioning long paths. However, the Win32API::File CPAN Forum entry seems to indicate only fear, which leads to anger, which leads to hate, and so on. There's an example in the Perlmonks post How to stat a file with a Unicode (UTF16-LE) filename in Windows?. It seems the Win32::CreateDirectory is the answer, and I'll try that the next time I get next to a Windows machine.

Then, supposing I can create the long path path. Now I have to teach Module::Build, and maybe other things, to handle it. That might be immediately easy with monkeypatches if Win32::GetANSIPathName() does what it says on the tin.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

゛清羽墨安 2024-08-17 22:36:55

Windows 对于每个需要处理字符串的函数有两个单独的系统调用,一个使用 ANSI 又名活动代码页作为编码(例如 cp1252)的“A”调用,一个使用 UTF-16le 的“W”调用。 Perl 使用“A”调用,而 \\?\ 仅适用于“W”调用。

您可以使用 Win32::API 访问“W”调用,如下面的脚本所示,但是 Win32 ::LongPath 不仅使用“W”调用,还会自动添加 \\?\

使用 Win32::API 调用 CreateDirectoryW 以使用长路径(\\?\ - 前缀路径)的示例:

#!/usr/bin/perl

use strict;
use warnings;

use Carp;
use Encode qw( encode );
use Symbol;

use Win32;

use Win32API::File qw(
    CreateFileW OsFHandleOpen
    FILE_GENERIC_READ FILE_GENERIC_WRITE
    OPEN_EXISTING CREATE_ALWAYS FILE_SHARE_READ
);

use Win32::API;
use File::Spec::Functions qw(catfile);

Win32::API->Import(
    Kernel32 => qq{BOOL CreateDirectoryW(LPWSTR lpPathNameW, VOID *p)}
);

my %modes = (
    '<' => {
        access => FILE_GENERIC_READ,
        create => OPEN_EXISTING,
        mode   => 'r',
    },
    '>' => {
        access => FILE_GENERIC_WRITE,
        create => CREATE_ALWAYS,
        mode   => 'w',
    },
    # and the rest ...
);

use ex::override open => sub(*;$@) {
    $_[0] = gensym;

    my %mode = %{ $modes{$_[1]} };

    my $os_fh = CreateFileW(
        encode('UCS-2le', "$_[2]\0"),
        $mode{access},
        FILE_SHARE_READ,
        [],
        $mode{create},
        0,
        [],
    ) or do {$! = $^E; return };

    OsFHandleOpen($_[0], $os_fh, $mode{mode}) or return;
    return 1;
};

my $path = '\\\\?\\' . Win32::GetLongPathName($ENV{TEMP});
my @comps = ('0123456789') x 30;

my $dir = mk_long_dir($path, \@comps);
my $file = 'test.txt';
my $str = "This is a test\n";

write_test_file($dir, $file, $str);

$str eq read_test_file($dir, $file) or die "Read failure\n";

sub write_test_file {
    my ($dir, $file, $str) = @_,

    my $path = catfile $dir, $file;

    open my $fh, '>', $path
        or croak "Cannot open '$path':$!";

    print $fh $str or die "Cannot print: $!";
    close $fh or die "Cannot close: $!";
    return;
}

sub read_test_file {
    my ($dir, $file) = @_,

    my $path = catfile $dir, $file;

    open my $fh, '<', $path
        or croak "Cannot open '$path': $!";

    my $contents = do { local $/; <$fh> };
    close $fh or die "Cannot close: $!";
    return $contents;
}

sub mk_long_dir {
    my ($path, $comps) = @_;

    for my $comp ( @$comps ) {
        $path = catfile $path, $comp;
        my $ucs_path = encode('UCS-2le', "$path\0");
        CreateDirectoryW($ucs_path, undef)
            or croak "Failed to create directory: '$path': $^E";
    }
    return $path;
}

Windows has two separate system call for each function that needs to deal with strings, an "A" call using the ANSI aka Active Code Page as the encoding (e.g. cp1252) and a "W" call using UTF-16le. Perl uses "A" calls, while \\?\ only works with "W" calls.

You can use Win32::API to access the "W" calls as shown in the script below, but Win32::LongPath not only uses the "W" calls, but automatically adds \\?\!

Example of using Win32::API to call CreateDirectoryW to use a long path (\\?\-prefixed path):

#!/usr/bin/perl

use strict;
use warnings;

use Carp;
use Encode qw( encode );
use Symbol;

use Win32;

use Win32API::File qw(
    CreateFileW OsFHandleOpen
    FILE_GENERIC_READ FILE_GENERIC_WRITE
    OPEN_EXISTING CREATE_ALWAYS FILE_SHARE_READ
);

use Win32::API;
use File::Spec::Functions qw(catfile);

Win32::API->Import(
    Kernel32 => qq{BOOL CreateDirectoryW(LPWSTR lpPathNameW, VOID *p)}
);

my %modes = (
    '<' => {
        access => FILE_GENERIC_READ,
        create => OPEN_EXISTING,
        mode   => 'r',
    },
    '>' => {
        access => FILE_GENERIC_WRITE,
        create => CREATE_ALWAYS,
        mode   => 'w',
    },
    # and the rest ...
);

use ex::override open => sub(*;$@) {
    $_[0] = gensym;

    my %mode = %{ $modes{$_[1]} };

    my $os_fh = CreateFileW(
        encode('UCS-2le', "$_[2]\0"),
        $mode{access},
        FILE_SHARE_READ,
        [],
        $mode{create},
        0,
        [],
    ) or do {$! = $^E; return };

    OsFHandleOpen($_[0], $os_fh, $mode{mode}) or return;
    return 1;
};

my $path = '\\\\?\\' . Win32::GetLongPathName($ENV{TEMP});
my @comps = ('0123456789') x 30;

my $dir = mk_long_dir($path, \@comps);
my $file = 'test.txt';
my $str = "This is a test\n";

write_test_file($dir, $file, $str);

$str eq read_test_file($dir, $file) or die "Read failure\n";

sub write_test_file {
    my ($dir, $file, $str) = @_,

    my $path = catfile $dir, $file;

    open my $fh, '>', $path
        or croak "Cannot open '$path':$!";

    print $fh $str or die "Cannot print: $!";
    close $fh or die "Cannot close: $!";
    return;
}

sub read_test_file {
    my ($dir, $file) = @_,

    my $path = catfile $dir, $file;

    open my $fh, '<', $path
        or croak "Cannot open '$path': $!";

    my $contents = do { local $/; <$fh> };
    close $fh or die "Cannot close: $!";
    return $contents;
}

sub mk_long_dir {
    my ($path, $comps) = @_;

    for my $comp ( @$comps ) {
        $path = catfile $path, $comp;
        my $ucs_path = encode('UCS-2le', "$path\0");
        CreateDirectoryW($ucs_path, undef)
            or croak "Failed to create directory: '$path': $^E";
    }
    return $path;
}
简美 2024-08-17 22:36:55

以下代码实际上创建了相当深(超过 260 个字符长)的目录结构。至少在我的机器上:

use Win32::API;

$cd = Win32::API->new('kernel32', 'CreateDirectoryW', 'PP', 'N');

$dir = '\\\\?\\c:\\!experiments';

$res = 1;

do
{
    print 'path length: ' . length($dir) . "\n";
    $dirname = pack('S*', unpack('C*', "$dir\0"));  #dirty way to produce UTF-16LE string

    $res = $cd->Call($dirname, 0);
    print "$res\n";

    $dir .= '\\abcde';

} while ( $res );

Following code actually creates quite deep (more than 260 characters long) directory structure. At least on my machine:

use Win32::API;

$cd = Win32::API->new('kernel32', 'CreateDirectoryW', 'PP', 'N');

$dir = '\\\\?\\c:\\!experiments';

$res = 1;

do
{
    print 'path length: ' . length($dir) . "\n";
    $dirname = pack('S*', unpack('C*', "$dir\0"));  #dirty way to produce UTF-16LE string

    $res = $cd->Call($dirname, 0);
    print "$res\n";

    $dir .= '\\abcde';

} while ( $res );
吻安 2024-08-17 22:36:55

我知道这不能解决您的具体问题。然而,在很多情况下,能够将很长的路径映射到驱动器号将允许人们回避这个问题,因此在处理很长的路径名时很有用,而不必费力地通过一大堆Windows 特定代码和文档。

尽管我付出了所有努力来弄清楚如何做到这一点,但我还是建议使用 SUBSTWin32::FileOp 提供 Subst取消subst。然后,您可以将顶级工作目录映射到未使用的驱动器号(可以使用 Substed 找到该驱动器号)。我会开始检查 Z 并逆向工作。

或者,您可以 shell 调用,不带参数调用 subst 实用程序来获取当前替换的列表,选择一个不存在的替换。

这些都不是完全安全的,因为替换可能会在构建过程中发生变化。

I understand this is not a solution to your specific problem. However, there are a lot of scenarios where being able to map a very long path to a drive-letter would allow one to sidestep the issue and would therefore be useful in dealing with very long path names without having to wade through a whole lot of Windows specific code and docs.

Despite all the effort I put into figuring out how to do this, I am going to recommend somehow using SUBST. Win32::FileOp provides Subst and Unsubst. You can then map the top level working directory to an unused drive letter (which you can find by using Substed). I would start checking with Z and working backwards.

Or, you can shell out, invoke subst utility with no parameters to get a list of current substitutions, choose one that is not there.

None of this is entirely safe as substitutions could change during the build process.

暖伴 2024-08-17 22:36:55

这确实应该是一条评论,但在评论中发布代码几乎没有用。

UNC 路径也不起作用:

C:\> net share
perlbuild    e:\home\src
#!/usr/bin/perl

use strict;
use warnings;

use File::Path qw(make_path);
use File::Slurp;
use Path::Class;

my $top = dir('//Computer/perlbuild');
my @comps = ('0123456789') x 30;

my $path = dir($top, @comps);

make_path $path, { verbose => 1 };

my $file = file($path, 'test.txt');

write_file "$file" => 'This is a test';

print read_file "$file";

结果:

mkdir \\Computer\perlbuild\0123456789\0123456789\0123456789\0123456789\0123456
789\0123456789\0123456789\0123456789\0123456789\0123456789\0123456789\0123456789
\0123456789\0123456789\0123456789\0123456789\0123456789\0123456789\0123456789\01
23456789\0123456789: No such file or directory; The filename or extension is too
 long at C:\Temp\k.pl line 15

This should really be a comment but posting code in comments is hardly useful.

UNC paths do not work either:

C:\> net share
perlbuild    e:\home\src
#!/usr/bin/perl

use strict;
use warnings;

use File::Path qw(make_path);
use File::Slurp;
use Path::Class;

my $top = dir('//Computer/perlbuild');
my @comps = ('0123456789') x 30;

my $path = dir($top, @comps);

make_path $path, { verbose => 1 };

my $file = file($path, 'test.txt');

write_file "$file" => 'This is a test';

print read_file "$file";

Result:

mkdir \\Computer\perlbuild\0123456789\0123456789\0123456789\0123456789\0123456
789\0123456789\0123456789\0123456789\0123456789\0123456789\0123456789\0123456789
\0123456789\0123456789\0123456789\0123456789\0123456789\0123456789\0123456789\01
23456789\0123456789: No such file or directory; The filename or extension is too
 long at C:\Temp\k.pl line 15
蒗幽 2024-08-17 22:36:55

我有三个想法,所有这些都是黑客:

  1. 从一些短目录名称开始(C:\data_directory\a\b\c\d\4\5\6\...),然后重命名目录(当然首先从最深的目录开始)。

  2. 创建一个中等长度路径的 Windows 快捷方式并从那里创建文件和子目录? (或者安装 Cygwin 并使用符号链接?)

  3. 在一个短名称的目录中创建所需的文件,对其进行 zip/tar 压缩,然后将它们解压到具有较长名称的目录中。或者“手动”创建 zip/tar 文件并将其解压到所需位置。

I had three thoughts, all of them kind of hacks:

  1. Start with some short directory names (C:\data_directory\a\b\c\d\4\5\6\...) and then rename the directories (starting with the deepest directory first of course).

  2. Create Windows shortcut to a moderately long path and create files and subdirectories from there? (Or install Cygwin and use symlinks?)

  3. Create the desired files in a directory with a short name, zip/tar them, and unpack them to the directory with the longer name. Or create zip/tar files "by hand" and unpack them in the desired location.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文