diff --git a/lib/Url.php b/lib/Url.php index 1f39249..1255d13 100644 --- a/lib/Url.php +++ b/lib/Url.php @@ -360,10 +360,7 @@ PCRE; protected function setPath(string $value): void { if ($this->specialScheme) { - $value = str_replace("\\", "/", $value); - } - if ($this->scheme === "file" && preg_match(self::WINDOWS_PATH_PATTERN, $value, $match)) { - $value = "/".$match[1].":".$match[2]; + $value = $this->collapsePath(str_replace("\\", "/", $value)); } $this->path = $this->percentEncode($value, $this->isUrn() ? "C0" : "path"); } @@ -384,6 +381,35 @@ PCRE; } } + protected function collapsePath(string $path): string { + if (preg_match("<^/?$>", $path)) { + return $path; + } + if ($this->scheme === "file" && preg_match(self::WINDOWS_PATH_PATTERN, $path, $match)) { + $path = "/".$match[1].":".$match[2]; + } + $abs = $path[0] === "/"; + $dir = $path[-1] === "/"; + $term = $dir || preg_match("i", $path); + $path = explode("/", substr($path, (int) $abs, strlen($path) - ($abs + $dir))); + $out = []; + foreach ($path as $s) { + if (preg_match('/^(?:\.|%2E)$/i', $s)) { + // current-directory segment; these should simply be omitted + continue; + } elseif (preg_match('/^(?:\.|%2E){2}$/i', $s)) { + // parent-directory segment; pop a directory off the output + array_pop($out); + } else { + $out[] = $s; + } + } + if (!$out) { + return $abs ? "/" : ""; + } + return ($abs ? "/" : "").implode("/", $out).($term ? "/" : ""); + } + protected function percentEncode(string $data, string $type): string { assert(array_key_exists($type, self::PERCENT_ENCODE_SETS), "Invalid percent-encoding set"); $out = ""; @@ -455,6 +481,8 @@ PCRE; if ($host === false) { throw new \InvalidArgumentException("Invalid host in URL"); } + } elseif ($this->specialScheme && $this->scheme !== "file") { + throw new \InvalidArgumentException("Invalid host in URL"); } return $host; }