C++ version of fax-pnh-filter

This code duplicates the functionality of the fax-pnh-filter bash script
without calling any external processes.

BUG=chromium:1082306
TEST=unit tests, tast run printer.PinPrint*

Change-Id: Iaacb8aca5fdbf788800bb38a0ce66c9f0e12bc28
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/lexmark-fax-pnh/+/2276150
Reviewed-by: Sean Kau <skau@chromium.org>
Reviewed-by: Nikita Podguzov <nikitapodguzov@chromium.org>
Tested-by: Brian Malcolm <bmalcolm@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
new file mode 100644
index 0000000..59061fb
--- /dev/null
+++ b/BUILD.gn
@@ -0,0 +1,46 @@
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//common-mk/pkg_config.gni")
+
+group("all") {
+  deps = [ ":fax-pnh-filter" ]
+  if (use.test) {
+    deps += [ ":token_replacer_testrunner" ]
+  }
+}
+
+pkg_config("target_defaults") {
+  pkg_deps = [
+    "libchrome-${libbase_ver}",
+  ]
+}
+
+executable("fax-pnh-filter") {
+  configs += [ ":target_defaults" ]
+  sources = [
+    "main.cc",
+    "token_replacer.cc",
+  ]
+}
+
+
+if (use.test) {
+  pkg_config("test_config") {
+    pkg_deps = [ "libchrome-test-${libbase_ver}" ]
+  }
+
+  executable("token_replacer_testrunner") {
+    configs += [
+      "//common-mk:test",
+      ":target_defaults",
+      ":test_config",
+    ]
+    sources = [
+      "token_replacer.cc",
+      "token_replacer_test.cc",
+    ]
+    deps = [ "//common-mk/testrunner" ]
+  }
+}
\ No newline at end of file
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..4a40808
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,2 @@
+bmalcolm@chromium.org
+skau@chromium.org
\ No newline at end of file
diff --git a/main.cc b/main.cc
new file mode 100644
index 0000000..1a17450
--- /dev/null
+++ b/main.cc
@@ -0,0 +1,43 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <fstream>
+#include <iostream>
+
+#include "token_replacer.h"
+
+// Read in the file, line-by-line, apply the transformation to each line, and
+// then write the line to out.
+void transform(const TokenReplacer& replacer,
+               std::istream& in,
+               std::ostream& out) {
+  std::string line;
+  while(std::getline(in, line)) {
+    out << replacer.TokenizeLine(line) << std::endl;
+  }
+}
+
+int main(int argc, char* argv[]) {
+  if (argc < 6 || argc > 7) {
+    std::cerr << "ERROR: " << argv[0]
+              << " job-id user title copies options [file]" << std::endl;
+    return 1;
+  }
+
+  std::string host = "localhost";
+  std::string user = argv[2];
+  std::string title = argv[3];
+  std::string copies = argv[4];
+
+  TokenReplacer replacer(host, user, title, copies);
+
+  if (argc < 7) {
+    transform(replacer, std::cin, std::cout);
+  } else {
+    std::ifstream fstream(argv[6]);
+    transform(replacer, fstream, std::cout);
+  }
+
+  return 0;
+}
diff --git a/token_replacer.cc b/token_replacer.cc
new file mode 100644
index 0000000..a8cb3bb
--- /dev/null
+++ b/token_replacer.cc
@@ -0,0 +1,40 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "token_replacer.h"
+
+TokenReplacer::TokenReplacer(const std::string& host,
+                             const std::string& user,
+                             const std::string& title,
+                             const std::string& copies)
+  : title_(EscapeTitle(title)),
+    re_host_("(STATIONID|PJL(.+?HOSTID)) = GETMYHOST"),
+    host_replacement_(R"($1 = ")" + host + R"(")"),
+    // GEYMYUSERNAME is not a typo (see fax-pnh-filter)
+    re_user_("(PJL (SET USERNAME|(.+?USERID))) = GEYMYUSERNAME"),
+    user_replacement_(R"($1 = ")" + user + R"(")"),
+    re_title_("(PJL SET JOBNAME) = GETMYJOBNAME"),
+    title_replacement_(R"($1 = ")" + title_ + R"(")"),
+    re_copies_("(PJL SET QTY) = GETMYCOPIES"),
+    copies_replacement_("$1 = " + copies ) {}
+
+const std::string TokenReplacer::TokenizeLine(std::string line) const {
+  line = std::regex_replace(line, re_host_, host_replacement_);
+  line = std::regex_replace(line, re_user_, user_replacement_);
+  line = std::regex_replace(line, re_title_, title_replacement_);
+  line = std::regex_replace(line, re_copies_, copies_replacement_);
+  return line;
+}
+
+std::string TokenReplacer::EscapeTitle(std::string title) {
+  // Replace each one or two backslash sequence with one backslash
+  std::regex slash(R"(\\{1,2})");
+  title = std::regex_replace(title, slash, R"(\)");
+
+  // Replace double-quotes with single-quotes
+  std::regex quote(R"(")");
+  title = std::regex_replace(title, quote, "'");
+
+  return title;
+}
\ No newline at end of file
diff --git a/token_replacer.h b/token_replacer.h
new file mode 100644
index 0000000..9c92e7d
--- /dev/null
+++ b/token_replacer.h
@@ -0,0 +1,54 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef __TOKEN_REPLACER_H__
+#define __TOKEN_REPLACER_H__
+
+#include <regex>
+#include <string>
+
+class TokenReplacer {
+ public:
+  TokenReplacer() = delete;
+  TokenReplacer(const std::string& host,
+                const std::string& user,
+                const std::string& title,
+                const std::string& copies);
+
+  // For testing purposes
+  const std::string& GetTitle() const { return title_; };
+
+  // Replaces tokens in the @PJL section of the print job as follows:
+  // 1) GETMYHOST is replaced with host
+  // 2) GEYMYUSERNAME is replaced with user
+  // 3) GETMYJOBNAME is replaced with an escaped title
+  // 4) GETMYCOPIES is replaced with copies
+  const std::string TokenizeLine(std::string line) const;
+
+ private:
+  // Escape a string according to the following rules (see myjob in
+  // fax-pnh-filter for the rules):
+  // 1) One or two backslashes in sequence are replaced with one backslash
+  // 2) double-quotes are replaced with single-quotes
+  //
+  // Note that fax-pnh-filter has additional rules for ampersands & slashes, but
+  // these are only to deal with the fact that the title string is being passed
+  // to sed on the command-line, so those characters are being escaped because
+  // they would be interpreted on the command-line as bash control characters.
+  // The outputted title from the filter would revert \& and \/ to & and /
+  // respectively. Therefore there is no need to escape those characters here.
+  static std::string EscapeTitle(std::string title);
+
+  const std::string title_;
+  const std::regex re_host_;
+  const std::string host_replacement_;
+  const std::regex re_user_;
+  const std::string user_replacement_;
+  const std::regex re_title_;
+  const std::string title_replacement_;
+  const std::regex re_copies_;
+  const std::string copies_replacement_;
+};
+
+#endif // __TOKEN_REPLACER_H__
\ No newline at end of file
diff --git a/token_replacer_test.cc b/token_replacer_test.cc
new file mode 100644
index 0000000..76dfc64
--- /dev/null
+++ b/token_replacer_test.cc
@@ -0,0 +1,114 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "token_replacer.h"
+
+#include <gtest/gtest.h>
+
+namespace {
+constexpr char kHostName[] = "hostname";
+constexpr char kUserName[] = "user@host.com";
+constexpr char kTitle[] = R"("This" & /that\\)";
+constexpr char kCopies[] = "42";
+} // namespace
+
+class TokenReplacerTest : public testing::Test {
+ protected:
+  TokenReplacerTest() : replacer_(kHostName, kUserName, kTitle, kCopies) {}
+
+  TokenReplacer replacer_;
+};
+
+TEST(Title, NoChange) {
+  TokenReplacer replacer("unused", "unused", "unchanged", "unused");
+
+  EXPECT_EQ(replacer.GetTitle(), "unchanged");
+}
+
+// One backslash => one backslash
+TEST(Title, Backslash) {
+  TokenReplacer replacer("unused", "unused", R"(this \ that)", "unused");
+
+  EXPECT_EQ(replacer.GetTitle(), R"(this \ that)");
+}
+
+// Two backslashes => one backslash
+TEST(Title, Backslash2) {
+  TokenReplacer replacer("unused", "unused", R"(this \\ that)", "unused");
+
+  EXPECT_EQ(replacer.GetTitle(), R"(this \ that)");
+}
+
+// Three backslashes => two backslashes
+TEST(Title, Backslash3) {
+  TokenReplacer replacer("unused", "unused", R"(this \\\ that)", "unused");
+
+  EXPECT_EQ(replacer.GetTitle(), R"(this \\ that)");
+}
+
+// Four backslashes => two backslashes
+TEST(Title, Backslash4) {
+  TokenReplacer replacer("unused", "unused", R"(this \\\\ that)", "unused");
+
+  EXPECT_EQ(replacer.GetTitle(), R"(this \\ that)");
+}
+
+// Five backslashes => three backslashes
+TEST(Title, Backslash5) {
+  TokenReplacer replacer("unused", "unused", R"(this \\\\\ that)", "unused");
+
+  EXPECT_EQ(replacer.GetTitle(), R"(this \\\ that)");
+}
+
+TEST(Title, Ampersand) {
+  TokenReplacer replacer("unused", "unused", "this & that", "unused");
+
+  EXPECT_EQ(replacer.GetTitle(), "this & that");
+}
+
+TEST(Title, DoubleQuotes) {
+  TokenReplacer replacer("unused", "unused", R"("this" and that)", "unused");
+
+  EXPECT_EQ(replacer.GetTitle(), "'this' and that");
+}
+
+TEST(Title, ForwardSlash) {
+  TokenReplacer replacer("unused", "unused", "this / that", "unused");
+
+  EXPECT_EQ(replacer.GetTitle(), "this / that");
+}
+
+TEST_F(TokenReplacerTest, UnchangedLine) {
+  EXPECT_EQ(replacer_.TokenizeLine("Unchanged"), "Unchanged");
+}
+
+TEST_F(TokenReplacerTest, ChangedHost) {
+  EXPECT_EQ(replacer_.TokenizeLine(
+      "@PJL SET STATIONID = GETMYHOST"),
+    R"(@PJL SET STATIONID = "hostname")");
+}
+
+TEST_F(TokenReplacerTest, ChangedUser) {
+  EXPECT_EQ(replacer_.TokenizeLine(
+      "@PJL SET USERNAME = GEYMYUSERNAME"),
+    R"(@PJL SET USERNAME = "user@host.com")");
+}
+
+TEST_F(TokenReplacerTest, ChangedTitle) {
+  EXPECT_EQ(replacer_.TokenizeLine(
+      "@PJL SET JOBNAME = GETMYJOBNAME"),
+    R"(@PJL SET JOBNAME = "'This' & /that\")");
+}
+
+TEST_F(TokenReplacerTest, ChangedCopies) {
+  EXPECT_EQ(replacer_.TokenizeLine(
+    "@PJL SET QTY = GETMYCOPIES"),
+    "@PJL SET QTY = 42");
+}
+
+TEST_F(TokenReplacerTest, ChangedJobInfo) {
+  EXPECT_EQ(replacer_.TokenizeLine(
+      "@PJL LJOBINFO USERID = GEYMYUSERNAME HOSTID = GETMYHOST"),
+    R"(@PJL LJOBINFO USERID = "user@host.com" HOSTID = "hostname")");
+}