<?php
/**
 * Filesystem path normalization.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Detection\Filesystem;

use InvalidArgumentException;

/**
 * Normalizes local paths and rejects wrappers and traversal.
 */
final class PathNormalizer {
	/**
	 * @param string $path Path to normalize.
	 * @return string
	 */
	public function normalize( $path ) {
		if ( false !== strpos( $path, "\0" ) || preg_match( '#^[a-z][a-z0-9+.-]*://#i', $path ) ) {
			throw new InvalidArgumentException( 'Unsafe filesystem path.' );
		}
		$path     = str_replace( '\\', '/', $path );
		$absolute = 0 === strpos( $path, '/' );
		$parts    = array();
		foreach ( explode( '/', $path ) as $part ) {
			if ( '' === $part || '.' === $part ) {
				continue;
			}
			if ( '..' === $part ) {
				if ( empty( $parts ) ) {
					throw new InvalidArgumentException( 'Path escapes its root.' );
				}
				array_pop( $parts );
				continue;
			}
			$parts[] = $part;
		}
		return ( $absolute ? '/' : '' ) . implode( '/', $parts );
	}

	/**
	 * @param string $path Candidate path.
	 * @param string $root Allowed root.
	 * @return bool
	 */
	public function is_within( $path, $root ) {
		$path = rtrim( $this->normalize( $path ), '/' );
		$root = rtrim( $this->normalize( $root ), '/' );
		return $path === $root || 0 === strpos( $path . '/', $root . '/' );
	}

	/**
	 * @param string $path Absolute path.
	 * @param string $root Allowed root.
	 * @return string
	 */
	public function relative_to( $path, $root ) {
		if ( ! $this->is_within( $path, $root ) ) {
			throw new InvalidArgumentException( 'Path is outside the allowed root.' );
		}
		return ltrim( substr( $this->normalize( $path ), strlen( rtrim( $this->normalize( $root ), '/' ) ) ), '/' );
	}
}
