/** * Normalize the given absolute path. * * This includes: * - Uppercase the drive letter if there is one. * - Remove redundant slashes after the drive spec. * - resolve all ./, .\, ../ and ..\ sequences. * - DOS style paths that start with a drive letter will have * \ as the separator. * @param string $path Path to normalize. * @return string */ function normalize($path) { $path = (string) $path; $orig = $path; $path = str_replace('/', DIRECTORY_SEPARATOR, str_replace('\\', DIRECTORY_SEPARATOR, $path)); // make sure we are dealing with an absolute path if (!StringHelper::startsWith(DIRECTORY_SEPARATOR, $path) && !(strlen($path) >= 2 && Character::isLetter($path[0]) && $path[1] === ':')) { throw new IOException("{$path} is not an absolute path"); } $dosWithDrive = false; $root = null; // Eliminate consecutive slashes after the drive spec if (strlen($path) >= 2 && Character::isLetter($path[0]) && $path[1] === ':') { $dosWithDrive = true; $ca = str_replace('/', '\\', $path); $ca = StringHelper::toCharArray($ca); $path = strtoupper($ca[0]) . ':'; for ($i = 2, $_i = count($ca); $i < $_i; $i++) { if ($ca[$i] !== '\\' || $ca[$i] === '\\' && $ca[$i - 1] !== '\\') { $path .= $ca[$i]; } } $path = str_replace('\\', DIRECTORY_SEPARATOR, $path); if (strlen($path) == 2) { $root = $path; $path = ""; } else { $root = substr($path, 0, 3); $path = substr($path, 3); } } else { if (strlen($path) == 1) { $root = DIRECTORY_SEPARATOR; $path = ""; } else { if ($path[1] == DIRECTORY_SEPARATOR) { // UNC drive $root = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR; $path = substr($path, 2); } else { $root = DIRECTORY_SEPARATOR; $path = substr($path, 1); } } } $s = array(); array_push($s, $root); $tok = strtok($path, DIRECTORY_SEPARATOR); while ($tok !== false) { $thisToken = $tok; if ("." === $thisToken) { $tok = strtok(DIRECTORY_SEPARATOR); continue; } elseif (".." === $thisToken) { if (count($s) < 2) { // using '..' in path that is too short throw new IOException("Cannot resolve path: {$orig}"); } else { array_pop($s); } } else { // plain component array_push($s, $thisToken); } $tok = strtok(DIRECTORY_SEPARATOR); } $sb = ""; for ($i = 0, $_i = count($s); $i < $_i; $i++) { if ($i > 1) { // not before the filesystem root and not after it, since root // already contains one $sb .= DIRECTORY_SEPARATOR; } $sb .= (string) $s[$i]; } $path = (string) $sb; if ($dosWithDrive === true) { $path = str_replace('/', '\\', $path); } return $path; }
/** * Returns the next path element from this tokenizer. * * @return the next path element from this tokenizer. * * @throws Exception if there are no more elements in this tokenizer's path. */ public function nextToken() { if ($this->lookahead !== null) { $token = $this->lookahead; $this->lookahead = null; } else { $token = trim(array_shift($this->tokens)); } if (strlen($token) === 1 && Character::isLetter($token[0]) && $this->dosStyleFilesystem && !empty($this->tokens)) { // we are on a dos style system so this path could be a drive // spec. We look at the next token $nextToken = trim(array_shift($this->tokens)); if (StringHelper::startsWith('\\', $nextToken) || StringHelper::startsWith('/', $nextToken)) { // we know we are on a DOS style platform and the next path // starts with a slash or backslash, so we know this is a // drive spec $token .= ':' . $nextToken; } else { // store the token just read for next time $this->lookahead = $nextToken; } } return $token; }