diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index 21182068d1d7f..212138f1d52b2 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -102,29 +102,61 @@ static char * php_zip_make_relative_path(char *path, size_t path_len) /* {{{ */ return NULL; } - if (IS_SLASH(path[0])) { - return path + 1; + if (path_len == 1 && (path[0] == '.' || IS_SLASH(path[0]) || path[0] == ':')) { + return NULL; } i = path_len; while (1) { - while (i > 0 && !IS_SLASH(path[i])) { + while (i > 0 && !(IS_SLASH(path[i]) || path[i] == ':')) { i--; } if (!i) { - return path; + if (IS_SLASH(path[0]) || path[0] == ':') { + path_begin = path + 1; + } else { + path_begin = path; + } + break; + } + + if (i == 1 && path[i] == ':') { + path_begin = path + i + 1; + break; } - if (i >= 2 && (path[i -1] == '.' || path[i -1] == ':')) { - /* i is the position of . or :, add 1 for / */ + if (i >= 1 && path[i - 1] == ':') { path_begin = path + i + 1; break; } + + if (i == 2 && path[i - 2] == '.' && path[i - 1] == '.') { + path_begin = path + 3; + break; + } + + if (i >= 3 && IS_SLASH(path[i - 3]) && path[i - 2] == '.' && path[i - 1] == '.') { + path_begin = path + i + 1; + break; + } + i--; } +#ifdef PHP_WIN32 + if (path[path_len - 1] == '.') { + path[path_len - 1] = '_'; + } + + for (i = 1; i < path_len; i++) { + if (IS_SLASH(path[i]) && path[i - 1] == '.') { + path[i - 1] = '_'; + } + } +#endif + return path_begin; } /* }}} */ diff --git a/ext/zip/tests/bug69477.phpt b/ext/zip/tests/bug69477.phpt new file mode 100644 index 0000000000000..5839311681610 --- /dev/null +++ b/ext/zip/tests/bug69477.phpt @@ -0,0 +1,90 @@ +--TEST-- +Bug #69477 (ZipArchive::extractTo() truncates path segments ending with dot) +--SKIPIF-- + +--FILE-- +open($zipfile, ZipArchive::CREATE)) { + exit('failed: unable to create archive'); +} + +// (string) Entry path in the ZIP => (string) Expected actual target path +if (PHP_OS_FAMILY === 'Windows') { + $paths = [ + '.a/b/c/file01.txt' => '.a/b/c/file01.txt', + 'a./b/c/file02.txt' => 'a_/b/c/file02.txt', + 'a/.b/c/file03.txt' => 'a/.b/c/file03.txt', + 'a/b./c/file04.txt' => 'a/b_/c/file04.txt', + 'a/b../c/file05.txt' => 'a/b._/c/file05.txt', + 'a/b.../c/file06.txt' => 'a/b.._/c/file06.txt', + 'a/..b/c/file07.txt' => 'a/..b/c/file07.txt', + 'a/...b/c/file08.txt' => 'a/...b/c/file08.txt', + 'a/../b./c./file09.txt' => 'b_/c_/file09.txt', + '//../b./c./file10.txt' => 'b_/c_/file10.txt', + '/../b./c./file11.txt' => 'b_/c_/file11.txt', + 'C:/a./b./file12.txt' => 'a_/b_/file12.txt', + 'a/b:/c/file13.txt' => 'c/file13.txt', + 'a/b/c/file14.' => 'a/b/c/file14_', + 'C:a./b./file15.txt' => 'a_/b_/file15.txt', + ]; +} else { + $paths = [ + '.a/b/c/file01.txt' => '.a/b/c/file01.txt', + 'a./b/c/file02.txt' => 'a./b/c/file02.txt', + 'a/.b/c/file03.txt' => 'a/.b/c/file03.txt', + 'a/b./c/file04.txt' => 'a/b./c/file04.txt', + 'a/b../c/file05.txt' => 'a/b../c/file05.txt', + 'a/b.../c/file06.txt' => 'a/b.../c/file06.txt', + 'a/..b/c/file07.txt' => 'a/..b/c/file07.txt', + 'a/...b/c/file08.txt' => 'a/...b/c/file08.txt', + 'a/../b./c./file09.txt' => 'b./c./file09.txt', + '//../b./c./file10.txt' => 'b./c./file10.txt', + '/../b./c./file11.txt' => 'b./c./file11.txt', + 'C:/a./b./file12.txt' => 'a./b./file12.txt', + 'a/b:/c/file13.txt' => 'c/file13.txt', + 'a/b/c/file14.' => 'a/b/c/file14.', + 'C:a./b./file15.txt' => 'a./b./file15.txt', + ]; +} + +foreach ($paths as $zippath => $realpath) { + $archive->addFromString($zippath, $zippath . ' => ' . $realpath); +} + +$archive->close(); + +$archive2 = new ZipArchive(); + +if (!$archive2->open($zipfile)) { + exit('failed: unable to open archive2'); +} + +$archive2->extractTo($dir); +$archive2->close(); + +foreach ($paths as $zippath => $realpath) { + if (!is_readable($dir . '/' . $realpath) || file_get_contents($dir . '/' . $realpath) !== $zippath . ' => ' . $realpath) { + exit('failed: ' . $zippath); + } +} + +echo 'ok'; +?> +--CLEAN-- + +--EXPECT-- +ok