text: ﻿// ScriptSharpSourceMap.cs
  //
  // Copyright 2012 Microsoft Corporation
  //
  // 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.Collections.Generic;
  using System.IO;
  using System.Security.Cryptography;
  using System.Xml;
  using NUglify.Helpers;
  using NUglify.JavaScript.Syntax;
  
  namespace NUglify.JavaScript
  {
      public sealed class ScriptSharpSourceMap : ISourceMap
      {
          private readonly XmlWriter m_writer;
          private string m_currentPackagePath;
          private string m_mapPath;
          private Dictionary<string, int> m_sourceFileIndexMap = new Dictionary<string, int>();
          private int currentIndex;
          private int m_lineOffset;
          private int m_columnOffset;
  
          /// <summary>
          /// Gets or sets an optional source root URI that will be added to the map object as the sourceRoot property if set
          /// </summary>
          public string SourceRoot
          {
              get;
              set;
          }
  
          /// <summary>
          /// Gets or sets a flag indicating whether or not to add a "safe" header to the map output file
          /// (not used by this implementation)
          /// </summary>
          public bool SafeHeader
          {
              get;
              set;
          }
  
          public static string ImplementationName
          {
              get { return "XML"; }
          }
  
          public string Name
          {
              get { return ImplementationName; }
          }
  
          public ScriptSharpSourceMap(TextWriter writer)
          {
              if (writer == null)
              {
                  throw new ArgumentNullException("writer");
              }
  
              var settings = new XmlWriterSettings()
                  {
                      CloseOutput = true,
                      Indent = true
                  };
              m_writer = XmlWriter.Create(writer, settings);
  
              m_writer.WriteStartDocument();
              m_writer.WriteStartElement("map");
              JavaScriptSymbol.WriteHeadersTo(m_writer);
              m_writer.WriteStartElement("scriptFiles");
          }
  
          public void StartPackage(string sourcePath, string mapPath)
          {
              m_currentPackagePath = sourcePath;
              m_mapPath = mapPath;
  
              m_writer.WriteStartElement("scriptFile");
              m_writer.WriteAttributeString("path", MakeRelative(sourcePath, m_mapPath) ?? string.Empty);
          }
  
          [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security.Cryptography", "CA5350:MD5CannotBeUsed", Justification="not using for encryption, just a checksum")]
          public void EndPackage()
          {
              if (m_currentPackagePath.IsNullOrWhiteSpace())
              {
                  return;
              }
  
              // Compute and print the output script checksum and close the scriptFile element
              // the checksum can be used to determine whether the symbols map file is still valid
              // or if the script has been tempered with
              using (FileStream stream = new FileStream(m_currentPackagePath, FileMode.Open))
              {
                  using (MD5 md5 = MD5.Create())
                  {
                      byte[] checksum = md5.ComputeHash(stream);
  
                      m_writer.WriteStartElement("checksum");
                      m_writer.WriteAttributeString("value", BitConverter.ToString(checksum));
                      m_writer.WriteEndElement(); //checksum
                      m_writer.WriteEndElement(); //scriptFile
                  }
              }
  
              m_currentPackagePath = null;
          }
  
          /// <summary>
          /// A new line has been inserted into the output code, so adjust the offsets accordingly
          /// for the next run.
          /// </summary>
          public void NewLineInsertedInOutput()
          {
              m_columnOffset = 0;
              ++m_lineOffset;
          }
  
          /// <summary>
          /// Signal the end of an output run by sending the NEXT position in the output
          /// </summary>
          /// <param name="lineNumber">0-based line number</param>
          /// <param name="columnPosition">0-based column position</param>
          public void EndOutputRun(int lineNumber, int columnPosition)
          {
              // the values are one-based, but we want a delta - so subtract one from them.
              // for example, if the run ends on line 35 column 12, when the next position comes 
              // in as line 0, column 0, we end up reporting that position at line 35 (0 + 35) column 12 (0 + 12).
              m_lineOffset += lineNumber;
              m_columnOffset += columnPosition;
          }
  
          public object StartSymbol(AstNode node, int startLine, int startColumn)
          {
              if (node != null 
                  && !node.Context.Document.IsGenerated)
              {
                  return JavaScriptSymbol.StartNew(node, startLine + m_lineOffset, startColumn + m_columnOffset, GetSourceFileIndex(node.Context.Document.FileContext));
              }
  
              return null;
          }
  
          public void MarkSegment(AstNode node, int startLine, int startColumn, string name, SourceContext context)
          {
              if (node == null || string.IsNullOrEmpty(name))
              {
                  return;
              }
  
              // see if this is within a function object node, 
              // AND if this segment has the same name as the function name
              // AND this context isn't the same as the entire function context.
              // this should only be true for the function NAME segment.
              var functionObject = node as FunctionObject;
              if (functionObject != null 
                  && functionObject.Binding != null
                  && string.CompareOrdinal(name, functionObject.Binding.Name) == 0
                  && context != functionObject.Context)
              {
                  // adjust the offsets
                  startLine += m_lineOffset;
                  startColumn += m_columnOffset;
  
                  // it does -- so this is the segment that corresponds to the function object's name, which
                  // for this format we want to output a separate segment for. It used to be its own Lookup
                  // node child of the function object, so we need to create a fake node here, start a new 
                  // symbol from it, end the symbol, then write it.
                  var fakeLookup = new LookupExpression(context) { Name = name };
                  var nameSymbol = JavaScriptSymbol.StartNew(fakeLookup, startLine, startColumn, GetSourceFileIndex(functionObject.Context.Document.FileContext));
  
                  // the name will never end on a different line -- it's a single unbreakable token. The length is just
                  // the length of the name, so add that number to the column start. And the parent context is the function
                  // name (again)
                  nameSymbol.End(startLine, startColumn + name.Length, name);
                  nameSymbol.WriteTo(m_writer);
              }
          }
  
          public void EndSymbol(object symbol, int endLine, int endColumn, string parentContext)
          {
              if (symbol == null)
              {
                  return;
              }
  
              // adjust the offsets
              endLine += m_lineOffset;
              endColumn += m_columnOffset;
  
              var javaScriptSymbol = (JavaScriptSymbol)symbol;
              javaScriptSymbol.End(endLine, endColumn, parentContext);
              javaScriptSymbol.WriteTo(m_writer);
          }
  
          public void EndFile(TextWriter writer, string newLine)
          {
              // do nothing.
          }
  
          public void Dispose()
          {
              EndPackage();
  
              m_writer.WriteEndElement(); //scriptFiles
              m_writer.WriteStartElement("sourceFiles");
  
              foreach (KeyValuePair<string, int> kvp in m_sourceFileIndexMap)
              {
                  m_writer.WriteStartElement("sourceFile");
                  m_writer.WriteAttributeString("id", kvp.Value.ToStringInvariant());
                  m_writer.WriteAttributeString("path", MakeRelative(kvp.Key, m_mapPath) ?? string.Empty);
                  m_writer.WriteEndElement(); //file
              }
  
              m_writer.WriteEndElement(); //sourceFiles
              m_writer.WriteEndElement(); //map
              m_writer.WriteEndDocument();
              ((IDisposable)m_writer).Dispose();
          }
  
          private int GetSourceFileIndex(string fileName)
          {
              int index;
              if (!m_sourceFileIndexMap.TryGetValue(fileName, out index))
              {
                  index = ++currentIndex;
                  m_sourceFileIndexMap.Add(fileName, index);
              }
  
              return index;
          }
  
          private static string MakeRelative(string path, string relativeFrom)
          {
              // if either one is null or blank, just return the original path
              if (!path.IsNullOrWhiteSpace() && !relativeFrom.IsNullOrWhiteSpace())
              {
                  try
                  {
                      var fromUri = new Uri(Normalize(relativeFrom));
                      var toUri = new Uri(Normalize(path));
                      var relativeUrl = fromUri.MakeRelativeUri(toUri);
  
                      return relativeUrl.ToString();
                  }
                  catch (UriFormatException)
                  {
                      // catch and return the original path
                  }
              }
  
              return path;
          }
  
          private static string Normalize(string path)
          {
              return Path.IsPathRooted(path) ? path : Path.Combine(Directory.GetCurrentDirectory(), path);
          }
  
          #region internal symbol object class
  
          private class JavaScriptSymbol
          {
              private const string SymbolDataFormat = "{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12}";
              private int m_startLine;
              private int m_endLine;
              private int m_startColumn;
              private int m_endColumn;
              private SourceContext m_sourceContext;
              private int m_sourceFileId;
              private string m_symbolType;
              private string m_parentFunction;
  
              private JavaScriptSymbol() { }
  
              public static JavaScriptSymbol StartNew(AstNode node, int startLine, int startColumn, int sourceFileId)
              {
                  if (startLine == int.MaxValue)
                  {
                      throw new ArgumentOutOfRangeException("startLine");
                  }
  
                  if (startColumn == int.MaxValue)
                  {
                      throw new ArgumentOutOfRangeException("startColumn");
                  }
  
                  return new JavaScriptSymbol
                  {
                      // destination line/col number are fed to us as zero-based, so add one to get to
                      // the one-based values we desire. Context objects store the source line/col as
                      // one-based already.
                      m_startLine = startLine + 1,
                      m_startColumn = startColumn + 1,
                      m_sourceContext = node != null ? node.Context : null,
                      m_symbolType = node != null ? node.GetType().Name : "[UNKNOWN]",
                      m_sourceFileId = sourceFileId,
                  };
              }
  
              public void End(int endLine, int endColumn, string parentFunction)
              {
                  if (endLine == int.MaxValue)
                  {
                      throw new ArgumentOutOfRangeException("endLine");
                  }
  
                  if (endColumn == int.MaxValue)
                  {
                      throw new ArgumentOutOfRangeException("endColumn");
                  }
  
                  // destination line/col number are fed to us as zero-based, so add one to get to
                  // the one-based values we desire.
                  m_endLine = endLine + 1;
                  m_endColumn = endColumn + 1;
                  m_parentFunction = parentFunction;
              }
  
              public static void WriteHeadersTo(XmlWriter writer)
              {
                  if (writer != null)
                  {
                      writer.WriteStartElement("headers");
                      writer.WriteString(SymbolDataFormat.FormatInvariant(
                          "DstStartLine",
                          "DstStartColumn",
                          "DstEndLine",
                          "DstEndColumn",
                          "SrcStartPosition",
                          "SrcEndPosition",
                          "SrcStartLine",
                          "SrcStartColumn",
                          "SrcEndLine",
                          "SrcEndColumn",
                          "SrcFileId",
                          "SymbolType",
                          "ParentFunction"));
  
                      writer.WriteEndElement(); //headers
                  }
              }
  
              public void WriteTo(XmlWriter writer)
              {
                  if (writer != null)
                  {
                      writer.WriteStartElement("s");
                      writer.WriteString(SymbolDataFormat.FormatInvariant(
                          m_startLine,
                          m_startColumn,
                          m_endLine,
                          m_endColumn,
                          m_sourceContext.StartPosition - m_sourceContext.SourceOffsetStart,
                          m_sourceContext.EndPosition - m_sourceContext.SourceOffsetEnd,
                          m_sourceContext.StartLineNumber,
                          m_sourceContext.StartColumn,
                          m_sourceContext.EndLineNumber,
                          m_sourceContext.EndColumn,
                          m_sourceFileId,
                          m_symbolType,
                          m_parentFunction));
  
                      writer.WriteEndElement(); //s
                  }
              }
          }
  
          #endregion
      }
  }
  