// SPDX-FileCopyrightText: 2023 Unity Technologies and the glTFast authors // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; namespace GLTFast { static class UriHelper { public static Uri GetBaseUri(Uri uri) { if (uri == null) return null; if (!uri.IsAbsoluteUri) { var directoryName = Path.GetDirectoryName(uri.OriginalString) ?? ""; return new Uri(directoryName, UriKind.Relative); } var uriString = uri.OriginalString; var index = uriString.LastIndexOfAny(new[] { '/', '\\' }); return index < 0 ? new Uri("", UriKind.Relative) : new Uri(uriString.Substring(0, index + 1)); } /// /// Get URI that is potentially relative to another URI /// /// Absolute or relative URI /// Base URI /// Absolute URI that is potentially relative to baseUri public static Uri GetUriString(string uri, Uri baseUri) { uri = Uri.UnescapeDataString(uri); if (Uri.TryCreate(uri, UriKind.Absolute, out var result)) { return result; } if (baseUri != null) { uri = RemoveDotSegments(uri, out var parentLevels); if (baseUri.IsAbsoluteUri) { for (int i = 0; i < parentLevels; i++) { baseUri = new Uri(baseUri, ".."); } return new Uri(Combine(baseUri.OriginalString, uri)); } var parentPath = baseUri.OriginalString; for (int i = 0; i < parentLevels; i++) { parentPath = Path.GetDirectoryName(parentPath); if (string.IsNullOrEmpty(parentPath)) { baseUri = new Uri("", UriKind.Relative); break; } baseUri = new Uri(parentPath, UriKind.Relative); } return new Uri(Path.Combine(baseUri.OriginalString, uri), UriKind.Relative); } return new Uri(uri, UriKind.RelativeOrAbsolute); } /// /// Removes relative dot segments "." and ".." and resolves them. /// See https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 /// /// Relative input URI /// Number of levels going beyond this paths hierarchy (due to "..") /// Resolved/compressed input URI without dot segments public static string RemoveDotSegments(string uri, out int parentLevels) { var segments = new List(); var start = 0; parentLevels = 0; while (true) { #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_WSA var i = uri.IndexOfAny(new char[]{Path.DirectorySeparatorChar,Path.AltDirectorySeparatorChar},start); #else var i = uri.IndexOf(Path.DirectorySeparatorChar, start); #endif var found = i >= 0; var len = found ? (i - start) : uri.Length - start; if (len > 0) { var segment = uri.Substring(start, len); if (segment == "..") { if (segments.Count > 0) { segments.RemoveAt(segments.Count - 1); } else { parentLevels++; } } else if (segment != ".") { segments.Add(segment); } } if (!found) { break; } start = i + 1; } var sb = new StringBuilder(); var first = true; foreach (var segment in segments) { if (!first) { sb.Append(Path.DirectorySeparatorChar); } sb.Append(segment); first = false; } return sb.ToString(); } /// /// Detect glTF type from URI /// /// Input URI /// True if glTF-binary, False if glTF (JSON), null if not sure. public static bool? IsGltfBinary(Uri uri) { string path = uri.IsAbsoluteUri ? uri.LocalPath : uri.OriginalString; var index = path.LastIndexOf('.', path.Length - 1, Mathf.Min(5, path.Length)); if (index < 0) return null; if (path.EndsWith(GltfGlobals.GlbExt, StringComparison.OrdinalIgnoreCase)) { return true; } if (path.EndsWith(GltfGlobals.GltfExt, StringComparison.OrdinalIgnoreCase)) { return false; } return null; } /// /// Detect image format from URI string /// /// Input URI string /// ImageFormat if detected correctly, otherwise internal static ImageFormat GetImageFormatFromUri(string uri) { if (string.IsNullOrEmpty(uri)) return ImageFormat.Unknown; var queryStartIndex = uri.LastIndexOf('?'); if (queryStartIndex < 0) queryStartIndex = uri.Length; var extStartIndex = uri.LastIndexOf('.', queryStartIndex - 1, Mathf.Min(5, queryStartIndex)); // we assume that the first period before the query string is the file format period. if (extStartIndex < 0) return ImageFormat.Unknown; // if we can't find a period, we don't know the file format. var fileExtension = uri.Substring(extStartIndex + 1, queryStartIndex - extStartIndex - 1); // extract the file ending if (fileExtension.Equals("png", StringComparison.OrdinalIgnoreCase)) return ImageFormat.PNG; if (fileExtension.Equals("jpg", StringComparison.OrdinalIgnoreCase) || fileExtension.Equals("jpeg", StringComparison.OrdinalIgnoreCase)) return ImageFormat.Jpeg; if (fileExtension.Equals("ktx", StringComparison.OrdinalIgnoreCase) || fileExtension.Equals("ktx2", StringComparison.OrdinalIgnoreCase)) return ImageFormat.Ktx; return ImageFormat.Unknown; } // // string-based IsGltfBinary alternative // // Profiling result: Faster/less memory, but for .glb/.gltf just barely better (unknown ~2x) // // Downside: less convenient // public static bool? IsGltfBinary( string uri ) { // // quick glTF-binary check // if (uri.EndsWith(GltfGlobals.glbExt, StringComparison.OrdinalIgnoreCase)) return true; // if (uri.EndsWith(GltfGlobals.gltfExt, StringComparison.OrdinalIgnoreCase)) return false; // // // thorough glTF-binary extension check that strips HTTP GET parameters // int getIndex = uri.LastIndexOf('?'); // if (getIndex >= 0) { // var ext = uri.Substring(getIndex - GltfGlobals.gltfExt.Length, GltfGlobals.gltfExt.Length); // if(ext.EndsWith(GltfGlobals.glbExt, StringComparison.OrdinalIgnoreCase)) return true; // if(ext.EndsWith(GltfGlobals.gltfExt, StringComparison.OrdinalIgnoreCase)) return false; // } // return null; // } internal static string Combine(string baseUri, string uri) { var sepIndex = baseUri.LastIndexOfAny(new[] { '/', '\\' }); var uriStartsWithSep = uri.IndexOfAny(new[] { '/', '\\' }, 0, 1) == 0; var trimmedUri = uriStartsWithSep ? uri.Substring(1) : uri; if (sepIndex > 0) { return sepIndex == baseUri.Length - 1 ? $"{baseUri}{trimmedUri}" : $"{baseUri}{baseUri[sepIndex]}{trimmedUri}"; } return $"{baseUri}/{trimmedUri}"; } } }