NetscootShared/Common/StoredPath.ps1
|
# Netscoot.StoredPath: a relative path as written inside a file, rewritten in place when its target or its file moves. # Add-Type in C# 5 so every engine module can name the type and Windows PowerShell 5.1 can compile it. if (-not ('Netscoot.StoredPath' -as [type])) { Add-Type -TypeDefinition @' using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; namespace Netscoot { public abstract class StoredPath { public string File { get; private set; } public string Raw { get; private set; } public string Target { get; private set; } protected StoredPath(string file, string raw, string target) { File = file; Raw = raw; Target = target; } // An attribute value in an MSBuild file, e.g. ProjectReference Include or Import Project. public static StoredPath InAttribute(string file, string attribute, string raw, string target) { return new AttributePath(file, attribute, raw, target, Path.DirectorySeparatorChar); } // A Project Path attribute in a .slnx, which always uses forward slashes. public static StoredPath InSlnxEntry(string file, string raw, string target) { return new AttributePath(file, "Path", raw, target, '/'); } // A quoted project path on a .sln Project(...) line. public static StoredPath InSolutionEntry(string file, string raw, string target) { return new SolutionEntryPath(file, raw, target); } // A solution item on a .sln SolutionItems line, stored as "path = path". public static StoredPath InSolutionItem(string file, string raw, string target) { return new SolutionItemPath(file, raw, target); } // The path string of a PowerShell dot-source, call or Import-Module. public static StoredPath InScript(string file, string raw, string target) { return new ScriptPath(file, raw, target); } public string RawPointingAt(string target) { return Format(RelativePath(Path.GetDirectoryName(File), target)); } public string RawFollowing(string newFile) { return Format(RelativePath(Path.GetDirectoryName(newFile), Target)); } public bool PointAt(string target) { return Rewrite(File, RawPointingAt(target)); } public bool FollowFile(string newFile) { return Rewrite(newFile, RawFollowing(newFile)); } protected abstract string Format(string relative); protected virtual string Token(string raw, char quote) { return quote + raw + quote; } protected virtual char[] Quotes { get { return new char[] { '"' }; } } protected virtual string Replace(string text, string oldRaw, string newRaw) { foreach (char q in Quotes) { text = text.Replace(Token(oldRaw, q), Token(newRaw, q)); } return text; } protected virtual char DefaultSeparator { get { return Path.DirectorySeparatorChar; } } protected string Styled(string path) { bool slash = Raw.IndexOf('/') >= 0; bool backslash = Raw.IndexOf('\\') >= 0; if (slash && !backslash) { return path.Replace('\\', '/'); } if (backslash && !slash) { return path.Replace('/', '\\'); } if (!slash && !backslash) { return path.Replace('\\', DefaultSeparator).Replace('/', DefaultSeparator); } return path; } private bool Rewrite(string file, string newRaw) { EncodedText content = EncodedText.Read(file); string text = Replace(content.Text, Raw, newRaw); if (string.Equals(text, content.Text, StringComparison.Ordinal)) { return false; } content.Write(file, text); return true; } private static string RelativePath(string fromDir, string to) { char sep = Path.DirectorySeparatorChar; StringComparison cmp = sep == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; char[] seps = new char[] { '\\', '/' }; string[] from = fromDir.Split(seps, StringSplitOptions.RemoveEmptyEntries); string[] dest = to.Split(seps, StringSplitOptions.RemoveEmptyEntries); int common = 0; while (common < from.Length && common < dest.Length && string.Equals(from[common], dest[common], cmp)) { common++; } if (common == 0 && sep == '\\') { return to; } List<string> parts = new List<string>(); for (int i = common; i < from.Length; i++) { parts.Add(".."); } for (int i = common; i < dest.Length; i++) { parts.Add(dest[i]); } if (parts.Count == 0) { return "."; } return string.Join(sep.ToString(), parts.ToArray()); } } // A file's text with the byte-order mark and encoding it was read in, so a rewrite keeps both. // BOM-less text that is not valid UTF-8 is a legacy code page, round-tripped byte for byte as Latin-1. internal sealed class EncodedText { private readonly byte[] bom; private readonly Encoding encoding; public string Text { get; private set; } private EncodedText(byte[] bom, Encoding encoding, string text) { this.bom = bom; this.encoding = encoding; Text = text; } public static EncodedText Read(string file) { byte[] bytes = System.IO.File.ReadAllBytes(file); Encoding[] withBom = new Encoding[] { new UTF32Encoding(false, true), new UTF32Encoding(true, true), new UTF8Encoding(true), new UnicodeEncoding(false, true), new UnicodeEncoding(true, true) }; foreach (Encoding candidate in withBom) { byte[] preamble = candidate.GetPreamble(); int matched = 0; while (matched < preamble.Length && matched < bytes.Length && bytes[matched] == preamble[matched]) { matched++; } if (matched == preamble.Length) { return new EncodedText(preamble, candidate, candidate.GetString(bytes, preamble.Length, bytes.Length - preamble.Length)); } } try { return new EncodedText(new byte[0], new UTF8Encoding(false), new UTF8Encoding(false, true).GetString(bytes)); } catch (DecoderFallbackException) { Encoding latin1 = Encoding.GetEncoding(28591); return new EncodedText(new byte[0], latin1, latin1.GetString(bytes)); } } public void Write(string file, string text) { byte[] body = encoding.GetBytes(text); byte[] all = new byte[bom.Length + body.Length]; Buffer.BlockCopy(bom, 0, all, 0, bom.Length); Buffer.BlockCopy(body, 0, all, bom.Length, body.Length); System.IO.File.WriteAllBytes(file, all); } } public sealed class AttributePath : StoredPath { private const string ThisFileDirectory = "$(MSBuildThisFileDirectory)"; private readonly char defaultSeparator; public string Attribute { get; private set; } internal AttributePath(string file, string attribute, string raw, string target, char separator) : base(file, raw, target) { Attribute = attribute; defaultSeparator = separator; } protected override char DefaultSeparator { get { return defaultSeparator; } } protected override char[] Quotes { get { return new char[] { '"', '\'' }; } } protected override string Format(string relative) { string styled = Styled(relative); if (Raw.StartsWith(ThisFileDirectory, StringComparison.OrdinalIgnoreCase)) { return ThisFileDirectory + styled; } return styled; } protected override string Token(string raw, char quote) { return Attribute + "=" + quote + raw + quote; } } public sealed class SolutionEntryPath : StoredPath { internal SolutionEntryPath(string file, string raw, string target) : base(file, raw, target) { } protected override char DefaultSeparator { get { return '\\'; } } protected override string Format(string relative) { return Styled(relative); } } public sealed class SolutionItemPath : StoredPath { internal SolutionItemPath(string file, string raw, string target) : base(file, raw, target) { } protected override char DefaultSeparator { get { return '\\'; } } protected override string Format(string relative) { return Styled(relative); } protected override string Replace(string text, string oldRaw, string newRaw) { string old = Regex.Escape(oldRaw); string pattern = @"(?m)^(\s*)" + old + @"(\s*=\s*)" + old + @"(\s*)$"; string escaped = newRaw.Replace("$", "$$"); return Regex.Replace(text, pattern, "${1}" + escaped + "${2}" + escaped + "${3}"); } } public sealed class ScriptPath : StoredPath { private const string ScriptRoot = "$PSScriptRoot"; internal ScriptPath(string file, string raw, string target) : base(file, raw, target) { } protected override string Format(string relative) { string styled = Styled(relative); string sep = Styled(Path.DirectorySeparatorChar.ToString()); if (Raw.StartsWith(ScriptRoot, StringComparison.OrdinalIgnoreCase)) { return ScriptRoot + sep + styled; } if (styled.StartsWith("." + sep) || styled.StartsWith(".." + sep)) { return styled; } return "." + sep + styled; } // A script path may be quoted or bare, so it matches only as a whole token: not inside a // longer path such as ..\helpers.ps1 when the path is .\helpers.ps1. protected override string Replace(string text, string oldRaw, string newRaw) { string pattern = @"(?<![^\s'""(=,])" + Regex.Escape(oldRaw) + @"(?![^\s'""),;}|])"; return Regex.Replace(text, pattern, newRaw.Replace("$", "$$")); } } } '@ } |