public static unsafe string PathGetRelative()

in Sharpmake/PathUtil.cs [352:463]


        public static unsafe string PathGetRelative(string relativeTo, string path, bool ignoreCase = false)
        {
            // ------------------ THIS IS NOT CORRECT ----------------
            // Force to always ignore case, whatever the user ask
            // This keep the legacy Sharpmake behavior. It may be fixed at a later date to reduce modification scope.
            ignoreCase = true;
            // ------------------ THIS IS NOT CORRECT ----------------

            relativeTo = SimplifyPath(relativeTo);
            path = SimplifyPath(path);

            // Check different root
            var relativeToLength = relativeTo.Length;
            var pathLength = path.Length;
            if (relativeToLength == 0 || pathLength == 0 || !IsCharEqual(relativeTo[0], path[0], ignoreCase))
                return path;

            // Compute common part length
            var lastCommonDirSepPosition = 0;
            var commonPartLength = 0;
            while (commonPartLength < relativeToLength && commonPartLength < pathLength && IsCharEqual(relativeTo[commonPartLength], path[commonPartLength], ignoreCase))
            {
                if (relativeTo[commonPartLength] == Path.DirectorySeparatorChar)
                    lastCommonDirSepPosition = commonPartLength;
                ++commonPartLength;
            }

#if NET7_0_OR_GREATER
            [Obsolete("Directly use 'char.IsAsciiLetter()' in 'IsCharEqual()' bellow (char.IsAsciiLetter() is available starting net7)")]
#endif
            static bool IsAsciiLetter(char c) => (uint)((c | 0x20) - 'a') <= 'z' - 'a';
            static bool IsCharEqual(char a, char b, bool ignoreCase) => a == b || (ignoreCase && (a | 0x20) == (b | 0x20) && IsAsciiLetter(a));

            // Check if both paths are the same (ignoring the last directory separator if any)
            if ((relativeToLength == commonPartLength && pathLength == commonPartLength)
                || (relativeToLength == commonPartLength && pathLength == commonPartLength + 1 && path[commonPartLength] == Path.DirectorySeparatorChar)
                || (pathLength == commonPartLength && relativeToLength == commonPartLength + 1 && relativeTo[commonPartLength] == Path.DirectorySeparatorChar))
            {
                return ".";
            }

            // Adjust 'commonPartLength' in case we stopped the comparison in the middle of an entry name:
            //   -> we went too far, we must move back 'commonPartLength' to the 'lastCommonDirSepPosition' position '+ 1'
            //      - /abc_def and /abc_xyz
            //      - /abc_    and /abc
            if (commonPartLength < relativeToLength && commonPartLength < pathLength
                || (relativeToLength == commonPartLength && path[commonPartLength] != Path.DirectorySeparatorChar)
                || (pathLength == commonPartLength && relativeTo[commonPartLength] != Path.DirectorySeparatorChar))
            {
                commonPartLength = lastCommonDirSepPosition + 1;
            }

            // Compute the number of ".." to add (to get out of the 'relativeTo' path)
            var dotDotCount = 0;
            var relativeToLengthWithoutTrailingDirSep = relativeTo[^1] == Path.DirectorySeparatorChar ? relativeToLength - 1 : relativeToLength;
            if (relativeToLengthWithoutTrailingDirSep > commonPartLength)
            {
                dotDotCount = 1;
                for (int i = commonPartLength + 1; i < relativeToLengthWithoutTrailingDirSep; i++)
                {
                    if (relativeTo[i] == Path.DirectorySeparatorChar)
                        ++dotDotCount;
                }
            }

            // Compute the length of the two parts to write
            // - The sequences that looks like this: ".." or "../.."  or ...
            var dotDotCountSequenceLength = dotDotCount == 0 ? 0 : dotDotCount * 2 + dotDotCount - 1;

            // - The remaining from the 'path' not yet added (skip the starting directory separator if any)
            var remainingStartPosition = commonPartLength < pathLength && path[commonPartLength] == Path.DirectorySeparatorChar ? commonPartLength + 1 : commonPartLength;
            var remainingLength = pathLength - remainingStartPosition;

            // This 'if' deviate from the standard .net behavior and is here to keep legacy Sharpmake behavior
            // Trim last directory separator if any
            if (remainingLength > 0 && path[^1] == Path.DirectorySeparatorChar)
                --remainingLength;

            // - The directory separator between the two parts (in case there is both dotDots and remains)
            var needToInsertDirSepBeforeRemainingPart = dotDotCountSequenceLength > 0 && remainingLength > 0;

            var finalLength =
                  dotDotCountSequenceLength
                + remainingLength
                + (needToInsertDirSepBeforeRemainingPart ? 1 : 0);

            // Allocate and write in the buffer
            Span<char> arrayPtr = stackalloc char[finalLength];
            int writePosition = 0;
            if (dotDotCount > 0)
            {
                arrayPtr[writePosition++] = '.';
                arrayPtr[writePosition++] = '.';
                for (int i = 0; i < dotDotCount - 1; i++)
                {
                    arrayPtr[writePosition++] = Path.DirectorySeparatorChar;
                    arrayPtr[writePosition++] = '.';
                    arrayPtr[writePosition++] = '.';
                }
            }

            if (needToInsertDirSepBeforeRemainingPart)
                arrayPtr[writePosition++] = Path.DirectorySeparatorChar;

            if (remainingLength > 0)
            {
                var remainAsSpan = path.AsSpan(remainingStartPosition, remainingLength);
                remainAsSpan.CopyTo(arrayPtr.Slice(writePosition, remainingLength));
            }

            return new string(arrayPtr);
        }