Skip to content

Commit

Permalink
improve logic expression evaluation (microsoft#7508)
Browse files Browse the repository at this point in the history
* better logic expression evaluation

Improve the logic expression evaluation currently used when filtering
dependencies.

Biggest improvements:
+  Allow '|' operator
+  Support nested '()'
+  Allow whitespace
+  Useful error message for malformed expressions

Also changed names of types to RawParagraph when that is what the original author was using.
  • Loading branch information
Rastaban authored Aug 3, 2019
1 parent 4d551ff commit 22e0b9f
Show file tree
Hide file tree
Showing 18 changed files with 345 additions and 59 deletions.
2 changes: 1 addition & 1 deletion ports/pango/CONTROL
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ Source: pango
Version: 1.40.11-4
Homepage: https://ftp.gnome.org/pub/GNOME/sources/pango/
Description: Text and font handling library.
Build-Depends: glib, gettext, cairo, fontconfig, freetype, harfbuzz[glib] (!windows-static)
Build-Depends: glib, gettext, cairo, fontconfig, freetype, harfbuzz[glib] (!(windows&static))
3 changes: 2 additions & 1 deletion toolsrc/include/vcpkg/binaryparagraph.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <vcpkg/packagespec.h>
#include <vcpkg/sourceparagraph.h>
#include <vcpkg/parse.h>

#include <unordered_map>

Expand All @@ -13,7 +14,7 @@ namespace vcpkg
struct BinaryParagraph
{
BinaryParagraph();
explicit BinaryParagraph(std::unordered_map<std::string, std::string> fields);
explicit BinaryParagraph(Parse::RawParagraph fields);
BinaryParagraph(const SourceParagraph& spgh, const Triplet& triplet, const std::string& abi_tag);
BinaryParagraph(const SourceParagraph& spgh, const FeatureParagraph& fpgh, const Triplet& triplet);

Expand Down
10 changes: 10 additions & 0 deletions toolsrc/include/vcpkg/logicexpression.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

#include <string>

namespace vcpkg
{
// Evaluate simple vcpkg logic expressions. An identifier in the expression is considered 'true'
// if it is a substring of the evaluation_context (typically the name of the triplet)
bool evaluate_expression(const std::string& expression, const std::string& evaluation_context);
}
1 change: 0 additions & 1 deletion toolsrc/include/vcpkg/paragraphs.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ namespace vcpkg::Paragraphs

Expected<RawParagraph> get_single_paragraph(const Files::Filesystem& fs, const fs::path& control_path);
Expected<std::vector<RawParagraph>> get_paragraphs(const Files::Filesystem& fs, const fs::path& control_path);
Expected<RawParagraph> parse_single_paragraph(const std::string& str);
Expected<std::vector<RawParagraph>> parse_paragraphs(const std::string& str);

Parse::ParseExpected<SourceControlFile> try_load_port(const Files::Filesystem& fs, const fs::path& control_path);
Expand Down
2 changes: 1 addition & 1 deletion toolsrc/include/vcpkg/statusparagraph.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ namespace vcpkg
struct StatusParagraph
{
StatusParagraph() noexcept;
explicit StatusParagraph(std::unordered_map<std::string, std::string>&& fields);
explicit StatusParagraph(Parse::RawParagraph&& fields);

bool is_installed() const { return want == Want::INSTALL && state == InstallState::INSTALLED; }

Expand Down
2 changes: 1 addition & 1 deletion toolsrc/src/vcpkg/binaryparagraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace vcpkg

BinaryParagraph::BinaryParagraph() = default;

BinaryParagraph::BinaryParagraph(std::unordered_map<std::string, std::string> fields)
BinaryParagraph::BinaryParagraph(Parse::RawParagraph fields)
{
using namespace vcpkg::Parse;

Expand Down
4 changes: 2 additions & 2 deletions toolsrc/src/vcpkg/build.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ namespace vcpkg::Build
Commands::Version::version());
}

static BuildInfo inner_create_buildinfo(std::unordered_map<std::string, std::string> pgh)
static BuildInfo inner_create_buildinfo(Parse::RawParagraph pgh)
{
Parse::ParagraphParser parser(std::move(pgh));

Expand Down Expand Up @@ -995,7 +995,7 @@ namespace vcpkg::Build

BuildInfo read_build_info(const Files::Filesystem& fs, const fs::path& filepath)
{
const Expected<std::unordered_map<std::string, std::string>> pghs =
const Expected<Parse::RawParagraph> pghs =
Paragraphs::get_single_paragraph(fs, filepath);
Checks::check_exit(VCPKG_LINE_INFO, pghs.get() != nullptr, "Invalid BUILD_INFO file for package");
return inner_create_buildinfo(*pghs.get());
Expand Down
2 changes: 1 addition & 1 deletion toolsrc/src/vcpkg/commands.cache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace vcpkg::Commands::Cache
std::vector<BinaryParagraph> output;
for (auto&& path : paths.get_filesystem().get_files_non_recursive(paths.packages))
{
const Expected<std::unordered_map<std::string, std::string>> pghs =
const Expected<Parse::RawParagraph> pghs =
Paragraphs::get_single_paragraph(paths.get_filesystem(), path / "CONTROL");
if (const auto p = pghs.get())
{
Expand Down
2 changes: 1 addition & 1 deletion toolsrc/src/vcpkg/commands.import.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ namespace vcpkg::Commands::Import
const fs::path include_directory(args.command_arguments[1]);
const fs::path project_directory(args.command_arguments[2]);

const Expected<std::unordered_map<std::string, std::string>> pghs =
const Expected<Parse::RawParagraph> pghs =
Paragraphs::get_single_paragraph(paths.get_filesystem(), control_file_path);
Checks::check_exit(VCPKG_LINE_INFO,
pghs.get() != nullptr,
Expand Down
285 changes: 285 additions & 0 deletions toolsrc/src/vcpkg/logicexpression.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@

#include "pch.h"

#include <vcpkg/logicexpression.h>
#include <vcpkg/base/checks.h>
#include <vcpkg/base/system.print.h>

#include <string>
#include <vector>


namespace vcpkg
{
struct ParseError
{
ParseError(int column, std::string line, std::string message)
:column(column), line(line), message(message)
{}

const int column;
const std::string line;
const std::string message;

void print_error() const
{
System::print2(System::Color::error,
"Error: ", message, "\n"
" on expression: \"", line, "\"\n",
" ", std::string(column, ' '), "^\n");
Checks::exit_fail(VCPKG_LINE_INFO);
}
};

// logic expression supports the following :
// primary-expression:
// ( logic-expression )
// identifier
// identifier:
// alpha-numeric string of characters
// logic-expression: <- this is the entry point
// not-expression
// not-expression | logic-expression
// not-expression & logic-expression
// not-expression:
// ! primary-expression
// primary-expression
//
// | and & have equal precidence and cannot be used together at the same nesting level
// for example a|b&c is not allowd but (a|b)&c and a|(b&c) are allowed.
class ExpressionParser
{
public:
ExpressionParser(const std::string& str, const std::string& evaluation_context)
: raw_text(str), evaluation_context(evaluation_context)
{
go_to_begin();

final_result = logic_expression();

if (current_iter != raw_text.end())
{
add_error("Invalid logic expression");
}

if (err)
{
err->print_error();
final_result = false;
}
}

bool get_result() const
{
return final_result;
}

bool has_error() const
{
return err == nullptr;
}

private:

bool final_result;

std::string::const_iterator current_iter;
const std::string& raw_text;
char current_char;

const std::string& evaluation_context;

std::unique_ptr<ParseError> err;

void add_error(std::string message, int column = -1)
{
// avoid castcading errors by only saving the first
if (!err)
{
if (column < 0)
{
column = current_column();
}
err = std::make_unique<ParseError>(column, raw_text, message);
}

// Avoid error loops by skipping to the end
skip_to_end();
}

int current_column() const
{
return static_cast<int>(current_iter - raw_text.begin());
}

void go_to_begin()
{
current_iter = raw_text.begin();
current_char = (current_iter != raw_text.end() ? *current_iter : current_char);

if (current_char == ' ' || current_char == '\t')
{
next_skip_whitespace();
}
}
void skip_to_end()
{
current_iter = raw_text.end();
current_char = '\0';
}
char current() const
{
return current_char;
}
char next()
{
if (current_char != '\0')
{
current_iter++;
current_char = (current_iter != raw_text.end() ? *current_iter : '\0');
}
return current();
}
void skip_whitespace()
{
while (current_char == ' ' || current_char == '\t')
{
current_char = next();
}
}
char next_skip_whitespace()
{
next();
skip_whitespace();
return current_char;
}

static bool is_alphanum(char ch)
{
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch == '-');
}

bool evaluate_identifier(const std::string name) const
{
return evaluation_context.find(name) != std::string::npos;
}

// identifier:
// alpha-numeric string of characters
bool identifier_expression()
{
auto curr = current();
std::string name;

for (curr = current(); is_alphanum(curr); curr = next())
{
name += curr;
}

if (name.empty())
{
add_error("Invalid logic expression, unexpected character");
return false;
}

bool result = evaluate_identifier(name);
skip_whitespace();
return result;
}

// not-expression:
// ! primary-expression
// primary-expression
bool not_expression()
{
if (current() == '!')
{
next_skip_whitespace();
return !primary_expression();
}

return primary_expression();
}


template <char oper, char other, bool operation(bool, bool)>
bool logic_expression_helper(bool seed)
{
do
{
// Support chains of the operator to avoid breaking backwards compatability
while (next() == oper) {};
seed = operation(not_expression(), seed);

} while (current() == oper);

if (current() == other)
{
add_error("Mixing & and | is not allowed, Use () to specify order of operations.");
}

skip_whitespace();
return seed;
}
static bool and_helper(bool left, bool right)
{
return left && right;
}
static bool or_helper(bool left, bool right)
{
return left || right;
}

// logic-expression: <- entry point
// not-expression
// not-expression | logic-expression
// not-expression & logic-expression
bool logic_expression()
{
auto result = not_expression();

switch (current())
{
case '|':
{
return logic_expression_helper< '|', '&', or_helper > (result);
}
case '&':
{
return logic_expression_helper< '&', '|', and_helper > (result);
}
default:
return result;
}
}

// primary-expression:
// ( logic-expression )
// identifier
bool primary_expression()
{
if (current() == '(')
{
next_skip_whitespace();
bool result = logic_expression();
if (current() != ')')
{
add_error("Error: missing closing )");
return result;
}
next_skip_whitespace();
return result;
}

return identifier_expression();
}
};


bool evaluate_expression(const std::string& expression, const std::string& evaluation_context)
{
ExpressionParser parser(expression, evaluation_context);

return parser.get_result();
}
}
Loading

0 comments on commit 22e0b9f

Please sign in to comment.