【问题标题】:How do I use escapeshellarg() on Windows but "aimed for Linux" (and vice versa)?如何在 Windows 上使用 escapeshellarg() 但“针对 Linux”(反之亦然)?
【发布时间】: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 安全验证工具可以理解并解析这两种类型?

【问题讨论】:

    标签: linux windows php


    【解决方案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 但我怀疑它有什么不同)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-23
    • 1970-01-01
    • 1970-01-01
    • 2011-12-20
    • 2014-12-04
    • 1970-01-01
    相关资源
    最近更新 更多