summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format315
-rw-r--r--.clang-tidy2
-rw-r--r--.dir-locals.el12
-rw-r--r--.gitignore2
-rw-r--r--meson.build185
-rw-r--r--src/args.cc389
-rw-r--r--src/args.hh64
-rw-r--r--src/buffer.cc213
-rw-r--r--src/buffer.hh31
-rw-r--r--src/check.hh39
-rw-r--r--src/config.h.in1
-rw-r--r--src/io.cc232
-rw-r--r--src/io.hh51
-rw-r--r--src/line.cc127
-rw-r--r--src/line.hh37
-rw-r--r--src/main.cc31
-rw-r--r--src/str.cc53
-rw-r--r--src/str.hh21
-rw-r--r--src/unique_fd.cc9
-rw-r--r--src/unique_fd.hh36
-rw-r--r--test/args.cc412
-rw-r--r--test/buffer.cc270
-rw-r--r--test/io.cc203
-rw-r--r--test/io_test_helper.cc82
-rw-r--r--test/io_test_helper.hh18
-rw-r--r--test/line.cc182
-rw-r--r--test/str.cc67
27 files changed, 3084 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..aa4a4dc
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,315 @@
+---
+Language: Cpp
+AccessModifierOffset: -1
+AlignAfterOpenBracket: Align
+AlignArrayOfStructures: None
+AlignConsecutiveAssignments:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: true
+AlignConsecutiveBitFields:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveDeclarations:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: true
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveMacros:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveShortCaseStatements:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCaseArrows: false
+ AlignCaseColons: false
+AlignConsecutiveTableGenBreakingDAGArgColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveTableGenCondOperatorColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveTableGenDefinitionColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignEscapedNewlines: Left
+AlignOperands: Align
+AlignTrailingComments:
+ Kind: Always
+ OverEmptyLines: 0
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowBreakBeforeNoexceptSpecifier: Never
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseExpressionOnASingleLine: true
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortCompoundRequirementOnASingleLine: true
+AllowShortEnumsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: false
+AllowShortNamespacesOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakBeforeMultilineStrings: true
+AttributeMacros:
+ - __capability
+BinPackArguments: true
+BinPackParameters: BinPack
+BitFieldColonSpacing: Both
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: false
+ AfterControlStatement: Never
+ AfterEnum: false
+ AfterExternBlock: false
+ AfterFunction: false
+ AfterNamespace: false
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BracedInitializerIndentWidth: 2
+BreakAdjacentStringLiterals: true
+BreakAfterAttributes: Leave
+BreakAfterJavaFieldAnnotations: false
+BreakAfterReturnType: None
+BreakArrays: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: Always
+BreakBeforeBraces: Attach
+BreakBeforeInlineASMColon: OnlyMultiline
+BreakBeforeTernaryOperators: true
+BreakBinaryOperations: Never
+BreakConstructorInitializers: BeforeColon
+BreakFunctionDefinitionParameters: false
+BreakInheritanceList: BeforeColon
+BreakStringLiterals: true
+BreakTemplateDeclarations: Yes
+ColumnLimit: 80
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: true
+DisableFormat: false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+ - foreach
+ - Q_FOREACH
+ - BOOST_FOREACH
+IfMacros:
+ - KJ_IF_MAYBE
+IncludeBlocks: Regroup
+IncludeCategories:
+ - Regex: '^<.*'
+ Priority: 2
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '.*'
+ Priority: 1
+ SortPriority: 0
+ CaseSensitive: false
+IncludeIsMainRegex: '([-_](lzma|z|test|unittest))?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseBlocks: false
+IndentCaseLabels: true
+IndentExportBlock: true
+IndentExternBlock: AfterExternBlock
+IndentGotoLabels: true
+IndentPPDirectives: AfterHash
+IndentRequiresClause: true
+IndentWidth: 2
+IndentWrappedFunctionNames: false
+InsertBraces: false
+InsertNewlineAtEOF: false
+InsertTrailingCommas: None
+IntegerLiteralSeparator:
+ Binary: 0
+ BinaryMinDigits: 0
+ Decimal: 0
+ DecimalMinDigits: 0
+ Hex: 0
+ HexMinDigits: 0
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLines:
+ AtEndOfFile: false
+ AtStartOfBlock: false
+ AtStartOfFile: true
+KeepFormFeed: false
+LambdaBodyIndentation: Signature
+LineEnding: DeriveLF
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MainIncludeChar: Quote
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Never
+ObjCBlockIndentWidth: 2
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PackConstructorInitializers: NextLine
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 1
+PenaltyBreakBeforeMemberAccess: 150
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakScopeResolution: 500
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyIndentedWhitespace: 0
+PenaltyReturnTypeOnItsOwnLine: 200
+PointerAlignment: Left
+PPIndentWidth: 1
+QualifierAlignment: Leave
+RawStringFormats:
+ - Language: Cpp
+ Delimiters:
+ - cc
+ - CC
+ - cpp
+ - Cpp
+ - CPP
+ - 'c++'
+ - 'C++'
+ CanonicalDelimiter: ''
+ BasedOnStyle: google
+ - Language: TextProto
+ Delimiters:
+ - pb
+ - PB
+ - proto
+ - PROTO
+ EnclosingFunctions:
+ - EqualsProto
+ - EquivToProto
+ - PARSE_PARTIAL_TEXT_PROTO
+ - PARSE_TEST_PROTO
+ - PARSE_TEXT_PROTO
+ - ParseTextOrDie
+ - ParseTextProtoOrDie
+ - ParseTestProto
+ - ParsePartialTestProto
+ CanonicalDelimiter: pb
+ BasedOnStyle: google
+ReferenceAlignment: Pointer
+ReflowComments: IndentOnly
+RemoveBracesLLVM: false
+RemoveEmptyLinesInUnwrappedLines: false
+RemoveParentheses: Leave
+RemoveSemicolon: false
+RequiresClausePosition: OwnLine
+RequiresExpressionIndentation: OuterScope
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SkipMacroDefinitionBody: false
+SortIncludes: CaseSensitive
+SortJavaStaticImport: Before
+SortUsingDeclarations: LexicographicNumeric
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeJsonColon: false
+SpaceBeforeParens: ControlStatements
+SpaceBeforeParensOptions:
+ AfterControlStatements: true
+ AfterForeachMacros: true
+ AfterFunctionDefinitionName: false
+ AfterFunctionDeclarationName: false
+ AfterIfMacros: true
+ AfterOverloadedOperator: false
+ AfterPlacementOperator: true
+ AfterRequiresInClause: false
+ AfterRequiresInExpression: false
+ BeforeNonEmptyParentheses: false
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeSquareBrackets: false
+SpaceInEmptyBlock: false
+SpacesBeforeTrailingComments: 2
+SpacesInAngles: Never
+SpacesInContainerLiterals: true
+SpacesInLineCommentPrefix:
+ Minimum: 1
+ Maximum: -1
+SpacesInParens: Never
+SpacesInParensOptions:
+ ExceptDoubleParentheses: false
+ InCStyleCasts: false
+ InConditionalStatements: false
+ InEmptyParentheses: false
+ Other: false
+SpacesInSquareBrackets: false
+Standard: Auto
+StatementAttributeLikeMacros:
+ - Q_EMIT
+StatementMacros:
+ - Q_UNUSED
+ - QT_REQUIRE_VERSION
+TableGenBreakInsideDAGArg: DontBreak
+TabWidth: 8
+UseTab: Never
+VerilogBreakBetweenInstancePorts: true
+WhitespaceSensitiveMacros:
+ - BOOST_PP_STRINGIZE
+ - CF_SWIFT_NAME
+ - NS_SWIFT_NAME
+ - PP_STRINGIZE
+ - STRINGIZE
+WrapNamespaceBodyWithEmptyLines: Leave
+...
+
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 0000000..bd4deb9
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,2 @@
+---
+Checks: 'bugprone-*,misc-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-parameters,-bugprone-unchecked-optional-access,-misc-non-private-member-variables-in-classes,-misc-const-correctness,-misc-no-recursion,-modernize-avoid-c-arrays,-modernize-use-trailing-return-type,-readability-magic-numbers,-readability-identifier-length,-readability-braces-around-statements,-readability-function-cognitive-complexity,-readability-redundant-inline-specifier,-readability-implicit-bool-conversion,-readability-qualified-auto'
diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644
index 0000000..224840b
--- /dev/null
+++ b/.dir-locals.el
@@ -0,0 +1,12 @@
+;;; Directory Local Variables -*- no-byte-compile: t; -*-
+;;; For more information see (info "(emacs) Directory Variables")
+
+((c++-mode . ((eval
+ .
+ (let ((project-path
+ (locate-dominating-file default-directory ".dir-locals.el")))
+ (setq-local flycheck-clangcheck-build-path
+ (concat project-path "build"))
+ (setq-local flycheck-clang-language-standard "c++23")
+ (setq-local flycheck-clang-definitions '("HAVE_CONFIG_H"))
+ (setq-local flycheck-clang-include-path '("../src" "../build")))))))
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..db9f3cd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/build/
+/compile_commands.json
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..f98276c
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,185 @@
+project(
+ 'jkc',
+ 'cpp',
+ version : '0.1',
+ meson_version : '>= 1.3.0',
+ default_options : [
+ 'warning_level=3',
+ 'cpp_std=c++26',
+ 'cpp_eh=none',
+ 'cpp_rtti=false',
+ 'default_library=static',
+ ],
+)
+
+cpp_flags = []
+cpp_optional_flags = []
+if get_option('buildtype') == 'release'
+ # If asserts are disabled parameters and variables used for only that
+ # end up causing warnings
+ cpp_optional_flags += ['-Wno-unused-parameter', '-Wno-unused-variable',
+ '-Wno-unused-but-set-variable']
+ cpp_flags += '-DNDEBUG'
+endif
+cpp = meson.get_compiler('cpp')
+foreach flag : cpp_optional_flags
+ if cpp.has_argument(flag)
+ cpp_flags += flag
+ endif
+endforeach
+add_project_arguments([cpp_flags], language: 'cpp')
+
+conf_data = configuration_data()
+conf_data.set('version', meson.project_version())
+configure_file(input: 'src/config.h.in',
+ output: 'config.h',
+ configuration : conf_data)
+
+dbus_dep = dependency('sdbus-c++', version: '>= 2.0.0')
+
+inc = include_directories('src')
+
+args_lib = library(
+ 'args',
+ sources: [
+ 'src/args.cc',
+ 'src/args.hh',
+ ],
+ include_directories: inc,
+)
+args_dep = declare_dependency(link_with: args_lib)
+
+buffer_lib = library(
+ 'buffer',
+ sources: [
+ 'src/buffer.cc',
+ 'src/buffer.hh',
+ ],
+ include_directories: inc,
+)
+buffer_dep = declare_dependency(link_with: buffer_lib)
+
+io_lib = library(
+ 'io',
+ sources: [
+ 'src/line.cc',
+ 'src/line.hh',
+ 'src/io.cc',
+ 'src/io.hh',
+ 'src/unique_fd.cc',
+ 'src/unique_fd.hh',
+ ],
+ include_directories: inc,
+)
+io_dep = declare_dependency(link_with: io_lib)
+
+str_lib = library(
+ 'str',
+ sources: [
+ 'src/str.cc',
+ 'src/str.hh',
+ ],
+ include_directories: inc,
+)
+str_dep = declare_dependency(link_with: str_lib)
+
+bluetooth_jukebox = executable(
+ 'bluetooth-jukebox',
+ sources: [
+ 'src/main.cc',
+ ],
+ include_directories: inc,
+ install : true,
+ dependencies : [
+ args_dep,
+ io_dep,
+ str_dep,
+ ],
+)
+
+gtest_main_dep = dependency('gtest_main', fallback : ['gtest_main'])
+
+test_dependencies = [
+ gtest_main_dep,
+]
+
+test('args', executable(
+ 'test_args',
+ sources: ['test/args.cc'],
+ include_directories: inc,
+ dependencies: [
+ args_dep,
+ test_dependencies,
+ ],
+))
+
+io_test_helper_lib = library(
+ 'io_test_helper',
+ sources: [
+ 'test/io_test_helper.cc',
+ 'test/io_test_helper.hh',
+ ],
+ include_directories: inc,
+ dependencies: io_dep,
+)
+io_test_helper_dep = declare_dependency(
+ link_with: io_test_helper_lib,
+ dependencies: io_dep,
+)
+
+test('line', executable(
+ 'test_line',
+ sources: ['test/line.cc'],
+ include_directories: inc,
+ dependencies: [
+ io_dep,
+ io_test_helper_dep,
+ test_dependencies,
+ ],
+))
+
+test('str', executable(
+ 'test_str',
+ sources: ['test/str.cc'],
+ include_directories: inc,
+ dependencies: [
+ str_dep,
+ test_dependencies,
+ ],
+))
+
+test('io', executable(
+ 'test_io',
+ sources: ['test/io.cc'],
+ include_directories: inc,
+ dependencies: [
+ io_dep,
+ io_test_helper_dep,
+ test_dependencies,
+ ],
+))
+
+test('buffer', executable(
+ 'test_buffer',
+ sources: ['test/buffer.cc'],
+ include_directories: inc,
+ dependencies : [
+ buffer_dep,
+ test_dependencies,
+ ],
+))
+
+run_clang_tidy = find_program('run-clang-tidy', required: false)
+
+if run_clang_tidy.found()
+ # The clang-tidy target generated by meson misses most of the
+ # source files, so create our own.
+ run_target(
+ 'clang-tidy',
+ command: [
+ run_clang_tidy,
+ '-quiet',
+ '-use-color',
+ ],
+ )
+endif
diff --git a/src/args.cc b/src/args.cc
new file mode 100644
index 0000000..1794941
--- /dev/null
+++ b/src/args.cc
@@ -0,0 +1,389 @@
+#include "args.hh"
+
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <format>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+std::string kEmpty;
+
+class OptionImpl : public Args::OptionArgument {
+ public:
+ enum Type : uint8_t {
+ NoArgument,
+ RequiredArgument,
+ OptionalArgument,
+ };
+
+ OptionImpl(Type type, char shortname, std::string longname, std::string arg,
+ std::string help)
+ : type(type),
+ shortname(shortname),
+ longname(std::move(longname)),
+ arg(std::move(arg)),
+ help(std::move(help)) {}
+
+ const Type type;
+ const char shortname;
+ const std::string longname;
+ const std::string arg;
+ const std::string help;
+
+ [[nodiscard]] bool is_set() const override { return is_set_; }
+
+ [[nodiscard]] bool has_argument() const override {
+ return value_.has_value();
+ }
+
+ [[nodiscard]] const std::string& argument() const override {
+ if (value_.has_value())
+ return value_.value();
+ assert(false);
+ return kEmpty;
+ }
+
+ void clear() {
+ is_set_ = false;
+ value_.reset();
+ }
+
+ void set_argument(std::string value) {
+ assert(type == Type::RequiredArgument || type == Type::OptionalArgument);
+ is_set_ = true;
+ value_ = std::move(value);
+ }
+
+ void set_no_argument() {
+ assert(type == Type::NoArgument || type == Type::OptionalArgument);
+ is_set_ = true;
+ value_.reset();
+ }
+
+ private:
+ bool is_set_{false};
+ std::optional<std::string> value_;
+};
+
+class ArgsImpl : public Args {
+ public:
+ explicit ArgsImpl(std::string prgname) : prgname_(std::move(prgname)) {}
+
+ std::shared_ptr<Option> option(char shortname, std::string longname,
+ std::string help) override {
+ auto opt = std::make_shared<OptionImpl>(
+ OptionImpl::Type::NoArgument, shortname, std::move(longname),
+ /* arg */ std::string(), std::move(help));
+ add(opt);
+ return opt;
+ }
+
+ std::shared_ptr<OptionArgument> option_argument(char shortname,
+ std::string longname,
+ std::string arg,
+ std::string help,
+ bool required) override {
+ auto opt = std::make_shared<OptionImpl>(
+ required ? OptionImpl::Type::RequiredArgument
+ : OptionImpl::Type::OptionalArgument,
+ shortname, std::move(longname), std::move(arg), std::move(help));
+ add(opt);
+ return opt;
+ }
+
+ bool run(int argc, char** argv,
+ std::vector<std::string_view>* arguments = nullptr) override {
+ last_error_.clear();
+ for (auto& opt : options_) {
+ opt->clear();
+ }
+
+ std::string_view prgname;
+ if (prgname_.empty()) {
+ if (argc > 0)
+ prgname = argv[0];
+ } else {
+ prgname = prgname_;
+ }
+
+ for (int a = 1; a < argc; ++a) {
+ assert(argv[a]);
+ if (argv[a][0] == '-' && argv[a][1] != '\0') {
+ if (argv[a][1] == '-') {
+ // long option
+ size_t eq = 2;
+ while (argv[a][eq] != '=' && argv[a][eq] != '\0')
+ ++eq;
+ size_t end = eq;
+ while (argv[a][end] != '\0')
+ ++end;
+
+ if (end == 2) {
+ // "--", no more options signal
+ if (arguments) {
+ for (++a; a < argc; ++a)
+ arguments->emplace_back(argv[a]);
+ }
+ break;
+ }
+
+ auto name = std::string_view(argv[a] + 2, eq - 2);
+ auto it = long_.find(name);
+ if (it == long_.end()) {
+ last_error_ =
+ std::format("{}: unrecognized option '--{}'", prgname, name);
+ return false;
+ }
+ auto& opt = options_[it->second];
+
+ if (eq < end) {
+ // long option with argument after equal sign
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ last_error_ =
+ std::format("{}: option '--{}' doesn't allow an argument",
+ prgname, name);
+ return false;
+ case OptionImpl::Type::RequiredArgument:
+ case OptionImpl::Type::OptionalArgument:
+ opt->set_argument(
+ std::string(argv[a] + eq + 1, end - (eq + 1)));
+ break;
+ }
+ } else {
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ opt->set_no_argument();
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ if (++a >= argc) {
+ last_error_ = std::format(
+ "{}: option '--{}' requires an argument", prgname, name);
+ return false;
+ }
+ opt->set_argument(argv[a]);
+ break;
+ }
+ }
+ } else {
+ // short options
+ char* current = argv[a] + 1;
+ for (; *current; ++current) {
+ auto it = short_.find(*current);
+ if (it == short_.end()) {
+ last_error_ =
+ std::format("{}: invalid option -- '{}'", prgname, *current);
+ return false;
+ }
+
+ auto& opt = options_[it->second];
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ opt->set_no_argument();
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ if (++a >= argc) {
+ last_error_ =
+ std::format("{}: option requires an argument -- '{}'",
+ prgname, *current);
+ return false;
+ }
+ opt->set_argument(argv[a]);
+ break;
+ }
+ }
+ }
+ } else {
+ if (arguments)
+ arguments->emplace_back(argv[a]);
+ }
+ }
+ return true;
+ }
+
+ void print_error(std::ostream& out) const override {
+ if (last_error_.empty())
+ return;
+
+ out << last_error_ << '\n';
+ }
+
+ void print_help(std::ostream& out, uint32_t width = 79) const override {
+ if (options_.empty())
+ return;
+
+ uint32_t indent = 0;
+ const uint32_t max_need = width / 2;
+ std::vector<uint32_t> option_need;
+ for (auto const& opt : options_) {
+ uint32_t need;
+ if (opt->longname.empty()) {
+ need = 4; // -O
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ need += 1 + (opt->arg.empty() ? 3 : opt->arg.size());
+ break;
+ }
+ } else {
+ need = 8 + opt->longname.size(); // -O, --option
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ // =ARG
+ need += 1 + (opt->arg.empty() ? 3 : opt->arg.size());
+ break;
+ case OptionImpl::Type::OptionalArgument:
+ // [=ARG]
+ need += 3 + (opt->arg.empty() ? 3 : opt->arg.size());
+ break;
+ }
+ }
+ need += 2; // margin
+
+ option_need.emplace_back(need);
+ if (need <= max_need) {
+ indent = std::max(indent, need);
+ }
+ }
+
+ print_wrap(out, width, /* indent */ 0,
+ "Mandatory arguments to long options"
+ " are mandatory for short options too.");
+ auto need_it = option_need.begin();
+ for (auto const& opt : options_) {
+ if (opt->longname.empty()) {
+ out << " -" << opt->shortname;
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ out << " " << (opt->arg.empty() ? "ARG" : opt->arg);
+ break;
+ }
+ } else {
+ if (opt->shortname != '\0') {
+ out << " -" << opt->shortname << ", --";
+ } else {
+ out << " --";
+ }
+ out << opt->longname;
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ out << "=" << (opt->arg.empty() ? "ARG" : opt->arg);
+ break;
+ case OptionImpl::Type::OptionalArgument:
+ out << "=[" << (opt->arg.empty() ? "ARG" : opt->arg) << ']';
+ break;
+ }
+ }
+
+ auto need = *need_it++;
+ if (need > max_need) {
+ out << '\n';
+ if (!opt->help.empty()) {
+ print_wrap(out, width, 0, opt->help);
+ }
+ } else {
+ if (opt->help.empty()) {
+ out << '\n';
+ } else {
+ out << " "; // add margin, already included in need
+ while (need++ < indent)
+ out << ' ';
+ print_wrap(out, width, indent, opt->help);
+ }
+ }
+ }
+ }
+
+ private:
+ void add(std::shared_ptr<OptionImpl> opt) {
+ if (opt->shortname == '\0' && opt->longname.empty()) {
+ assert(false);
+ } else {
+ auto idx = options_.size();
+ if (opt->shortname != '\0')
+ short_.emplace(opt->shortname, idx);
+ if (!opt->longname.empty())
+ long_.emplace(opt->longname, idx);
+ }
+ options_.emplace_back(std::move(opt));
+ }
+
+ static inline bool is_whitespace(char c) { return c == ' ' || c == '\t'; }
+
+ static void print_wrap(std::ostream& out, uint32_t width, uint32_t indent,
+ const std::string& str) {
+ if (indent + str.size() <= width) {
+ out << str << '\n';
+ return;
+ }
+ if (width <= indent || indent + 10 > width) {
+ out << '\n';
+ out << str << '\n';
+ return;
+ }
+ const std::string indent_str(indent, ' ');
+ const uint32_t avail = width - indent;
+ size_t offset = 0;
+ while (offset + avail < str.size()) {
+ uint32_t i = avail;
+ while (i > 0 && !is_whitespace(str[offset + i]))
+ --i;
+ if (i == 0) {
+ out << str.substr(offset, avail - 1);
+ out << "-\n";
+ offset += avail - 1;
+ } else {
+ out << str.substr(offset, i);
+ out << '\n';
+ offset += i;
+ }
+ out << indent_str;
+ }
+ out << str.substr(offset);
+ out << '\n';
+ }
+
+ const std::string prgname_;
+ std::vector<std::shared_ptr<OptionImpl>> options_;
+ std::map<char, size_t> short_;
+ std::map<std::string_view, size_t> long_;
+ std::string last_error_;
+};
+
+} // namespace
+
+std::shared_ptr<Args::Option> Args::option(std::string longname,
+ std::string help) {
+ return option(/* shortname */ '\0', std::move(longname), std::move(help));
+}
+
+std::shared_ptr<Args::OptionArgument> Args::option_argument(
+ std::string longname, std::string arg, std::string help, bool required) {
+ return option_argument(/* shortname */ '\0', std::move(longname),
+ std::move(arg), std::move(help), required);
+}
+
+std::unique_ptr<Args> Args::create(std::string prgname) {
+ return std::make_unique<ArgsImpl>(std::move(prgname));
+}
diff --git a/src/args.hh b/src/args.hh
new file mode 100644
index 0000000..14f3716
--- /dev/null
+++ b/src/args.hh
@@ -0,0 +1,64 @@
+#ifndef ARGS_HH
+#define ARGS_HH
+
+#include <cstdint>
+#include <iosfwd>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+class Args {
+ public:
+ virtual ~Args() = default;
+
+ class Option {
+ public:
+ virtual ~Option() = default;
+
+ [[nodiscard]] virtual bool is_set() const = 0;
+
+ protected:
+ Option() = default;
+ Option(Option const&) = delete;
+ Option& operator=(Option const&) = delete;
+ };
+
+ class OptionArgument : public Option {
+ public:
+ [[nodiscard]] virtual bool has_argument() const = 0;
+ [[nodiscard]] virtual const std::string& argument() const = 0;
+ };
+
+ static std::unique_ptr<Args> create(std::string prgname = std::string());
+
+ virtual std::shared_ptr<Option> option(char shortname,
+ std::string longname = std::string(),
+ std::string help = std::string()) = 0;
+
+ std::shared_ptr<Option> option(std::string longname,
+ std::string help = std::string());
+
+ virtual std::shared_ptr<OptionArgument> option_argument(
+ char shortname, std::string longname = std::string(),
+ std::string arg = std::string(), std::string help = std::string(),
+ bool required = true) = 0;
+
+ std::shared_ptr<OptionArgument> option_argument(
+ std::string longname, std::string arg = std::string(),
+ std::string help = std::string(), bool required = true);
+
+ virtual bool run(int argc, char** argv,
+ std::vector<std::string_view>* arguments = nullptr) = 0;
+
+ virtual void print_error(std::ostream& out) const = 0;
+
+ virtual void print_help(std::ostream& out, uint32_t width = 79) const = 0;
+
+ protected:
+ Args() = default;
+ Args(Args const&) = delete;
+ Args& operator=(Args const&) = delete;
+};
+
+#endif // ARGS_HH
diff --git a/src/buffer.cc b/src/buffer.cc
new file mode 100644
index 0000000..18913a5
--- /dev/null
+++ b/src/buffer.cc
@@ -0,0 +1,213 @@
+#include "buffer.hh"
+
+#include <algorithm>
+#include <cassert>
+#include <cstring>
+#include <memory>
+#include <utility>
+
+namespace {
+
+class FixedBuffer : public Buffer {
+ public:
+ explicit FixedBuffer(size_t size) : size_(size) {}
+
+ void const* rptr(size_t& avail, size_t need) override {
+ if (rptr_ < wptr_) {
+ avail = wptr_ - rptr_;
+ } else if (rptr_ == wptr_ && !full_) {
+ avail = 0;
+ } else {
+ avail = (data_.get() + size_) - rptr_;
+ if (avail < need && rptr_ > data_.get()) {
+ rotate();
+ return rptr(avail, need);
+ }
+ }
+ return rptr_;
+ }
+
+ void consume(size_t size) override {
+ if (size == 0)
+ return;
+ if (rptr_ < wptr_) {
+ assert(std::cmp_greater_equal(wptr_ - rptr_, size));
+ rptr_ += size;
+ if (rptr_ == wptr_)
+ reset();
+ } else {
+ assert(rptr_ != wptr_ || full_);
+ assert(std::cmp_greater_equal((data_.get() + size_) - rptr_, size));
+ rptr_ += size;
+ if (rptr_ == data_.get() + size_) {
+ rptr_ = data_.get();
+ if (rptr_ == wptr_)
+ reset();
+ }
+ }
+ }
+
+ void* wptr(size_t& avail, size_t need) override {
+ if (wptr_ == nullptr) {
+ data_ = std::make_unique_for_overwrite<char[]>(size_);
+ rptr_ = wptr_ = data_.get();
+ }
+
+ if (wptr_ < rptr_) {
+ avail = rptr_ - wptr_;
+ } else if (rptr_ == wptr_ && full_) {
+ avail = 0;
+ } else {
+ avail = (data_.get() + size_) - wptr_;
+ if (avail < need && rptr_ > data_.get()) {
+ rotate();
+ return wptr(avail, need);
+ }
+ }
+ return wptr_;
+ }
+
+ void commit(size_t size) override {
+ if (size == 0)
+ return;
+ if (wptr_ < rptr_) {
+ assert(std::cmp_greater_equal(rptr_ - wptr_, size));
+ wptr_ += size;
+ if (wptr_ == rptr_) {
+ full_ = true;
+ }
+ } else {
+ assert(rptr_ != wptr_ || !full_);
+ assert(std::cmp_greater_equal((data_.get() + size_) - wptr_, size));
+ wptr_ += size;
+ if (wptr_ == data_.get() + size_) {
+ wptr_ = data_.get();
+ if (wptr_ == rptr_)
+ full_ = true;
+ }
+ }
+ }
+
+ [[nodiscard]] bool full() const override { return rptr_ == wptr_ && full_; }
+
+ [[nodiscard]] bool empty() const override { return rptr_ == wptr_ && !full_; }
+
+ private:
+ void reset() {
+ rptr_ = wptr_ = data_.get();
+ full_ = false;
+ }
+
+ void rotate() {
+ if (rptr_ < wptr_) {
+ size_t size = wptr_ - rptr_;
+ memmove(data_.get(), rptr_, size);
+ wptr_ = data_.get() + size;
+ } else {
+ size_t to_move = (data_.get() + size_) - rptr_;
+ if (wptr_ + to_move > rptr_) {
+ auto tmp = std::make_unique_for_overwrite<char[]>(to_move);
+ memcpy(tmp.get(), rptr_, to_move);
+ memmove(data_.get() + to_move, data_.get(), wptr_ - data_.get());
+ memcpy(data_.get(), tmp.get(), to_move);
+ } else {
+ memmove(data_.get() + to_move, data_.get(), wptr_ - data_.get());
+ memcpy(data_.get(), rptr_, to_move);
+ }
+ wptr_ += to_move;
+ }
+ rptr_ = data_.get();
+ }
+
+ size_t const size_;
+ std::unique_ptr<char[]> data_;
+ char* rptr_{nullptr};
+ char* wptr_{nullptr};
+ bool full_{false};
+};
+
+class DynamicBuffer : public Buffer {
+ public:
+ DynamicBuffer(size_t start_size, size_t max_size)
+ : start_size_(start_size), max_size_(max_size) {}
+
+ void const* rptr(size_t& avail, size_t /* need */) override {
+ avail = wptr_ - rptr_;
+ return rptr_;
+ }
+
+ void consume(size_t size) override {
+ assert(std::cmp_greater_equal(wptr_ - rptr_, size));
+ rptr_ += size;
+ if (rptr_ == wptr_) {
+ reset();
+ }
+ }
+
+ void* wptr(size_t& avail, size_t need) override {
+ avail = end_ - wptr_;
+ if (avail < need) {
+ if (end_ == nullptr) {
+ size_t size = std::min(max_size_, std::max(need, start_size_));
+ data_ = std::make_unique_for_overwrite<char[]>(size);
+ end_ = data_.get() + size;
+ rptr_ = wptr_ = data_.get();
+ avail = end_ - wptr_;
+ } else if (std::cmp_greater_equal(rptr_ - data_.get(), need - avail)) {
+ memmove(data_.get(), rptr_, wptr_ - rptr_);
+ wptr_ = data_.get() + (wptr_ - rptr_);
+ rptr_ = data_.get();
+ avail = end_ - wptr_;
+ } else if (std::cmp_less(end_ - data_.get(), max_size_)) {
+ size_t current_size = end_ - data_.get();
+ size_t new_size = std::min(
+ max_size_, current_size + std::max(need - avail, current_size));
+ auto tmp = std::make_unique_for_overwrite<char[]>(new_size);
+ memcpy(tmp.get(), rptr_, wptr_ - rptr_);
+ end_ = tmp.get() + new_size;
+ wptr_ = tmp.get() + (wptr_ - rptr_);
+ rptr_ = tmp.get();
+ data_ = std::move(tmp);
+ avail = end_ - wptr_;
+ }
+ }
+ return wptr_;
+ }
+
+ void commit(size_t size) override {
+ assert(std::cmp_greater_equal(end_ - wptr_, size));
+ wptr_ += size;
+ }
+
+ [[nodiscard]] bool full() const override {
+ return rptr_ == data_.get() && wptr_ == end_ &&
+ std::cmp_equal(end_ - data_.get(), max_size_);
+ }
+
+ [[nodiscard]] bool empty() const override { return rptr_ == wptr_; }
+
+ private:
+ void reset() {
+ if (std::cmp_greater(end_ - data_.get(), start_size_)) {
+ data_ = std::make_unique_for_overwrite<char[]>(start_size_);
+ }
+ rptr_ = wptr_ = data_.get();
+ }
+
+ size_t const start_size_;
+ size_t const max_size_;
+ std::unique_ptr<char[]> data_;
+ char* end_{nullptr};
+ char* rptr_{nullptr};
+ char* wptr_{nullptr};
+};
+
+} // namespace
+
+std::unique_ptr<Buffer> Buffer::fixed(size_t size) {
+ return std::make_unique<FixedBuffer>(size);
+}
+
+std::unique_ptr<Buffer> Buffer::dynamic(size_t start_size, size_t max_size) {
+ return std::make_unique<DynamicBuffer>(start_size, max_size);
+}
diff --git a/src/buffer.hh b/src/buffer.hh
new file mode 100644
index 0000000..685cd36
--- /dev/null
+++ b/src/buffer.hh
@@ -0,0 +1,31 @@
+#ifndef BUFFER_HH
+#define BUFFER_HH
+
+#include <cstddef>
+#include <memory>
+
+class Buffer {
+ public:
+ virtual ~Buffer() = default;
+
+ virtual void const* rptr(size_t& avail, size_t need = 1) = 0;
+ virtual void consume(size_t size) = 0;
+
+ virtual void* wptr(size_t& avail, size_t need = 1) = 0;
+ virtual void commit(size_t size) = 0;
+
+ [[nodiscard]] virtual bool full() const = 0;
+ [[nodiscard]] virtual bool empty() const = 0;
+
+ [[nodiscard]]
+ static std::unique_ptr<Buffer> fixed(size_t size);
+ [[nodiscard]]
+ static std::unique_ptr<Buffer> dynamic(size_t start_size, size_t max_size);
+
+ protected:
+ Buffer() = default;
+ Buffer(Buffer const&) = delete;
+ Buffer& operator=(Buffer const&) = delete;
+};
+
+#endif // BUFFER_HH
diff --git a/src/check.hh b/src/check.hh
new file mode 100644
index 0000000..91c1717
--- /dev/null
+++ b/src/check.hh
@@ -0,0 +1,39 @@
+#ifndef CHECK_HH
+#define CHECK_HH
+
+#include <cstdlib>
+#include <stdckdint.h>
+#include <type_traits>
+
+namespace check {
+
+template <typename T>
+ requires std::is_arithmetic_v<T>
+T add(T a, T b) {
+ T ret;
+ if (ckd_add(&ret, a, b))
+ abort();
+ return ret;
+}
+
+template <typename T>
+ requires std::is_arithmetic_v<T>
+T sub(T a, T b) {
+ T ret;
+ if (ckd_sub(&ret, a, b))
+ abort();
+ return ret;
+}
+
+template <typename T>
+ requires std::is_arithmetic_v<T>
+T mul(T a, T b) {
+ T ret;
+ if (ckd_mul(&ret, a, b))
+ abort();
+ return ret;
+}
+
+} // namespace check
+
+#endif // CHECK_HH
diff --git a/src/config.h.in b/src/config.h.in
new file mode 100644
index 0000000..eaab018
--- /dev/null
+++ b/src/config.h.in
@@ -0,0 +1 @@
+#define VERSION "@version@"
diff --git a/src/io.cc b/src/io.cc
new file mode 100644
index 0000000..99c0518
--- /dev/null
+++ b/src/io.cc
@@ -0,0 +1,232 @@
+#include "io.hh"
+
+#include "unique_fd.hh"
+
+#include <algorithm>
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+#include <expected>
+#include <fcntl.h>
+#include <limits>
+#include <memory>
+#include <optional>
+#include <string>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <utility>
+
+namespace io {
+
+namespace {
+
+class BasicReader : public Reader {
+ public:
+ explicit BasicReader(unique_fd fd) : fd_(std::move(fd)) {}
+
+ [[nodiscard]]
+ std::expected<size_t, ReadError> read(void* dst, size_t max) override {
+ ssize_t ret = ::read(
+ fd_.get(), dst,
+ std::min(static_cast<size_t>(std::numeric_limits<ssize_t>::max()),
+ max));
+ if (ret < 0) {
+ switch (errno) {
+ case EINTR:
+ return read(dst, max);
+ default:
+ return std::unexpected(ReadError::Error);
+ }
+ } else if (ret == 0 && max > 0) {
+ return std::unexpected(ReadError::Eof);
+ }
+ offset_ += ret;
+ return ret;
+ }
+
+ [[nodiscard]]
+ std::expected<size_t, ReadError> skip(size_t max) override {
+ off_t ret;
+ if (sizeof(size_t) > sizeof(off_t)) {
+ ret = lseek(
+ fd_.get(),
+ // NOLINTNEXTLINE(bugprone-narrowing-conversions)
+ std::min(static_cast<size_t>(std::numeric_limits<off_t>::max()), max),
+ SEEK_CUR);
+ } else {
+ ret = lseek(fd_.get(), static_cast<off_t>(max), SEEK_CUR);
+ }
+ if (ret < 0) {
+ return std::unexpected(ReadError::Error);
+ }
+ // Don't want skip to go past (cached) file end.
+ if (!size_.has_value() || ret >= size_.value()) {
+ // When going past end, double check that it still is the end.
+ off_t ret2 = lseek(fd_.get(), 0, SEEK_END);
+ if (ret2 < 0) {
+ // We're screwed, but try to go back to original position and then
+ // return error.
+ size_.reset();
+ lseek(fd_.get(), offset_, SEEK_SET);
+ return std::unexpected(ReadError::Error);
+ }
+ size_ = ret2;
+ if (ret >= ret2) {
+ auto distance = ret2 - offset_;
+ offset_ = ret2;
+ if (distance == 0 && max > 0)
+ return std::unexpected(ReadError::Eof);
+ return distance;
+ }
+ // Seek back to where we should be
+ if (lseek(fd_.get(), ret, SEEK_SET) < 0) {
+ return std::unexpected(ReadError::Error);
+ }
+ }
+ auto distance = ret - offset_;
+ offset_ = ret;
+ return distance;
+ }
+
+ private:
+ unique_fd fd_;
+ off_t offset_{0};
+ std::optional<off_t> size_;
+};
+
+class MemoryReader : public Reader {
+ public:
+ MemoryReader(void* ptr, size_t size) : ptr_(ptr), size_(size) {}
+
+ [[nodiscard]]
+ std::expected<size_t, ReadError> read(void* dst, size_t max) override {
+ size_t avail = size_ - offset_;
+ if (avail == 0 && max > 0)
+ return std::unexpected(io::ReadError::Eof);
+ size_t ret = std::min(max, avail);
+ memcpy(dst, reinterpret_cast<char*>(ptr_) + offset_, ret);
+ offset_ += ret;
+ return ret;
+ }
+
+ [[nodiscard]]
+ std::expected<size_t, ReadError> skip(size_t max) override {
+ size_t avail = size_ - offset_;
+ size_t ret = std::min(max, avail);
+ offset_ += ret;
+ return ret;
+ }
+
+ protected:
+ void* ptr_;
+ size_t const size_;
+
+ private:
+ size_t offset_{0};
+};
+
+class MmapReader : public MemoryReader {
+ public:
+ MmapReader(unique_fd fd, void* ptr, size_t size)
+ : MemoryReader(ptr, size), fd_(std::move(fd)) {}
+
+ ~MmapReader() override { munmap(ptr_, size_); }
+
+ private:
+ unique_fd fd_;
+};
+
+class StringReader : public MemoryReader {
+ public:
+ explicit StringReader(std::string data)
+ : MemoryReader(nullptr, data.size()), data_(std::move(data)) {
+ ptr_ = data_.data();
+ }
+
+ private:
+ std::string data_;
+};
+
+} // namespace
+
+std::expected<size_t, ReadError> Reader::repeat_read(void* dst, size_t max) {
+ auto ret = read(dst, max);
+ if (!ret.has_value() || ret.value() == max)
+ return ret;
+
+ char* d = reinterpret_cast<char*>(dst);
+ size_t offset = ret.value();
+ while (true) {
+ ret = read(d + offset, max - offset);
+ if (!ret.has_value())
+ break;
+ offset += ret.value();
+ if (offset == max)
+ break;
+ }
+ return offset;
+}
+
+std::expected<size_t, ReadError> Reader::repeat_skip(size_t max) {
+ auto ret = skip(max);
+ if (!ret.has_value() || ret.value() == max)
+ return ret;
+
+ size_t offset = ret.value();
+ while (true) {
+ ret = skip(max - offset);
+ if (!ret.has_value())
+ break;
+ offset += ret.value();
+ if (offset == max)
+ break;
+ }
+ return offset;
+}
+
+std::expected<std::unique_ptr<Reader>, OpenError> open(
+ const std::string& file_path) {
+ return openat(AT_FDCWD, file_path);
+}
+
+std::expected<std::unique_ptr<Reader>, OpenError> openat(
+ int dirfd, const std::string& file_path) {
+ unique_fd fd(::openat(dirfd, file_path.c_str(), O_RDONLY));
+ if (fd) {
+ struct stat buf;
+ if (fstat(fd.get(), &buf) == 0) {
+ if (std::cmp_less_equal(buf.st_size,
+ std::numeric_limits<size_t>::max())) {
+ auto size = static_cast<size_t>(buf.st_size);
+ void* ptr = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd.get(), 0);
+ if (ptr != MAP_FAILED) {
+ return std::make_unique<MmapReader>(std::move(fd), ptr, size);
+ }
+ }
+ }
+ return std::make_unique<BasicReader>(std::move(fd));
+ }
+ OpenError err;
+ switch (errno) {
+ case EINTR:
+ return openat(dirfd, file_path);
+ case EACCES:
+ err = OpenError::NoAccess;
+ break;
+ case ENOENT:
+ err = OpenError::NoSuchFile;
+ break;
+ default:
+ err = OpenError::Error;
+ break;
+ }
+ return std::unexpected(err);
+}
+
+std::unique_ptr<Reader> memory(std::string data) {
+ return std::make_unique<StringReader>(std::move(data));
+}
+
+} // namespace io
diff --git a/src/io.hh b/src/io.hh
new file mode 100644
index 0000000..7c21028
--- /dev/null
+++ b/src/io.hh
@@ -0,0 +1,51 @@
+#ifndef IO_HH
+#define IO_HH
+
+#include <cstddef>
+#include <expected>
+#include <memory>
+#include <string>
+
+namespace io {
+
+enum class ReadError {
+ Error,
+ Eof,
+ InvalidData, // invalid data read (not used by raw file)
+ MaxTooSmall, // max argument needs to be bigger (not used by raw file)
+};
+
+enum class OpenError {
+ NoSuchFile,
+ NoAccess,
+ Error,
+};
+
+class Reader {
+ public:
+ virtual ~Reader() = default;
+
+ [[nodiscard]] virtual std::expected<size_t, ReadError> read(void* dst,
+ size_t max) = 0;
+ [[nodiscard]] virtual std::expected<size_t, ReadError> skip(size_t max) = 0;
+
+ [[nodiscard]] std::expected<size_t, ReadError> repeat_read(void* dst,
+ size_t max);
+ [[nodiscard]] std::expected<size_t, ReadError> repeat_skip(size_t max);
+
+ protected:
+ Reader() = default;
+
+ Reader(Reader const&) = delete;
+ Reader& operator=(Reader const&) = delete;
+};
+
+[[nodiscard]] std::expected<std::unique_ptr<Reader>, OpenError> open(
+ const std::string& file_path);
+[[nodiscard]] std::expected<std::unique_ptr<Reader>, OpenError> openat(
+ int dirfd, const std::string& file_path);
+[[nodiscard]] std::unique_ptr<Reader> memory(std::string data);
+
+} // namespace io
+
+#endif // IO_HH
diff --git a/src/line.cc b/src/line.cc
new file mode 100644
index 0000000..23370fc
--- /dev/null
+++ b/src/line.cc
@@ -0,0 +1,127 @@
+#include "line.hh"
+
+#include "check.hh"
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <cstring>
+#include <expected>
+#include <memory>
+#include <string_view>
+#include <utility>
+
+namespace line {
+
+namespace {
+
+const char kLineTerminators[] = "\r\n";
+
+class ReaderImpl : public Reader {
+ public:
+ ReaderImpl(std::unique_ptr<io::Reader> reader, size_t max_len)
+ : reader_(std::move(reader)),
+ max_len_(max_len),
+ buffer_(std::make_unique_for_overwrite<char[]>(
+ check::add(max_len, static_cast<size_t>(2)))),
+ rptr_(buffer_.get()),
+ wptr_(buffer_.get()),
+ search_(rptr_),
+ end_(buffer_.get() + check::add(max_len, static_cast<size_t>(2))) {}
+
+ [[nodiscard]] std::expected<std::string_view, io::ReadError> read() override {
+ while (true) {
+ search_ = std::find_first_of(search_, wptr_, kLineTerminators,
+ kLineTerminators + 2);
+ if (search_ < wptr_) {
+ if (std::cmp_greater(search_ - rptr_, max_len_)) {
+ return line(max_len_, 0);
+ }
+
+ size_t tlen;
+ if (*search_ == '\n') {
+ tlen = 1;
+ } else {
+ if (search_ + 1 == wptr_) {
+ make_space_if_needed();
+ auto got = fill();
+ if (!got.has_value()) {
+ if (got.error() == io::ReadError::Eof) {
+ return line(search_ - rptr_, 1);
+ }
+ return std::unexpected(got.error());
+ }
+ }
+ if (search_[1] == '\n') {
+ tlen = 2;
+ } else {
+ tlen = 1;
+ }
+ }
+ return line(search_ - rptr_, tlen);
+ }
+ if (std::cmp_greater_equal(wptr_ - rptr_, max_len_)) {
+ return line(max_len_, 0);
+ }
+
+ make_space_if_needed();
+ auto got = fill();
+ if (!got.has_value()) {
+ if (got.error() == io::ReadError::Eof && rptr_ != wptr_) {
+ return line(wptr_ - rptr_, 0);
+ }
+ return std::unexpected(got.error());
+ }
+ }
+ }
+
+ [[nodiscard]] uint64_t number() const override { return number_; }
+
+ private:
+ std::string_view line(size_t len, size_t terminator_len) {
+ assert(len <= max_len_);
+ auto ret = std::string_view(rptr_, len);
+ rptr_ += len + terminator_len;
+ search_ = rptr_;
+ ++number_;
+ return ret;
+ }
+
+ void make_space_if_needed() {
+ size_t free = rptr_ - buffer_.get();
+ if (free == 0)
+ return;
+ size_t avail = end_ - wptr_;
+ if (avail > 1024)
+ return;
+ memmove(buffer_.get(), rptr_, wptr_ - rptr_);
+ search_ -= free;
+ wptr_ -= free;
+ rptr_ = buffer_.get();
+ }
+
+ std::expected<size_t, io::ReadError> fill() {
+ auto ret = reader_->read(wptr_, end_ - wptr_);
+ if (ret.has_value())
+ wptr_ += ret.value();
+ return ret;
+ }
+
+ std::unique_ptr<io::Reader> reader_;
+ size_t const max_len_;
+ uint64_t number_{0};
+ std::unique_ptr<char[]> buffer_;
+ char* rptr_;
+ char* wptr_;
+ char* search_;
+ char* const end_;
+};
+
+} // namespace
+
+std::unique_ptr<Reader> open(std::unique_ptr<io::Reader> reader,
+ size_t max_len) {
+ return std::make_unique<ReaderImpl>(std::move(reader), max_len);
+}
+
+} // namespace line
diff --git a/src/line.hh b/src/line.hh
new file mode 100644
index 0000000..a8eeea8
--- /dev/null
+++ b/src/line.hh
@@ -0,0 +1,37 @@
+#ifndef LINE_HH
+#define LINE_HH
+
+#include "io.hh" // IWYU pragma: export
+
+#include <cstddef>
+#include <expected>
+#include <memory>
+#include <optional>
+#include <string_view>
+
+namespace line {
+
+class Reader {
+ public:
+ virtual ~Reader() = default;
+
+ // Returned view is only valid until next call to read.
+ [[nodiscard]]
+ virtual std::expected<std::string_view, io::ReadError> read() = 0;
+ // Starts at zero. Returns next line.
+ // So, before first read it is zero, after first read it is one.
+ [[nodiscard]] virtual uint64_t number() const = 0;
+
+ protected:
+ Reader() = default;
+
+ Reader(Reader const&) = delete;
+ Reader& operator=(Reader const&) = delete;
+};
+
+[[nodiscard]] std::unique_ptr<Reader> open(std::unique_ptr<io::Reader> reader,
+ size_t max_len = 8192);
+
+} // namespace line
+
+#endif // LINE_HH
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 0000000..e66f95a
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,31 @@
+#include "args.hh"
+#include "config.h"
+
+#include <iostream>
+
+#ifndef VERSION
+# define VERSION "unknown"
+#endif
+
+int main(int argc, char** argv) {
+ auto args = Args::create();
+ auto opt_help = args->option('h', "help", "display this text and exit.");
+ auto opt_version = args->option('V', "version", "display version and exit.");
+ if (!args->run(argc, argv)) {
+ args->print_error(std::cerr);
+ std::cerr << "Try 'bluetooth-jukebox --help' for more information.\n";
+ return 1;
+ }
+ if (opt_help->is_set()) {
+ std::cout << "Usage: bluetooth-jukebox [OPTION...]\n"
+ << "\n";
+ args->print_help(std::cout);
+ return 0;
+ }
+ if (opt_version->is_set()) {
+ std::cout << "bluetooth-jukebox " << VERSION
+ << " written by Joel Klinghed <the_jk@spawned.biz>.\n";
+ return 0;
+ }
+ return 0;
+}
diff --git a/src/str.cc b/src/str.cc
new file mode 100644
index 0000000..44db3a6
--- /dev/null
+++ b/src/str.cc
@@ -0,0 +1,53 @@
+#include "str.hh"
+
+#include <cstddef>
+#include <string_view>
+#include <vector>
+
+namespace str {
+
+namespace {
+
+[[nodiscard]]
+inline bool is_space(char c) {
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n';
+}
+
+} // namespace
+
+void split(std::string_view str, std::vector<std::string_view>& out,
+ char separator, bool keep_empty) {
+ out.clear();
+
+ size_t offset = 0;
+ while (true) {
+ auto next = str.find(separator, offset);
+ if (next == std::string_view::npos) {
+ if (keep_empty || offset < str.size())
+ out.push_back(str.substr(offset));
+ break;
+ }
+ if (keep_empty || offset < next)
+ out.push_back(str.substr(offset, next - offset));
+ offset = next + 1;
+ }
+}
+
+std::vector<std::string_view> split(std::string_view str, char separator,
+ bool keep_empty) {
+ std::vector<std::string_view> vec;
+ split(str, vec, separator, keep_empty);
+ return vec;
+}
+
+std::string_view trim(std::string_view str) {
+ size_t s = 0;
+ size_t e = str.size();
+ while (s < e && is_space(str[s]))
+ ++s;
+ while (e > s && is_space(str[e - 1]))
+ --e;
+ return str.substr(s, e - s);
+}
+
+} // namespace str
diff --git a/src/str.hh b/src/str.hh
new file mode 100644
index 0000000..e1ee549
--- /dev/null
+++ b/src/str.hh
@@ -0,0 +1,21 @@
+#ifndef STR_HH
+#define STR_HH
+
+#include <string_view>
+#include <vector>
+
+namespace str {
+
+void split(std::string_view str, std::vector<std::string_view>& out,
+ char separator = ' ', bool keep_empty = false);
+
+[[nodiscard]] std::vector<std::string_view> split(std::string_view str,
+ char separator = ' ',
+ bool keep_empty = false);
+
+[[nodiscard]]
+std::string_view trim(std::string_view str);
+
+} // namespace str
+
+#endif // STR_HH
diff --git a/src/unique_fd.cc b/src/unique_fd.cc
new file mode 100644
index 0000000..135a449
--- /dev/null
+++ b/src/unique_fd.cc
@@ -0,0 +1,9 @@
+#include "unique_fd.hh"
+
+#include <unistd.h>
+
+void unique_fd::reset(int fd) {
+ if (fd_ != -1)
+ close(fd_);
+ fd_ = fd;
+}
diff --git a/src/unique_fd.hh b/src/unique_fd.hh
new file mode 100644
index 0000000..2950905
--- /dev/null
+++ b/src/unique_fd.hh
@@ -0,0 +1,36 @@
+#ifndef UNIQUE_FD_HH
+#define UNIQUE_FD_HH
+
+class unique_fd {
+ public:
+ constexpr unique_fd() : fd_(-1) {}
+ explicit constexpr unique_fd(int fd) : fd_(fd) {}
+ unique_fd(unique_fd& fd) = delete;
+ unique_fd& operator=(unique_fd& fd) = delete;
+ unique_fd(unique_fd&& fd) : fd_(fd.release()) {}
+ unique_fd& operator=(unique_fd&& fd) {
+ reset(fd.release());
+ return *this;
+ }
+ ~unique_fd() { reset(); }
+
+ bool operator==(unique_fd const& fd) const { return get() == fd.get(); }
+ bool operator!=(unique_fd const& fd) const { return get() != fd.get(); }
+
+ int get() const { return fd_; }
+ explicit operator bool() const { return fd_ != -1; }
+ int operator*() const { return fd_; }
+
+ int release() {
+ int ret = fd_;
+ fd_ = -1;
+ return ret;
+ }
+
+ void reset(int fd = -1);
+
+ private:
+ int fd_;
+};
+
+#endif // UNIQUE_FD_HH
diff --git a/test/args.cc b/test/args.cc
new file mode 100644
index 0000000..22159d2
--- /dev/null
+++ b/test/args.cc
@@ -0,0 +1,412 @@
+#include "args.hh"
+
+#include <cstddef>
+#include <gtest/gtest.h>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#define SETUP_OPTIONS(args) \
+ auto short_only = (args)->option('a', "", "an option"); \
+ auto short_long = (args)->option('b', "bold", "set font style to bold"); \
+ auto long_only = (args)->option("cold", "use if it is cold outside"); \
+ auto short_only_req = (args)->option_argument('d', "", "", "distance"); \
+ auto short_long_req = (args)->option_argument( \
+ 'e', "eat", "FOOD", "what to order, what to eat?"); \
+ auto long_only_req = \
+ (args)->option_argument("form", "", "circle, shape or something else?"); \
+ auto short_only_opt = (args)->option_argument('g', "", "", "", false); \
+ auto short_long_opt = (args)->option_argument('h', "hold", "", "", false); \
+ auto long_only_opt = (args)->option_argument("invert", "", "", false);
+
+namespace {
+
+class Arguments {
+ public:
+ [[nodiscard]] int c() const { return static_cast<int>(str_.size()); };
+ [[nodiscard]] char** v() const { return ptr_.get(); }
+
+ explicit Arguments(std::vector<std::string> str) : str_(std::move(str)) {
+ ptr_ = std::make_unique<char*[]>(str_.size());
+ for (size_t i = 0; i < str_.size(); ++i) {
+ ptr_[i] = const_cast<char*>(str_[i].c_str());
+ }
+ }
+
+ Arguments(Arguments const&) = delete;
+ Arguments& operator=(Arguments const&) = delete;
+
+ private:
+ std::unique_ptr<char*[]> ptr_;
+ std::vector<std::string> str_;
+};
+
+class Builder {
+ public:
+ Arguments build() { return Arguments(std::move(str_)); }
+
+ Builder& add(std::string str) {
+ str_.emplace_back(std::move(str));
+ return *this;
+ }
+
+ private:
+ std::vector<std::string> str_;
+};
+
+} // namespace
+
+TEST(args, empty) {
+ auto args = Args::create();
+ EXPECT_TRUE(args->run(0, nullptr));
+ {
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("", ss.str());
+ }
+ {
+ std::stringstream ss;
+ args->print_help(ss);
+ EXPECT_EQ("", ss.str());
+ }
+}
+
+TEST(args, options_not_set) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ EXPECT_TRUE(args->run(0, nullptr));
+ EXPECT_FALSE(short_only->is_set());
+ EXPECT_FALSE(short_long->is_set());
+ EXPECT_FALSE(long_only->is_set());
+ EXPECT_FALSE(short_only_req->is_set());
+ EXPECT_FALSE(short_long_req->is_set());
+ EXPECT_FALSE(long_only_req->is_set());
+ EXPECT_FALSE(short_only_opt->is_set());
+ EXPECT_FALSE(short_long_opt->is_set());
+ EXPECT_FALSE(long_only_opt->is_set());
+}
+
+TEST(args, options_not_set_arguments) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("bar").add("fum").build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ ASSERT_EQ(2, out.size());
+ EXPECT_EQ("bar", out[0]);
+ EXPECT_EQ("fum", out[1]);
+}
+
+TEST(args, options_set) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo")
+ .add("-a")
+ .add("-b")
+ .add("--cold")
+ .add("-d")
+ .add("10")
+ .add("-e")
+ .add("hamburger")
+ .add("--form=circle")
+ .add("-g")
+ .add("-h")
+ .add("--invert")
+ .build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+ EXPECT_TRUE(short_only->is_set());
+ EXPECT_TRUE(short_long->is_set());
+ EXPECT_TRUE(long_only->is_set());
+ EXPECT_TRUE(short_only_req->is_set());
+ EXPECT_TRUE(short_only_req->has_argument());
+ EXPECT_EQ("10", short_only_req->argument());
+ EXPECT_TRUE(short_long_req->is_set());
+ EXPECT_TRUE(short_long_req->has_argument());
+ EXPECT_EQ("hamburger", short_long_req->argument());
+ EXPECT_TRUE(long_only_req->is_set());
+ EXPECT_TRUE(long_only_req->has_argument());
+ EXPECT_EQ("circle", long_only_req->argument());
+ EXPECT_TRUE(short_only_opt->is_set());
+ EXPECT_FALSE(short_only_opt->has_argument());
+ EXPECT_TRUE(short_long_opt->is_set());
+ EXPECT_FALSE(short_long_opt->has_argument());
+ EXPECT_TRUE(long_only_opt->is_set());
+ EXPECT_FALSE(long_only_opt->has_argument());
+}
+
+TEST(args, options_set_variant) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo")
+ .add("-a")
+ .add("--bold")
+ .add("--cold")
+ .add("-d")
+ .add("10")
+ .add("--eat=hamburger")
+ .add("--form")
+ .add("circle")
+ .add("-g")
+ .add("--hold=foo")
+ .add("--invert=bar")
+ .build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+ EXPECT_TRUE(short_only->is_set());
+ EXPECT_TRUE(short_long->is_set());
+ EXPECT_TRUE(long_only->is_set());
+ EXPECT_TRUE(short_only_req->is_set());
+ EXPECT_TRUE(short_only_req->has_argument());
+ EXPECT_EQ("10", short_only_req->argument());
+ EXPECT_TRUE(short_long_req->is_set());
+ EXPECT_TRUE(short_long_req->has_argument());
+ EXPECT_EQ("hamburger", short_long_req->argument());
+ EXPECT_TRUE(long_only_req->is_set());
+ EXPECT_TRUE(long_only_req->has_argument());
+ EXPECT_EQ("circle", long_only_req->argument());
+ EXPECT_TRUE(short_only_opt->is_set());
+ EXPECT_FALSE(short_only_opt->has_argument());
+ EXPECT_TRUE(short_long_opt->is_set());
+ EXPECT_TRUE(short_long_opt->has_argument());
+ EXPECT_EQ("foo", short_long_opt->argument());
+ EXPECT_TRUE(long_only_opt->is_set());
+ EXPECT_TRUE(long_only_opt->has_argument());
+ EXPECT_EQ("bar", long_only_opt->argument());
+}
+
+TEST(args, options_short_missing_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("-d").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: option requires an argument -- 'd'\n", ss.str());
+}
+
+TEST(args, options_short_unknown_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("-X").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: invalid option -- 'X'\n", ss.str());
+}
+
+TEST(args, options_long_missing_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--form").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: option '--form' requires an argument\n", ss.str());
+}
+
+TEST(args, options_long_unsupported_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--cold=feet").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: option '--cold' doesn't allow an argument\n", ss.str());
+}
+
+TEST(args, options_long_unknown_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--experience").build();
+ EXPECT_FALSE(args->run(arg.c(), arg.v()));
+ std::stringstream ss;
+ args->print_error(ss);
+ EXPECT_EQ("foo: unrecognized option '--experience'\n", ss.str());
+}
+
+TEST(args, options_short_dash_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("-d").add("-").build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+ EXPECT_TRUE(short_only_req->is_set());
+ EXPECT_TRUE(short_only_req->has_argument());
+ EXPECT_EQ("-", short_only_req->argument());
+}
+
+TEST(args, options_long_dash_dash_value) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--eat").add("--").build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+ EXPECT_TRUE(short_long_req->is_set());
+ EXPECT_TRUE(short_long_req->has_argument());
+ EXPECT_EQ("--", short_long_req->argument());
+}
+
+TEST(args, options_dash_dash) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo")
+ .add("--")
+ .add("-d")
+ .add("10")
+ .add("--eat=hamburger")
+ .add("--form")
+ .add("circle")
+ .build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ ASSERT_EQ(5, out.size());
+ EXPECT_EQ("-d", out[0]);
+ EXPECT_EQ("10", out[1]);
+ EXPECT_EQ("--eat=hamburger", out[2]);
+ EXPECT_EQ("--form", out[3]);
+ EXPECT_EQ("circle", out[4]);
+ EXPECT_FALSE(short_only_req->is_set());
+ EXPECT_FALSE(short_long_req->is_set());
+ EXPECT_FALSE(long_only_req->is_set());
+}
+
+TEST(args, options_dash_dash_end) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ Builder builder;
+ auto arg = builder.add("foo").add("--").build();
+ std::vector<std::string_view> out;
+ EXPECT_TRUE(args->run(arg.c(), arg.v(), &out));
+ EXPECT_TRUE(out.empty());
+}
+
+TEST(args, help) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ std::stringstream ss;
+ args->print_help(ss, 50);
+ EXPECT_EQ(R"(Mandatory arguments to long options are mandatory
+ for short options too.
+ -a an option
+ -b, --bold set font style to bold
+ --cold use if it is cold outside
+ -d ARG distance
+ -e, --eat=FOOD what to order, what to eat?
+ --form=ARG circle, shape or something
+ else?
+ -g
+ -h, --hold=[ARG]
+ --invert=[ARG]
+)",
+ ss.str());
+}
+
+TEST(args, help_wide) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ std::stringstream ss;
+ args->print_help(ss, 100);
+ EXPECT_EQ(
+ R"(Mandatory arguments to long options are mandatory for short options too.
+ -a an option
+ -b, --bold set font style to bold
+ --cold use if it is cold outside
+ -d ARG distance
+ -e, --eat=FOOD what to order, what to eat?
+ --form=ARG circle, shape or something else?
+ -g
+ -h, --hold=[ARG]
+ --invert=[ARG]
+)",
+ ss.str());
+}
+
+TEST(args, help_narrow) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ std::stringstream ss;
+ args->print_help(ss, 35);
+ EXPECT_EQ(R"(Mandatory arguments to long options
+ are mandatory for short options
+ too.
+ -a an option
+ -b, --bold set font style to
+ bold
+ --cold use if it is cold
+ outside
+ -d ARG distance
+ -e, --eat=FOOD
+what to order, what to eat?
+ --form=ARG
+circle, shape or something else?
+ -g
+ -h, --hold=[ARG]
+ --invert=[ARG]
+)",
+ ss.str());
+}
+
+TEST(args, help_very_narrow) {
+ auto args = Args::create();
+ SETUP_OPTIONS(args);
+ std::stringstream ss;
+ args->print_help(ss, 20);
+ EXPECT_EQ(R"(Mandatory arguments
+ to long options are
+ mandatory for short
+ options too.
+ -a an option
+ -b, --bold
+set font style to
+ bold
+ --cold
+use if it is cold
+ outside
+ -d ARG distance
+ -e, --eat=FOOD
+what to order, what
+ to eat?
+ --form=ARG
+circle, shape or
+ something else?
+ -g
+ -h, --hold=[ARG]
+ --invert=[ARG]
+)",
+ ss.str());
+}
+
+TEST(args, help_long_word) {
+ auto args = Args::create();
+ std::stringstream ss;
+ auto opt = args->option('a', "arg", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ args->print_help(ss, 20);
+ EXPECT_EQ(R"(Mandatory arguments
+ to long options are
+ mandatory for short
+ options too.
+ -a, --arg
+aaaaaaaaaaaaaaaaaaa-
+aaaaaaaaaaaaaaaaaaa
+)",
+ ss.str());
+}
diff --git a/test/buffer.cc b/test/buffer.cc
new file mode 100644
index 0000000..897bd88
--- /dev/null
+++ b/test/buffer.cc
@@ -0,0 +1,270 @@
+#include "buffer.hh"
+
+#include <cstdint>
+#include <cstring>
+#include <gtest/gtest.h>
+#include <memory>
+#include <utility>
+
+namespace {
+
+enum class BufferType : uint8_t {
+ Fixed,
+ Dynamic,
+};
+
+class BufferTest : public testing::TestWithParam<BufferType> {
+ protected:
+ static std::unique_ptr<Buffer> make(size_t min_size, size_t max_size) {
+ switch (GetParam()) {
+ case BufferType::Fixed:
+ return Buffer::fixed(min_size);
+ case BufferType::Dynamic:
+ return Buffer::dynamic(min_size, max_size);
+ }
+ std::unreachable();
+ }
+};
+
+} // namespace
+
+TEST_P(BufferTest, empty) {
+ auto buffer = make(10, 100);
+ EXPECT_TRUE(buffer->empty());
+ EXPECT_FALSE(buffer->full());
+ size_t avail;
+ buffer->rptr(avail);
+ EXPECT_EQ(0, avail);
+ buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+}
+
+TEST_P(BufferTest, write_read) {
+ auto buffer = make(10, 100);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "Hello", 6);
+ buffer->commit(6);
+ EXPECT_FALSE(buffer->empty());
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(6, avail);
+ EXPECT_STREQ("Hello", reinterpret_cast<const char*>(rptr));
+ buffer->consume(3);
+ rptr = buffer->rptr(avail);
+ EXPECT_EQ(3, avail);
+ EXPECT_STREQ("lo", reinterpret_cast<const char*>(rptr));
+ buffer->consume(3);
+ EXPECT_TRUE(buffer->empty());
+}
+
+TEST_P(BufferTest, write_read2) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("01234", tmp);
+ buffer->consume(5);
+
+ wptr = buffer->wptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(wptr, "abcde", 5);
+ buffer->commit(5);
+
+ rptr = buffer->rptr(avail);
+ EXPECT_LE(5, avail);
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("56789", tmp);
+ buffer->consume(5);
+
+ rptr = buffer->rptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("abcde", tmp);
+ buffer->consume(5);
+}
+
+TEST_P(BufferTest, write_read3) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("01234", tmp);
+ buffer->consume(5);
+
+ wptr = buffer->wptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(wptr, "abcde", 5);
+ buffer->commit(5);
+
+ rptr = buffer->rptr(avail, 10);
+ EXPECT_EQ(10, avail);
+ memcpy(tmp, rptr, 10);
+ tmp[10] = '\0';
+ EXPECT_STREQ("56789abcde", tmp);
+ buffer->consume(5);
+}
+
+TEST_P(BufferTest, write_read4) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 10);
+ tmp[10] = '\0';
+ EXPECT_STREQ("0123456789", tmp);
+ buffer->consume(10);
+}
+
+TEST_P(BufferTest, write_read5) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "01234", 5);
+ buffer->commit(5);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(5, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 3);
+ tmp[3] = '\0';
+ EXPECT_STREQ("012", tmp);
+ buffer->consume(3);
+
+ wptr = buffer->wptr(avail, 8);
+ EXPECT_EQ(8, avail);
+ memcpy(wptr, "<xxxxxx>", 8);
+ buffer->commit(8);
+
+ rptr = buffer->rptr(avail, 10);
+ EXPECT_EQ(10, avail);
+ memcpy(tmp, rptr, 10);
+ tmp[10] = '\0';
+ EXPECT_STREQ("34<xxxxxx>", tmp);
+ buffer->consume(10);
+}
+
+TEST_P(BufferTest, write_read6) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+
+ auto* rptr = buffer->rptr(avail);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 8);
+ tmp[8] = '\0';
+ EXPECT_STREQ("01234567", tmp);
+ buffer->consume(8);
+
+ wptr = buffer->wptr(avail, 3);
+ EXPECT_LE(3, avail);
+ memcpy(wptr, "abc", 3);
+ buffer->commit(3);
+
+ rptr = buffer->rptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("89abc", tmp);
+ buffer->consume(5);
+}
+
+TEST_P(BufferTest, full) {
+ auto buffer = make(10, 10);
+ size_t avail;
+ auto* wptr = buffer->wptr(avail);
+ EXPECT_EQ(10, avail);
+ memcpy(wptr, "0123456789", 10);
+ buffer->commit(10);
+ EXPECT_TRUE(buffer->full());
+ std::ignore = buffer->wptr(avail);
+ EXPECT_EQ(0, avail);
+ buffer->commit(0);
+
+ auto* rptr = buffer->rptr(avail, 10);
+ EXPECT_EQ(10, avail);
+ char tmp[11];
+ memcpy(tmp, rptr, 5);
+ tmp[5] = '\0';
+ EXPECT_STREQ("01234", tmp);
+ buffer->consume(5);
+ EXPECT_FALSE(buffer->full());
+
+ wptr = buffer->wptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(wptr, "abcde", 5);
+ buffer->commit(5);
+ EXPECT_TRUE(buffer->full());
+
+ rptr = buffer->rptr(avail, 10);
+ EXPECT_EQ(10, avail);
+ memcpy(tmp, rptr, 10);
+ tmp[10] = '\0';
+ EXPECT_STREQ("56789abcde", tmp);
+ buffer->consume(10);
+ EXPECT_FALSE(buffer->full());
+ EXPECT_TRUE(buffer->empty());
+
+ std::ignore = buffer->rptr(avail, 10);
+ EXPECT_EQ(0, avail);
+ buffer->consume(0);
+}
+
+INSTANTIATE_TEST_SUITE_P(AllTypes, BufferTest,
+ testing::Values(BufferType::Fixed,
+ BufferType::Dynamic));
+
+TEST(buffer, dynamic_increase) {
+ auto buffer = Buffer::dynamic(10, 20);
+
+ size_t avail;
+ auto* wptr = buffer->wptr(avail, 15);
+ EXPECT_EQ(15, avail);
+ memcpy(wptr, "0123456789abcde", 15);
+ buffer->commit(15);
+ EXPECT_FALSE(buffer->full());
+ wptr = buffer->wptr(avail, 5);
+ EXPECT_EQ(5, avail);
+ memcpy(wptr, "fghij", 5);
+ buffer->commit(5);
+ EXPECT_TRUE(buffer->full());
+
+ auto* rptr = buffer->rptr(avail, 20);
+ EXPECT_EQ(20, avail);
+ char tmp[21];
+ memcpy(tmp, rptr, 20);
+ tmp[20] = '\0';
+ EXPECT_STREQ("0123456789abcdefghij", tmp);
+ buffer->consume(20);
+ EXPECT_FALSE(buffer->full());
+ EXPECT_TRUE(buffer->empty());
+}
diff --git a/test/io.cc b/test/io.cc
new file mode 100644
index 0000000..04d97d6
--- /dev/null
+++ b/test/io.cc
@@ -0,0 +1,203 @@
+#include "io.hh"
+
+#include "io_test_helper.hh"
+
+#include <cerrno>
+#include <cstdlib>
+#include <dirent.h>
+#include <fcntl.h>
+#include <gtest/gtest.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <utility>
+
+namespace {
+
+bool remove_recursive(int fd) {
+ auto* dir = fdopendir(fd);
+ if (!dir)
+ return false;
+ while (auto* ent = readdir(dir)) {
+ if (ent->d_name[0] == '.') {
+ if (ent->d_name[1] == '\0')
+ continue;
+ if (ent->d_name[1] == '.' && ent->d_name[2] == '\0')
+ continue;
+ }
+ bool is_dir;
+ if (ent->d_type == DT_DIR) {
+ is_dir = true;
+ } else if (ent->d_type == DT_UNKNOWN) {
+ struct stat buf;
+ if (fstatat(dirfd(dir), ent->d_name, &buf, AT_SYMLINK_NOFOLLOW) == 0) {
+ is_dir = S_ISDIR(buf.st_mode);
+ } else {
+ if (errno != ENOENT) {
+ closedir(dir);
+ return false;
+ }
+ is_dir = false;
+ }
+ } else {
+ is_dir = false;
+ }
+
+ if (is_dir) {
+ int fd2 = openat(dirfd(dir), ent->d_name, O_RDONLY | O_DIRECTORY);
+ if (fd2 == -1) {
+ if (errno != ENOENT) {
+ closedir(dir);
+ return false;
+ }
+ } else {
+ if (!remove_recursive(fd2)) {
+ closedir(dir);
+ return false;
+ }
+ }
+ }
+ if (unlinkat(dirfd(dir), ent->d_name, is_dir ? AT_REMOVEDIR : 0)) {
+ if (errno != ENOENT) {
+ closedir(dir);
+ return false;
+ }
+ }
+ }
+ closedir(dir);
+ return true;
+}
+
+class IoTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ tmpdir_ = P_tmpdir "/jkc-test-io-XXXXXX";
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ auto* ret = mkdtemp(tmpdir_.data());
+ ASSERT_EQ(ret, tmpdir_.data());
+ dirfd_ = open(tmpdir_.c_str(), O_PATH | O_DIRECTORY);
+ ASSERT_NE(-1, dirfd_);
+ }
+
+ void TearDown() override {
+ int fd = openat(dirfd_, ".", O_RDONLY | O_DIRECTORY);
+ EXPECT_NE(-1, fd);
+ if (fd != -1) {
+ EXPECT_TRUE(remove_recursive(fd));
+ }
+ close(dirfd_);
+ rmdir(tmpdir_.c_str());
+ }
+
+ [[nodiscard]] int dirfd() const { return dirfd_; }
+
+ void touch(const std::string& name, const std::string& value = "") {
+ auto fd = openat(dirfd(), name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0700);
+ EXPECT_NE(-1, fd);
+ if (fd == -1)
+ return;
+ size_t offset = 0;
+ while (offset < value.size()) {
+ auto ret = write(fd, value.data() + offset, value.size() - offset);
+ EXPECT_LT(0, ret);
+ if (ret <= 0) {
+ break;
+ }
+ offset += ret;
+ }
+ close(fd);
+ }
+
+ private:
+ int dirfd_{-1};
+ std::string tmpdir_;
+};
+
+} // namespace
+
+TEST_F(IoTest, no_such_file) {
+ auto ret = io::openat(dirfd(), "no-such-file");
+ ASSERT_FALSE(ret.has_value());
+ EXPECT_EQ(io::OpenError::NoSuchFile, ret.error());
+}
+
+TEST_F(IoTest, read_empty) {
+ touch("test");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ std::string tmp(10, ' ');
+ auto ret2 = ret.value()->read(tmp.data(), tmp.size());
+ ASSERT_FALSE(ret2.has_value());
+ EXPECT_EQ(io::ReadError::Eof, ret2.error());
+}
+
+TEST_F(IoTest, skip_empty) {
+ touch("test");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = ret.value()->skip(10);
+ ASSERT_FALSE(ret2.has_value());
+ EXPECT_EQ(io::ReadError::Eof, ret2.error());
+}
+
+TEST_F(IoTest, read) {
+ touch("test", "hello world");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ std::string tmp(12, ' ');
+ auto ret2 = ret.value()->repeat_read(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret2.has_value());
+ EXPECT_EQ(11, ret2.value());
+ tmp.resize(ret2.value());
+ EXPECT_EQ("hello world", tmp);
+}
+
+TEST_F(IoTest, skip) {
+ touch("test", "hello world");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = ret.value()->repeat_skip(6);
+ ASSERT_TRUE(ret2.has_value());
+ EXPECT_EQ(6, ret2.value());
+ std::string tmp(12, ' ');
+ auto ret3 = ret.value()->repeat_read(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret3.has_value());
+ EXPECT_EQ(5, ret3.value());
+ tmp.resize(ret3.value());
+ EXPECT_EQ("world", tmp);
+}
+
+TEST_F(IoTest, read_block) {
+ touch("test", "hello world");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = io_make_max_block(std::move(ret.value()), 2);
+ std::string tmp(12, ' ');
+ auto ret3 = ret2->repeat_read(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret3.has_value());
+ EXPECT_EQ(11, ret3.value());
+ tmp.resize(ret3.value());
+ EXPECT_EQ("hello world", tmp);
+}
+
+TEST_F(IoTest, skip_block) {
+ touch("test", "hello world");
+
+ auto ret = io::openat(dirfd(), "test");
+ ASSERT_TRUE(ret.has_value());
+ auto ret2 = io_make_max_block(std::move(ret.value()), 2);
+ auto ret3 = ret2->repeat_skip(6);
+ ASSERT_TRUE(ret3.has_value());
+ EXPECT_EQ(6, ret3.value());
+ std::string tmp(12, ' ');
+ auto ret4 = ret2->repeat_read(tmp.data(), tmp.size());
+ ASSERT_TRUE(ret4.has_value());
+ EXPECT_EQ(5, ret4.value());
+ tmp.resize(ret4.value());
+ EXPECT_EQ("world", tmp);
+}
diff --git a/test/io_test_helper.cc b/test/io_test_helper.cc
new file mode 100644
index 0000000..9ac663a
--- /dev/null
+++ b/test/io_test_helper.cc
@@ -0,0 +1,82 @@
+#include "io_test_helper.hh"
+
+#include "io.hh"
+
+#include <algorithm>
+#include <cstddef>
+#include <expected>
+#include <memory>
+#include <utility>
+
+namespace {
+
+class BreakingReader : public io::Reader {
+ public:
+ BreakingReader(std::unique_ptr<io::Reader> reader, size_t offset,
+ io::ReadError error)
+ : reader_(std::move(reader)), offset_(offset), error_(error) {}
+
+ [[nodiscard]]
+ std::expected<size_t, io::ReadError> read(void* dst, size_t max) override {
+ if (offset_ == 0)
+ return std::unexpected(error_);
+ size_t avail = std::min(offset_, max);
+ auto ret = reader_->read(dst, avail);
+ if (ret.has_value()) {
+ offset_ -= ret.value();
+ }
+ return ret;
+ }
+
+ [[nodiscard]]
+ std::expected<size_t, io::ReadError> skip(size_t max) override {
+ if (offset_ == 0)
+ return std::unexpected(error_);
+ size_t avail = std::min(offset_, max);
+ auto ret = reader_->skip(avail);
+ if (ret.has_value()) {
+ offset_ -= ret.value();
+ }
+ return ret;
+ }
+
+ private:
+ std::unique_ptr<io::Reader> reader_;
+ size_t offset_;
+ io::ReadError const error_;
+};
+
+class MaxBlockReader : public io::Reader {
+ public:
+ MaxBlockReader(std::unique_ptr<io::Reader> reader, size_t max_block_size)
+ : reader_(std::move(reader)), max_block_size_(max_block_size) {}
+
+ [[nodiscard]]
+ std::expected<size_t, io::ReadError> read(void* dst, size_t max) override {
+ size_t avail = std::min(max_block_size_, max);
+ return reader_->read(dst, avail);
+ }
+
+ [[nodiscard]]
+ std::expected<size_t, io::ReadError> skip(size_t max) override {
+ size_t avail = std::min(max_block_size_, max);
+ return reader_->skip(avail);
+ }
+
+ private:
+ std::unique_ptr<io::Reader> reader_;
+ size_t const max_block_size_;
+};
+
+} // namespace
+
+std::unique_ptr<io::Reader> io_make_breaking(std::unique_ptr<io::Reader> reader,
+ size_t offset,
+ io::ReadError error) {
+ return std::make_unique<BreakingReader>(std::move(reader), offset, error);
+}
+
+std::unique_ptr<io::Reader> io_make_max_block(
+ std::unique_ptr<io::Reader> reader, size_t max_block_size) {
+ return std::make_unique<MaxBlockReader>(std::move(reader), max_block_size);
+}
diff --git a/test/io_test_helper.hh b/test/io_test_helper.hh
new file mode 100644
index 0000000..b99b8fa
--- /dev/null
+++ b/test/io_test_helper.hh
@@ -0,0 +1,18 @@
+#ifndef IO_TEST_HELPER_HH
+#define IO_TEST_HELPER_HH
+
+#include "io.hh" // IWYU pragma: export
+
+#include <cstddef>
+#include <memory>
+
+[[nodiscard]]
+std::unique_ptr<io::Reader> io_make_breaking(
+ std::unique_ptr<io::Reader> reader, size_t offset = 0,
+ io::ReadError error = io::ReadError::Error);
+
+[[nodiscard]]
+std::unique_ptr<io::Reader> io_make_max_block(
+ std::unique_ptr<io::Reader> reader, size_t max_block_size);
+
+#endif // IO_TEST_HELPER_HH
diff --git a/test/line.cc b/test/line.cc
new file mode 100644
index 0000000..e0ea002
--- /dev/null
+++ b/test/line.cc
@@ -0,0 +1,182 @@
+#include "line.hh"
+
+#include "io_test_helper.hh"
+
+#include <cstddef>
+#include <gtest/gtest.h>
+#include <limits>
+#include <utility>
+
+TEST(line, empty) {
+ auto reader = line::open(io::memory(""));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(0, reader->number());
+}
+
+TEST(line, one_line) {
+ auto reader = line::open(io::memory("foo"));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(1, reader->number());
+}
+
+TEST(line, many_lines) {
+ auto reader = line::open(io::memory("foo\nbar\nfoobar\n"));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("bar", line.value());
+ EXPECT_EQ(2, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foobar", line.value());
+ EXPECT_EQ(3, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(3, reader->number());
+}
+
+TEST(line, many_lines_mixed) {
+ auto reader = line::open(io::memory("foo\r\nbar\rfoobar\n"));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("bar", line.value());
+ EXPECT_EQ(2, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foobar", line.value());
+ EXPECT_EQ(3, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(3, reader->number());
+}
+
+TEST(line, empty_line) {
+ auto reader = line::open(io::memory("\n"));
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(1, reader->number());
+}
+
+TEST(line, max_line) {
+ auto reader = line::open(io::memory("012345678901234567890123456789"), 10);
+ EXPECT_EQ(0, reader->number());
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("0123456789", line.value());
+ EXPECT_EQ(1, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("0123456789", line.value());
+ EXPECT_EQ(2, reader->number());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("0123456789", line.value());
+ EXPECT_EQ(3, reader->number());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+ EXPECT_EQ(3, reader->number());
+}
+
+TEST(line, read_error) {
+ auto reader = line::open(
+ io_make_breaking(io::memory("foo bar fum\nfim zam"), /* offset */ 5));
+ auto line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Error, line.error());
+}
+
+TEST(line, read_error_newline) {
+ auto reader = line::open(
+ io_make_breaking(io::memory("foo bar\r\nfim zam"), /* offset */ 8));
+ auto line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Error, line.error());
+}
+
+TEST(line, blocky) {
+ auto reader = line::open(io_make_max_block(io::memory("foo bar\r\nfim zam"),
+ /* max_block_size */ 1));
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo bar", line.value());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("fim zam", line.value());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+}
+
+TEST(line, blocky_newline) {
+ auto reader = line::open(io_make_max_block(io::memory("foo bar\r\nfim zam"),
+ /* max_block_size */ 8));
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo bar", line.value());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("fim zam", line.value());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+}
+
+TEST(line, eof_newline) {
+ auto reader = line::open(io::memory("foo bar\r"));
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo bar", line.value());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+}
+
+TEST(line, max_newline) {
+ auto reader = line::open(io::memory("foo bar\r"), 6);
+ auto line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("foo ba", line.value());
+ line = reader->read();
+ ASSERT_TRUE(line.has_value());
+ EXPECT_EQ("r", line.value());
+ line = reader->read();
+ ASSERT_FALSE(line.has_value());
+ EXPECT_EQ(io::ReadError::Eof, line.error());
+}
+
+TEST(line, max_line_overflow) {
+ EXPECT_DEATH_IF_SUPPORTED(
+ {
+ std::ignore =
+ line::open(io::memory(""), std::numeric_limits<size_t>::max());
+ },
+ "");
+}
diff --git a/test/str.cc b/test/str.cc
new file mode 100644
index 0000000..6d7edf2
--- /dev/null
+++ b/test/str.cc
@@ -0,0 +1,67 @@
+#include "str.hh"
+
+#include <gtest/gtest.h>
+
+TEST(str, split) {
+ auto ret = str::split("");
+ EXPECT_EQ(0, ret.size());
+
+ ret = str::split("", ' ', true);
+ ASSERT_EQ(1, ret.size());
+ EXPECT_EQ("", ret[0]);
+
+ ret = str::split(" ");
+ EXPECT_EQ(0, ret.size());
+
+ ret = str::split(" ", ' ', true);
+ ASSERT_EQ(2, ret.size());
+ EXPECT_EQ("", ret[0]);
+ EXPECT_EQ("", ret[1]);
+
+ ret = str::split(" a b ");
+ ASSERT_EQ(2, ret.size());
+ EXPECT_EQ("a", ret[0]);
+ EXPECT_EQ("b", ret[1]);
+
+ ret = str::split(" a b ", ' ', true);
+ ASSERT_EQ(4, ret.size());
+ EXPECT_EQ("", ret[0]);
+ EXPECT_EQ("a", ret[1]);
+ EXPECT_EQ("b", ret[2]);
+ EXPECT_EQ("", ret[3]);
+
+ ret = str::split(" a b", ' ', true);
+ ASSERT_EQ(3, ret.size());
+ EXPECT_EQ("", ret[0]);
+ EXPECT_EQ("a", ret[1]);
+ EXPECT_EQ("b", ret[2]);
+}
+
+TEST(str, trim) {
+ auto ret = str::trim("");
+ EXPECT_EQ("", ret);
+
+ ret = str::trim(" ");
+ EXPECT_EQ("", ret);
+
+ ret = str::trim(" ");
+ EXPECT_EQ("", ret);
+
+ ret = str::trim("foo");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::trim(" foo");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::trim(" foo ");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::trim("foo ");
+ EXPECT_EQ("foo", ret);
+
+ ret = str::trim(" foo bar ");
+ EXPECT_EQ("foo bar", ret);
+
+ ret = str::trim("\tfoo bar\r\n");
+ EXPECT_EQ("foo bar", ret);
+}