Skip to content
This repository has been archived by the owner on Apr 16, 2020. It is now read-only.

Use SourceLinkMap from Microsoft.SourceLink.Tools to implement SourceLink JSON parsing. #389

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions dotnet-sourcelink-shared/SourceLinkJson.cs

This file was deleted.

198 changes: 198 additions & 0 deletions dotnet-sourcelink-shared/SourceLinkMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this header correct in this repo?


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

// TODO: Use a source package once available (https://github.com/dotnet/sourcelink/issues/443)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks to be waiting on this dotnet/sourcelink#443 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to wait for that work item to be done. I just haven't had time to finish this PR. Feel free to take it over.


namespace Microsoft.SourceLink.Tools
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should this file include a comment pointing toward the source in the dotnet/sourcelink repo?

{
internal sealed class SourceLinkMap
{
private readonly List<(FilePathPattern key, UriPattern value)> _entries;

public SourceLinkMap(List<(FilePathPattern key, UriPattern value)> entries)
{
Debug.Assert(entries != null);
_entries = entries;
}

internal struct FilePathPattern
{
public readonly string Path;
public readonly bool IsPrefix;

public FilePathPattern(string path, bool isPrefix)
{
Debug.Assert(path != null);

Path = path;
IsPrefix = isPrefix;
}
}

internal struct UriPattern
{
public readonly string Prefix;
public readonly string Suffix;

public UriPattern(string prefix, string suffix)
{
Debug.Assert(prefix != null);
Debug.Assert(suffix != null);

Prefix = prefix;
Suffix = suffix;
}
}

internal static SourceLinkMap Parse(string json, Action<string> reportDiagnostic)
{
bool errorReported = false;

void ReportInvalidJsonDataOnce(string message)
{
if (!errorReported)
{
// Bad source link format
reportDiagnostic($"The JSON format is invalid: {message}");
}

errorReported = true;
}

var list = new List<(FilePathPattern key, UriPattern value)>();
try
{
// trim BOM if present:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// trim BOM if present:
// trim UTF8 BOM if present:

Also: Doesn't Newtonsoft.Json handle a UTF8 BOM? @JamesNK?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It uses StreamReader, so yes

var root = JObject.Parse(json.TrimStart('\uFEFF'));
var documents = root["documents"];

if (documents.Type != JTokenType.Object)
{
ReportInvalidJsonDataOnce($"expected object: {documents}");
return null;
}

foreach (var token in documents)
{
if (!(token is JProperty property))
{
ReportInvalidJsonDataOnce($"expected property: {token}");
continue;
}

string value = (property.Value.Type == JTokenType.String) ?
property.Value.Value<string>() : null;

if (value == null ||
!TryParseEntry(property.Name, value, out var path, out var uri))
{
ReportInvalidJsonDataOnce($"invalid mapping: '{property.Name}': '{value}'");
continue;
}

list.Add((path, uri));
}
}
catch (JsonReaderException e)
{
reportDiagnostic(e.Message);
return null;
}

// Sort the map by decreasing file path length. This ensures that the most specific paths will checked before the least specific
// and that absolute paths will be checked before a wildcard path with a matching base
list.Sort((left, right) => -left.key.Path.Length.CompareTo(right.key.Path.Length));

return new SourceLinkMap(list);
}

private static bool TryParseEntry(string key, string value, out FilePathPattern path, out UriPattern uri)
{
path = default;
uri = default;

// VALIDATION RULES
// 1. The only acceptable wildcard is one and only one '*', which if present will be replaced by a relative path
// 2. If the filepath does not contain a *, the uri cannot contain a * and if the filepath contains a * the uri must contain a *
// 3. If the filepath contains a *, it must be the final character
// 4. If the uri contains a *, it may be anywhere in the uri

int filePathStar = key.IndexOf('*');
if (filePathStar == key.Length - 1)
{
key = key.Substring(0, filePathStar);

if (key.IndexOf('*') >= 0)
{
return false;
}
}
else if (filePathStar >= 0 || key.Length == 0)
{
return false;
}

string uriPrefix, uriSuffix;
int uriStar = value.IndexOf('*');
if (uriStar >= 0)
{
if (filePathStar < 0)
{
return false;
}

uriPrefix = value.Substring(0, uriStar);
uriSuffix = value.Substring(uriStar + 1);

if (uriSuffix.IndexOf('*') >= 0)
{
return false;
}
}
else
{
uriPrefix = value;
uriSuffix = "";
}

path = new FilePathPattern(key, isPrefix: filePathStar >= 0);
uri = new UriPattern(uriPrefix, uriSuffix);
return true;
}

public string GetUri(string path)
{
if (path.IndexOf('*') >= 0)
{
return null;
}

// Note: the mapping function is case-insensitive.

foreach (var (file, uri) in _entries)
{
if (file.IsPrefix)
{
if (path.StartsWith(file.Path, StringComparison.OrdinalIgnoreCase))
{
var escapedPath = string.Join("/", path.Substring(file.Path.Length).Split(new[] { '/', '\\' }).Select(Uri.EscapeDataString));
return uri.Prefix + escapedPath + uri.Suffix;
}
}
else if (string.Equals(path, file.Path, StringComparison.OrdinalIgnoreCase))
{
Debug.Assert(uri.Suffix.Length == 0);
return uri.Prefix;
}
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)FileUtil.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SourceLinkJson.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SourceLinkMap.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Version.cs" />
</ItemGroup>
Expand Down
29 changes: 3 additions & 26 deletions dotnet-sourcelink/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.SourceLink.Tools;
using Newtonsoft.Json;
using NuGet.Packaging;
using System;
Expand Down Expand Up @@ -464,40 +465,16 @@ public static IEnumerable<Document> GetDocumentsWithUrls(DebugReaderProvider drp
if (bytes != null)
{
var text = Encoding.UTF8.GetString(bytes);
var json = JsonConvert.DeserializeObject<SourceLinkJson>(text);
var map = SourceLinkMap.Parse(text, Console.Error.WriteLine);
foreach (var doc in GetDocuments(drp))
{
if (!doc.IsEmbedded)
doc.Url = GetUrl(doc.Name, json);
doc.Url = map.GetUri(doc.Name);
yield return doc;
}
}
}

public static string GetUrl(string file, SourceLinkJson json)
{
if (json == null) return null;
foreach (var key in json.documents.Keys)
{
if (key.Contains("*"))
{
var pattern = Regex.Escape(key).Replace(@"\*", "(.+)");
var regex = new Regex(pattern);
var m = regex.Match(file);
if (!m.Success) continue;
var url = json.documents[key];
var path = m.Groups[1].Value.Replace(@"\", "/");
return url.Replace("*", path);
}
else
{
if (!key.Equals(file, StringComparison.Ordinal)) continue;
return json.documents[key];
}
}
return null;
}

static async Task<IEnumerable<Document>> GetDocumentsWithUrlHashes(DebugReaderProvider drp, IAuthenticationHeaderValueProvider authenticationHeaderValueProvider)
{
// https://github.com/ctaggart/SourceLink/blob/v1/Exe/Http.fs
Expand Down