zoco

php底层rtrim的一个“bug”

2017-10-19


背景

trim系列函数是用于去除字符串中首尾的空格或其他字符。ltrim函数只去除掉字符串首部的字符,rtrim函数只去除字符串尾部的字符。

string trim ( string $str [, string $character_mask = " \t\n\r\0\x0B" ] )

看一个例子:

$str = "e?type";

echo $str;
echo "\n";

echo rtrim($str, '?type');
echo "\n";

不知道别人怎么想,我觉得这个返回的应该是”e”吧。

但是实际上返回了一个空的字符串。

又是一个黑魔法吗?

源码

看一下php的底层实现:

NmqrdO.png

这个是php7 trim系列函数的源码,红框内就是rtrim的代码。

NmqXyq.png

这个是php_charmask的源码。

执行步骤

trim执行步骤

trim、ltrim、rtrim三个函数都是调用了php_do_trim函数,区别在于第二个参数mode的不同。本文主要对trim函数进行分析,ltrim和rtrim函数跟trim的类似。然后php_do_trim会调用了php_trim来实现功能,因此trim函数的核心函数时php_trim函数。其执行步骤如下:

  1. 根据what的值设置保存过滤字符的mask数组
  2. 过滤在字符串首部的待过滤字符
  3. 过滤在字符串尾部的待过滤字符

php_trim函数执行的流程图如下:

NmLA61.png

源码解读

php_trim函数先调用了php_charmask,这个函数试将过滤字符设置为mask[char] = 1的形式,这样就是一个哈希数组,然后可用于后面的判断。如果第二个参数是范围值时,调用了memset函数给mask数组赋值。

根据源码提炼出了以下的小demo:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void php_charmask(unsigned char *input, size_t len, char *mask);
char *rtrim(char *str,char *character_mask);

int main(int argc, char const *argv[])
{
    printf("%s\n",rtrim("e?type","?type"));    
    return 0;
}

char *rtrim(char *str,char *character_mask)
{
    char *res;
    char mask[256];
    register size_t i;
    
    size_t len = strlen(str);
    
    php_charmask((unsigned char*)character_mask, strlen(character_mask), mask);
    
    if (len > 0) {
        i = len - 1;
        do {
            if (mask[(unsigned char)str[i]]) {
                len--;
            } else {
                break;
            }
        } while (i-- != 0);
    }
    
    res = (char *) malloc(sizeof(char) * (len+1));
    memcpy(res,str,len);
    
    return res;
}

void php_charmask(unsigned char *input, size_t len, char *mask)
{
    unsigned char *end;
    unsigned char c;
    
    memset(mask, 0, 256);
    
    for (end = input+len; input < end; input++) {
        c = *input;
        mask[c]= 1;
    }
}

在php_charmask中,构造了一个简单的hash数组

NmLB1s.png

在最上面的例子中,?type分别落到了这个数组中的不同位置(这也就是为什么mask的大小要设置成256,和ASCII的数量一致)。

所以在最上面的例子中,从后往前e,p,y,t,?,e都命中了hash。

结论

其实这也不算一个bug,文档说的很清楚,trim系列函数是字符级别的,可是惯性就让我们以为是字符串级别的,以后想清楚了再用。