/* * Copyright 2019 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Diagnostics; using System.IO; namespace SourceGen { /// /// Manages references to external files, notably symbol files (.sym65) and extension /// scripts. Identifiers look like "RT:subdir/file.sym65". /// /// Instances are immutable. /// public class ExternalFile { private const string INVALID_IDENT = "!!!INVALID!!!"; // probably don't need localization /// /// Pathname separator character for use for file identifiers. We want this /// to be the same on all platforms, with local conversion, so we should probably be /// using something like ':' that makes Windows barf. In practice, being rigorous /// doesn't seem important, and '/' is pretty universal these days. Just don't do \. /// private const char PATH_SEP_CHAR = '/'; private const string RUNTIME_DIR_PREFIX = "RT:"; private const string PROJECT_DIR_PREFIX = "PROJ:"; private enum Location { Unknown = 0, RuntimeDir, ProjectDir } // Not sure there's value in tracking the type, except for some validation checks. private enum Type { Unknown = 0, SymbolFile, ExtensionScript } /// /// Identifier for this file. /// public string Identifier { get { return mIdent; } } private string mIdent; /// /// File location. /// private Location mIdentLocation; /// /// File type. /// private Type mIdentType; /// /// Identifier without location prefix or filename extension. /// private string mInnards; /// /// Creates a new ExternalFile instance from the identifier. /// public static ExternalFile CreateFromIdent(string ident) { if (!DecodeIdent(ident, out Location identLocation, out Type identType, out string innards)) { return null; } return new ExternalFile(ident, identLocation, identType, innards); } /// /// Creates a new ExternalFile instance from a full path. /// /// Full path of external file, in canonical /// form. /// Full path to directory in which project file lives, in /// canonical form. If the project hasn't been saved yet, pass an empty string. /// New object, or null if the path isn't valid. public static ExternalFile CreateFromPath(string pathName, string projectDir) { string stripDir; string rtDir = RuntimeDataAccess.GetDirectory(); string prefix; // Check path prefix for RT:, and full directory name for PROJ:. if (pathName.StartsWith(rtDir)) { stripDir = rtDir; prefix = RUNTIME_DIR_PREFIX; } else if (!string.IsNullOrEmpty(projectDir) && Path.GetDirectoryName(pathName) == projectDir) { stripDir = projectDir; prefix = PROJECT_DIR_PREFIX; } else { Debug.WriteLine("Path not in RuntimeData or project: " + pathName); return null; } // Remove directory component. string partialPath = pathName.Substring(stripDir.Length); // If directory string didn't end with '/' or '\\', remove char from start. if (partialPath[0] == '\\' || partialPath[0] == '/') { partialPath = partialPath.Substring(1); } // Replace canonical path sep with '/'. partialPath = partialPath.Replace(Path.DirectorySeparatorChar, PATH_SEP_CHAR); string ident = prefix + partialPath; Debug.WriteLine("Converted path '" + pathName + "' to ident '" + ident + "'"); return CreateFromIdent(ident); } /// /// Internal constructor. /// private ExternalFile(string ident, Location identLocation, Type identType, string innards) { mIdent = ident; mIdentLocation = identLocation; mIdentType = identType; mInnards = innards; } /// /// Decodes an ident string into its constituent parts. /// private static bool DecodeIdent(string ident, out Location identLocation, out Type identType, out string innards) { identLocation = Location.Unknown; identType = Type.Unknown; innards = string.Empty; int prefixLen; if (ident.StartsWith(RUNTIME_DIR_PREFIX)) { identLocation = Location.RuntimeDir; prefixLen = RUNTIME_DIR_PREFIX.Length; } else if (ident.StartsWith(PROJECT_DIR_PREFIX)) { identLocation = Location.ProjectDir; prefixLen = PROJECT_DIR_PREFIX.Length; } else { return false; } int extLen; if (ident.EndsWith(PlatformSymbols.FILENAME_EXT)) { identType = Type.SymbolFile; extLen = PlatformSymbols.FILENAME_EXT.Length; } else if (ident.EndsWith(Sandbox.ScriptManager.FILENAME_EXT)) { identType = Type.ExtensionScript; extLen = Sandbox.ScriptManager.FILENAME_EXT.Length; } else { return false; } // Fail idents with no actual name, e.g. "RT:.cs". if (ident.Length == prefixLen + extLen) { return false; } innards = ident.Substring(prefixLen, ident.Length - prefixLen - extLen); return true; } /// /// Strips the prefix and filename extension off of an identifier. /// /// Stripped identifier, or null if the identifier was malformed. public string GetInnards() { return mInnards; } /// /// Converts an identifier to a full path. For PROJ: identifiers, the project /// directory argument is used. /// /// Identifier to convert. /// Full path to directory in which project file lives, in /// canonical form. If the project hasn't been saved yet, pass an empty string. /// Full path, or null if the identifier points to a file outside the /// directory, or if this is a ProjectDir ident and the project dir isn't set. public string GetPathName(string projectDir) { string dir; bool subdirAllowed; switch (mIdentLocation) { case Location.RuntimeDir: dir = RuntimeDataAccess.GetDirectory(); if (string.IsNullOrEmpty(dir)) { // Could happen if we can't find the runtime data directory, though // we probably should've failed before now. Debug.Assert(false); return null; } subdirAllowed = true; break; case Location.ProjectDir: if (string.IsNullOrEmpty(projectDir)) { // Shouldn't happen in practice -- we don't create PROJ: identifiers // unless a project directory has been established. Debug.Assert(false); return null; } dir = projectDir; subdirAllowed = false; break; default: Debug.Assert(false); return null; } int extLen = mIdent.IndexOf(':') + 1; string fullPath = Path.GetFullPath(Path.Combine(dir, mIdent.Substring(extLen))); // Confirm the file actually lives in the directory. RT: files can be anywhere // below the RuntimeData directory, while PROJ: files must live in the project // directory. if (subdirAllowed) { dir += Path.DirectorySeparatorChar; if (!fullPath.StartsWith(dir)) { Debug.WriteLine("WARNING: ident resolves outside subdir: " + mIdent); Debug.Assert(false); return null; } } else { if (dir != Path.GetDirectoryName(fullPath)) { Debug.WriteLine("WARNING: ident resolves outside dir: " + mIdent); return null; } } return fullPath; } /// /// Generates a script DLL name from the ident. If the ident is for a project-scope /// extension script, the project's file name will be included. /// /// Full path to project. /// DLL filename. public string GenerateDllName(string projectFileName) { switch (mIdentLocation) { case Location.RuntimeDir: return "RT_" + mInnards.Replace(PATH_SEP_CHAR, '_') + ".dll"; case Location.ProjectDir: string noExt = Path.GetFileNameWithoutExtension(projectFileName); return "PROJ_" + noExt + "_" + mInnards.Replace(PATH_SEP_CHAR, '_') + ".dll"; default: Debug.Assert(false); return null; } } } }