如何在 Windows 上使用 escapeshellarg() 但“针对 Linux”(反之亦然)?

Posted

技术标签:

【中文标题】如何在 Windows 上使用 escapeshellarg() 但“针对 Linux”(反之亦然)?【英文标题】:How do I use escapeshellarg() on Windows but "aimed for Linux" (and vice versa)? 【发布时间】:2019-10-23 01:32:22 【问题描述】:

如果 php 在 Windows 上运行,escapeshellarg() 会以某种方式转义文件名(例如),然后在其周围添加 " (DOUBLE) 引号。

如果 PHP 在 Linux 上运行,escapeshellarg() 使用基于 Linux 的转义,然后在其周围添加 ' (SINGLE) 引号。

在我的情况下,我在 Windows 上生成一个 SHA256SUMS 文件,但针对的是 Linux。因为我使用 escapeshellarg() 来转义文件名,所以我最终得到了一个类似的文件:

cabcdccas12exdqdqadanacvdkjsc123ccfcfq3rdwcndwf2qefcf "cool filename with spaces.zip"

但是,Linux 工具可能期望:

cabcdccas12exdqdqadanacvdkjsc123ccfcfq3rdwcndwf2qefcf 'cool filename with spaces.zip'

查看手册,似乎没有办法执行以下操作:escapeshellarg($blabla, TARGET_OS_LINUX);以便它使用 Linux 的规则而不是运行脚本的操作系统 (Windows)。

我不能只是 str_replace 引号,因为它不会考虑所有特定于平台的规则。

另外,是的,我需要在文件名中包含空格(以及任何其他跨平台有效字符)。

遗憾的是,在我拥有的唯一信息来源中,我没有提及首选的引用样式:https://help.ubuntu.com/community/HowToSHA256SUM

也许读取该 SHA256SUMS 文件的 SHA256 安全验证工具可以理解并解析这两种类型?

【问题讨论】:

【参考方案1】:

escapeshellarg() 的行为是硬编码的,具体取决于 PHP 是在 Windows 上运行还是在任何其他操作系统上运行。 您应该重新实现 escapeshellarg() 以获得一致的行为。

这是我在 PHP 中使用 Windows/其他操作系统切换重新实现 escapeshellarg() 的尝试:

<?php namespace polyfill;

const TARGET_OS_WINDOWS = 1;
const TARGET_OS_UNIX    = 2;

function escapeshellarg(string $input, int $os_mode = 0): string

    if (false !== strpos($input, "\x00"))
    
        throw new \UnexpectedValueException(__FUNCTION__ . '(): Argument #1 ($input) must not contain any null bytes');
    
    
    if ($os_mode == 0)
    
        $os_mode = TARGET_OS_UNIX;
        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN')
            $os_mode = TARGET_OS_WINDOWS;
    
    
    $maxlen = 4096;
    if ($os_mode === TARGET_OS_WINDOWS) $maxlen = 8192;
    if (strlen($input) > $maxlen - 2) return "";

    if ($os_mode === TARGET_OS_WINDOWS)
    
        $output =
            str_replace(['"', '%', '!'],
                        [' ', ' ', ' '],
                        $input);

        # https://bugs.php.net/bug.php?id=69646
        if (substr($output, -1) === "\\")
        
            $k = 0; $n = strlen($output) - 1;
            for (; $n >= 0 && substr($output, $n, 1) === "\\"; $n--, $k++);
            if ($k % 2) $output .= "\\";
        
        
        $output = "\"$output\"";
    
    else
    
        $output = str_replace("'", "'\''", $input);
        
        $output = "'$output'";
    
    
    if (strlen($output) > $maxlen) return "";
    return $output;

它应该在功能上几乎等同于原生 PHP escapeshellarg(),除了:

它需要第二个参数来设置您是否希望在 Windows 模式下输出, 如果输入字符串包含空字节,它会引发 \UnexpectedValueException 而不是某种 PHP 错误, 它不会因为输入太长而发出错误,并且 在类 Unix 平台上硬编码为 4096 作为最大参数长度。

要使用这个替换功能:

# In Unix/Linux/macOS mode
\polyfill\escapeshellarg($blabla, \polyfill\TARGET_OS_UNIX);

# In Windows mode
\polyfill\escapeshellarg($blabla, \polyfill\TARGET_OS_WINDOWS);

# In auto-detect (running OS) mode
\polyfill\escapeshellarg($blabla);

参考

这是来自PHP 7.3.10 (./ext/standard/exec.c) 的完整 C 实现:

PHPAPI zend_string *php_escape_shell_arg(char *str)

    size_t x, y = 0;
    size_t l = strlen(str);
    zend_string *cmd;
    uint64_t estimate = (4 * (uint64_t)l) + 3;

    /* max command line length - two single quotes - \0 byte length */
    if (l > cmd_max_len - 2 - 1) 
        php_error_docref(NULL, E_ERROR, "Argument exceeds the allowed length of %zu bytes", cmd_max_len);
        return ZSTR_EMPTY_ALLOC();
    

    cmd = zend_string_safe_alloc(4, l, 2, 0); /* worst case */

#ifdef PHP_WIN32
    ZSTR_VAL(cmd)[y++] = '"';
#else
    ZSTR_VAL(cmd)[y++] = '\'';
#endif

    for (x = 0; x < l; x++) 
        int mb_len = php_mblen(str + x, (l - x));

        /* skip non-valid multibyte characters */
        if (mb_len < 0) 
            continue;
         else if (mb_len > 1) 
            memcpy(ZSTR_VAL(cmd) + y, str + x, mb_len);
            y += mb_len;
            x += mb_len - 1;
            continue;
        

        switch (str[x]) 
#ifdef PHP_WIN32
        case '"':
        case '%':
        case '!':
            ZSTR_VAL(cmd)[y++] = ' ';
            break;
#else
        case '\'':
            ZSTR_VAL(cmd)[y++] = '\'';
            ZSTR_VAL(cmd)[y++] = '\\';
            ZSTR_VAL(cmd)[y++] = '\'';
#endif
            /* fall-through */
        default:
            ZSTR_VAL(cmd)[y++] = str[x];
        
    
#ifdef PHP_WIN32
    if (y > 0 && '\\' == ZSTR_VAL(cmd)[y - 1]) 
        int k = 0, n = y - 1;
        for (; n >= 0 && '\\' == ZSTR_VAL(cmd)[n]; n--, k++);
        if (k % 2) 
            ZSTR_VAL(cmd)[y++] = '\\';
        
    

    ZSTR_VAL(cmd)[y++] = '"';
#else
    ZSTR_VAL(cmd)[y++] = '\'';
#endif
    ZSTR_VAL(cmd)[y] = '\0';

    if (y > cmd_max_len + 1) 
        php_error_docref(NULL, E_ERROR, "Escaped argument exceeds the allowed length of %zu bytes", cmd_max_len);
        zend_string_release_ex(cmd, 0);
        return ZSTR_EMPTY_ALLOC();
    

    if ((estimate - y) > 4096) 
        /* realloc if the estimate was way overill
         * Arbitrary cutoff point of 4096 */
        cmd = zend_string_truncate(cmd, y, 0);
    
    ZSTR_LEN(cmd) = y;
    return cmd;


// … [truncated] …

/*  proto string escapeshellarg(string arg)
   Quote and escape an argument for use in a shell command */
PHP_FUNCTION(escapeshellarg)

    char *argument;
    size_t argument_len;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_STRING(argument, argument_len)
    ZEND_PARSE_PARAMETERS_END();

    if (argument) 
        if (argument_len != strlen(argument)) 
            php_error_docref(NULL, E_ERROR, "Input string contains NULL bytes");
            return;
        
        RETVAL_STR(php_escape_shell_arg(argument));
    

/*  */

逻辑相当简单。以下是散文中的一些等效功能测试用例:

输入的字符串不能包含 NUL 字符。 应用于输入字符串, 在 Windows 模式下, 添加 " 字符。 将所有"%! 字符替换为 。 如果末尾包含奇数个\ 字符,则在末尾添加一个\ 字符。 (Bug #69646) 附加一个" 字符。 在其他平台模式下, 添加 ' 字符。 将所有' 字符替换为'\'' 附加一个' 字符。 在 Windows 上,如果输出长度超过 8192 个字符,则发出 E_ERROR 并返回空字符串。 在其他平台上,如果输出长度超过 4096 个字符(或编译时覆盖的最大值),则发出 E_ERROR 并返回空字符串。

【讨论】:

$arg 未定义。应该是$input 吗?在这一行:if (strlen($arg) &gt; $maxlen - 2) return ""; 其次,这个 polyfill 功能是否在某些 packagegist 或 github repo 中可用? @ttk:是的,错误的变量使用现在已经修复。同样不,此功能不在任何包或存储库中;我所做的只是把它写在这个答案中。 增加了对空字节的检测(至少在 Linux 上,不可能为参数转义空字节。关于 Windows 的 idk 但我怀疑它有什么不同)

以上是关于如何在 Windows 上使用 escapeshellarg() 但“针对 Linux”(反之亦然)?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用APACHE在windows上>?

我们如何通过 git 在 windows 上使用 linux 命令?

如何使用 Visual Studio 2017 在 Windows 上构建 OpenSSL?

如何在 Windows 上使用作曲家?

如何让 SWT 浏览器控件在 Windows 上使用 Mozilla 而不是 IE?

如何在Linux上运行Windows应用程序