// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Buffers.Binary; using System.Collections.Immutable; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Shared.Utilities; /// <param name="ContentHash">Content hash of the original document the containing the invocation to be intercepted. /// (See <see cref="SourceText.GetContentHash()"/>)</param> /// <param name="Position">The position in the file of the invocation that was intercepted. This is the absolute /// start of the name token being invoked (e.g. <c>this.$$Goo(x, y, z)</c>) (see <see /// cref="SyntaxToken.FullSpan"/>).</param> internal record struct InterceptsLocationData(ImmutableArray<byte> ContentHash, int Position) { public readonly bool Equals(InterceptsLocationData other) => Position == other.Position && ImmutableArrayComparer<byte>.Instance.Equals(ContentHash, other.ContentHash); public override readonly int GetHashCode() => Hash.Combine(ImmutableArrayComparer<byte>.Instance.GetHashCode(ContentHash), Position); } internal static class InterceptsLocationUtilities { public static ImmutableArray<InterceptsLocationData> GetInterceptsLocationData(ImmutableArray<AttributeData> attributes) { using var result = TemporaryArray<InterceptsLocationData>.Empty; foreach (var attribute in attributes) { if (TryGetInterceptsLocationData(attribute, out var data)) result.Add(data); } return result.ToImmutableAndClear(); } public static bool TryGetInterceptsLocationData(AttributeData attribute, out InterceptsLocationData result) { if (attribute is { AttributeClass.Name: "InterceptsLocationAttribute", ConstructorArguments: [{ Value: int version }, { Value: string attributeData }] }) { return TryGetInterceptsLocationData(version, attributeData, out result); } result = default; return false; } public static bool TryGetInterceptsLocationData(int version, string attributeData, out InterceptsLocationData result) { if (version == 1) return TryGetInterceptsLocationDataVersion1(attributeData, out result); // Add more supported versions here in the future if the compiler adds any. result = default; return false; } private static bool TryGetInterceptsLocationDataVersion1(string attributeData, out InterceptsLocationData result) { result = default; if (!Base64Utilities.TryGetDecodedLength(attributeData, out var decodedLength)) return false; // V1 format: // - 16 bytes of target file content hash (xxHash128) // - int32 position (little endian) // - utf-8 display filename const int HashIndex = 0; const int HashSize = 16; const int PositionIndex = HashIndex + HashSize; const int PositionSize = sizeof(int); const int DisplayNameIndex = PositionIndex + PositionSize; const int MinLength = DisplayNameIndex; if (decodedLength < MinLength) return false; var rentedArray = decodedLength < 1024 ? null : System.Buffers.ArrayPool<byte>.Shared.Rent(decodedLength); try { var bytes = rentedArray is null ? stackalloc byte[decodedLength] : rentedArray.AsSpan(0, decodedLength); if (!Base64Utilities.TryFromBase64Chars(attributeData.AsSpan(), bytes, out _)) return false; var contentHash = bytes[HashIndex..HashSize].ToImmutableArray(); var position = BinaryPrimitives.ReadInt32LittleEndian(bytes[PositionIndex..]); result = new(contentHash, position); return true; } finally { if (rentedArray is not null) System.Buffers.ArrayPool<byte>.Shared.Return(rentedArray); } } } |