diff options
| -rw-r--r-- | .clang-format | 315 | ||||
| -rw-r--r-- | .clang-tidy | 2 | ||||
| -rw-r--r-- | .dir-locals.el | 12 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | meson.build | 185 | ||||
| -rw-r--r-- | src/args.cc | 389 | ||||
| -rw-r--r-- | src/args.hh | 64 | ||||
| -rw-r--r-- | src/buffer.cc | 213 | ||||
| -rw-r--r-- | src/buffer.hh | 31 | ||||
| -rw-r--r-- | src/check.hh | 39 | ||||
| -rw-r--r-- | src/config.h.in | 1 | ||||
| -rw-r--r-- | src/io.cc | 232 | ||||
| -rw-r--r-- | src/io.hh | 51 | ||||
| -rw-r--r-- | src/line.cc | 127 | ||||
| -rw-r--r-- | src/line.hh | 37 | ||||
| -rw-r--r-- | src/main.cc | 31 | ||||
| -rw-r--r-- | src/str.cc | 53 | ||||
| -rw-r--r-- | src/str.hh | 21 | ||||
| -rw-r--r-- | src/unique_fd.cc | 9 | ||||
| -rw-r--r-- | src/unique_fd.hh | 36 | ||||
| -rw-r--r-- | test/args.cc | 412 | ||||
| -rw-r--r-- | test/buffer.cc | 270 | ||||
| -rw-r--r-- | test/io.cc | 203 | ||||
| -rw-r--r-- | test/io_test_helper.cc | 82 | ||||
| -rw-r--r-- | test/io_test_helper.hh | 18 | ||||
| -rw-r--r-- | test/line.cc | 182 | ||||
| -rw-r--r-- | test/str.cc | 67 |
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); +} |
