From f1f711963b8e5f0b2b5a9d80a522cbd616a153a9 Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Sat, 4 Jun 2022 21:15:40 -0400 Subject: Add and test QPDFLogger class --- include/qpdf/QPDFLogger.hh | 158 +++++++++++++++++++++++++ libqpdf/CMakeLists.txt | 1 + libqpdf/QPDFLogger.cc | 244 +++++++++++++++++++++++++++++++++++++++ libtests/CMakeLists.txt | 1 + libtests/logger.cc | 109 +++++++++++++++++ libtests/qtest/logger.test | 33 ++++++ libtests/qtest/logger/exp-stderr | 7 ++ libtests/qtest/logger/exp-stdout | 6 + qpdf/sizes.cc | 2 + 9 files changed, 561 insertions(+) create mode 100644 include/qpdf/QPDFLogger.hh create mode 100644 libqpdf/QPDFLogger.cc create mode 100644 libtests/logger.cc create mode 100644 libtests/qtest/logger.test create mode 100644 libtests/qtest/logger/exp-stderr create mode 100644 libtests/qtest/logger/exp-stdout diff --git a/include/qpdf/QPDFLogger.hh b/include/qpdf/QPDFLogger.hh new file mode 100644 index 00000000..340706c9 --- /dev/null +++ b/include/qpdf/QPDFLogger.hh @@ -0,0 +1,158 @@ +// Copyright (c) 2005-2022 Jay Berkenbilt +// +// This file is part of qpdf. +// +// 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. +// +// Versions of qpdf prior to version 7 were released under the terms +// of version 2.0 of the Artistic License. At your option, you may +// continue to consider qpdf to be licensed under those terms. Please +// see the manual for additional information. + +#ifndef QPDFLOGGER_HH +#define QPDFLOGGER_HH + +#include +#include +#include +#include + +class QPDFLogger +{ + public: + QPDF_DLL + QPDFLogger(); + + QPDF_DLL + static std::shared_ptr defaultLogger(); + + // Defaults: + // + // info -- if save is standard output, standard error, else standard output + // warn -- whatever error points to + // error -- standard error + // save -- undefined unless set + // + // On deletion, finish() is called for the standard output and + // standard error pipelines, which flushes output. If you supply + // any custom pipelines, you must call finish() on them yourself. + // Note that calling finish is not needed for string, stdio, or + // ostream pipelines. + // + // NOTES ABOUT THE SAVE PIPELINE + // + // You should never set the save pipeline to the same destination + // as something else. Doing so will corrupt your save output. If + // you want to save to standard output, use the method + // saveToStandardOutput(). In addition to setting the save + // pipeline, that does the following extra things: + // + // * If standard output has been used, a logic error is thrown + // * If info is set to standard output at the time of the set save + // call, it is switched to standard error. + // + // This is not a guarantee. You can still mess this up in ways + // that are not checked. Here are a few examples: + // + // * Don't set any pipeline to standard output *after* passing it + // to setSave() + // * Don't use a separate mechanism to write stdout/stderr other + // than QPDFLogger::standardOutput() + // * Don't set anything to the same custom pipeline that save is + // set to. + // + // Just be sure that if you change pipelines around, you should + // avoid having the save pipeline also be used for any other + // purpose. The special case for saving to standard output allows + // you to call saveToStandardOutput() early without having to + // worry about the info pipeline. + + QPDF_DLL + void info(char const*); + QPDF_DLL + void info(std::string const&); + QPDF_DLL + std::shared_ptr getInfo(bool null_okay = false); + + QPDF_DLL + void warn(char const*); + QPDF_DLL + void warn(std::string const&); + QPDF_DLL + std::shared_ptr getWarn(bool null_okay = false); + + QPDF_DLL + void error(char const*); + QPDF_DLL + void error(std::string const&); + QPDF_DLL + std::shared_ptr getError(bool null_okay = false); + + QPDF_DLL + std::shared_ptr getSave(bool null_okay = false); + + QPDF_DLL + std::shared_ptr standardOutput(); + QPDF_DLL + std::shared_ptr standardError(); + QPDF_DLL + std::shared_ptr discard(); + + // Passing a null pointer resets to default + QPDF_DLL + void setInfo(std::shared_ptr); + QPDF_DLL + void setWarn(std::shared_ptr); + QPDF_DLL + void setError(std::shared_ptr); + // See notes above about the save pipeline + QPDF_DLL + void setSave(std::shared_ptr); + QPDF_DLL + void saveToStandardOutput(); + + // Shortcut for logic to reset output to new output/error streams. + // out_stream is used for info, err_stream is used for error, and + // warning is cleared so that it follows error. + QPDF_DLL + void setOutputStreams(std::ostream* out_stream, std::ostream* err_stream); + + private: + std::shared_ptr + throwIfNull(std::shared_ptr, bool null_okay); + + class Members + { + friend class QPDFLogger; + + public: + QPDF_DLL + ~Members(); + + private: + Members(); + Members(Members const&) = delete; + + std::shared_ptr p_discard; + std::shared_ptr p_real_stdout; + std::shared_ptr p_stdout; + std::shared_ptr p_stderr; + std::shared_ptr p_info; + std::shared_ptr p_warn; + std::shared_ptr p_error; + std::shared_ptr p_save; + }; + std::shared_ptr m; +}; + +#endif // QPDFLOGGER_HH diff --git a/libqpdf/CMakeLists.txt b/libqpdf/CMakeLists.txt index 1777bb08..a6b036a7 100644 --- a/libqpdf/CMakeLists.txt +++ b/libqpdf/CMakeLists.txt @@ -67,6 +67,7 @@ set(libqpdf_SOURCES QPDFJob_argv.cc QPDFJob_config.cc QPDFJob_json.cc + QPDFLogger.cc QPDFMatrix.cc QPDFNameTreeObjectHelper.cc QPDFNumberTreeObjectHelper.cc diff --git a/libqpdf/QPDFLogger.cc b/libqpdf/QPDFLogger.cc new file mode 100644 index 00000000..859d6dcd --- /dev/null +++ b/libqpdf/QPDFLogger.cc @@ -0,0 +1,244 @@ +#include + +#include +#include +#include +#include + +namespace +{ + class Pl_Track: public Pipeline + { + public: + Pl_Track(char const* identifier, Pipeline* next) : + Pipeline(identifier, next), + used(false) + { + } + + virtual void + write(unsigned char const* data, size_t len) override + { + this->used = true; + getNext()->write(data, len); + } + + virtual void + finish() override + { + getNext()->finish(); + } + + bool + getUsed() const + { + return used; + } + + private: + bool used; + }; +}; // namespace + +QPDFLogger::Members::Members() : + p_discard(new Pl_Discard()), + p_real_stdout(new Pl_OStream("standard output", std::cout)), + p_stdout(new Pl_Track("track stdout", p_real_stdout.get())), + p_stderr(new Pl_OStream("standard error", std::cerr)), + p_info(p_stdout), + p_warn(nullptr), + p_error(p_stderr), + p_save(nullptr) +{ +} + +QPDFLogger::Members::~Members() +{ + p_stdout->finish(); + p_stderr->finish(); +} + +QPDFLogger::QPDFLogger() : + m(new Members()) +{ +} + +std::shared_ptr +QPDFLogger::defaultLogger() +{ + static auto l = std::make_shared(); + return l; +} + +void +QPDFLogger::info(char const* s) +{ + getInfo(false)->writeCStr(s); +} + +void +QPDFLogger::info(std::string const& s) +{ + getInfo(false)->writeString(s); +} + +std::shared_ptr +QPDFLogger::getInfo(bool null_okay) +{ + return throwIfNull(this->m->p_info, null_okay); +} + +void +QPDFLogger::warn(char const* s) +{ + getWarn(false)->writeCStr(s); +} + +void +QPDFLogger::warn(std::string const& s) +{ + getWarn(false)->writeString(s); +} + +std::shared_ptr +QPDFLogger::getWarn(bool null_okay) +{ + if (this->m->p_warn) { + return this->m->p_warn; + } + return getError(null_okay); +} + +void +QPDFLogger::error(char const* s) +{ + getError(false)->writeCStr(s); +} + +void +QPDFLogger::error(std::string const& s) +{ + getError(false)->writeString(s); +} + +std::shared_ptr +QPDFLogger::getError(bool null_okay) +{ + return throwIfNull(this->m->p_error, null_okay); +} + +std::shared_ptr +QPDFLogger::getSave(bool null_okay) +{ + return throwIfNull(this->m->p_save, null_okay); +} + +std::shared_ptr +QPDFLogger::standardOutput() +{ + return this->m->p_stdout; +} + +std::shared_ptr +QPDFLogger::standardError() +{ + return this->m->p_stderr; +} + +std::shared_ptr +QPDFLogger::discard() +{ + return this->m->p_discard; +} + +void +QPDFLogger::setInfo(std::shared_ptr p) +{ + if (p == nullptr) { + if (this->m->p_save == this->m->p_stdout) { + p = this->m->p_stderr; + } else { + p = this->m->p_stdout; + } + } + this->m->p_info = p; +} + +void +QPDFLogger::setWarn(std::shared_ptr p) +{ + this->m->p_warn = p; +} + +void +QPDFLogger::setError(std::shared_ptr p) +{ + if (p == nullptr) { + p = this->m->p_stderr; + } + this->m->p_error = p; +} + +void +QPDFLogger::setSave(std::shared_ptr p) +{ + if (p == this->m->p_stdout) { + auto pt = dynamic_cast(p.get()); + if (pt->getUsed()) { + throw std::logic_error( + "QPDFLogger: called setSave on standard output after standard" + " output has already been used"); + } + if (this->m->p_info == this->m->p_stdout) { + this->m->p_info = this->m->p_stderr; + } + } + this->m->p_save = p; +} + +void +QPDFLogger::saveToStandardOutput() +{ + setSave(standardOutput()); +} + +void +QPDFLogger::setOutputStreams(std::ostream* out_stream, std::ostream* err_stream) +{ + if (out_stream == &std::cout) { + out_stream = nullptr; + } + if (err_stream == &std::cerr) { + err_stream = nullptr; + } + std::shared_ptr new_out; + std::shared_ptr new_err; + + if (out_stream == nullptr) { + if (this->m->p_save == this->m->p_stdout) { + new_out = this->m->p_stderr; + } else { + new_out = this->m->p_stdout; + } + } else { + new_out = std::make_shared("output", *out_stream); + } + if (err_stream == nullptr) { + new_err = this->m->p_stderr; + } else { + new_err = std::make_shared("error output", *err_stream); + } + this->m->p_info = new_out; + this->m->p_warn = nullptr; + this->m->p_error = new_err; +} + +std::shared_ptr +QPDFLogger::throwIfNull(std::shared_ptr p, bool null_okay) +{ + if (!(null_okay || p)) { + throw std::logic_error( + "QPDFLogger: requested a null pipeline without null_okay == true"); + } + return p; +} diff --git a/libtests/CMakeLists.txt b/libtests/CMakeLists.txt index 9eb9a490..196cd3eb 100644 --- a/libtests/CMakeLists.txt +++ b/libtests/CMakeLists.txt @@ -16,6 +16,7 @@ set(TEST_PROGRAMS json json_handler json_parse + logger lzw main_from_wmain matrix diff --git a/libtests/logger.cc b/libtests/logger.cc new file mode 100644 index 00000000..f10319f6 --- /dev/null +++ b/libtests/logger.cc @@ -0,0 +1,109 @@ +#include + +#include +#include +#include + +static void +test1() +{ + // Standard behavior + + auto logger = QPDFLogger::defaultLogger(); + + logger->info("info to stdout\n"); + logger->warn("warn to stderr\n"); + logger->error("error to stderr\n"); + assert(logger->getSave(true) == nullptr); + try { + logger->getSave(); + assert(false); + } catch (std::logic_error& e) { + *(logger->getInfo()) << "getSave exception: " << e.what() << "\n"; + } + try { + logger->saveToStandardOutput(); + assert(false); + } catch (std::logic_error& e) { + *(logger->getInfo()) + << "saveToStandardOutput exception: " << e.what() << "\n"; + } + logger->setWarn(logger->discard()); + logger->warn("warning not seen\n"); + logger->setWarn(nullptr); + logger->warn("restored warning to stderr\n"); +} + +static void +test2() +{ + // First call saveToStandardOutput. Then use info, which then to + // go stderr. + QPDFLogger l; + l.saveToStandardOutput(); + l.info(std::string("info to stderr\n")); + *(l.getSave()) << "save to stdout\n"; + l.setInfo(nullptr); + l.info("info still to stderr\n"); + l.setSave(nullptr); + l.setInfo(nullptr); + l.info("info back to stdout\n"); +} + +static void +test3() +{ + // Error/warning + QPDFLogger l; + + // Warning follows error when error is set explicitly. + std::string errors; + auto pl_error = std::make_shared("errors", nullptr, errors); + l.setError(pl_error); + l.warn("warn follows error\n"); + assert(errors == "warn follows error\n"); + l.error("error too\n"); + assert(errors == "warn follows error\nerror too\n"); + + // Set warnings -- now they're separate + std::string warnings; + auto pl_warn = std::make_shared("warnings", nullptr, warnings); + l.setWarn(pl_warn); + l.warn(std::string("warning now separate\n")); + l.error(std::string("new error\n")); + assert(warnings == "warning now separate\n"); + assert(errors == "warn follows error\nerror too\nnew error\n"); + std::string errors2; + pl_error = std::make_shared("errors", nullptr, errors2); + l.setError(pl_error); + l.warn("new warning\n"); + l.error("another new error\n"); + assert(warnings == "warning now separate\nnew warning\n"); + assert(errors == "warn follows error\nerror too\nnew error\n"); + assert(errors2 == "another new error\n"); + + // Restore warnings to default -- follows error again + l.setWarn(nullptr); + l.warn("warning 3\n"); + l.error("error 3\n"); + assert(warnings == "warning now separate\nnew warning\n"); + assert(errors == "warn follows error\nerror too\nnew error\n"); + assert(errors2 == "another new error\nwarning 3\nerror 3\n"); + + // Restore everything to default + l.setInfo(nullptr); + l.setWarn(nullptr); + l.setError(nullptr); + l.info("after reset, info to stdout\n"); + l.warn("after reset, warn to stderr\n"); + l.error("after reset, error to stderr\n"); +} + +int +main() +{ + test1(); + test2(); + test3(); + return 0; +} diff --git a/libtests/qtest/logger.test b/libtests/qtest/logger.test new file mode 100644 index 00000000..86767eff --- /dev/null +++ b/libtests/qtest/logger.test @@ -0,0 +1,33 @@ +#!/usr/bin/env perl +require 5.008; +use warnings; +use strict; + +chdir("logger") or die "chdir testdir failed: $!\n"; + +require TestDriver; + +my $td = new TestDriver('logger'); + +cleanup(); + +$td->runtest("logger", + {$td->COMMAND => "logger >stdout 2>stderr"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check stdout", + {$td->FILE => "stdout"}, + {$td->FILE => "exp-stdout"}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check stderr", + {$td->FILE => "stderr"}, + {$td->FILE => "exp-stderr"}, + $td->NORMALIZE_NEWLINES); + +cleanup(); +$td->report(3); + +sub cleanup +{ + unlink "stdout", "stderr"; +} diff --git a/libtests/qtest/logger/exp-stderr b/libtests/qtest/logger/exp-stderr new file mode 100644 index 00000000..5d42c91a --- /dev/null +++ b/libtests/qtest/logger/exp-stderr @@ -0,0 +1,7 @@ +warn to stderr +error to stderr +restored warning to stderr +info to stderr +info still to stderr +after reset, warn to stderr +after reset, error to stderr diff --git a/libtests/qtest/logger/exp-stdout b/libtests/qtest/logger/exp-stdout new file mode 100644 index 00000000..a51d446a --- /dev/null +++ b/libtests/qtest/logger/exp-stdout @@ -0,0 +1,6 @@ +info to stdout +getSave exception: QPDFLogger: requested a null pipeline without null_okay == true +saveToStandardOutput exception: QPDFLogger: called setSave on standard output after standard output has already been used +save to stdout +info back to stdout +after reset, info to stdout diff --git a/qpdf/sizes.cc b/qpdf/sizes.cc index ac4bae6e..62eef6a8 100644 --- a/qpdf/sizes.cc +++ b/qpdf/sizes.cc @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,7 @@ main() print_size(QPDFJob::EncConfig); print_size(QPDFJob::PagesConfig); print_size(QPDFJob::UOConfig); + print_size(QPDFLogger); print_size(QPDFMatrix); print_size(QPDFNameTreeObjectHelper); print_size(QPDFNameTreeObjectHelper::iterator); -- cgit v1.2.3-54-g00ecf