// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Security; using System.Text; using Microsoft.SqlTools.ManagedBatchParser; namespace Microsoft.SqlTools.ServiceLayer.BatchParser { /// /// The Parser class on which the Batch Parser is based on /// public sealed class Parser : IDisposable { private readonly ICommandHandler commandHandler; private Lexer lexer; private List tokenBuffer; private readonly IVariableResolver variableResolver; private SqlCmdCommand sqlCmdCommand; /// /// Constructor for the Parser class /// public Parser(ICommandHandler commandHandler, IVariableResolver variableResolver, TextReader reader, string name) { this.commandHandler = commandHandler; this.variableResolver = variableResolver; lexer = new Lexer(reader, name); tokenBuffer = new List(); DisableVariableSubstitution = variableResolver == null; } /// /// Whether to enable or disable the variable substitution. /// public bool DisableVariableSubstitution { get; set; } /// /// Whether to throw an error when a variable to be substituted was not specified. /// /// When DisableVariableSubstitution is true, this value is ignored, i.e. no exception will be thrown. public bool ThrowOnUnresolvedVariable { get; set; } private Token LookaheadToken { get { return lexer.CurrentToken; } } private LexerTokenType LookaheadTokenType { get { return lexer.CurrentTokenType; } } public void Dispose() { if (lexer != null) { lexer.Dispose(); lexer = null; } } private bool AcceptWhitespaceOrComment() { bool found = false; while (LookaheadTokenType == LexerTokenType.Comment || LookaheadTokenType == LexerTokenType.Whitespace) { Accept(); found = true; } return found; } private bool Accept(LexerTokenType lexerTokenType) { if (LookaheadTokenType == lexerTokenType) { Accept(); return true; } return false; } private void AddTokenToStringBuffer() { Token token = LookaheadToken; if (token.TokenType != LexerTokenType.Comment) { // All variable references must be valid CheckVariableReferences(token); } if (token.TokenType == LexerTokenType.NewLine && token.Text.Length == 0) { // Add "virtual" token representing new line that was not in original file PositionStruct beginPos = token.Begin; PositionStruct endPos = new PositionStruct(beginPos.Line + 1, 1, beginPos.Offset, beginPos.Filename); tokenBuffer.Add(new Token(LexerTokenType.NewLine, beginPos, endPos, Environment.NewLine, beginPos.Filename)); } else { tokenBuffer.Add(token); } } private void CheckVariableReferences(Token token) { // AddVariableReferences will raise error if reference is not constructed correctly AddVariableReferences(token, 0, null); } private void AddVariableReferences(Token token, int offset, IList variableRefs) { if (!lexer.RecognizeSqlCmdSyntax || DisableVariableSubstitution) { // variables are recognized only in sqlcmd mode and the substitution was not explicitly disabled return; } string text = token.Text; int i = 0; int startIndex = -1; int state = 0; while (i < text.Length) { char ch = text[i]; if (state == 0 && ch == '$') { state = 1; } else if (state == 1) { if (ch == '(') { state = 2; } else if (ch != '$') { state = 0; } } else if (state == 2) { if (Lexer.IsStartIdentifierChar(ch)) { startIndex = i; state = 3; } else { RaiseError(ErrorCode.InvalidVariableName, GetSubToken(token, i - 2, i + 1)); } } else if (state == 3) { if (ch == ')') { if (variableRefs != null) { variableRefs.Add(new VariableReference(offset + startIndex - 2, i - startIndex + 3, text.Substring(startIndex, i - startIndex))); } state = 0; } else if (Lexer.IsIdentifierChar(ch) == false) { RaiseError(ErrorCode.InvalidVariableName, GetSubToken(token, startIndex - 2, i + 1)); } } i++; } if (state == 2 || state == 3) { RaiseError(ErrorCode.InvalidVariableName, GetSubToken(token, startIndex - 2, i)); } } private static Token GetSubToken(Token token, int startOffset, int endOffset, LexerTokenType? newTokenType = null) { LexerTokenType tokenType = newTokenType.HasValue ? newTokenType.Value : token.TokenType; string text = token.Text.Substring(startOffset, endOffset - startOffset); string filename = token.Begin.Filename; PositionStruct beginPos, endPos; int beginLine, beginColumn; LineInfo.CalculateLineColumnForOffset(token, startOffset, out beginLine, out beginColumn); beginPos = new PositionStruct(beginLine, beginColumn, token.Begin.Offset + startOffset, filename); int endLine, endColumn; LineInfo.CalculateLineColumnForOffset(token, endOffset, out endLine, out endColumn); endPos = new PositionStruct(endLine, endColumn, token.Begin.Offset + endOffset, filename); return new Token(tokenType, beginPos, endPos, text, filename); } private LexerTokenType Accept() { lexer.ConsumeToken(); return LookaheadTokenType; } private void ExecuteBatch(int repeatCount) { BatchParserAction action; action = commandHandler.Go(new TextBlock(this, tokenBuffer), repeatCount, this.sqlCmdCommand); if (action == BatchParserAction.Abort) { RaiseError(ErrorCode.Aborted); } tokenBuffer = new List(); } private bool Expect(LexerTokenType lexerTokenType) { if (LookaheadTokenType == lexerTokenType) return true; RaiseError(ErrorCode.TokenExpected); return false; } private bool ExpectAndAccept(LexerTokenType lexerTokenType) { if (Accept(lexerTokenType)) return true; RaiseError(ErrorCode.TokenExpected); return false; } public void Parse() { Accept(); ParseLines(); } private void ParseGo() { int repeatCount = 1; AcceptWhitespaceOrComment(); if (LookaheadTokenType == LexerTokenType.Text) { string text = ResolveVariables(LookaheadToken, 0, null); // This statement originally placed after the for loop, But it moved here, the reason is: // When passing the long number like 999999999999999999999999, instead of throwing error // it accept and iterate the loop bool result = Int32.TryParse(text, out repeatCount); if (result == false) { RaiseError(ErrorCode.InvalidNumber); } for (int i = 0; i < text.Length; i++) { if (Char.IsDigit(text[i]) == false) { RaiseError(ErrorCode.InvalidNumber); } } Accept(LexerTokenType.Text); AcceptWhitespaceOrComment(); } if (LookaheadTokenType != LexerTokenType.Eof) { ExpectAndAccept(LexerTokenType.NewLine); } ExecuteBatch(repeatCount); } private void ParseInclude() { BatchParserAction action; Accept(LexerTokenType.Whitespace); Expect(LexerTokenType.Text); Token token = LookaheadToken; if (token.Text.StartsWith("--", StringComparison.Ordinal)) { RaiseError(ErrorCode.TokenExpected); } lexer.SetTextState(TextRuleFlags.ReportWhitespace | TextRuleFlags.RecognizeLineComment | TextRuleFlags.RecognizeDoubleQuotedString); Accept(); AcceptWhitespaceOrComment(); if (LookaheadTokenType != LexerTokenType.Eof) { Expect(LexerTokenType.NewLine); } TextReader textReader; string newFilename; string tokenText = token.Text; IEnumerable tokens; if (tokenText.IndexOf('"') != -1) { tokens = SplitQuotedTextToken(token); } else { tokens = new[] { token }; } action = commandHandler.Include(new TextBlock(this, tokens), out textReader, out newFilename); if (action == BatchParserAction.Abort) { RaiseError(ErrorCode.Aborted); } if (textReader != null) { this.PushInput(textReader, newFilename); } } private IEnumerable SplitQuotedTextToken(Token token) { string tokenText = token.Text; if (tokenText.Length <= 1) { return new[] { token }; } IList tokens = new List(); int offset = 0; int quotePos = tokenText.IndexOf('"'); while (quotePos != -1) { int closingQuotePos; if (quotePos != offset) { tokens.Add(GetSubToken(token, offset, quotePos)); offset = quotePos; } closingQuotePos = tokenText.IndexOf('"', quotePos + 1); if (closingQuotePos == -1) { // odd number of " characters break; } if (closingQuotePos + 1 < tokenText.Length && tokenText[closingQuotePos + 1] == '"') { // two consequtive " characters: report one of them tokens.Add(GetSubToken(token, quotePos + 1, closingQuotePos + 1, LexerTokenType.TextVerbatim)); } else { // Add the contents of the quoted string, but remove the " characters tokens.Add(GetSubToken(token, quotePos + 1, closingQuotePos, LexerTokenType.TextVerbatim)); } offset = closingQuotePos + 1; quotePos = tokenText.IndexOf('"', offset); } if (offset != tokenText.Length) { tokens.Add(GetSubToken(token, offset, tokenText.Length)); } return tokens; } internal void PushInput(TextReader reader, string filename) { lexer.PushInput(reader, filename); } private void ParseLines() { do { LexerTokenType tokenType = LookaheadTokenType; switch (tokenType) { case LexerTokenType.OnError: RemoveLastWhitespaceToken(); Token onErrorToken = LookaheadToken; Accept(); ParseOnErrorCommand(onErrorToken); break; case LexerTokenType.Eof: if (tokenBuffer.Count > 0) { ExecuteBatch(1); } return; case LexerTokenType.Go: RemoveLastWhitespaceToken(); Accept(); ParseGo(); break; case LexerTokenType.Include: RemoveLastWhitespaceToken(); Accept(); ParseInclude(); break; case LexerTokenType.Comment: case LexerTokenType.NewLine: case LexerTokenType.Text: case LexerTokenType.Whitespace: AddTokenToStringBuffer(); Accept(); break; case LexerTokenType.Setvar: Token setvarToken = LookaheadToken; RemoveLastWhitespaceToken(); Accept(); ParseSetvar(setvarToken); break; case LexerTokenType.Connect: Token connectToken = LookaheadToken; RemoveLastWhitespaceToken(); Accept(); ParseConnect(connectToken); break; case LexerTokenType.Ed: case LexerTokenType.ErrorCommand: case LexerTokenType.Execute: case LexerTokenType.Exit: case LexerTokenType.Help: case LexerTokenType.List: case LexerTokenType.ListVar: case LexerTokenType.Out: case LexerTokenType.Perftrace: case LexerTokenType.Quit: case LexerTokenType.Reset: case LexerTokenType.Serverlist: case LexerTokenType.Xml: RaiseError(ErrorCode.UnsupportedCommand, string.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionError_CommandNotSupported, tokenType)); break; default: RaiseError(ErrorCode.UnrecognizedToken); break; } } while (true); } private void RemoveLastWhitespaceToken() { if (tokenBuffer.Count > 0 && tokenBuffer[tokenBuffer.Count - 1].TokenType == LexerTokenType.Whitespace) { tokenBuffer.RemoveAt(tokenBuffer.Count - 1); } } private void ParseOnErrorCommand(Token onErrorToken) { ExpectAndAccept(LexerTokenType.Whitespace); Expect(LexerTokenType.Text); string action; action = ResolveVariables(LookaheadToken, 0, null); OnErrorAction onErrorAction; if (action.Equals("exit", StringComparison.OrdinalIgnoreCase)) { onErrorAction = OnErrorAction.Exit; } else if (action.Equals("ignore", StringComparison.OrdinalIgnoreCase)) { onErrorAction = OnErrorAction.Ignore; } else { RaiseError(ErrorCode.UnrecognizedToken); return; } Accept(LexerTokenType.Text); AcceptWhitespaceOrComment(); if (LookaheadTokenType != LexerTokenType.Eof) { Expect(LexerTokenType.NewLine); } BatchParserAction parserAction; parserAction = commandHandler.OnError(onErrorToken, onErrorAction); this.sqlCmdCommand = new OnErrorSqlCmdCommand(onErrorAction); if (parserAction == BatchParserAction.Abort) { RaiseError(ErrorCode.Aborted); } } private void ParseSetvar(Token setvarToken) { string variableName; string variableValue = null; Accept(LexerTokenType.Whitespace); Expect(LexerTokenType.Text); variableName = LookaheadToken.Text; Accept(); Accept(LexerTokenType.Whitespace); switch (LookaheadTokenType) { case LexerTokenType.Text: // No variable substitution variableValue = UnquoteVariableValue(LookaheadToken.Text); lexer.SetTextState(TextRuleFlags.ReportWhitespace | TextRuleFlags.RecognizeLineComment | TextRuleFlags.RecognizeDoubleQuotedString); Accept(); AcceptWhitespaceOrComment(); if (LookaheadTokenType != LexerTokenType.NewLine && LookaheadTokenType != LexerTokenType.Eof) { RaiseError(ErrorCode.UnrecognizedToken); } if (LookaheadTokenType != LexerTokenType.Eof) { Accept(); } break; case LexerTokenType.NewLine: case LexerTokenType.Eof: Accept(); break; default: RaiseError(ErrorCode.UnrecognizedToken); break; } variableResolver.SetVariable(setvarToken.Begin, variableName, variableValue); } private void ParseConnect(Token connectToken) { string serverName = null; string userName = null; string password = null; Accept(LexerTokenType.Whitespace); Expect(LexerTokenType.Text); serverName = ResolveVariables(LookaheadToken, 0, null); if (serverName == null) { //found some text but couldn't parse for servername RaiseError(ErrorCode.UnrecognizedToken); } Accept(); Accept(LexerTokenType.Whitespace); switch (LookaheadTokenType) { case LexerTokenType.Text: userName = ParseUserName(); password = ParsePassword(); if(userName == null || password == null) { //found some text but couldn't parse for user/password RaiseError(ErrorCode.UnrecognizedToken); } break; case LexerTokenType.NewLine: case LexerTokenType.Eof: Accept(); break; default: RaiseError(ErrorCode.UnrecognizedToken); break; } this.sqlCmdCommand = new ConnectSqlCmdCommand(serverName, userName, password); } private string ParseUserName() { string username = null; if (LookaheadToken.Text == "-U") { Accept(); Accept(LexerTokenType.Whitespace); if (LookaheadTokenType == LexerTokenType.Text) { username = ResolveVariables(LookaheadToken, 0, null); Accept(); Accept(LexerTokenType.Whitespace); } } return username; } private string ParsePassword() { string password = null; if (LookaheadToken.Text == "-P") { Accept(); Accept(LexerTokenType.Whitespace); if (LookaheadTokenType == LexerTokenType.Text) { password = ResolveVariables(LookaheadToken, 0, null); Accept(); Accept(LexerTokenType.Whitespace); } } return password; } internal void RaiseError(ErrorCode errorCode, string message = null) => RaiseError(errorCode, LookaheadToken, message); internal static void RaiseError(ErrorCode errorCode, Token token, string message = null) => throw new BatchParserException(errorCode, token, message ?? string.Format(CultureInfo.CurrentCulture, SR.BatchParser_IncorrectSyntax, token.Text)); internal string ResolveVariables(Token inputToken, int offset, List variableRefs) { List variableRefsLocal = new List(); AddVariableReferences(inputToken, offset, variableRefsLocal); string inputString = inputToken.Text; if (variableRefsLocal.Count == 0) { // no variable references to substitute return inputString; } StringBuilder sb = new StringBuilder(); int lastChar = 0; PositionStruct? variablePos = null; foreach (VariableReference reference in variableRefsLocal) { int line; int column; if (variablePos != null) { LineInfo.CalculateLineColumnForOffset(inputToken.Text, reference.Start - offset, variablePos.Value.Offset, variablePos.Value.Line, variablePos.Value.Column, out line, out column); } else { LineInfo.CalculateLineColumnForOffset(inputToken, reference.Start - offset, out line, out column); } variablePos = new PositionStruct( line: line, column: column, offset: reference.Start - offset, filename: inputToken.Filename); // If the variableResolver is not defined (=null), then we are a little // more robust and avoid throwing a NRE. It may just mean that the caller // did not bother implementing the IVariableResolver interface, presumably // because they were not interested in any variable replacement. string value = variableResolver?.GetVariable(variablePos.Value, reference.VariableName); if (value == null) { // Undefined variable if (ThrowOnUnresolvedVariable) { RaiseError(ErrorCode.VariableNotDefined, inputToken, string.Format(CultureInfo.CurrentCulture, SR.BatchParser_VariableNotDefined, reference.VariableName)); } continue; } reference.VariableValue = value; sb.Append(inputToken.Text, lastChar, reference.Start - offset - lastChar); sb.Append(value); lastChar = reference.Start - offset + reference.Length; } if (lastChar != inputString.Length) { sb.Append(inputString, lastChar, inputString.Length - lastChar); } if (variableRefs != null) { variableRefs.AddRange(variableRefsLocal); } return sb.ToString(); } private string UnquoteVariableValue(string s) { if (string.IsNullOrEmpty(s) == false) { if (s.Length >= 2 && s[0] == '"' && s[s.Length - 1] == s[0]) { // all " characters must be doubled for (int i = 1; i < s.Length - 1; i++) { if (s[i] == '"') { if (i + 1 >= s.Length - 1 || s[i + 1] != '"') { RaiseError(ErrorCode.TokenExpected); } // skip next character i++; } } return s.Substring(1, s.Length - 2).Replace("\"\"", "\""); } else if (s[0] == '"' || s[s.Length - 1] == '"') { RaiseError(ErrorCode.TokenExpected); } else { if (s.Length >= 2 && s[0] == '-' && s[1] == '-') { return null; } if (s.IndexOf('"') >= 0) { RaiseError(ErrorCode.TokenExpected); } } } return s; } public void SetRecognizeSqlCmdSyntax(bool recognizeSqlCmdSyntax) { lexer.RecognizeSqlCmdSyntax = recognizeSqlCmdSyntax; } public static TextReader GetBufferedTextReaderForFile(string filename) { Exception handledException = null; try { return new StringReader(File.ReadAllText(filename)); } catch (IOException exception) { handledException = exception; } catch (NotSupportedException exception) { handledException = exception; } catch (UnauthorizedAccessException exception) { handledException = exception; } catch (SecurityException exception) { handledException = exception; } if (null != handledException) { throw new Exception(handledException.Message, handledException); } Debug.Fail("This code should not be reached."); return null; } internal void SetBatchDelimiter(string batchSeparator) { // Not implemeneted as only GO is supported now } public static string TokenTypeToCommandString(LexerTokenType lexerTokenType) { switch (lexerTokenType) { case LexerTokenType.Connect: return "Connect"; case LexerTokenType.Ed: return "Ed"; case LexerTokenType.ErrorCommand: return "Error"; case LexerTokenType.Execute: return "!!"; case LexerTokenType.Exit: return "Exit"; case LexerTokenType.Help: return "Help"; case LexerTokenType.List: return "List"; case LexerTokenType.ListVar: return "ListVar"; case LexerTokenType.OnError: return "On Error"; case LexerTokenType.Out: return "Out"; case LexerTokenType.Perftrace: return "PerfTrace"; case LexerTokenType.Quit: return "Quit"; case LexerTokenType.Reset: return "Reset"; case LexerTokenType.Serverlist: return "ServerList"; case LexerTokenType.Xml: return "Xml"; default: Debug.Fail("Unknown batch parser command"); return lexerTokenType.ToString(); } } } }