src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs (118 lines of code) (raw):

using System; using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; namespace Avalonia.Platform.Storage.FileIO; /// <summary> /// In order to have unique bookmarks across platforms, we prepend a platform specific suffix before native bookmark. /// And always encoding them in base64 before returning to the user. /// </summary> /// <remarks> /// Bookmarks are encoded as: /// 0-6 - avalonia prefix with version number /// 7-15 - platform key /// 16+ - native bookmark value /// Which is then encoded in Base64. /// </remarks> internal static class StorageBookmarkHelper { private const int HeaderLength = 16; private static ReadOnlySpan<byte> AvaHeaderPrefix => "ava.v1."u8; private static ReadOnlySpan<byte> FakeBclBookmarkPlatform => "bcl"u8; [return: NotNullIfNotNull(nameof(nativeBookmark))] public static string? EncodeBookmark(ReadOnlySpan<byte> platform, string? nativeBookmark) => nativeBookmark is null ? null : EncodeBookmark(platform, Encoding.UTF8.GetBytes(nativeBookmark)); public static string? EncodeBookmark(ReadOnlySpan<byte> platform, ReadOnlySpan<byte> nativeBookmarkBytes) { if (nativeBookmarkBytes.Length == 0) { return null; } if (platform.Length > HeaderLength) { throw new ArgumentException($"Platform name should not be longer than {HeaderLength} bytes", nameof(platform)); } var arrayLength = HeaderLength + nativeBookmarkBytes.Length; var arrayPool = ArrayPool<byte>.Shared.Rent(arrayLength); try { // Write platform into first 16 bytes. var arraySpan = arrayPool.AsSpan(0, arrayLength); AvaHeaderPrefix.CopyTo(arraySpan); platform.CopyTo(arraySpan.Slice(AvaHeaderPrefix.Length)); // Write bookmark bytes. nativeBookmarkBytes.CopyTo(arraySpan.Slice(HeaderLength)); // We must use span overload because ArrayPool might return way too big array. #if NET6_0_OR_GREATER return Convert.ToBase64String(arraySpan); #else return Convert.ToBase64String(arraySpan.ToArray(), Base64FormattingOptions.None); #endif } finally { ArrayPool<byte>.Shared.Return(arrayPool); } } public enum DecodeResult { Success = 0, InvalidFormat, InvalidPlatform } public static DecodeResult TryDecodeBookmark(ReadOnlySpan<byte> platform, string? base64bookmark, out byte[]? nativeBookmark) { if (platform.Length > HeaderLength || platform.Length == 0 || base64bookmark is null || base64bookmark.Length % 4 != 0) { nativeBookmark = null; return DecodeResult.InvalidFormat; } Span<byte> decodedBookmark; #if NET6_0_OR_GREATER // Each base64 character represents 6 bits, but to be safe, var arrayPool = ArrayPool<byte>.Shared.Rent(HeaderLength + base64bookmark.Length * 6); if (Convert.TryFromBase64Chars(base64bookmark, arrayPool, out int bytesWritten)) { decodedBookmark = arrayPool.AsSpan().Slice(0, bytesWritten); } else { nativeBookmark = null; return DecodeResult.InvalidFormat; } #else decodedBookmark = Convert.FromBase64String(base64bookmark).AsSpan(); #endif try { if (decodedBookmark.Length < HeaderLength // Check if decoded string starts with the correct prefix, checking v1 at the same time. && !AvaHeaderPrefix.SequenceEqual(decodedBookmark.Slice(0, AvaHeaderPrefix.Length))) { nativeBookmark = null; return DecodeResult.InvalidFormat; } var actualPlatform = decodedBookmark.Slice(AvaHeaderPrefix.Length, platform.Length); if (!actualPlatform.SequenceEqual(platform)) { nativeBookmark = null; return DecodeResult.InvalidPlatform; } nativeBookmark = decodedBookmark.Slice(HeaderLength).ToArray(); return DecodeResult.Success; } finally { #if NET6_0_OR_GREATER ArrayPool<byte>.Shared.Return(arrayPool); #endif } } public static string EncodeBclBookmark(string localPath) => EncodeBookmark(FakeBclBookmarkPlatform, localPath); public static bool TryDecodeBclBookmark(string nativeBookmark, [NotNullWhen(true)] out string? localPath) { var decodeResult = TryDecodeBookmark(FakeBclBookmarkPlatform, nativeBookmark, out var bytes); if (decodeResult == DecodeResult.Success) { localPath = Encoding.UTF8.GetString(bytes!); return true; } if (decodeResult == DecodeResult.InvalidFormat && nativeBookmark.IndexOfAny(Path.GetInvalidPathChars()) < 0 && !string.IsNullOrEmpty(Path.GetDirectoryName(nativeBookmark))) { // Attempt to restore old BCL bookmarks. // Don't check for File.Exists here, as it will be done at later point in TryGetStorageItem. // Just validate if it looks like a valid file path. localPath = nativeBookmark; return true; } localPath = null; return false; } }