--- /dev/null
+<?php
+/**
+ * Copyright (c) 2015 Ori Livneh <ori@wikimedia.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @file
+ * @author Ori Livneh <ori@wikimedia.org>
+ */
+
+namespace RelPath;
+
+/**
+ * Split a path into path components.
+ *
+ * @param string $path File path.
+ * @return array Array of path components.
+ */
+function splitPath( $path ) {
+ $fragments = array();
+
+ while ( true ) {
+ $cur = dirname( $path );
+ if ( $cur[0] === DIRECTORY_SEPARATOR ) {
+ // dirname() on Windows sometimes returns a leading backslash, but other
+ // times retains the leading forward slash. Slashes other than the leading one
+ // are returned as-is, and therefore do not need to be touched.
+ // Furthermore, don't break on *nix where \ is allowed in file/directory names.
+ $cur[0] = '/';
+ }
+
+ if ( $cur === $path || ( $cur === '.' && basename( $path ) === $path ) ) {
+ break;
+ }
+
+ $fragment = trim( substr( $path, strlen( $cur ) ), '/' );
+
+ if ( !$fragments ) {
+ $fragments[] = $fragment;
+ } elseif ( $fragment === '..' && basename( $cur ) !== '..' ) {
+ $cur = dirname( $cur );
+ } elseif ( $fragment !== '.' ) {
+ $fragments[] = $fragment;
+ }
+
+ $path = $cur;
+ }
+
+ if ( $path !== '' ) {
+ $fragments[] = trim( $path, '/' );
+ }
+
+ return array_reverse( $fragments );
+}
+
+/**
+ * Return a relative filepath to path either from the current directory or from
+ * an optional start directory. Both paths must be absolute.
+ *
+ * @param string $path File path.
+ * @param string $start Start directory. Optional; if not specified, the current
+ * working directory will be used.
+ * @return string|bool Relative path, or false if input was invalid.
+ */
+function getRelativePath( $path, $start = null ) {
+ if ( $start === null ) {
+ // @codeCoverageIgnoreStart
+ $start = getcwd();
+ }
+ // @codeCoverageIgnoreEnd
+
+ if ( substr( $path, 0, 1 ) !== '/' || substr( $start, 0, 1 ) !== '/' ) {
+ return false;
+ }
+
+ $pathParts = splitPath( $path );
+ $countPathParts = count( $pathParts );
+
+ $startParts = splitPath( $start );
+ $countStartParts = count( $startParts );
+
+ $commonLength = min( $countPathParts, $countStartParts );
+ for ( $i = 0; $i < $commonLength; $i++ ) {
+ if ( $startParts[$i] !== $pathParts[$i] ) {
+ break;
+ }
+ }
+
+ $relList = ( $countStartParts > $i )
+ ? array_fill( 0, $countStartParts - $i, '..' )
+ : array();
+
+ $relList = array_merge( $relList, array_slice( $pathParts, $i ) );
+
+ return implode( '/', $relList ) ?: '.';
+}
+
+
+/**
+ * Join path components.
+ *
+ * @param string $base Base path.
+ * @param string $path File path to join to base path.
+ * @return string
+ */
+function joinPath( $base, $path ) {
+ if ( substr( $path, 0, 1 ) === '/' ) {
+ return $path; // $path is absolute.
+ }
+
+ if ( substr( $base, 0, 1 ) !== '/' ) {
+ return false; // $base is relative.
+ }
+
+ $pathParts = splitPath( $path );
+ $resultParts = splitPath( $base );
+
+ while ( ( $part = array_shift( $pathParts ) ) !== null ) {
+ switch ( $part ) {
+ case '.':
+ break;
+ case '..':
+ if ( count( $resultParts ) > 1 ) {
+ array_pop( $resultParts );
+ }
+ break;
+ default:
+ $resultParts[] = $part;
+ break;
+ }
+ }
+
+ return implode( '/', $resultParts );
+}