学长问了我一个php的问题。下面这个代码为什么创建不了文件。
<?php
file_put_contents("data:,123",'123');
?>
This function is identical to calling fopen(), fwrite() and fclose() successively to write data to a file.
file_put_contents
是对fopen(), fwrite() and fclose() 的一个封装
file_put_contents
当成功写入文件的时候会返回,写入文件的长度。
并且可以知道的是,file_put_contents
是可以解析协议的,比如file://
,php://
这些协议都是可以的。(可以自己尝试一下)
并且也是可以成功解析data://
但是data://
协议的话却可以成功返回,写入文件的长度,但是无法成功写入文件。
那就看看源码是怎么解释的吧。(第一次写,请师傅斧正。
2.1 准备php源码调试环境
这个部分,之前我有一篇文章中写了。所以就不在赘述了
2.2 分析流程
2.2.1 file://协议过程
准备好环境之后呢。直接看源码吧。首先找到file_put_contents
这个函数的源码,然后点下几个断点,先捋一捋整个的过程。
首先试试file://
协议是否能够被解析,并且成功写入文件。
一步步跟一下,发现到
stream = php_stream_open_wrapper_ex(filename, mode, ((flags & PHP_FILE_USE_INCLUDE_PATH) ? USE_PATH : 0) | REPORT_ERRORS, NULL, context);
创建一个文件,但是还不会把内容写进去
继续往下跟进会发现到
case IS_STRING:
if (Z_STRLEN_P(data)) {
// 将内容写入文件中
numbytes = php_stream_write(stream, Z_STRVAL_P(data), Z_STRLEN_P(data));
if (numbytes != Z_STRLEN_P(data)) {
php_error_docref(NULL, E_WARNING, "Only %zd of %zd bytes written, possibly out of free disk space", numbytes, Z_STRLEN_P(data));
numbytes = -1;
}
}
break;
之后在,关闭文件句柄
文件写入和文件句柄关闭没什么好讲的。应该要把重心放到文件的创建以及协议的解析上面。
所以断点就来到了php_stream_open_wrapper_ex
单步进入看看。稍微走几步。来到了wrapper
(包装器
以下是一些概念
流Streams这个概念是在php4.3引进的
流有点类似数据库抽象层,在数据库抽象层方面,不管使用何种数据库,在抽象层之上都使用相同的方式操作数据,而流是对数据的抽象,它不管是本地文件还是远程文件还是压缩文件等等,只要来的是流式数据,那么操作方式就是一样的
有了流这个概念就引申出了包装器wrapper这个概念,每个流都对应一种包装器,流是从统一操作这个角度产生的一个概念,而包装器呢是从理解流数据内容出发产生的一个概念,也就是这个统一的操作方式怎么操作或配置不同的内容
官方手册说:“一个包装器是告诉流怎么处理特殊协议或编码的附加代码”
重点在于官方手册说:“一个包装器是告诉流怎么处理特殊协议或编码的附加代码”
那么单步进入这个方法。(稍微精简一下,
PHPAPI php_stream_wrapper *php_stream_locate_url_wrapper(const char *path, const char **path_for_open, int options)
{
HashTable *wrapper_hash = (FG(stream_wrappers) ? FG(stream_wrappers) : &url_stream_wrappers_hash);
php_stream_wrapper *wrapper = NULL;
const char *p, *protocol = NULL;
size_t n = 0;
...
for (p = path; isalnum((int)*p) || *p == '+' || *p == '-' || *p == '.'; p++) {
n++;
}
// 判断是一个协议,如果是的话,那么就把值赋给protocol
if ((*p == ':') && (n > 1) && (!strncmp("//", p+1, 2) || (n == 4 && !memcmp("data:", path, 5)))) {
protocol = path;
}
...
// 如果不存在协议或者是file协议的话,进入
if (!protocol || !strncasecmp(protocol, "file", n)) {
/* fall back on regular file access */
// 生成一个文件包装器。进行统一的文件操作。
php_stream_wrapper *plain_files_wrapper = (php_stream_wrapper*)&php_plain_files_wrapper;
// 如果存在协议,进入
if (protocol) {
int localhost = 0;
// 判断是否满足以下的这个格式(话说有一些题目考的就这个点,真相了
if (!strncasecmp(path, "file://localhost/", 17)) {
localhost = 1;
}
// 概念(windows平台上总是会有这种宏,所以用来判断是否是windows平台。
#ifdef PHP_WIN32
if (localhost == 0 && path[n+3] != '\0' && path[n+3] != '/' && path[n+4] != ':') {
#else
if (localhost == 0 && path[n+3] != '\0' && path[n+3] != '/') {
#endif
// 判断文件是否可达?(没懂。。
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Remote host file access not supported, %s", path);
}
return NULL;
}
// 这个if就是把字符串处理成文件的位置(比如file://D:/flag 处理之后的结果就是 D:/flag)
if (path_for_open) {
/* skip past protocol and :/, but handle windows correctly */
*path_for_open = (char*)path + n + 1;
if (localhost == 1) {
(*path_for_open) += 11;
}
while (*(++*path_for_open)=='/') {
/* intentionally empty */
}
#ifdef PHP_WIN32
if (*(*path_for_open + 1) != ':')
#endif
(*path_for_open)--;
}
}
// 这个地方没有看懂,请师傅们指点一下
if (FG(stream_wrappers)) {
/* The file:// wrapper may have been disabled/overridden */
}
// 最后通过这里返回
return plain_files_wrapper;
}
...
}
返回之后,进过一个判断,来到了
跟进一下
stream = wrapper->wops->stream_opener(wrapper,
path_to_open, mode, options ^ REPORT_ERRORS,
opened_path, context STREAMS_REL_CC);
判断是否设置了open_dir
这里由于我没有设置,所以直接进入了php_stream_fopen_rel
PHPAPI php_stream *_php_stream_fopen(const char *filename, const char *mode, zend_string **opened_path, int options STREAMS_DC)
{
// 一大堆的定义
char realpath[MAXPATHLEN];
int open_flags;
int fd;
php_stream *ret;
int persistent = options & STREAM_OPEN_PERSISTENT;
char *persistent_id = NULL;
// 解析fopen mod (w , w+ , a ....)
if (FAILURE == php_stream_parse_fopen_modes(mode, &open_flags)) {
if (options & REPORT_ERRORS) {
zend_value_error("`%s' is not a valid mode for fopen", mode);
}
return NULL;
}
//
if (options & STREAM_ASSUME_REALPATH) {
strlcpy(realpath, filename, sizeof(realpath));
} else {
// 判断filepath 和 realpath 是否都存在
if (expand_filepath(filename, realpath) == NULL) {
return NULL;
}
}
...
#ifdef PHP_WIN32
// 创建文件(!)
fd = php_win32_ioutil_open(realpath, open_flags, 0666);
#else
fd = open(realpath, open_flags, 0666);
#endif
// 判断是否写入,如果写入 进(后面一堆操作,确实没看太懂,好像和偏移有关)
if (fd != -1) {
...
//从这里返回
return ret;
}
}
创建完文件后,还有一大堆操作,技术有限确实没搞懂了。
那接着从返回往下看,返回值后,又做了一些花里胡哨的操作,就返回了stream
之后就是文件的写入以及文件句柄的关闭了。
2.2.2 php://协议
那么不是file://
协议又会是怎么样的一个样子呢?有前置知识可以知道,php://
也会解析,那么到底是什么地方不同呢?
还是依然从steam
往下走
stream = php_stream_open_wrapper_ex(filename, mode, ((flags & PHP_FILE_USE_INCLUDE_PATH) ? USE_PATH : 0) | REPORT_ERRORS, NULL, context);
进入wapper
,跟进一下。走到了return
单步进入
接着单步进入,这个时候不同了,他走到了另一个奇怪的地方(ext\standard\php_fopen_wrapper.c
)
源码如下(大致意思就是解析php://)
php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
zend_string **opened_path, php_stream_context *context STREAMS_DC) /* {{{ */
{
int fd = -1;
int mode_rw = 0;
php_stream * stream = NULL;
char *p, *token = NULL, *pathdup;
zend_long max_memory;
FILE *file = NULL;
#ifdef PHP_WIN32
int pipe_requested = 0;
#endif
if (!strncasecmp(path, "php://", 6)) {
path += 6;
}
...(php://协议的一堆操作检验 比如:php://stdin, php://stdout 和 php://stderr ....)
// 进入filter的判断
} else if (!strncasecmp(path, "filter/", 7)) {
/* Save time/memory when chain isn't specified */
if (strchr(mode, 'r') || strchr(mode, '+')) {
mode_rw |= PHP_STREAM_FILTER_READ;
}
if (strchr(mode, 'w') || strchr(mode, '+') || strchr(mode, 'a')) {
mode_rw |= PHP_STREAM_FILTER_WRITE;
}
pathdup = estrndup(path + 6, strlen(path + 6));
p = strstr(pathdup, "/resource=");
if (!p) {
zend_throw_error(NULL, "No URL resource specified");
efree(pathdup);
return NULL;
}
//重点在这: 这里有一个'解包'操作
if (!(stream = php_stream_open_wrapper(p + 10, mode, options, opened_path))) {
efree(pathdup);
return NULL;
}
...
#endif
return stream;
}
进入了之后,一直下一步,知道断定停在了 else if (!strncasecmp(path, "filter/", 7))
,再往下调一下,就到了
// '解包'操作
if (!(stream = php_stream_open_wrapper(p + 10, mode, options, opened_path))) {
efree(pathdup);
return NULL;
}
单步进入看看,跳到了(main\streams\streams.c
)
又是熟悉的味道,进入看看,
来到了(main\streams\streams.c
),然后来到了,一个判断是否存在协议的点。由于protocol
是空的,所以断点一定是会进入这个断点的。
所以最后的结果就一定是返回一个文件包装器。
跳出之后又来到了main\streams\streams.c
这个就和file://
协议创建文件方式一模一样了。然后也是一样的,文件写入,文件句柄关闭
2.2.3 data://协议过程
上面就是一个文件从创建到写入到文件关闭的具体过程了。
那么修改一下代码,看一下data://
是怎么个过程,为什么无法创建文件呢?
前面的过程大致都是一样的。从php_stream_open_wrapper_ex
进入走到wrapper
,单步进入
到了协议的判断这个地方,这里很明显不会进去。
而是跳转到了判断是否允许远程文件包含,肯定也是会跳过的。
这里似乎很明显不是返回的文件包装器。那么接着往下看
不出意料,他调到了一个奇怪的地方
static php_stream * php_stream_url_wrap_rfc2397(php_stream_wrapper *wrapper, const char *path,
const char *mode, int options, zend_string **opened_path,
php_stream_context *context STREAMS_DC) /* {{{ */
{
...(开头又是一大堆定义)
ZVAL_NULL(&meta);
// 判断是否为data协议
if (memcmp(path, "data:", 5)) {
return NULL;
}
path += 5;
dlen = strlen(path);
...(一堆检验data协议的操作)
if (comma != path) {
/* meta info */
mlen = comma - path;
dlen -= mlen;
semi = memchr(path, ';', mlen);
sep = memchr(path, '/', mlen);
if (!semi && !sep) {
php_stream_wrapper_log_error(wrapper, options, "rfc2397: illegal media type");
return NULL;
}
array_init(&meta);
if (!semi) { /* there is only a mime type */
add_assoc_stringl(&meta, "mediatype", (char *) path, mlen);
mlen = 0;
}
/* get parameters and potentially ';base64' */
...(一堆检验data协议的操作)
} else {
array_init(&meta);
}
add_assoc_bool(&meta, "base64", base64);
/* skip ',' */
comma++;
dlen--;
// base64解码
if (base64) {
...(解码操作)
}
// php为data协议创建了一个临时写入流,将输入写入进去
if ((stream = php_stream_temp_create_rel(0, ~0u)) != NULL) {
/* store data */
php_stream_temp_write(stream, comma, ilen);
php_stream_temp_seek(stream, 0, SEEK_SET, &newoffs);
/* set special stream stuff (enforce exact mode) */
vlen = strlen(mode);
if (vlen >= sizeof(stream->mode)) {
vlen = sizeof(stream->mode) - 1;
}
memcpy(stream->mode, mode, vlen);
stream->mode[vlen] = '\0';
stream->ops = &php_stream_rfc2397_ops;
ts = (php_stream_temp_data*)stream->abstract;
assert(ts != NULL);
ts->mode = mode && mode[0] == 'r' && mode[1] != '+' ? TEMP_STREAM_READONLY : 0;
ZVAL_COPY_VALUE(&ts->meta, &meta);
}
if (base64_comma) {
zend_string_free(base64_comma);
} else {
efree(comma);
}
return stream;
}
这里可以看出解析data协议的时候,并不会尝试去获取一个可以写文件的包装器,而是php专门为data
协议创建写入流用来存储data
协议解析的信息。
并且在最后会将这个流return出去,回到file.c
,然后继续的这个流进行写入。
这俩次写入应该是会互相影响的,从前置知识可知,file_put_contents
是对三个file操作函数的一次封装。那个把这个函数拆解一下。
<?php
$stream = fopen('data://text/plain,Mrkaixin', 'w+');
fwrite($stream, 'data');
rewind($stream);
var_dump(fread($stream, 8));
fclose($stream);
结果如下
最后的结果是dataixin
,可以看到会有一个覆盖的操作。这个data
直接覆盖了Mrkaixin
的开头的前四个字符。
2.2.2 如何区分php:// 和 data://
到这里肯定还是会有一个问题,就是它是怎么区分data
和php://
在main\streams\streams.c
由于实力原因就不往下跟了。(逃。。
2.3 结论
到了这里其实结论也就呼之欲出了
正是data://
中为没有创建文件包装器的操作,而是仅仅在解析data协议的时候创建了一个写入流存储数据,从而导致无法创建文件。
@j7ur8 师傅给php提了个bug,官方回答如下
According to the docs https://www.php.net/manual/en/wrappers.data.php#refsect1-wrappers.data-options writing is not supported so seems to be working as designed.
If anything it shouldn't say it wrote 4 bytes instead.(这句话确实没懂)
2.4 Reference
file_put_contents can't create file with data:// wrappers
溜了溜了,感觉看源码还蛮有意思的,文章写得浅,请师傅们指点。