diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2026-01-02 22:42:31 +0100 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2026-01-02 22:42:31 +0100 |
| commit | 6ed8f5151719fbc14ec0ac6d28a346d1f74cf2ca (patch) | |
| tree | ebe7588e89e1aa2ae5376acf85f3a3a7b2ec7e10 | |
61 files changed, 7358 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..27aad9a --- /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,-misc-header-include-cycle' diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..36206e4 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,13 @@ +;;; 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" +"/usr/include/librsvg-2.0" "/usr/include/gdk-pixbuf-2.0" "/usr/include/glycin-2" "/usr/include/cairo" "/usr/include/libxml2" "/usr/include/pango-1.0" "/usr/include/libmount" "/usr/include/blkid" "/usr/include/fribidi" "/usr/include/pixman-1" "/usr/include/harfbuzz" "/usr/include/freetype2" "/usr/include/libpng16" "/usr/include/glib-2.0" "/usr/lib/glib-2.0/include" "/usr/include/sysprof-6"))))))) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..7edf83c --- /dev/null +++ b/meson.build @@ -0,0 +1,373 @@ +project( + 'sawmill', + 'cpp', + 'c', + version : '0.10', + meson_version : '>= 1.3.0', + default_options : [ + 'warning_level=3', + 'cpp_std=c++26', + 'cpp_eh=none', + 'cpp_rtti=false', + 'default_library=static', + ], +) + +c_flags = [] +c_optional_flags = [] +cpp_flags = [] +if get_option('buildtype') == 'release' + # If asserts are disabled parameters and variables used for only that + # end up causing warnings + c_optional_flags += ['-Wno-unused-parameter', '-Wno-unused-variable', + '-Wno-unused-but-set-variable'] + c_flags += '-DNDEBUG' + cpp_flags += '-DNDEBUG' +endif +cpp = meson.get_compiler('cpp') +foreach flag : c_optional_flags + if cpp.has_argument(flag) + cpp_flags += flag + endif +endforeach +c = meson.get_compiler('c') +foreach flag : c_optional_flags + if c.has_argument(flag) + c_flags += flag + endif +endforeach +add_project_arguments([cpp_flags], language: 'cpp') +add_project_arguments([c_flags], language: 'c') + +jpeg = dependency('libjpeg', version: '>= 2.1.5', required: false) +png = dependency('libpng', version: '>= 1.6.0', required: true) +rsvg = dependency('librsvg-2.0', version: '>= 2.61.0', required: false) +xpm = dependency('xpm', version: '>= 3.5.0', required: false) + +image_deps = [png] + +if jpeg.found() + image_deps += jpeg +endif + +if rsvg.found() + image_deps += rsvg +endif + +if xpm.found() + image_deps += xpm +endif + +conf_data = configuration_data() +conf_data.set('version', meson.project_version()) +conf_data.set10('have_jpeg', jpeg.found()) +conf_data.set10('have_png', png.found()) +conf_data.set10('have_rsvg', rsvg.found()) +conf_data.set10('have_xpm', xpm.found()) +configure_file(input: 'src/config.h.in', + output: 'config.h', + configuration : conf_data) + +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) + +io_lib = library( + 'io', + sources: [ + 'src/check.hh', + '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) + +paths_lib = library( + 'paths', + sources: [ + 'src/paths.cc', + 'src/paths.hh', + ], + include_directories: inc, + dependencies: [str_dep], +) +paths_dep = declare_dependency( + link_with: paths_lib, + dependencies: [str_dep], +) + +cfg_lib = library( + 'cfg', + sources: [ + 'src/cfg.cc', + 'src/cfg.hh', + ], + include_directories: inc, + dependencies: [io_dep, paths_dep, str_dep], +) +cfg_dep = declare_dependency( + link_with: cfg_lib, + dependencies: [io_dep, paths_dep, str_dep], +) + +logger_lib = library( + 'logger', + sources: [ + 'src/logger.cc', + 'src/logger.hh', + ], + include_directories: inc, +) +logger_dep = declare_dependency(link_with: logger_lib) + +if xpm.found() + xpm_color_inc = include_directories('src/xpm') + xpm_color_lib = library( + 'xpm_color', + sources: [ + 'src/xpm/dix-config.h', + 'src/xpm/dix/dix_priv.h', + 'src/xpm/include/dix.h', + 'src/xpm/color.c', + ], + c_args: ['-Wno-sign-compare', '-Wno-unused-parameter'], + include_directories: xpm_color_inc, + dependencies: [xpm], + ) + xpm_color_dep = declare_dependency( + link_with: xpm_color_lib, + dependencies: [xpm], + ) + + image_deps += xpm_color_dep +endif + +image_processor_lib = library( + 'image_processor', + sources: [ + 'src/colour.cc', + 'src/colour.hh', + 'src/image.cc', + 'src/image.hh', + 'src/image_processor.cc', + 'src/image_processor.hh', + 'src/size.hh', + ], + include_directories: inc, + dependencies : image_deps, +) +image_processor_dep = declare_dependency( + link_with: image_processor_lib, + dependencies: image_deps, +) + +spawner_lib = library( + 'spawner', + sources: [ + 'src/spawner.cc', + 'src/spawner.hh', + ], + include_directories: inc, + dependencies : [ + io_dep, + ], +) +spawner_dep = declare_dependency( + link_with: spawner_lib, + dependencies: [ + io_dep, + ], +) + +sawmill = executable( + 'sawmill', + sources: [ + 'src/main.cc', + ], + include_directories: inc, + install : true, + dependencies : [ + args_dep, + cfg_dep, + image_processor_dep, + spawner_dep, + ], +) + +gtest_main_dep = dependency('gtest_main', fallback : ['gtest', 'gtest_main_dep']) +gmock_dep = dependency('gmock', fallback : ['gtest', 'gmock_dep']) + +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, +) + +testdir_lib = library( + 'testdir', + sources: [ + 'test/testdir.cc', + 'test/testdir.hh', + ], + include_directories: inc, + dependencies: test_dependencies, +) +testdir_dep = declare_dependency( + link_with: testdir_lib, + dependencies: test_dependencies, +) + +testenv_lib = library( + 'testenv', + sources: [ + 'test/testenv.cc', + 'test/testenv.hh', + ], + include_directories: inc, + dependencies: test_dependencies, +) +testenv_dep = declare_dependency( + link_with: testenv_lib, + dependencies: test_dependencies, +) + +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('paths', executable( + 'test_paths', + sources: ['test/paths.cc'], + include_directories: inc, + dependencies : [ + paths_dep, + test_dependencies, + testenv_dep, + ], +)) + +test('cfg', executable( + 'test_cfg', + sources: ['test/cfg.cc'], + include_directories: inc, + dependencies : [ + cfg_dep, + test_dependencies, + testdir_dep, + testenv_dep, + ], +)) + +test('u8', executable( + 'test_u8', + sources: [ + 'src/u.hh', + 'src/u8.hh', + 'test/u8.cc', + ], + include_directories: inc, + dependencies : [ + test_dependencies, + ], +)) + +test('image_processor', executable( + 'test_image_processor', + sources: ['test/image_processor.cc'], + cpp_args: ['-DTESTDIR="@0@/test/data"'.format(meson.current_source_dir())], + include_directories: inc, + dependencies : [ + gmock_dep, + image_processor_dep, + spawner_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/cfg.cc b/src/cfg.cc new file mode 100644 index 0000000..b65246f --- /dev/null +++ b/src/cfg.cc @@ -0,0 +1,199 @@ +#include "cfg.hh" + +#include "io.hh" +#include "line.hh" +#include "paths.hh" +#include "str.hh" + +#include <charconv> +#include <cstdint> +#include <format> +#include <functional> +#include <map> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <system_error> +#include <utility> +#include <vector> + +namespace cfg { + +namespace { + +inline char ascii_lowercase(char c) { + if (c >= 'A' && c <= 'Z') { + // NOLINTNEXTLINE(bugprone-narrowing-conversions) + return c | 0x20; + } + return c; +} + +bool ascii_lowercase_eq(std::string_view a, std::string_view b) { + if (a.size() != b.size()) + return false; + auto it_a = a.begin(); + auto it_b = b.begin(); + for (; it_a != a.end(); ++it_a, ++it_b) { + if (ascii_lowercase(*it_a) != *it_b) + return false; + } + return true; +} + +class ConfigSingleImpl : public Config { + public: + ConfigSingleImpl() = default; + + bool load(std::filesystem::path const& path, + std::vector<std::string>& errors) { + auto io_reader = io::open(std::string(path)); + if (!io_reader.has_value()) { + errors.push_back( + std::format("Unable to open {} for reading", path.string())); + return false; + } + bool all_ok = true; + auto line_reader = line::open(std::move(io_reader.value())); + while (true) { + auto line = line_reader->read(); + if (line.has_value()) { + auto trimmed = str::trim(line.value()); + if (trimmed.empty() || trimmed.front() == '#') + continue; + auto eq = trimmed.find('='); + if (eq == std::string_view::npos) { + errors.push_back( + std::format("{}:{}: Invalid line, expected key = value.", + path.string(), line_reader->number())); + all_ok = false; + continue; + } + auto key = str::trim(trimmed.substr(0, eq)); + auto value = str::trim(trimmed.substr(eq + 1)); + auto ret = values_.emplace(key, value); + if (!ret.second) { + errors.push_back(std::format("{}:{}: Duplicate key {} ignored.", + path.string(), line_reader->number(), + key)); + all_ok = false; + continue; + } + } else { + switch (line.error()) { + case io::ReadError::Eof: + break; + default: + errors.push_back(std::format("{}: Read error", path.string())); + all_ok = false; + break; + } + break; + } + } + return all_ok; + } + + [[nodiscard]] + std::optional<std::string_view> get(std::string_view name) const override { + auto it = values_.find(name); + if (it == values_.end()) + return std::nullopt; + return it->second; + } + + private: + std::map<std::string, std::string, std::less<>> values_; +}; + +class ConfigXdgImpl : public Config { + public: + ConfigXdgImpl() = default; + + bool load(std::string_view name, std::vector<std::string>& errors) { + bool all_ok = true; + for (auto const& dir : paths::config_dirs()) { + auto file = dir / name; + if (std::filesystem::exists(file)) { + auto cfg = std::make_unique<ConfigSingleImpl>(); + if (!cfg->load(file, errors)) + all_ok = false; + configs_.push_back(std::move(cfg)); + } + } + return all_ok; + } + + [[nodiscard]] + std::optional<std::string_view> get(std::string_view name) const override { + for (auto const& config : configs_) { + auto ret = config->get(name); + if (ret.has_value()) + return ret; + } + return std::nullopt; + } + + private: + std::vector<std::unique_ptr<ConfigSingleImpl>> configs_; +}; + +} // namespace + +bool Config::has(std::string_view name) const { return get(name).has_value(); } + +std::optional<int64_t> Config::get_int64(std::string_view name) const { + auto str = get(name); + if (str.has_value()) { + auto* const end = str->data() + str->size(); + int64_t ret; + // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage) + auto [ptr, ec] = std::from_chars(str->data(), end, ret); + if (ec == std::errc() && ptr == end) + return ret; + } + return std::nullopt; +} + +std::optional<uint64_t> Config::get_uint64(std::string_view name) const { + auto str = get(name); + if (str.has_value()) { + auto* const end = str->data() + str->size(); + uint64_t ret; + // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage) + auto [ptr, ec] = std::from_chars(str->data(), end, ret); + if (ec == std::errc() && ptr == end) + return ret; + } + return std::nullopt; +} + +std::optional<bool> Config::get_bool(std::string_view name) const { + auto str = get(name); + if (str.has_value()) { + if (ascii_lowercase_eq(str.value(), "true") || + ascii_lowercase_eq(str.value(), "yes")) + return true; + if (ascii_lowercase_eq(str.value(), "false") || + ascii_lowercase_eq(str.value(), "no")) + return false; + } + return std::nullopt; +} + +std::unique_ptr<Config> load_all(std::string_view name, + std::vector<std::string>& errors) { + auto ret = std::make_unique<ConfigXdgImpl>(); + ret->load(name, errors); + return ret; +} + +std::unique_ptr<Config> load_one(std::filesystem::path const& path, + std::vector<std::string>& errors) { + auto ret = std::make_unique<ConfigSingleImpl>(); + ret->load(path, errors); + return ret; +} + +} // namespace cfg diff --git a/src/cfg.hh b/src/cfg.hh new file mode 100644 index 0000000..3be9a00 --- /dev/null +++ b/src/cfg.hh @@ -0,0 +1,45 @@ +#ifndef CFG_HH +#define CFG_HH + +#include <filesystem> +#include <memory> +#include <optional> +#include <string_view> +#include <vector> + +namespace cfg { + +class Config { + public: + virtual ~Config() = default; + + [[nodiscard]] + virtual std::optional<std::string_view> get(std::string_view name) const = 0; + + [[nodiscard]] + bool has(std::string_view name) const; + [[nodiscard]] + std::optional<int64_t> get_int64(std::string_view name) const; + [[nodiscard]] + std::optional<uint64_t> get_uint64(std::string_view name) const; + [[nodiscard]] + std::optional<bool> get_bool(std::string_view name) const; + + protected: + Config() = default; + + Config(Config const&) = delete; + Config& operator=(Config const&) = delete; +}; + +[[nodiscard]] +std::unique_ptr<Config> load_all(std::string_view name, + std::vector<std::string>& errors); + +[[nodiscard]] +std::unique_ptr<Config> load_one(std::filesystem::path const& path, + std::vector<std::string>& errors); + +} // namespace cfg + +#endif // CFG_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/colour.cc b/src/colour.cc new file mode 100644 index 0000000..e9df340 --- /dev/null +++ b/src/colour.cc @@ -0,0 +1,15 @@ +#include "colour.hh" + +#include <cstdint> + +Colour::Colour() : argb(0) {} + +Colour::Colour(uint32_t argb) : argb(argb) {} + +Colour::Colour(uint8_t r, uint8_t g, uint8_t b) + : argb(0xff000000 | (static_cast<uint32_t>(r) << 16) | + (static_cast<uint32_t>(g) << 8) | b) {} + +Colour::Colour(uint8_t a, uint8_t r, uint8_t g, uint8_t b) + : argb((static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) | + (static_cast<uint32_t>(g) << 8) | b) {} diff --git a/src/colour.hh b/src/colour.hh new file mode 100644 index 0000000..8362bb3 --- /dev/null +++ b/src/colour.hh @@ -0,0 +1,20 @@ +#ifndef COLOUR_HH +#define COLOUR_HH + +#include <cstdint> + +struct Colour { + Colour(); + explicit Colour(uint32_t argb); + Colour(uint8_t r, uint8_t g, uint8_t b); + Colour(uint8_t a, uint8_t r, uint8_t g, uint8_t b); + + uint8_t alpha() const { return argb >> 24; }; + uint8_t red() const { return (argb >> 16) & 0xff; }; + uint8_t green() const { return (argb >> 8) & 0xff; }; + uint8_t blue() const { return argb & 0xff; }; + + uint32_t argb; +}; + +#endif // COLOUR_HH diff --git a/src/config.h.in b/src/config.h.in new file mode 100644 index 0000000..5280a3d --- /dev/null +++ b/src/config.h.in @@ -0,0 +1,6 @@ +#define VERSION "@version@" + +#define HAVE_JPEG @have_jpeg@ +#define HAVE_PNG @have_png@ +#define HAVE_RSVG @have_rsvg@ +#define HAVE_XPM @have_xpm@ diff --git a/src/image.cc b/src/image.cc new file mode 100644 index 0000000..3e3b485 --- /dev/null +++ b/src/image.cc @@ -0,0 +1,15 @@ +#include "image.hh" + +#include "size.hh" + +#include <cstddef> +#include <cstdint> +#include <memory> +#include <utility> + +Image::Image(Format format, Size size, size_t scanline, + std::unique_ptr<uint8_t[]> pixels) + : format_(format), + size_(size), + scanline_(scanline), + pixels_(std::move(pixels)) {} diff --git a/src/image.hh b/src/image.hh new file mode 100644 index 0000000..8191393 --- /dev/null +++ b/src/image.hh @@ -0,0 +1,62 @@ +#ifndef IMAGE_HH +#define IMAGE_HH + +#include "size.hh" + +#include <cstddef> +#include <cstdint> +#include <memory> + +class Image { + public: + enum class Format : uint8_t { + RGBA_8888, + ARGB_8888, + BGRA_8888, + ABGR_8888, + }; + + [[nodiscard]] + Size const& size() const { + return size_; + } + + [[nodiscard]] + uint32_t width() const { + return size_.width; + } + + [[nodiscard]] + uint32_t height() const { + return size_.height; + } + + [[nodiscard]] + Format format() const { + return format_; + } + + [[nodiscard]] + size_t scanline() const { + return scanline_; + } + + [[nodiscard]] + uint8_t const* data() const { + return pixels_.get(); + } + + Image(Format format, Size size, size_t scanline, + std::unique_ptr<uint8_t[]> pixels); + + private: + Image(Image const&) = delete; + Image& operator=(Image const&) = delete; + + Format const format_; + Size const size_; + size_t const scanline_; + std::unique_ptr<uint8_t[]> pixels_; +}; + +#endif // IMAGE_HH diff --git a/src/image_loader.hh b/src/image_loader.hh new file mode 100644 index 0000000..d283624 --- /dev/null +++ b/src/image_loader.hh @@ -0,0 +1,15 @@ +#ifndef IMAGE_LOADER_HH +#define IMAGE_LOADER_HH + +#include "image.hh" // IWYU pragma: export + +#include <cstdint> + +enum class ImageLoadError : uint8_t { + kNoSuchFile, + kUnsupportedFormat, + kProcessError, + kError, +}; + +#endif // IMAGE_LOADER_HH diff --git a/src/image_processor.cc b/src/image_processor.cc new file mode 100644 index 0000000..55e3cac --- /dev/null +++ b/src/image_processor.cc @@ -0,0 +1,947 @@ +#include "image_processor.hh" + +#include "colour.hh" +#include "config.h" +#include "image.hh" +#include "image_loader.hh" +#include "io.hh" +#include "size.hh" +#include "spawner.hh" + +#include <algorithm> +#include <bit> +#include <cassert> +#include <cctype> +#include <charconv> +#include <cstdint> +#include <cstring> +#include <expected> +#include <filesystem> +#include <memory> +#include <optional> +#include <span> +#include <string> +#include <system_error> +#include <utility> + +#if HAVE_JPEG || HAVE_PNG +# include <cerrno> +# include <csetjmp> +# include <cstddef> +# include <cstdio> +#endif + +#if HAVE_JPEG +# include <jpeglib.h> +#endif + +#if HAVE_PNG +# include <png.h> +#endif + +#if HAVE_RSVG +# include <cairo.h> +# include <cmath> +# include <gio/gio.h> +# include <glib-object.h> +# include <librsvg/rsvg.h> +#endif + +#if HAVE_XPM +# include <X11/xpm.h> +extern "C" { +# include "xpm/include/dix.h" +} +#endif + +namespace { + +struct Request { + bool head; + Image::Format format; + uint32_t max_width; + uint32_t max_height; + uint32_t background; + size_t path_len; +}; + +struct Response { + uint32_t width; + uint32_t height; + uint8_t error; + size_t scanline; +}; + +struct Result { + Response response; + Image::Format format; + std::unique_ptr<uint8_t[]> pixels; +}; + +std::expected<void, ImageLoadError> write_request( + io::Writer& writer, std::filesystem::path const& path, bool head, + Image::Format format, uint32_t max_width, uint32_t max_height, + Colour background) { + auto path_str = path.native(); + Request request{.head = head, .format = format, .max_width = max_width, + .max_height = max_height, .background = background.argb, + .path_len = path_str.size()}; + auto ret = writer.repeat_write(&request, sizeof(request)); + if (!ret.has_value() || ret.value() != sizeof(request)) { + return std::unexpected(ImageLoadError::kProcessError); + } + ret = writer.repeat_write(path_str.data(), path_str.size()); + if (!ret.has_value() || ret.value() != path_str.size()) { + return std::unexpected(ImageLoadError::kProcessError); + } + return {}; +} + +std::expected<Response, ImageLoadError> read_response(io::Reader& reader) { + Response response; + auto ret = reader.repeat_read(&response, sizeof(response)); + if (!ret.has_value() || ret.value() != sizeof(response)) { + return std::unexpected(ImageLoadError::kProcessError); + } + if (response.width == 0) { + return std::unexpected(static_cast<ImageLoadError>(response.error)); + } + return response; +} + +Size rescale(Size const& size, uint32_t max_width, uint32_t max_height) { + if (max_width > 0 && size.width > max_height) { + if (max_height > 0 && size.height > max_height) { + auto sx = static_cast<float>(size.width) / static_cast<float>(max_width); + auto sy = static_cast<float>(size.height) / static_cast<float>(max_height); + if (sx >= sy) { + return Size{max_width, (size.height * max_width) / size.width}; + } + return Size{(size.width * max_height) / size.height, max_height}; + } + return Size{max_width, (size.height * max_width) / size.width}; + } + if (max_height > 0 && size.height > max_height) { + return Size{(size.width * max_height) / size.height, max_height}; + } + return size; +} + +void swap_four_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height, + size_t scanline) { + assert(height * scanline <= pixels.size()); + auto* row = pixels.data(); + while (height--) { + auto* const pixel = reinterpret_cast<uint32_t*>(row); + for (uint32_t x = 0; x < width; ++x) { + pixel[x] = std::byteswap(pixel[x]); + } + row += scanline; + } +} + +void swap_two_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height, + size_t scanline, size_t offset) { + assert(offset < 4); + assert(height * scanline <= pixels.size()); + auto* row = pixels.data(); + while (height--) { + auto* pixel = row + offset; + for (uint32_t x = 0; x < width; ++x, pixel += 4) { + std::swap(pixel[0], pixel[2]); + } + row += scanline; + } +} + +void shift_left_bytes(std::span<uint8_t> pixels, uint32_t width, + uint32_t height, size_t scanline) { + assert(height * scanline <= pixels.size()); + auto* row = pixels.data(); + while (height--) { + auto* const pixel = reinterpret_cast<uint32_t*>(row); + for (uint32_t x = 0; x < width; ++x) { + pixel[x] = std::rotl(pixel[x], 8); + } + row += scanline; + } +} + +void shift_right_bytes(std::span<uint8_t> pixels, uint32_t width, + uint32_t height, size_t scanline) { + assert(height * scanline <= pixels.size()); + auto* row = pixels.data(); + while (height--) { + auto* const pixel = reinterpret_cast<uint32_t*>(row); + for (uint32_t x = 0; x < width; ++x) { + pixel[x] = std::rotr(pixel[x], 8); + } + row += scanline; + } +} + +void set_alpha_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height, + size_t scanline, size_t offset) { + assert(height * scanline <= pixels.size()); + assert(offset < 4); + auto* row = pixels.data(); + while (height--) { + auto* pixel = row + offset; + for (uint32_t x = 0; x < width; ++x, pixel += 4) { + *pixel = 0xff; + } + row += scanline; + } +} + +void insert_alpha_bytes(std::span<uint8_t> pixels, uint32_t width, + uint32_t height, size_t scanline, size_t offset) { + assert(static_cast<size_t>(width) * 4 <= scanline); + assert(height * scanline <= pixels.size()); + + if (offset == 0) { + auto* row = pixels.data(); + while (height--) { + auto* read_pixel = row + static_cast<size_t>((width - 1) * 3); + auto* write_pixel = row + static_cast<size_t>((width - 1) * 4); + for (uint32_t x = 0; x < width; ++x, read_pixel -= 3, write_pixel -= 4) { + write_pixel[0] = 0xff; + std::copy_n(read_pixel, 3, write_pixel + 1); + } + row += scanline; + } + } else { + assert(offset == 3); + + auto* row = pixels.data(); + while (height--) { + auto* read_pixel = row + static_cast<size_t>((width - 1) * 3); + auto* write_pixel = row + static_cast<size_t>((width - 1) * 4); + for (uint32_t x = 0; x < width; ++x, read_pixel -= 3, write_pixel -= 4) { + std::copy_n(read_pixel, 3, write_pixel); + write_pixel[3] = 0xff; + } + row += scanline; + } + } +} + +void rearrange_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height, + size_t scanline, Image::Format source, + Image::Format target) { + switch (source) { + case Image::Format::RGBA_8888: + switch (target) { + case Image::Format::RGBA_8888: + return; + case Image::Format::ARGB_8888: + shift_right_bytes(pixels, width, height, scanline); + return; + case Image::Format::BGRA_8888: + swap_two_bytes(pixels, width, height, scanline, 0); + return; + case Image::Format::ABGR_8888: + swap_four_bytes(pixels, width, height, scanline); + return; + } + break; + case Image::Format::ARGB_8888: + switch (target) { + case Image::Format::RGBA_8888: + shift_left_bytes(pixels, width, height, scanline); + return; + case Image::Format::ARGB_8888: + return; + case Image::Format::BGRA_8888: + swap_four_bytes(pixels, width, height, scanline); + return; + case Image::Format::ABGR_8888: + swap_two_bytes(pixels, width, height, scanline, 1); + return; + } + break; + case Image::Format::BGRA_8888: + switch (target) { + case Image::Format::RGBA_8888: + swap_two_bytes(pixels, width, height, scanline, 0); + return; + case Image::Format::ARGB_8888: + swap_four_bytes(pixels, width, height, scanline); + return; + case Image::Format::BGRA_8888: + return; + case Image::Format::ABGR_8888: + shift_left_bytes(pixels, width, height, scanline); + return; + } + break; + case Image::Format::ABGR_8888: + switch (target) { + case Image::Format::RGBA_8888: + swap_four_bytes(pixels, width, height, scanline); + return; + case Image::Format::ARGB_8888: + swap_two_bytes(pixels, width, height, scanline, 1); + return; + case Image::Format::BGRA_8888: + shift_right_bytes(pixels, width, height, scanline); + return; + case Image::Format::ABGR_8888: + return; + } + break; + } +} + +#if HAVE_JPEG +struct jpeg_extended_error_mgr { + struct jpeg_error_mgr err; + jmp_buf jmpbuf; +}; + +void jpeg_error_exit(j_common_ptr info) { + auto* err = reinterpret_cast<jpeg_extended_error_mgr*>(info->err); + jpeg_destroy(info); + longjmp(err->jmpbuf, 1); +} + +void jpeg_error_output_nothing(j_common_ptr /* info */) { + // be silent, do nothing +} + +# if BITS_IN_JSAMPLE != 8 +# error Unsupported libjpeg setup +# endif + +std::expected<Result, ImageLoadError> load_jpeg( + std::filesystem::path const& path, Request const& request) { + jpeg_extended_error_mgr err; + FILE* fh = nullptr; + if (setjmp(err.jmpbuf)) { + // This is better than exit() which is the default behavior, + // but almost guaranteed to leak some memory here. + if (fh) + fclose(fh); + return std::unexpected(ImageLoadError::kError); + } + + fh = fopen(path.c_str(), "rb"); + if (fh == nullptr) { + if (errno == ENOENT) { + return std::unexpected(ImageLoadError::kNoSuchFile); + } + return std::unexpected(ImageLoadError::kError); + } + + jpeg_decompress_struct info; + info.err = jpeg_std_error(&err.err); + info.err->error_exit = jpeg_error_exit; + info.err->output_message = jpeg_error_output_nothing; + + jpeg_create_decompress(&info); + + jpeg_stdio_src(&info, fh); + if (jpeg_read_header(&info, TRUE) != JPEG_HEADER_OK) { + jpeg_destroy_decompress(&info); + fclose(fh); + return std::unexpected(ImageLoadError::kError); + } + + if (request.head) { + Response resp{.width = info.image_width, .height = info.image_height, .error = 0, .scanline = 0}; + jpeg_destroy_decompress(&info); + fclose(fh); + return Result{.response=resp, .format=Image::Format::RGBA_8888, .pixels=nullptr}; + } + + enum class Conversion : uint8_t { + kNone, + kSetAlphaFirst, + kSetAlphaLast, + kAddAlphaFirst, + kAddAlphaLast, + } conversion = Conversion::kNone; + Image::Format output_format = request.format; + +# if JCS_EXTENSIONS +# if JCS_ALPHA_EXTENSIONS + switch (request.format) { + case Image::Format::RGBA_8888: + info.out_color_space = JCS_EXT_RGBA; + break; + case Image::Format::BGRA_8888: + info.out_color_space = JCS_EXT_BGRA; + break; + case Image::Format::ABGR_8888: + info.out_color_space = JCS_EXT_ABGR; + break; + case Image::Format::ARGB_8888: + info.out_color_space = JCS_EXT_ARGB; + break; + } +# else + switch (request.format) { + case Image::Format::RGBA_8888: + info.out_color_space = JCS_EXT_RGBX; + conversion = Conversion::kSetAlphaLast; + break; + case Image::Format::BGRA_8888: + info.out_color_space = JCS_EXT_BGRX; + conversion = Conversion::kSetAlphaLast; + break; + case Image::Format::ABGR_8888: + info.out_color_space = JCS_EXT_XBGR; + conversion = Conversion::kSetAlphaFirst; + break; + case Image::Format::ARGB_8888: + info.out_color_space = JCS_EXT_XRGB; + conversion = Conversion::kSetAlphaFirst; + break; + } +# endif +# else +# if RGB_RED != 0 || RGB_GREEN != 1 || RGB_BLUE != 2 || RGB_PIXELSIZE != 3 +# error Unsupported libjpeg setup +# endif + info.out_color_space = JCS_RGB; + switch (request.format) { + case Image::Format::RGBA_8888: + case Image::Format::BGRA_8888: + format = Image::Format::RGBA_8888; + conversion = Conversion::kAddAlphaLast; + break; + case Image::Format::ABGR_8888: + case Image::Format::ARGB_8888: + format = Image::Format::ARGB_8888; + conversion = Conversion::kAddAlphaFirst; + break; + } +# endif + + if (request.max_width || request.max_height) { + auto new_size = rescale(Size{info.image_width, info.image_height}, + request.max_width, request.max_height); + unsigned int denom = 2; + while (info.image_width / denom >= new_size.width && + info.image_height / denom >= new_size.height) { + info.scale_denom = denom; + denom *= 2; + } + } + + jpeg_start_decompress(&info); + + size_t scanline = static_cast<size_t>(info.output_width) * 4; + auto pixels = std::make_unique<uint8_t[]>(scanline * info.output_height); + + auto buffer = std::make_unique<JSAMPROW[]>(info.rec_outbuf_height); + + while (info.output_scanline < info.output_height) { + int buf_height = 1; + buffer[0] = reinterpret_cast<JSAMPROW>(pixels.get() + + (info.output_scanline * scanline)); + while (buf_height < info.rec_outbuf_height && + info.output_scanline + buf_height < info.output_height) { + buffer[buf_height] = reinterpret_cast<JSAMPROW>( + pixels.get() + ((info.output_scanline + buf_height) * scanline)); + ++buf_height; + } + jpeg_read_scanlines(&info, buffer.get(), buf_height); + } + + switch (conversion) { + case Conversion::kNone: + break; + case Conversion::kAddAlphaFirst: + insert_alpha_bytes(std::span{pixels.get(), scanline * info.output_height}, + info.output_width, info.output_height, scanline, 0); + break; + case Conversion::kAddAlphaLast: + insert_alpha_bytes(std::span{pixels.get(), scanline * info.output_height}, + info.output_width, info.output_height, scanline, 3); + break; + case Conversion::kSetAlphaFirst: + set_alpha_bytes(std::span{pixels.get(), scanline * info.output_height}, + info.output_width, info.output_height, scanline, 0); + break; + case Conversion::kSetAlphaLast: + set_alpha_bytes(std::span{pixels.get(), scanline * info.output_height}, + info.output_width, info.output_height, scanline, 3); + break; + } + + Response resp{.width = info.output_width, .height = info.output_height, .error = 0, .scanline = scanline}; + jpeg_destroy_decompress(&info); + fclose(fh); + return Result{.response = resp, .format = output_format, .pixels = std::move(pixels)}; +} +#endif + +#if HAVE_PNG +// NOLINTBEGIN(misc-include-cleaner) +void png_error(png_structp png_ptr, png_const_charp /* message */) { + // Don't write anything + longjmp(png_jmpbuf(png_ptr), 1); +} + +void png_warning(png_structp /* png_ptr */, png_const_charp /* message */) { + // Do nothing +} + +std::expected<Result, ImageLoadError> load_png( + std::filesystem::path const& path, Request const& request) { + FILE* fh = fopen(path.c_str(), "rb"); + if (fh == nullptr) { + if (errno == ENOENT) { + return std::unexpected(ImageLoadError::kNoSuchFile); + } + return std::unexpected(ImageLoadError::kError); + } + + uint8_t header[8]; + if (fread(header, 1, sizeof(header), fh) != sizeof(header)) { + fclose(fh); + return std::unexpected(ImageLoadError::kError); + } + + if (png_sig_cmp(header, 0, sizeof(header))) { + fclose(fh); + return std::unexpected(ImageLoadError::kError); + } + + png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, + png_error, png_warning); + + if (!png_ptr) { + fclose(fh); + return std::unexpected(ImageLoadError::kError); + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + fclose(fh); + return std::unexpected(ImageLoadError::kError); + } + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + fclose(fh); + return std::unexpected(ImageLoadError::kError); + } + + png_init_io(png_ptr, fh); + png_set_sig_bytes(png_ptr, sizeof(header)); + + png_set_alpha_mode(png_ptr, PNG_ALPHA_PNG, PNG_DEFAULT_sRGB); + + png_read_info(png_ptr, info_ptr); + + png_uint_32 width; + png_uint_32 height; + int bit_depth; + int color_type; + + png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, + nullptr, nullptr, nullptr); + + if (request.head) { + Response resp{.width =width, .height = height, .error = 0, .scanline = 0}; + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + fclose(fh); + return Result{.response = resp, .format = Image::Format::RGBA_8888, .pixels = nullptr}; + } + + if (request.background != 0) { + png_color_16 background_color; + Colour colour{request.background}; + background_color.red = colour.red() << 8 | colour.red(); + background_color.green = colour.green() << 8 | colour.green(); + background_color.blue = colour.blue() << 8 | colour.blue(); + png_set_background(png_ptr, &background_color, PNG_BACKGROUND_GAMMA_SCREEN, + 0, 1); + } else { + png_color_16p background_color; + if (png_get_bKGD(png_ptr, info_ptr, &background_color)) + png_set_background(png_ptr, background_color, PNG_BACKGROUND_GAMMA_FILE, + 1, 1); + } + + if (color_type == PNG_COLOR_TYPE_PALETTE) + png_set_palette_to_rgb(png_ptr); + + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) + png_set_tRNS_to_alpha(png_ptr); + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) + png_set_expand_gray_1_2_4_to_8(png_ptr); + + if (bit_depth == 16) + png_set_scale_16(png_ptr); + + if (color_type == PNG_COLOR_TYPE_GRAY || + color_type == PNG_COLOR_TYPE_GRAY_ALPHA) + png_set_gray_to_rgb(png_ptr); + + switch (request.format) { + case Image::Format::RGBA_8888: + png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER); + break; + case Image::Format::ARGB_8888: + png_set_swap_alpha(png_ptr); + png_set_filler(png_ptr, 0xff, PNG_FILLER_BEFORE); + break; + case Image::Format::BGRA_8888: + png_set_bgr(png_ptr); + png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER); + break; + case Image::Format::ABGR_8888: + png_set_bgr(png_ptr); + png_set_swap_alpha(png_ptr); + png_set_filler(png_ptr, 0xff, PNG_FILLER_BEFORE); + break; + } + + png_read_update_info(png_ptr, info_ptr); + + width = png_get_image_width(png_ptr, info_ptr); + height = png_get_image_height(png_ptr, info_ptr); + bit_depth = png_get_bit_depth(png_ptr, info_ptr); + color_type = png_get_color_type(png_ptr, info_ptr); + size_t scanline = png_get_rowbytes(png_ptr, info_ptr); + + /* Guard against integer overflow */ + if (height > PNG_SIZE_MAX / scanline) + png_error(png_ptr, "image_data buffer would be too large"); + + auto pixels = std::make_unique<uint8_t[]>(scanline * height); + auto rows = std::make_unique<png_bytep[]>(height); + + for (png_uint_32 y = 0; y < height; ++y) + rows[y] = pixels.get() + (y * scanline); + + png_read_image(png_ptr, rows.get()); + + png_read_end(png_ptr, nullptr); + + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + + fclose(fh); + + Response resp{.width = width, .height = height, .error = 0, .scanline = scanline}; + return Result{.response = resp, .format = request.format, .pixels = std::move(pixels)}; +} +// NOLINTEND(misc-include-cleaner) +#endif + +#if HAVE_RSVG +std::expected<Result, ImageLoadError> load_svg( + std::filesystem::path const& path, Request const& request) { + GFile* file = g_file_new_for_path(path.c_str()); + if (!file) { + return std::unexpected(ImageLoadError::kNoSuchFile); + } + RsvgHandle* handle = + rsvg_handle_new_from_gfile_sync(file, RSVG_HANDLE_FLAGS_NONE, nullptr, nullptr); + + if (!handle) { + g_object_unref(file); + return std::unexpected(ImageLoadError::kError); + } + g_object_unref(file); + + // TODO: Get this as part of the request? + rsvg_handle_set_dpi(handle, 96.0); + + Size size; + { + gdouble width_double; // NOLINT(misc-include-cleaner) + gdouble height_double; // NOLINT(misc-include-cleaner) + if (!rsvg_handle_get_intrinsic_size_in_pixels(handle, &width_double, + &height_double)) { + g_object_unref(handle); + return std::unexpected(ImageLoadError::kError); + } + size.width = static_cast<uint32_t>(ceil(width_double)); + size.height = static_cast<uint32_t>(ceil(height_double)); + } + + if (request.head) { + Response resp{.width = size.width, .height = size.height, .error = 0, .scanline = 0}; + g_object_unref(handle); + return Result{.response = resp, .format = Image::Format::RGBA_8888, .pixels = nullptr}; + } + + if (request.max_width || request.max_height) { + size = rescale(size, request.max_width, request.max_height); + } + + size_t scanline = static_cast<size_t>(size.width) * 4; + auto pixels = std::make_unique<uint8_t[]>(scanline * size.height); + + cairo_surface_t* surface = cairo_image_surface_create_for_data( + reinterpret_cast<unsigned char*>(pixels.get()), CAIRO_FORMAT_ARGB32, + static_cast<int>(size.width), static_cast<int>(size.height), + static_cast<int>(scanline)); + + if (!surface) { + g_object_unref(handle); + return std::unexpected(ImageLoadError::kError); + } + + cairo_t* cr = cairo_create(surface); + + if (request.background) { + cairo_rectangle(cr, 0, 0, size.width, size.height); + Colour colour{request.background}; + cairo_set_source_rgba(cr, colour.red() / 255.0, colour.green() / 255.0, + colour.blue() / 255.0, colour.alpha() / 255.0); + cairo_fill(cr); + } + + RsvgRectangle viewport = { + .x = 0.0, + .y = 0.0, + .width = static_cast<double>(size.width), + .height = static_cast<double>(size.height), + }; + + if (!rsvg_handle_render_document(handle, cr, &viewport, nullptr)) { + cairo_destroy(cr); + cairo_surface_destroy(surface); + g_object_unref(handle); + return std::unexpected(ImageLoadError::kError); + } + + cairo_destroy(cr); + cairo_surface_destroy(surface); + g_object_unref(handle); + + // Cairo uses premultiplied pixels, we don't + { + uint8_t* row = pixels.get(); + for (uint32_t y = 0; y < size.height; ++y) { + uint8_t* pixel = row; + for (uint32_t x = 0; x < size.width; ++x, pixel += 4) { + if constexpr (std::endian::native == std::endian::big) { + if (pixel[0] != 0xff && pixel[0] > 0) { + pixel[1] = (static_cast<uint16_t>(pixel[1]) * 255) / pixel[0]; + pixel[2] = (static_cast<uint16_t>(pixel[2]) * 255) / pixel[0]; + pixel[3] = (static_cast<uint16_t>(pixel[3]) * 255) / pixel[0]; + } + } else { + if (pixel[3] != 0xff && pixel[3] > 0) { + pixel[0] = (static_cast<uint16_t>(pixel[0]) * 255) / pixel[3]; + pixel[1] = (static_cast<uint16_t>(pixel[1]) * 255) / pixel[3]; + pixel[2] = (static_cast<uint16_t>(pixel[2]) * 255) / pixel[3]; + } + } + } + row += scanline; + } + } + + return Result{ + .response = Response{.width = size.width, .height = size.height, .error = 0, .scanline = scanline}, + .format = std::endian::native == std::endian::big ? Image::Format::ARGB_8888 + : Image::Format::BGRA_8888, + .pixels = std::move(pixels) + }; +} +#endif + +#if HAVE_XPM + +std::optional<uint32_t> xpm_parse_color(XpmColor const& color) { + if (color.c_color) { + auto const len = strlen(color.c_color); + if (color.c_color[0] == '#' && len == 7) { + uint32_t rgb; + if (std::from_chars(color.c_color + 1, color.c_color + 7, rgb, 16).ec == std::errc()) { + return 0xff000000 | rgb; + } + } + if (len == 4 && (strcmp(color.c_color, "None") == 0 || + strcmp(color.c_color, "none") == 0)) + return 0; + unsigned short red; + unsigned short green; + unsigned short blue; + if (dixLookupBuiltinColor(0, color.c_color, len, + &red, &green, &blue)) { + return 0xff000000 | (static_cast<uint32_t>(red & 0xff) << 16) | + (green & 0xff00) | (blue & 0xff); + } + } + if (color.m_color) { + if (strcmp(color.m_color, "white") == 0) { + return 0xffffffff; + } + if (strcmp(color.m_color, "black") == 0) { + return 0xff000000; + } + } + return std::nullopt; +} + +std::expected<Result, ImageLoadError> load_xpm( + std::filesystem::path const& path, Request const& request) { + XpmImage image; + XpmInfo info; + auto ret = XpmReadFileToXpmImage(path.c_str(), &image, &info); + if (ret != XpmSuccess) { + if (ret == XpmOpenFailed) + return std::unexpected(ImageLoadError::kNoSuchFile); + return std::unexpected(ImageLoadError::kError); + } + + if (request.head) { + Response resp{.width = image.width, .height = image.height, .error = 0, .scanline = 0}; + XpmFreeXpmImage(&image); + XpmFreeXpmInfo(&info); + return Result{.response = resp, .format = Image::Format::RGBA_8888, .pixels = nullptr}; + } + + size_t scanline = static_cast<size_t>(image.width) * 4; + auto pixels = std::make_unique<uint8_t[]>(scanline * image.height); + + auto colors = std::make_unique<uint32_t[]>(image.ncolors); + for (unsigned int i = 0; i < image.ncolors; ++i) { + auto ret = xpm_parse_color(image.colorTable[i]); + if (ret.has_value()) { + colors[i] = ret.value(); + } else { + return std::unexpected(ImageLoadError::kUnsupportedFormat); + } + } + + auto* out_row = pixels.get(); + auto* in = image.data; + for (unsigned int y = 0; y < image.height; ++y) { + auto* out_pixel = reinterpret_cast<uint32_t*>(out_row); + for (unsigned int x = 0; x < image.width; ++x) { + out_pixel[x] = colors[*in++]; + } + out_row += scanline; + } + + XpmFreeXpmImage(&image); + XpmFreeXpmInfo(&info); + + return Result{ + .response = Response{.width = image.width, .height = image.height, .error = 0, .scanline = scanline}, + .format = std::endian::native == std::endian::big ? Image::Format::ARGB_8888 : Image::Format::BGRA_8888, + .pixels = std::move(pixels), + }; +} +#endif + +} // namespace + +namespace image_processor { + +std::expected<Size, ImageLoadError> peek(Process& process, + std::filesystem::path const& path) { + auto ret = write_request(process.writer(), path, true, + Image::Format::ARGB_8888, 0, 0, {}); + if (!ret) { + return std::unexpected(ret.error()); + } + auto ret2 = read_response(process.reader()); + if (!ret2) + return std::unexpected(ret2.error()); + return Size{ret2->width, ret2->height}; +} + +std::expected<std::unique_ptr<Image>, ImageLoadError> load( + Process& process, std::filesystem::path const& path, Image::Format format, + uint32_t max_width, uint32_t max_height, std::optional<Colour> background) { + auto ret = write_request(process.writer(), path, false, format, max_width, + max_height, background.value_or({})); + if (!ret) { + return std::unexpected(ret.error()); + } + auto ret2 = read_response(process.reader()); + if (!ret2) + return std::unexpected(ret2.error()); + size_t size = static_cast<size_t>(ret2->height) * ret2->scanline; + auto pixels = std::make_unique<uint8_t[]>(size); + auto ret3 = process.reader().repeat_read(pixels.get(), size); + if (!ret3.has_value() || ret3.value() != size) { + return std::unexpected(ret.error()); + } + return std::make_unique<Image>(format, Size{ret2->width, ret2->height}, + ret2->scanline, std::move(pixels)); +} + +int run(std::unique_ptr<io::Reader> reader, + std::unique_ptr<io::Writer> writer) { + while (true) { + Request request; + auto ret = reader->repeat_read(&request, sizeof(request)); + if (!ret.has_value() || ret.value() != sizeof(request)) + break; + std::string path_str(request.path_len, ' '); + ret = reader->repeat_read(path_str.data(), path_str.size()); + if (!ret.has_value() || ret.value() != path_str.size()) + break; + std::filesystem::path path(std::move(path_str)); + + auto extension = path.extension().native(); + std::ranges::transform(extension, extension.begin(), + [](unsigned char c){ return std::tolower(c); }); + std::expected<Result, ImageLoadError> result{ + std::unexpected(ImageLoadError::kUnsupportedFormat)}; +#if HAVE_JPEG + if (extension == ".jpeg" || extension == ".jpg" || extension == ".jpe" || + extension == ".jfif" || extension == ".jfi" || extension == ".jif") { + result = load_jpeg(path, request); + } +#endif +#if HAVE_PNG + if (path.extension() == ".png") { + result = load_png(path, request); + } +#endif +#if HAVE_RSVG + if (path.extension() == ".svg" || path.extension() == ".svgz") { + result = load_svg(path, request); + } +#endif +#if HAVE_XPM + if (path.extension() == ".xpm") { + result = load_xpm(path, request); + } +#endif + if (!request.head && result.has_value() && + result->format != request.format) { + auto size = static_cast<size_t>(result->response.height) * + result->response.scanline; + rearrange_bytes(std::span(result->pixels.get(), size), + result->response.width, result->response.height, + result->response.scanline, result->format, + request.format); + } + if (result.has_value()) { + auto ret2 = + writer->repeat_write(&result->response, sizeof(result->response)); + if (!ret2.has_value() || ret2.value() != sizeof(result->response)) + break; + if (!request.head) { + auto size = static_cast<size_t>(result->response.height) * + result->response.scanline; + ret2 = writer->repeat_write(result->pixels.get(), size); + if (!ret2.has_value() || ret2.value() != size) + break; + } + } else { + Response response{.width = 0, .height = 0, .error = static_cast<uint8_t>(result.error()), .scanline = 0}; + auto ret2 = writer->repeat_write(&response, sizeof(response)); + if (!ret2.has_value() || ret2.value() != sizeof(response)) + break; + } + } + return -1; +} + +} // namespace image_processor diff --git a/src/image_processor.hh b/src/image_processor.hh new file mode 100644 index 0000000..551ac4f --- /dev/null +++ b/src/image_processor.hh @@ -0,0 +1,29 @@ +#ifndef IMAGE_PROCESSOR_HH +#define IMAGE_PROCESSOR_HH + +#include "colour.hh" +#include "image_loader.hh" +#include "io.hh" +#include "spawner.hh" + +#include <filesystem> +#include <memory> +#include <optional> + +namespace image_processor { + +[[nodiscard]] +std::expected<Size, ImageLoadError> peek(Process& process, + std::filesystem::path const& path); + +[[nodiscard]] +std::expected<std::unique_ptr<Image>, ImageLoadError> load( + Process& process, std::filesystem::path const& path, Image::Format format, + uint32_t max_width = 0, uint32_t max_height = 0, + std::optional<Colour> background = std::nullopt); + +int run(std::unique_ptr<io::Reader> reader, std::unique_ptr<io::Writer> writer); + +} // namespace image_processor + +#endif // IMAGE_PROCESSOR_HH diff --git a/src/io.cc b/src/io.cc new file mode 100644 index 0000000..660c5f7 --- /dev/null +++ b/src/io.cc @@ -0,0 +1,358 @@ +#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; + } + + [[nodiscard]] + int raw_fd() const override { + return fd_.get(); + } + + 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; + } + + [[nodiscard]] + int raw_fd() const override { + return -1; + } + + 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_); } + + [[nodiscard]] + int raw_fd() const override { + return fd_.get(); + } + + 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_; +}; + +class BasicWriter : public Writer { + public: + explicit BasicWriter(unique_fd fd) : fd_(std::move(fd)) {} + + [[nodiscard]] + std::expected<size_t, WriteError> write(void const* dst, + size_t size) override { + ssize_t ret = ::write( + fd_.get(), dst, + std::min(static_cast<size_t>(std::numeric_limits<ssize_t>::max()), + size)); + if (ret < 0) { + switch (errno) { + case EINTR: + return write(dst, size); + default: + return std::unexpected(WriteError::Error); + } + } else if (ret == 0 && size > 0) { + return std::unexpected(WriteError::Error); + } + return ret; + } + + [[nodiscard]] + std::expected<void, WriteError> close() override { + if (::close(fd_.release()) == 0) + return {}; + return std::unexpected(WriteError::Error); + } + + [[nodiscard]] + int raw_fd() const override { + return fd_.get(); + } + + private: + unique_fd fd_; +}; + +} // 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<size_t, WriteError> Writer::repeat_write(void const* dst, + size_t size) { + auto ret = write(dst, size); + if (!ret.has_value() || ret.value() == size) + return ret; + + char const* d = reinterpret_cast<char const*>(dst); + size_t offset = ret.value(); + while (true) { + ret = write(d + offset, size - offset); + if (!ret.has_value()) + break; + offset += ret.value(); + if (offset == size) + 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)); +} + +std::expected<std::unique_ptr<Writer>, CreateError> create( + const std::string& file_path, bool replace_existing) { + return createat(AT_FDCWD, file_path, replace_existing); +} + +std::expected<std::unique_ptr<Writer>, CreateError> createat( + int dirfd, const std::string& file_path, bool replace_existing) { + int flags = O_WRONLY | O_CREAT; + if (replace_existing) { + flags |= O_TRUNC; + } else { + flags |= O_EXCL; + } + unique_fd fd(::openat(dirfd, file_path.c_str(), flags, 0666)); + if (fd) { + return std::make_unique<BasicWriter>(std::move(fd)); + } + CreateError err; + switch (errno) { + case EINTR: + return createat(dirfd, file_path, replace_existing); + case EACCES: + err = CreateError::NoAccess; + break; + case EEXIST: + err = CreateError::Exists; + break; + default: + err = CreateError::Error; + break; + } + return std::unexpected(err); +} + +std::expected<std::pair<std::unique_ptr<Reader>, std::unique_ptr<Writer>>, + PipeError> +pipe() { + int fds[2]; + if (::pipe(fds) == 0) { + return std::make_pair(reader_from_raw(fds[0]), writer_from_raw(fds[1])); + } + return std::unexpected(PipeError::Error); +} + +std::unique_ptr<Reader> reader_from_raw(int fd) { + return std::make_unique<BasicReader>(unique_fd{fd}); +} + +std::unique_ptr<Writer> writer_from_raw(int fd) { + return std::make_unique<BasicWriter>(unique_fd{fd}); +} + +} // namespace io diff --git a/src/io.hh b/src/io.hh new file mode 100644 index 0000000..90ad52e --- /dev/null +++ b/src/io.hh @@ -0,0 +1,109 @@ +#ifndef IO_HH +#define IO_HH + +#include <cstddef> +#include <expected> +#include <memory> +#include <string> +#include <utility> + +namespace io { + +enum class ReadError : uint8_t { + 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 WriteError : uint8_t { + Error, +}; + +enum class OpenError : uint8_t { + NoSuchFile, + NoAccess, + Error, +}; + +enum class CreateError : uint8_t { + Exists, + NoAccess, + Error, +}; + +enum class PipeError : uint8_t { + 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); + + [[nodiscard]] + virtual int raw_fd() const = 0; + + protected: + Reader() = default; + + Reader(Reader const&) = delete; + Reader& operator=(Reader const&) = delete; +}; + +class Writer { + public: + virtual ~Writer() = default; + + [[nodiscard]] + virtual std::expected<size_t, WriteError> write(void const* dst, + size_t size) = 0; + + // Use this instead of the destructor if you care if the call returned an + // error or not. Regardless of returned value, after this method is called + // write will always fail. + [[nodiscard]] + virtual std::expected<void, WriteError> close() = 0; + + [[nodiscard]] + virtual int raw_fd() const = 0; + + [[nodiscard]] std::expected<size_t, WriteError> repeat_write(void const* dst, + size_t size); + + protected: + Writer() = default; + + Writer(Writer const&) = delete; + Writer& operator=(Writer 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); + +[[nodiscard]] std::expected<std::unique_ptr<Writer>, CreateError> create( + const std::string& file_path, bool replace_existing); +[[nodiscard]] std::expected<std::unique_ptr<Writer>, CreateError> createat( + int dirfd, const std::string& file_path, bool replace_existing); + +[[nodiscard]] std::expected< + std::pair<std::unique_ptr<Reader>, std::unique_ptr<Writer>>, PipeError> +pipe(); + +// Takes ownership of the fd. +[[nodiscard]] std::unique_ptr<Reader> reader_from_raw(int fd); +[[nodiscard]] std::unique_ptr<Writer> writer_from_raw(int fd); + +} // 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/logger.cc b/src/logger.cc new file mode 100644 index 0000000..e6198da --- /dev/null +++ b/src/logger.cc @@ -0,0 +1,125 @@ +#include "logger.hh" + +#include <cstdint> +#include <format> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> + +namespace logger { + +namespace { + +class BaseLogger : public Logger { + protected: + enum class Level : uint8_t { + kError, + kWarning, + kInfo, + kDebug, + }; + + public: + void err(std::string_view message) final { write(Level::kError, message); } + + void warn(std::string_view message) final { write(Level::kWarning, message); } + + void info(std::string_view message) final { write(Level::kInfo, message); } + +#ifndef NDEBUG + void dbg(std::string_view message) final { write(Level::kDebug, message); } +#endif + + protected: + BaseLogger() = default; + + virtual void write(Level level, std::string_view message) = 0; +}; + +class NoopLogger : public BaseLogger { + public: + NoopLogger() = default; + + protected: + void write(Level /* level */, std::string_view /* message */) override {} +}; + +class StderrLogger : public BaseLogger { + public: + explicit StderrLogger(bool verbose) : verbose_(verbose) {} + + protected: + void write(Level level, std::string_view message) override { + switch (level) { + case Level::kError: + std::cerr << "Error: "; + break; + case Level::kWarning: + std::cerr << "Warning: "; + break; + case Level::kInfo: + if (!verbose_) + return; + std::cerr << "Info: "; + break; + case Level::kDebug: + if (!verbose_) + return; + std::cerr << "Debug: "; + break; + } + std::cerr << message << '\n'; + } + + private: + bool const verbose_; +}; + +class PrefixLogger : public Logger { + public: + PrefixLogger(Logger& logger, std::string prefix) + : logger_(logger), prefix_(std::move(prefix)) {} + + void err(std::string_view message) override { + logger_.err(std::format("{}: {}", prefix_, message)); + } + + void warn(std::string_view message) override { + logger_.warn(std::format("{}: {}", prefix_, message)); + } + + void info(std::string_view message) override { + logger_.info(std::format("{}: {}", prefix_, message)); + } + +#ifndef NDEBUG + void dbg(std::string_view message) override { + logger_.dbg(std::format("{}: {}", prefix_, message)); + } +#endif + + private: + Logger& logger_; + std::string prefix_; +}; + +} // namespace + +[[nodiscard]] +std::unique_ptr<Logger> noop() { + return std::make_unique<NoopLogger>(); +} + +[[nodiscard]] +std::unique_ptr<Logger> stderr(bool verbose) { + return std::make_unique<StderrLogger>(verbose); +} + +[[nodiscard]] +std::unique_ptr<Logger> prefix(Logger& logger, std::string prefix) { + return std::make_unique<PrefixLogger>(logger, std::move(prefix)); +} + +} // namespace logger diff --git a/src/logger.hh b/src/logger.hh new file mode 100644 index 0000000..df6879a --- /dev/null +++ b/src/logger.hh @@ -0,0 +1,41 @@ +#ifndef LOGGER_HH +#define LOGGER_HH + +#include <memory> +#include <string> +#include <string_view> + +namespace logger { + +class Logger { + public: + virtual ~Logger() = default; + + virtual void err(std::string_view message) = 0; + virtual void warn(std::string_view message) = 0; + virtual void info(std::string_view message) = 0; + +#if defined(NDEBUG) + void dbg(std::string_view) {} +#else + virtual void dbg(std::string_view message) = 0; +#endif + + protected: + Logger() = default; + Logger(Logger const&) = delete; + Logger& operator=(Logger const&) = delete; +}; + +[[nodiscard]] +std::unique_ptr<Logger> noop(); + +[[nodiscard]] +std::unique_ptr<Logger> stderr(bool verbose = false); + +[[nodiscard]] +std::unique_ptr<Logger> prefix(Logger& logger, std::string prefix); + +} // namespace logger + +#endif // LOGGER_HH diff --git a/src/main.cc b/src/main.cc new file mode 100644 index 0000000..9756c87 --- /dev/null +++ b/src/main.cc @@ -0,0 +1,47 @@ +#include "args.hh" +#include "config.h" +#include "image_processor.hh" +#include "spawner.hh" + +#include <stdlib.h> // NOLINT(modernize-deprecated-headers) +#include <iostream> +#include <string_view> +#include <vector> + +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"); + std::vector<std::string_view> arguments; + if (!args->run(argc, argv, &arguments)) { + args->print_error(std::cerr); + std::cerr << "Try `sawmill --help` for usage.\n"; + return EXIT_FAILURE; + } + if (opt_help->is_set()) { + std::cout << "Usage: `sawmill [OPTION]...`\n" + << "This is a window manager for Wayland.\n" + << "\n"; + args->print_help(std::cout); + return EXIT_SUCCESS; + } + if (opt_version->is_set()) { + std::cout << "sawmill " << VERSION << " written by Joel Klinghed.\n"; + return EXIT_SUCCESS; + } + auto spawner = Spawner::create(); + if (spawner.has_value()) { + auto processor = spawner.value()->run(Spawner::Exec::kImageProcessor); + if (processor.has_value()) { + auto ret = image_processor::peek(**processor, "/home/the_jk/Downloads/image2vector.svg"); + if (ret.has_value()) { + std::cout << ret->width << "x" << ret->height << '\n'; + } else { + std::cerr << "Bad file\n"; + } + return EXIT_SUCCESS; + } + } + std::cerr << "Unable to launch spawner.\n"; + return EXIT_FAILURE; +} diff --git a/src/paths.cc b/src/paths.cc new file mode 100644 index 0000000..091be6e --- /dev/null +++ b/src/paths.cc @@ -0,0 +1,100 @@ +#include "paths.hh" + +#include "str.hh" + +#include <algorithm> +#include <cerrno> +#include <cstddef> +#include <cstdlib> +#include <iterator> +#include <memory> +#include <pwd.h> +#include <string_view> +#include <unistd.h> +#include <unordered_set> +#include <vector> + +namespace paths { + +namespace { + +std::vector<std::filesystem::path> xdg_read_dirs( + const char* userdir_env_name, std::string_view userdir_home_default, + const char* dirs_env_name, + std::vector<std::filesystem::path> const& dirs_default_value) { + std::vector<std::filesystem::path> ret; + std::unordered_set<std::filesystem::path> tmp; + auto* env_userdir = getenv(userdir_env_name); + if (env_userdir != nullptr && env_userdir[0] != '\0') { + ret.emplace_back(env_userdir); + } else { + ret.emplace_back(home() / userdir_home_default); + } + tmp.insert(ret.back()); + auto* env_dirs = getenv(dirs_env_name); + if (env_dirs != nullptr && env_dirs[0] != '\0') { + for (auto dir : str::split(env_dirs, ':')) { + if (tmp.emplace(dir).second) { + ret.emplace_back(dir); + } + } + } else { + std::ranges::copy_if( + dirs_default_value, std::back_inserter(ret), + [&tmp](auto const& dir) { return tmp.emplace(dir).second; }); + } + return ret; +} + +} // namespace + +std::filesystem::path home() { + { + auto* str = getenv("HOME"); + if (str != nullptr && str[0] != '\0') + return str; + } + + { + auto maybe_size = sysconf(_SC_GETPW_R_SIZE_MAX); + size_t size = maybe_size > 0 ? static_cast<size_t>(maybe_size) : 1024; + auto buffer = std::make_unique<char[]>(size); + struct passwd pwd; + struct passwd* ret; + int err; + while (true) { + err = getpwuid_r(geteuid(), &pwd, buffer.get(), size, &ret); + if (err == 0) + break; + if (err != ERANGE) + break; + auto new_size = size * 2; + if (new_size < size) + break; + buffer = std::make_unique<char[]>(new_size); + size = new_size; + } + if (err == 0 && ret) { + if (ret->pw_dir != nullptr && ret->pw_dir[0] != '\0') { + return ret->pw_dir; + } + } + } + + return "/"; +} + +std::vector<std::filesystem::path> config_dirs() { + static const std::vector<std::filesystem::path> fallback{"/etc/xdg"}; + return xdg_read_dirs("XDG_CONFIG_HOME", ".config", "XDG_CONFIG_DIRS", + fallback); +} + +std::vector<std::filesystem::path> data_dirs() { + static const std::vector<std::filesystem::path> fallback{"/usr/local/share/", + "/usr/share/"}; + return xdg_read_dirs("XDG_DATA_HOME", ".local/share", "XDG_DATA_DIRS", + fallback); +} + +} // namespace paths diff --git a/src/paths.hh b/src/paths.hh new file mode 100644 index 0000000..8bd4c75 --- /dev/null +++ b/src/paths.hh @@ -0,0 +1,20 @@ +#ifndef PATHS_HH +#define PATHS_HH + +#include <filesystem> // IWYU pragma: export +#include <vector> + +namespace paths { + +// Return home directory, goes HOME, /etc/passwd entry, / in that order. +std::filesystem::path home(); + +// Return config directories for reading, in order of priority. +std::vector<std::filesystem::path> config_dirs(); + +// Return data directories for reading, in order of priority. +std::vector<std::filesystem::path> data_dirs(); + +} // namespace paths + +#endif // PATHS_HH diff --git a/src/size.hh b/src/size.hh new file mode 100644 index 0000000..8fcc53f --- /dev/null +++ b/src/size.hh @@ -0,0 +1,30 @@ +#ifndef SIZE_HH +#define SIZE_HH + +#include <compare> +#include <cstdint> + +struct Size final { + Size() : width(0), height(0) {} + Size(uint32_t width, uint32_t height) : width(width), height(height) {} + + bool empty() const { return width == 0 || height == 0; } + + operator bool() const { return !empty(); } + + std::strong_ordering operator<=>(Size const& other) const { + if (empty()) { + return other.empty() ? std::strong_ordering::equal + : std::strong_ordering::less; + } + if (other.empty()) + return std::strong_ordering::greater; + auto ret = width <=> other.width; + return ret == std::strong_ordering::equal ? height <=> other.height : ret; + } + + uint32_t width; + uint32_t height; +}; + +#endif // SIZE_HH diff --git a/src/spawner.cc b/src/spawner.cc new file mode 100644 index 0000000..e49ac34 --- /dev/null +++ b/src/spawner.cc @@ -0,0 +1,236 @@ +#include "spawner.hh" + +#include "image_processor.hh" +#include "io.hh" +#include "unique_fd.hh" + +#include <cassert> +#include <csignal> +#include <expected> +#include <linux/prctl.h> +#include <memory> +#include <sys/prctl.h> +#include <sys/syscall.h> +#include <unistd.h> +#include <utility> + +namespace { + +int pidfd_getfd(int pidfd, int targetfd, unsigned int flags) { + return static_cast<int>(syscall(SYS_pidfd_getfd, pidfd, targetfd, flags)); +} + +// NOLINTNEXTLINE(misc-include-cleaner) +int pidfd_open(pid_t pid, unsigned int flags) { + return static_cast<int>(syscall(SYS_pidfd_open, pid, flags)); +} + +class ProcessImpl : public Process { + public: + ProcessImpl( + pid_t pid, + std::pair<std::unique_ptr<io::Reader>, std::unique_ptr<io::Writer>> pipe) + : pid_(pid), pipe_(std::move(pipe)) { + assert(pid_); + } + + [[nodiscard]] + pid_t pid() const { return pid_; } + + [[nodiscard]] + io::Reader& reader() const override { return *pipe_.first; } + + [[nodiscard]] + io::Writer& writer() const override { return *pipe_.second; } + + void kill() { + if (pid_ != 0) { + ::kill(pid_, SIGTERM); // NOLINT(misc-include-cleaner) + pid_ = 0; + } + } + + private: + pid_t pid_; + std::pair<std::unique_ptr<io::Reader>, std::unique_ptr<io::Writer>> pipe_; +}; + +struct Request { + Spawner::Exec exec; + int reader; + int writer; +}; + +struct Response { + pid_t pid; +}; + +void spawner_runner(unique_fd parent, std::unique_ptr<io::Reader> reader, + std::unique_ptr<io::Writer> writer) { + while (true) { + Request request; + { + auto ret = reader->repeat_read(&request, sizeof(request)); + if (!ret.has_value() || ret.value() != sizeof(request)) { + break; + } + } + + // Need to not return a response before child has duped fds. + // So use a short-lived pipe to sync child start. + auto sync_pipe = io::pipe(); + pid_t child_pid = 0; + + if (sync_pipe.has_value()) { + child_pid = fork(); + if (child_pid == 0) { + // Child process + sync_pipe->first.reset(); + reader.reset(); + writer.reset(); + + unique_fd child_reader_fd{pidfd_getfd(parent.get(), request.reader, 0)}; + unique_fd child_writer_fd{pidfd_getfd(parent.get(), request.writer, 0)}; + + parent.reset(); + + if (child_reader_fd && child_writer_fd) { + char c = 1; + if (sync_pipe->second->write(&c, sizeof(c)).has_value()) { + auto child_reader = io::reader_from_raw(child_reader_fd.release()); + auto child_writer = io::writer_from_raw(child_writer_fd.release()); + + switch (request.exec) { + case Spawner::Exec::kImageProcessor: + _exit(image_processor::run(std::move(child_reader), + std::move(child_writer))); + } + } + } + + _exit(1); + // _exit obviously never returns but to help tools and compilers… + return; + } + + // Parent process + if (child_pid == -1) { + // fork() failed, we use zero as error value. + child_pid = 0; + } else { + sync_pipe->second.reset(); + + char c = 1; + std::ignore = sync_pipe->first->read(&c, sizeof(c)); + } + } + + Response response{child_pid}; + { + auto ret = writer->repeat_write(&response, sizeof(response)); + if (!ret.has_value() || ret.value() != sizeof(response)) { + break; + } + } + } +} + +class SpawnerImpl : public Spawner { + public: + explicit SpawnerImpl(std::unique_ptr<ProcessImpl> process) + : process_(std::move(process)) {} + + ~SpawnerImpl() override { + if (process_) { + process_->kill(); + } + } + + [[nodiscard]] + std::expected<std::unique_ptr<Process>, Error> run(Exec exec) override { + if (!process_) { + return std::unexpected(Error::kError); + } + + auto reader_pipe = io::pipe(); + auto writer_pipe = io::pipe(); + if (!reader_pipe.has_value() || !writer_pipe.has_value()) { + return std::unexpected(Error::kError); + } + + Request req{ + .exec = exec, + .reader = writer_pipe->first->raw_fd(), + .writer = reader_pipe->second->raw_fd(), + }; + { + auto ret = process_->writer().repeat_write(&req, sizeof(req)); + if (!ret.has_value() || ret.value() != sizeof(req)) { + process_->kill(); + process_.reset(); + return std::unexpected(Error::kError); + } + } + + Response resp; + { + auto ret = process_->reader().repeat_read(&resp, sizeof(resp)); + if (!ret.has_value() || ret.value() != sizeof(resp)) { + process_->kill(); + process_.reset(); + return std::unexpected(Error::kError); + } + } + + if (resp.pid == 0) + return std::unexpected(Error::kError); + + return std::make_unique<ProcessImpl>( + resp.pid, std::make_pair(std::move(reader_pipe->first), + std::move(writer_pipe->second))); + } + + private: + std::unique_ptr<ProcessImpl> process_; +}; + +} // namespace + +std::expected<std::unique_ptr<Spawner>, Spawner::Error> Spawner::create() { + auto reader_pipe = io::pipe(); + auto writer_pipe = io::pipe(); + if (!reader_pipe.has_value() || !writer_pipe.has_value()) { + return std::unexpected(Error::kError); + } + + auto pid = getpid(); + unique_fd pidfd{pidfd_open(pid, 0)}; + if (!pidfd) { + return std::unexpected(Error::kError); + } + + // Needed for pidfd_getfd to work in child. + if (prctl(PR_SET_PTRACER, pid) != 0) { + return std::unexpected(Error::kError); + } + + pid = fork(); + if (pid == 0) { + // Child process + reader_pipe->first.reset(); + writer_pipe->second.reset(); + + spawner_runner(std::move(pidfd), std::move(writer_pipe->first), + std::move(reader_pipe->second)); + _exit(0); + } + + // Parent process + if (pid == -1) { + return std::unexpected(Error::kError); + } + + return std::make_unique<SpawnerImpl>(std::make_unique<ProcessImpl>( + pid, std::make_pair(std::move(reader_pipe->first), + std::move(writer_pipe->second)))); +} diff --git a/src/spawner.hh b/src/spawner.hh new file mode 100644 index 0000000..c4f511b --- /dev/null +++ b/src/spawner.hh @@ -0,0 +1,49 @@ +#ifndef SPAWNER_HH +#define SPAWNER_HH + +#include <cstdint> +#include <expected> +#include <memory> + +namespace io { +class Reader; +class Writer; +}; // namespace io + +class Process { + public: + virtual ~Process() = default; + + virtual io::Reader& reader() const = 0; + virtual io::Writer& writer() const = 0; + + protected: + Process() = default; + Process(Process const&) = delete; + Process& operator=(Process const&) = delete; +}; + +class Spawner { + public: + enum class Exec : uint8_t { + kImageProcessor, + }; + + enum class Error : uint8_t { + kError, + }; + + virtual ~Spawner() = default; + + [[nodiscard]] + virtual std::expected<std::unique_ptr<Process>, Error> run(Exec exec) = 0; + + static std::expected<std::unique_ptr<Spawner>, Error> create(); + + protected: + Spawner() = default; + Spawner(Spawner const&) = delete; + Spawner& operator=(Spawner const&) = delete; +}; + +#endif // SPAWNER_HH diff --git a/src/str.cc b/src/str.cc new file mode 100644 index 0000000..129e0f3 --- /dev/null +++ b/src/str.cc @@ -0,0 +1,94 @@ +#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; +} + +void split(std::string_view str, std::vector<std::string_view>& out, + std::string_view 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 + separator.size(); + } +} + +std::vector<std::string_view> split(std::string_view str, + std::string_view 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); +} + +std::string_view ltrim(std::string_view str) { + size_t s = 0; + size_t const e = str.size(); + while (s < e && is_space(str[s])) + ++s; + return str.substr(s, e - s); +} + +std::string_view rtrim(std::string_view str) { + size_t e = str.size(); + while (e > 0 && is_space(str[e - 1])) + --e; + return str.substr(0, e); +} + +} // namespace str diff --git a/src/str.hh b/src/str.hh new file mode 100644 index 0000000..c736f4b --- /dev/null +++ b/src/str.hh @@ -0,0 +1,34 @@ +#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); + +void split(std::string_view str, std::vector<std::string_view>& out, + std::string_view separator, bool keep_empty = false); + +[[nodiscard]] std::vector<std::string_view> split(std::string_view str, + std::string_view separator, + bool keep_empty = false); + +[[nodiscard]] +std::string_view trim(std::string_view str); + +[[nodiscard]] +std::string_view ltrim(std::string_view str); + +[[nodiscard]] +std::string_view rtrim(std::string_view str); + +} // namespace str + +#endif // STR_HH diff --git a/src/u.hh b/src/u.hh new file mode 100644 index 0000000..439d6dc --- /dev/null +++ b/src/u.hh @@ -0,0 +1,21 @@ +#ifndef U_HH +#define U_HH + +#include <cstdint> + +namespace u { + +enum class ReadError : uint8_t { + Invalid, // Invalid sequence + End, // At end (it == end) + Incomplete, // Too few bytes +}; + +enum class ReadErrorReplace : uint8_t { + End, // At end (it == end) + Incomplete, // Too few bytes +}; + +} // namespace u + +#endif // U_HH diff --git a/src/u8.hh b/src/u8.hh new file mode 100644 index 0000000..d673caa --- /dev/null +++ b/src/u8.hh @@ -0,0 +1,196 @@ +#ifndef U8_HH +#define U8_HH + +#include "u.hh" // IWYU pragma: export + +#include <cstdint> // IWYU pragma: export +#include <expected> +#include <iterator> +#include <type_traits> +#include <utility> + +namespace u8 { + +template <std::forward_iterator T> + requires std::is_same_v<std::iter_value_t<T>, uint8_t> +std::expected<uint32_t, u::ReadError> read(T& start, T const& end) { + if (start == end) + return std::unexpected(u::ReadError::End); + uint32_t u; + switch (*start >> 4) { + case 0xf: + // 11110uvv 10vvwwww 10xxxxyy 10yyzzzz + if (std::distance(start, end) < 4) { + return std::unexpected(u::ReadError::Incomplete); + } + u = (*start & 0x07) << 18; + std::advance(start, 1); + if ((*start & 0xc0) != 0x80) { + std::advance(start, 3); + return std::unexpected(u::ReadError::Invalid); + } + u |= (*start & 0x3f) << 12; + std::advance(start, 1); + if ((*start & 0xc0) != 0x80) { + std::advance(start, 2); + return std::unexpected(u::ReadError::Invalid); + } + u |= (*start & 0x3f) << 6; + std::advance(start, 1); + if ((*start & 0xc0) != 0x80) { + std::advance(start, 1); + return std::unexpected(u::ReadError::Invalid); + } + u |= *start & 0x3f; + if (u < 0x10000 || u > 0x10ffff) { + std::advance(start, 1); + return std::unexpected(u::ReadError::Invalid); + } + break; + case 0xe: + // 1110wwww 10xxxxyy 10yyzzzz + if (std::distance(start, end) < 3) { + return std::unexpected(u::ReadError::Incomplete); + } + u = (*start & 0x0f) << 12; + std::advance(start, 1); + if ((*start & 0xc0) != 0x80) { + std::advance(start, 2); + return std::unexpected(u::ReadError::Invalid); + } + u |= (*start & 0x3f) << 6; + std::advance(start, 1); + if ((*start & 0xc0) != 0x80) { + std::advance(start, 1); + return std::unexpected(u::ReadError::Invalid); + } + u |= *start & 0x3f; + if (u < 0x800 || (u >= 0xd800 && u <= 0xdfff)) { + std::advance(start, 1); + return std::unexpected(u::ReadError::Invalid); + } + break; + case 0xd: + case 0xc: + // 110xxxyy 10yyzzzz + if (std::distance(start, end) < 2) { + return std::unexpected(u::ReadError::Incomplete); + } + u = (*start & 0x1f) << 6; + std::advance(start, 1); + if ((*start & 0xc0) != 0x80) { + std::advance(start, 1); + return std::unexpected(u::ReadError::Invalid); + } + u |= *start & 0x3f; + if (u < 0x80) { + std::advance(start, 1); + return std::unexpected(u::ReadError::Invalid); + } + break; + case 0xb: + case 0xa: + case 0x9: + case 0x8: + std::advance(start, 1); + return std::unexpected(u::ReadError::Invalid); + default: + // 0yyyzzzz + u = *start; + break; + } + std::advance(start, 1); + return u; +} + +template <std::forward_iterator T> + requires std::is_same_v<std::iter_value_t<T>, uint8_t> +std::expected<uint32_t, u::ReadErrorReplace> read_replace(T& start, + T const& end, + bool eof) { + auto const tmp = start; + auto ret = read(start, end); + if (ret.has_value()) + return *ret; + switch (ret.error()) { + case u::ReadError::Incomplete: + if (eof) + break; + return std::unexpected(u::ReadErrorReplace::Incomplete); + case u::ReadError::End: + return std::unexpected(u::ReadErrorReplace::End); + case u::ReadError::Invalid: + break; + } + start = tmp + 1; + return 0xfffd; +} + +template <std::forward_iterator T> + requires std::is_same_v<std::iter_value_t<T>, uint8_t> +bool write(T& start, T const& end, uint32_t code) { + if (code < 0x80) { + if (start == end) + return false; + *start = static_cast<uint8_t>(code); + } else if (code < 0x800) { + if (std::distance(start, end) < 2) + return false; + *start = 0xc0 | static_cast<uint8_t>(code >> 6); + std::advance(start, 1); + *start = 0x80 | static_cast<uint8_t>(code & 0x3f); + } else if (code < 0x10000) { + if (std::distance(start, end) < 3) + return false; + *start = 0xe0 | static_cast<uint8_t>(code >> 12); + std::advance(start, 1); + *start = 0x80 | static_cast<uint8_t>((code >> 6) & 0x3f); + std::advance(start, 1); + *start = 0x80 | static_cast<uint8_t>(code & 0x3f); + } else { + if (std::distance(start, end) < 4) + return false; + *start = 0xf0 | static_cast<uint8_t>(code >> 18); + std::advance(start, 1); + *start = 0x80 | static_cast<uint8_t>((code >> 12) & 0x3f); + std::advance(start, 1); + *start = 0x80 | static_cast<uint8_t>((code >> 6) & 0x3f); + std::advance(start, 1); + *start = 0x80 | static_cast<uint8_t>(code & 0x3f); + } + std::advance(start, 1); + return true; +} + +template <std::forward_iterator T> + requires std::is_same_v<std::iter_value_t<T>, uint8_t> +bool skip(T& start, T const& end) { + if (start == end) + return false; + switch (*start >> 4) { + case 0xf: + if (std::distance(start, end) < 4) + return false; + std::advance(start, 4); + break; + case 0xe: + if (std::distance(start, end) < 3) + return false; + std::advance(start, 3); + break; + case 0xc: + case 0xd: + if (std::distance(start, end) < 2) + return false; + std::advance(start, 2); + break; + default: + std::advance(start, 1); + break; + } + return true; +} + +} // namespace u8 + +#endif // U8_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/src/xpm/.clang-tidy b/src/xpm/.clang-tidy new file mode 100644 index 0000000..8f60b95 --- /dev/null +++ b/src/xpm/.clang-tidy @@ -0,0 +1,2 @@ +--- +Checks: '' diff --git a/src/xpm/color.c b/src/xpm/color.c new file mode 100644 index 0000000..ead7073 --- /dev/null +++ b/src/xpm/color.c @@ -0,0 +1,882 @@ +/*********************************************************** + +Copyright 1987, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + +Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts. + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Digital not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + +******************************************************************/ + +#include <dix-config.h> + +#include <X11/keysym.h> + +#include "dix/dix_priv.h" +#include "include/dix.h" + +typedef struct _builtinColor { + unsigned char red; + unsigned char green; + unsigned char blue; + const char *name; +} BuiltinColor; + +static const BuiltinColor BuiltinColors[] = { +/* R G B name */ + { 240, 248, 255, "alice blue" }, + { 240, 248, 255, "AliceBlue" }, + { 250, 235, 215, "antique white" }, + { 250, 235, 215, "AntiqueWhite" }, + { 255, 239, 219, "AntiqueWhite1" }, + { 238, 223, 204, "AntiqueWhite2" }, + { 205, 192, 176, "AntiqueWhite3" }, + { 139, 131, 120, "AntiqueWhite4" }, + { 0, 255, 255, "aqua" }, + { 127, 255, 212, "aquamarine" }, + { 127, 255, 212, "aquamarine1" }, + { 118, 238, 198, "aquamarine2" }, + { 102, 205, 170, "aquamarine3" }, + { 69, 139, 116, "aquamarine4" }, + { 240, 255, 255, "azure" }, + { 240, 255, 255, "azure1" }, + { 224, 238, 238, "azure2" }, + { 193, 205, 205, "azure3" }, + { 131, 139, 139, "azure4" }, + { 245, 245, 220, "beige" }, + { 255, 228, 196, "bisque" }, + { 255, 228, 196, "bisque1" }, + { 238, 213, 183, "bisque2" }, + { 205, 183, 158, "bisque3" }, + { 139, 125, 107, "bisque4" }, + { 0, 0, 0, "black" }, + { 255, 235, 205, "blanched almond" }, + { 255, 235, 205, "BlanchedAlmond" }, + { 0, 0, 255, "blue" }, + { 138, 43, 226, "blue violet" }, + { 0, 0, 255, "blue1" }, + { 0, 0, 238, "blue2" }, + { 0, 0, 205, "blue3" }, + { 0, 0, 139, "blue4" }, + { 138, 43, 226, "BlueViolet" }, + { 165, 42, 42, "brown" }, + { 255, 64, 64, "brown1" }, + { 238, 59, 59, "brown2" }, + { 205, 51, 51, "brown3" }, + { 139, 35, 35, "brown4" }, + { 222, 184, 135, "burlywood" }, + { 255, 211, 155, "burlywood1" }, + { 238, 197, 145, "burlywood2" }, + { 205, 170, 125, "burlywood3" }, + { 139, 115, 85, "burlywood4" }, + { 95, 158, 160, "cadet blue" }, + { 95, 158, 160, "CadetBlue" }, + { 152, 245, 255, "CadetBlue1" }, + { 142, 229, 238, "CadetBlue2" }, + { 122, 197, 205, "CadetBlue3" }, + { 83, 134, 139, "CadetBlue4" }, + { 127, 255, 0, "chartreuse" }, + { 127, 255, 0, "chartreuse1" }, + { 118, 238, 0, "chartreuse2" }, + { 102, 205, 0, "chartreuse3" }, + { 69, 139, 0, "chartreuse4" }, + { 210, 105, 30, "chocolate" }, + { 255, 127, 36, "chocolate1" }, + { 238, 118, 33, "chocolate2" }, + { 205, 102, 29, "chocolate3" }, + { 139, 69, 19, "chocolate4" }, + { 255, 127, 80, "coral" }, + { 255, 114, 86, "coral1" }, + { 238, 106, 80, "coral2" }, + { 205, 91, 69, "coral3" }, + { 139, 62, 47, "coral4" }, + { 100, 149, 237, "cornflower blue" }, + { 100, 149, 237, "CornflowerBlue" }, + { 255, 248, 220, "cornsilk" }, + { 255, 248, 220, "cornsilk1" }, + { 238, 232, 205, "cornsilk2" }, + { 205, 200, 177, "cornsilk3" }, + { 139, 136, 120, "cornsilk4" }, + { 220, 20, 60, "crimson" }, + { 0, 255, 255, "cyan" }, + { 0, 255, 255, "cyan1" }, + { 0, 238, 238, "cyan2" }, + { 0, 205, 205, "cyan3" }, + { 0, 139, 139, "cyan4" }, + { 0, 0, 139, "dark blue" }, + { 0, 139, 139, "dark cyan" }, + { 184, 134, 11, "dark goldenrod" }, + { 169, 169, 169, "dark gray" }, + { 0, 100, 0, "dark green" }, + { 169, 169, 169, "dark grey" }, + { 189, 183, 107, "dark khaki" }, + { 139, 0, 139, "dark magenta" }, + { 85, 107, 47, "dark olive green" }, + { 255, 140, 0, "dark orange" }, + { 153, 50, 204, "dark orchid" }, + { 139, 0, 0, "dark red" }, + { 233, 150, 122, "dark salmon" }, + { 143, 188, 143, "dark sea green" }, + { 72, 61, 139, "dark slate blue" }, + { 47, 79, 79, "dark slate gray" }, + { 47, 79, 79, "dark slate grey" }, + { 0, 206, 209, "dark turquoise" }, + { 148, 0, 211, "dark violet" }, + { 0, 0, 139, "DarkBlue" }, + { 0, 139, 139, "DarkCyan" }, + { 184, 134, 11, "DarkGoldenrod" }, + { 255, 185, 15, "DarkGoldenrod1" }, + { 238, 173, 14, "DarkGoldenrod2" }, + { 205, 149, 12, "DarkGoldenrod3" }, + { 139, 101, 8, "DarkGoldenrod4" }, + { 169, 169, 169, "DarkGray" }, + { 0, 100, 0, "DarkGreen" }, + { 169, 169, 169, "DarkGrey" }, + { 189, 183, 107, "DarkKhaki" }, + { 139, 0, 139, "DarkMagenta" }, + { 85, 107, 47, "DarkOliveGreen" }, + { 202, 255, 112, "DarkOliveGreen1" }, + { 188, 238, 104, "DarkOliveGreen2" }, + { 162, 205, 90, "DarkOliveGreen3" }, + { 110, 139, 61, "DarkOliveGreen4" }, + { 255, 140, 0, "DarkOrange" }, + { 255, 127, 0, "DarkOrange1" }, + { 238, 118, 0, "DarkOrange2" }, + { 205, 102, 0, "DarkOrange3" }, + { 139, 69, 0, "DarkOrange4" }, + { 153, 50, 204, "DarkOrchid" }, + { 191, 62, 255, "DarkOrchid1" }, + { 178, 58, 238, "DarkOrchid2" }, + { 154, 50, 205, "DarkOrchid3" }, + { 104, 34, 139, "DarkOrchid4" }, + { 139, 0, 0, "DarkRed" }, + { 233, 150, 122, "DarkSalmon" }, + { 143, 188, 143, "DarkSeaGreen" }, + { 193, 255, 193, "DarkSeaGreen1" }, + { 180, 238, 180, "DarkSeaGreen2" }, + { 155, 205, 155, "DarkSeaGreen3" }, + { 105, 139, 105, "DarkSeaGreen4" }, + { 72, 61, 139, "DarkSlateBlue" }, + { 47, 79, 79, "DarkSlateGray" }, + { 151, 255, 255, "DarkSlateGray1" }, + { 141, 238, 238, "DarkSlateGray2" }, + { 121, 205, 205, "DarkSlateGray3" }, + { 82, 139, 139, "DarkSlateGray4" }, + { 47, 79, 79, "DarkSlateGrey" }, + { 0, 206, 209, "DarkTurquoise" }, + { 148, 0, 211, "DarkViolet" }, + { 255, 20, 147, "deep pink" }, + { 0, 191, 255, "deep sky blue" }, + { 255, 20, 147, "DeepPink" }, + { 255, 20, 147, "DeepPink1" }, + { 238, 18, 137, "DeepPink2" }, + { 205, 16, 118, "DeepPink3" }, + { 139, 10, 80, "DeepPink4" }, + { 0, 191, 255, "DeepSkyBlue" }, + { 0, 191, 255, "DeepSkyBlue1" }, + { 0, 178, 238, "DeepSkyBlue2" }, + { 0, 154, 205, "DeepSkyBlue3" }, + { 0, 104, 139, "DeepSkyBlue4" }, + { 105, 105, 105, "dim gray" }, + { 105, 105, 105, "dim grey" }, + { 105, 105, 105, "DimGray" }, + { 105, 105, 105, "DimGrey" }, + { 30, 144, 255, "dodger blue" }, + { 30, 144, 255, "DodgerBlue" }, + { 30, 144, 255, "DodgerBlue1" }, + { 28, 134, 238, "DodgerBlue2" }, + { 24, 116, 205, "DodgerBlue3" }, + { 16, 78, 139, "DodgerBlue4" }, + { 178, 34, 34, "firebrick" }, + { 255, 48, 48, "firebrick1" }, + { 238, 44, 44, "firebrick2" }, + { 205, 38, 38, "firebrick3" }, + { 139, 26, 26, "firebrick4" }, + { 255, 250, 240, "floral white" }, + { 255, 250, 240, "FloralWhite" }, + { 34, 139, 34, "forest green" }, + { 34, 139, 34, "ForestGreen" }, + { 255, 0, 255, "fuchsia" }, + { 220, 220, 220, "gainsboro" }, + { 248, 248, 255, "ghost white" }, + { 248, 248, 255, "GhostWhite" }, + { 255, 215, 0, "gold" }, + { 255, 215, 0, "gold1" }, + { 238, 201, 0, "gold2" }, + { 205, 173, 0, "gold3" }, + { 139, 117, 0, "gold4" }, + { 218, 165, 32, "goldenrod" }, + { 255, 193, 37, "goldenrod1" }, + { 238, 180, 34, "goldenrod2" }, + { 205, 155, 29, "goldenrod3" }, + { 139, 105, 20, "goldenrod4" }, + { 190, 190, 190, "gray" }, + { 0, 0, 0, "gray0" }, + { 3, 3, 3, "gray1" }, + { 26, 26, 26, "gray10" }, + { 255, 255, 255, "gray100" }, + { 28, 28, 28, "gray11" }, + { 31, 31, 31, "gray12" }, + { 33, 33, 33, "gray13" }, + { 36, 36, 36, "gray14" }, + { 38, 38, 38, "gray15" }, + { 41, 41, 41, "gray16" }, + { 43, 43, 43, "gray17" }, + { 46, 46, 46, "gray18" }, + { 48, 48, 48, "gray19" }, + { 5, 5, 5, "gray2" }, + { 51, 51, 51, "gray20" }, + { 54, 54, 54, "gray21" }, + { 56, 56, 56, "gray22" }, + { 59, 59, 59, "gray23" }, + { 61, 61, 61, "gray24" }, + { 64, 64, 64, "gray25" }, + { 66, 66, 66, "gray26" }, + { 69, 69, 69, "gray27" }, + { 71, 71, 71, "gray28" }, + { 74, 74, 74, "gray29" }, + { 8, 8, 8, "gray3" }, + { 77, 77, 77, "gray30" }, + { 79, 79, 79, "gray31" }, + { 82, 82, 82, "gray32" }, + { 84, 84, 84, "gray33" }, + { 87, 87, 87, "gray34" }, + { 89, 89, 89, "gray35" }, + { 92, 92, 92, "gray36" }, + { 94, 94, 94, "gray37" }, + { 97, 97, 97, "gray38" }, + { 99, 99, 99, "gray39" }, + { 10, 10, 10, "gray4" }, + { 102, 102, 102, "gray40" }, + { 105, 105, 105, "gray41" }, + { 107, 107, 107, "gray42" }, + { 110, 110, 110, "gray43" }, + { 112, 112, 112, "gray44" }, + { 115, 115, 115, "gray45" }, + { 117, 117, 117, "gray46" }, + { 120, 120, 120, "gray47" }, + { 122, 122, 122, "gray48" }, + { 125, 125, 125, "gray49" }, + { 13, 13, 13, "gray5" }, + { 127, 127, 127, "gray50" }, + { 130, 130, 130, "gray51" }, + { 133, 133, 133, "gray52" }, + { 135, 135, 135, "gray53" }, + { 138, 138, 138, "gray54" }, + { 140, 140, 140, "gray55" }, + { 143, 143, 143, "gray56" }, + { 145, 145, 145, "gray57" }, + { 148, 148, 148, "gray58" }, + { 150, 150, 150, "gray59" }, + { 15, 15, 15, "gray6" }, + { 153, 153, 153, "gray60" }, + { 156, 156, 156, "gray61" }, + { 158, 158, 158, "gray62" }, + { 161, 161, 161, "gray63" }, + { 163, 163, 163, "gray64" }, + { 166, 166, 166, "gray65" }, + { 168, 168, 168, "gray66" }, + { 171, 171, 171, "gray67" }, + { 173, 173, 173, "gray68" }, + { 176, 176, 176, "gray69" }, + { 18, 18, 18, "gray7" }, + { 179, 179, 179, "gray70" }, + { 181, 181, 181, "gray71" }, + { 184, 184, 184, "gray72" }, + { 186, 186, 186, "gray73" }, + { 189, 189, 189, "gray74" }, + { 191, 191, 191, "gray75" }, + { 194, 194, 194, "gray76" }, + { 196, 196, 196, "gray77" }, + { 199, 199, 199, "gray78" }, + { 201, 201, 201, "gray79" }, + { 20, 20, 20, "gray8" }, + { 204, 204, 204, "gray80" }, + { 207, 207, 207, "gray81" }, + { 209, 209, 209, "gray82" }, + { 212, 212, 212, "gray83" }, + { 214, 214, 214, "gray84" }, + { 217, 217, 217, "gray85" }, + { 219, 219, 219, "gray86" }, + { 222, 222, 222, "gray87" }, + { 224, 224, 224, "gray88" }, + { 227, 227, 227, "gray89" }, + { 23, 23, 23, "gray9" }, + { 229, 229, 229, "gray90" }, + { 232, 232, 232, "gray91" }, + { 235, 235, 235, "gray92" }, + { 237, 237, 237, "gray93" }, + { 240, 240, 240, "gray94" }, + { 242, 242, 242, "gray95" }, + { 245, 245, 245, "gray96" }, + { 247, 247, 247, "gray97" }, + { 250, 250, 250, "gray98" }, + { 252, 252, 252, "gray99" }, + { 0, 255, 0, "green" }, + { 173, 255, 47, "green yellow" }, + { 0, 255, 0, "green1" }, + { 0, 238, 0, "green2" }, + { 0, 205, 0, "green3" }, + { 0, 139, 0, "green4" }, + { 173, 255, 47, "GreenYellow" }, + { 190, 190, 190, "grey" }, + { 0, 0, 0, "grey0" }, + { 3, 3, 3, "grey1" }, + { 26, 26, 26, "grey10" }, + { 255, 255, 255, "grey100" }, + { 28, 28, 28, "grey11" }, + { 31, 31, 31, "grey12" }, + { 33, 33, 33, "grey13" }, + { 36, 36, 36, "grey14" }, + { 38, 38, 38, "grey15" }, + { 41, 41, 41, "grey16" }, + { 43, 43, 43, "grey17" }, + { 46, 46, 46, "grey18" }, + { 48, 48, 48, "grey19" }, + { 5, 5, 5, "grey2" }, + { 51, 51, 51, "grey20" }, + { 54, 54, 54, "grey21" }, + { 56, 56, 56, "grey22" }, + { 59, 59, 59, "grey23" }, + { 61, 61, 61, "grey24" }, + { 64, 64, 64, "grey25" }, + { 66, 66, 66, "grey26" }, + { 69, 69, 69, "grey27" }, + { 71, 71, 71, "grey28" }, + { 74, 74, 74, "grey29" }, + { 8, 8, 8, "grey3" }, + { 77, 77, 77, "grey30" }, + { 79, 79, 79, "grey31" }, + { 82, 82, 82, "grey32" }, + { 84, 84, 84, "grey33" }, + { 87, 87, 87, "grey34" }, + { 89, 89, 89, "grey35" }, + { 92, 92, 92, "grey36" }, + { 94, 94, 94, "grey37" }, + { 97, 97, 97, "grey38" }, + { 99, 99, 99, "grey39" }, + { 10, 10, 10, "grey4" }, + { 102, 102, 102, "grey40" }, + { 105, 105, 105, "grey41" }, + { 107, 107, 107, "grey42" }, + { 110, 110, 110, "grey43" }, + { 112, 112, 112, "grey44" }, + { 115, 115, 115, "grey45" }, + { 117, 117, 117, "grey46" }, + { 120, 120, 120, "grey47" }, + { 122, 122, 122, "grey48" }, + { 125, 125, 125, "grey49" }, + { 13, 13, 13, "grey5" }, + { 127, 127, 127, "grey50" }, + { 130, 130, 130, "grey51" }, + { 133, 133, 133, "grey52" }, + { 135, 135, 135, "grey53" }, + { 138, 138, 138, "grey54" }, + { 140, 140, 140, "grey55" }, + { 143, 143, 143, "grey56" }, + { 145, 145, 145, "grey57" }, + { 148, 148, 148, "grey58" }, + { 150, 150, 150, "grey59" }, + { 15, 15, 15, "grey6" }, + { 153, 153, 153, "grey60" }, + { 156, 156, 156, "grey61" }, + { 158, 158, 158, "grey62" }, + { 161, 161, 161, "grey63" }, + { 163, 163, 163, "grey64" }, + { 166, 166, 166, "grey65" }, + { 168, 168, 168, "grey66" }, + { 171, 171, 171, "grey67" }, + { 173, 173, 173, "grey68" }, + { 176, 176, 176, "grey69" }, + { 18, 18, 18, "grey7" }, + { 179, 179, 179, "grey70" }, + { 181, 181, 181, "grey71" }, + { 184, 184, 184, "grey72" }, + { 186, 186, 186, "grey73" }, + { 189, 189, 189, "grey74" }, + { 191, 191, 191, "grey75" }, + { 194, 194, 194, "grey76" }, + { 196, 196, 196, "grey77" }, + { 199, 199, 199, "grey78" }, + { 201, 201, 201, "grey79" }, + { 20, 20, 20, "grey8" }, + { 204, 204, 204, "grey80" }, + { 207, 207, 207, "grey81" }, + { 209, 209, 209, "grey82" }, + { 212, 212, 212, "grey83" }, + { 214, 214, 214, "grey84" }, + { 217, 217, 217, "grey85" }, + { 219, 219, 219, "grey86" }, + { 222, 222, 222, "grey87" }, + { 224, 224, 224, "grey88" }, + { 227, 227, 227, "grey89" }, + { 23, 23, 23, "grey9" }, + { 229, 229, 229, "grey90" }, + { 232, 232, 232, "grey91" }, + { 235, 235, 235, "grey92" }, + { 237, 237, 237, "grey93" }, + { 240, 240, 240, "grey94" }, + { 242, 242, 242, "grey95" }, + { 245, 245, 245, "grey96" }, + { 247, 247, 247, "grey97" }, + { 250, 250, 250, "grey98" }, + { 252, 252, 252, "grey99" }, + { 240, 255, 240, "honeydew" }, + { 240, 255, 240, "honeydew1" }, + { 224, 238, 224, "honeydew2" }, + { 193, 205, 193, "honeydew3" }, + { 131, 139, 131, "honeydew4" }, + { 255, 105, 180, "hot pink" }, + { 255, 105, 180, "HotPink" }, + { 255, 110, 180, "HotPink1" }, + { 238, 106, 167, "HotPink2" }, + { 205, 96, 144, "HotPink3" }, + { 139, 58, 98, "HotPink4" }, + { 205, 92, 92, "indian red" }, + { 205, 92, 92, "IndianRed" }, + { 255, 106, 106, "IndianRed1" }, + { 238, 99, 99, "IndianRed2" }, + { 205, 85, 85, "IndianRed3" }, + { 139, 58, 58, "IndianRed4" }, + { 75, 0, 130, "indigo" }, + { 255, 255, 240, "ivory" }, + { 255, 255, 240, "ivory1" }, + { 238, 238, 224, "ivory2" }, + { 205, 205, 193, "ivory3" }, + { 139, 139, 131, "ivory4" }, + { 240, 230, 140, "khaki" }, + { 255, 246, 143, "khaki1" }, + { 238, 230, 133, "khaki2" }, + { 205, 198, 115, "khaki3" }, + { 139, 134, 78, "khaki4" }, + { 230, 230, 250, "lavender" }, + { 255, 240, 245, "lavender blush" }, + { 255, 240, 245, "LavenderBlush" }, + { 255, 240, 245, "LavenderBlush1" }, + { 238, 224, 229, "LavenderBlush2" }, + { 205, 193, 197, "LavenderBlush3" }, + { 139, 131, 134, "LavenderBlush4" }, + { 124, 252, 0, "lawn green" }, + { 124, 252, 0, "LawnGreen" }, + { 255, 250, 205, "lemon chiffon" }, + { 255, 250, 205, "LemonChiffon" }, + { 255, 250, 205, "LemonChiffon1" }, + { 238, 233, 191, "LemonChiffon2" }, + { 205, 201, 165, "LemonChiffon3" }, + { 139, 137, 112, "LemonChiffon4" }, + { 173, 216, 230, "light blue" }, + { 240, 128, 128, "light coral" }, + { 224, 255, 255, "light cyan" }, + { 238, 221, 130, "light goldenrod" }, + { 250, 250, 210, "light goldenrod yellow" }, + { 211, 211, 211, "light gray" }, + { 144, 238, 144, "light green" }, + { 211, 211, 211, "light grey" }, + { 255, 182, 193, "light pink" }, + { 255, 160, 122, "light salmon" }, + { 32, 178, 170, "light sea green" }, + { 135, 206, 250, "light sky blue" }, + { 132, 112, 255, "light slate blue" }, + { 119, 136, 153, "light slate gray" }, + { 119, 136, 153, "light slate grey" }, + { 176, 196, 222, "light steel blue" }, + { 255, 255, 224, "light yellow" }, + { 173, 216, 230, "LightBlue" }, + { 191, 239, 255, "LightBlue1" }, + { 178, 223, 238, "LightBlue2" }, + { 154, 192, 205, "LightBlue3" }, + { 104, 131, 139, "LightBlue4" }, + { 240, 128, 128, "LightCoral" }, + { 224, 255, 255, "LightCyan" }, + { 224, 255, 255, "LightCyan1" }, + { 209, 238, 238, "LightCyan2" }, + { 180, 205, 205, "LightCyan3" }, + { 122, 139, 139, "LightCyan4" }, + { 238, 221, 130, "LightGoldenrod" }, + { 255, 236, 139, "LightGoldenrod1" }, + { 238, 220, 130, "LightGoldenrod2" }, + { 205, 190, 112, "LightGoldenrod3" }, + { 139, 129, 76, "LightGoldenrod4" }, + { 250, 250, 210, "LightGoldenrodYellow" }, + { 211, 211, 211, "LightGray" }, + { 144, 238, 144, "LightGreen" }, + { 211, 211, 211, "LightGrey" }, + { 255, 182, 193, "LightPink" }, + { 255, 174, 185, "LightPink1" }, + { 238, 162, 173, "LightPink2" }, + { 205, 140, 149, "LightPink3" }, + { 139, 95, 101, "LightPink4" }, + { 255, 160, 122, "LightSalmon" }, + { 255, 160, 122, "LightSalmon1" }, + { 238, 149, 114, "LightSalmon2" }, + { 205, 129, 98, "LightSalmon3" }, + { 139, 87, 66, "LightSalmon4" }, + { 32, 178, 170, "LightSeaGreen" }, + { 135, 206, 250, "LightSkyBlue" }, + { 176, 226, 255, "LightSkyBlue1" }, + { 164, 211, 238, "LightSkyBlue2" }, + { 141, 182, 205, "LightSkyBlue3" }, + { 96, 123, 139, "LightSkyBlue4" }, + { 132, 112, 255, "LightSlateBlue" }, + { 119, 136, 153, "LightSlateGray" }, + { 119, 136, 153, "LightSlateGrey" }, + { 176, 196, 222, "LightSteelBlue" }, + { 202, 225, 255, "LightSteelBlue1" }, + { 188, 210, 238, "LightSteelBlue2" }, + { 162, 181, 205, "LightSteelBlue3" }, + { 110, 123, 139, "LightSteelBlue4" }, + { 255, 255, 224, "LightYellow" }, + { 255, 255, 224, "LightYellow1" }, + { 238, 238, 209, "LightYellow2" }, + { 205, 205, 180, "LightYellow3" }, + { 139, 139, 122, "LightYellow4" }, + { 0, 255, 0, "lime" }, + { 50, 205, 50, "lime green" }, + { 50, 205, 50, "LimeGreen" }, + { 250, 240, 230, "linen" }, + { 255, 0, 255, "magenta" }, + { 255, 0, 255, "magenta1" }, + { 238, 0, 238, "magenta2" }, + { 205, 0, 205, "magenta3" }, + { 139, 0, 139, "magenta4" }, + { 176, 48, 96, "maroon" }, + { 255, 52, 179, "maroon1" }, + { 238, 48, 167, "maroon2" }, + { 205, 41, 144, "maroon3" }, + { 139, 28, 98, "maroon4" }, + { 102, 205, 170, "medium aquamarine" }, + { 0, 0, 205, "medium blue" }, + { 186, 85, 211, "medium orchid" }, + { 147, 112, 219, "medium purple" }, + { 60, 179, 113, "medium sea green" }, + { 123, 104, 238, "medium slate blue" }, + { 0, 250, 154, "medium spring green" }, + { 72, 209, 204, "medium turquoise" }, + { 199, 21, 133, "medium violet red" }, + { 102, 205, 170, "MediumAquamarine" }, + { 0, 0, 205, "MediumBlue" }, + { 186, 85, 211, "MediumOrchid" }, + { 224, 102, 255, "MediumOrchid1" }, + { 209, 95, 238, "MediumOrchid2" }, + { 180, 82, 205, "MediumOrchid3" }, + { 122, 55, 139, "MediumOrchid4" }, + { 147, 112, 219, "MediumPurple" }, + { 171, 130, 255, "MediumPurple1" }, + { 159, 121, 238, "MediumPurple2" }, + { 137, 104, 205, "MediumPurple3" }, + { 93, 71, 139, "MediumPurple4" }, + { 60, 179, 113, "MediumSeaGreen" }, + { 123, 104, 238, "MediumSlateBlue" }, + { 0, 250, 154, "MediumSpringGreen" }, + { 72, 209, 204, "MediumTurquoise" }, + { 199, 21, 133, "MediumVioletRed" }, + { 25, 25, 112, "midnight blue" }, + { 25, 25, 112, "MidnightBlue" }, + { 245, 255, 250, "mint cream" }, + { 245, 255, 250, "MintCream" }, + { 255, 228, 225, "misty rose" }, + { 255, 228, 225, "MistyRose" }, + { 255, 228, 225, "MistyRose1" }, + { 238, 213, 210, "MistyRose2" }, + { 205, 183, 181, "MistyRose3" }, + { 139, 125, 123, "MistyRose4" }, + { 255, 228, 181, "moccasin" }, + { 255, 222, 173, "navajo white" }, + { 255, 222, 173, "NavajoWhite" }, + { 255, 222, 173, "NavajoWhite1" }, + { 238, 207, 161, "NavajoWhite2" }, + { 205, 179, 139, "NavajoWhite3" }, + { 139, 121, 94, "NavajoWhite4" }, + { 0, 0, 128, "navy" }, + { 0, 0, 128, "navy blue" }, + { 0, 0, 128, "NavyBlue" }, + { 253, 245, 230, "old lace" }, + { 253, 245, 230, "OldLace" }, + { 128, 128, 0, "olive" }, + { 107, 142, 35, "olive drab" }, + { 107, 142, 35, "OliveDrab" }, + { 192, 255, 62, "OliveDrab1" }, + { 179, 238, 58, "OliveDrab2" }, + { 154, 205, 50, "OliveDrab3" }, + { 105, 139, 34, "OliveDrab4" }, + { 255, 165, 0, "orange" }, + { 255, 69, 0, "orange red" }, + { 255, 165, 0, "orange1" }, + { 238, 154, 0, "orange2" }, + { 205, 133, 0, "orange3" }, + { 139, 90, 0, "orange4" }, + { 255, 69, 0, "OrangeRed" }, + { 255, 69, 0, "OrangeRed1" }, + { 238, 64, 0, "OrangeRed2" }, + { 205, 55, 0, "OrangeRed3" }, + { 139, 37, 0, "OrangeRed4" }, + { 218, 112, 214, "orchid" }, + { 255, 131, 250, "orchid1" }, + { 238, 122, 233, "orchid2" }, + { 205, 105, 201, "orchid3" }, + { 139, 71, 137, "orchid4" }, + { 238, 232, 170, "pale goldenrod" }, + { 152, 251, 152, "pale green" }, + { 175, 238, 238, "pale turquoise" }, + { 219, 112, 147, "pale violet red" }, + { 238, 232, 170, "PaleGoldenrod" }, + { 152, 251, 152, "PaleGreen" }, + { 154, 255, 154, "PaleGreen1" }, + { 144, 238, 144, "PaleGreen2" }, + { 124, 205, 124, "PaleGreen3" }, + { 84, 139, 84, "PaleGreen4" }, + { 175, 238, 238, "PaleTurquoise" }, + { 187, 255, 255, "PaleTurquoise1" }, + { 174, 238, 238, "PaleTurquoise2" }, + { 150, 205, 205, "PaleTurquoise3" }, + { 102, 139, 139, "PaleTurquoise4" }, + { 219, 112, 147, "PaleVioletRed" }, + { 255, 130, 171, "PaleVioletRed1" }, + { 238, 121, 159, "PaleVioletRed2" }, + { 205, 104, 137, "PaleVioletRed3" }, + { 139, 71, 93, "PaleVioletRed4" }, + { 255, 239, 213, "papaya whip" }, + { 255, 239, 213, "PapayaWhip" }, + { 255, 218, 185, "peach puff" }, + { 255, 218, 185, "PeachPuff" }, + { 255, 218, 185, "PeachPuff1" }, + { 238, 203, 173, "PeachPuff2" }, + { 205, 175, 149, "PeachPuff3" }, + { 139, 119, 101, "PeachPuff4" }, + { 205, 133, 63, "peru" }, + { 255, 192, 203, "pink" }, + { 255, 181, 197, "pink1" }, + { 238, 169, 184, "pink2" }, + { 205, 145, 158, "pink3" }, + { 139, 99, 108, "pink4" }, + { 221, 160, 221, "plum" }, + { 255, 187, 255, "plum1" }, + { 238, 174, 238, "plum2" }, + { 205, 150, 205, "plum3" }, + { 139, 102, 139, "plum4" }, + { 176, 224, 230, "powder blue" }, + { 176, 224, 230, "PowderBlue" }, + { 160, 32, 240, "purple" }, + { 155, 48, 255, "purple1" }, + { 145, 44, 238, "purple2" }, + { 125, 38, 205, "purple3" }, + { 85, 26, 139, "purple4" }, + { 102, 51, 153, "rebecca purple" }, + { 102, 51, 153, "RebeccaPurple" }, + { 255, 0, 0, "red" }, + { 255, 0, 0, "red1" }, + { 238, 0, 0, "red2" }, + { 205, 0, 0, "red3" }, + { 139, 0, 0, "red4" }, + { 188, 143, 143, "rosy brown" }, + { 188, 143, 143, "RosyBrown" }, + { 255, 193, 193, "RosyBrown1" }, + { 238, 180, 180, "RosyBrown2" }, + { 205, 155, 155, "RosyBrown3" }, + { 139, 105, 105, "RosyBrown4" }, + { 65, 105, 225, "royal blue" }, + { 65, 105, 225, "RoyalBlue" }, + { 72, 118, 255, "RoyalBlue1" }, + { 67, 110, 238, "RoyalBlue2" }, + { 58, 95, 205, "RoyalBlue3" }, + { 39, 64, 139, "RoyalBlue4" }, + { 139, 69, 19, "saddle brown" }, + { 139, 69, 19, "SaddleBrown" }, + { 250, 128, 114, "salmon" }, + { 255, 140, 105, "salmon1" }, + { 238, 130, 98, "salmon2" }, + { 205, 112, 84, "salmon3" }, + { 139, 76, 57, "salmon4" }, + { 244, 164, 96, "sandy brown" }, + { 244, 164, 96, "SandyBrown" }, + { 46, 139, 87, "sea green" }, + { 46, 139, 87, "SeaGreen" }, + { 84, 255, 159, "SeaGreen1" }, + { 78, 238, 148, "SeaGreen2" }, + { 67, 205, 128, "SeaGreen3" }, + { 46, 139, 87, "SeaGreen4" }, + { 255, 245, 238, "seashell" }, + { 255, 245, 238, "seashell1" }, + { 238, 229, 222, "seashell2" }, + { 205, 197, 191, "seashell3" }, + { 139, 134, 130, "seashell4" }, + { 160, 82, 45, "sienna" }, + { 255, 130, 71, "sienna1" }, + { 238, 121, 66, "sienna2" }, + { 205, 104, 57, "sienna3" }, + { 139, 71, 38, "sienna4" }, + { 192, 192, 192, "silver" }, + { 135, 206, 235, "sky blue" }, + { 135, 206, 235, "SkyBlue" }, + { 135, 206, 255, "SkyBlue1" }, + { 126, 192, 238, "SkyBlue2" }, + { 108, 166, 205, "SkyBlue3" }, + { 74, 112, 139, "SkyBlue4" }, + { 106, 90, 205, "slate blue" }, + { 112, 128, 144, "slate gray" }, + { 112, 128, 144, "slate grey" }, + { 106, 90, 205, "SlateBlue" }, + { 131, 111, 255, "SlateBlue1" }, + { 122, 103, 238, "SlateBlue2" }, + { 105, 89, 205, "SlateBlue3" }, + { 71, 60, 139, "SlateBlue4" }, + { 112, 128, 144, "SlateGray" }, + { 198, 226, 255, "SlateGray1" }, + { 185, 211, 238, "SlateGray2" }, + { 159, 182, 205, "SlateGray3" }, + { 108, 123, 139, "SlateGray4" }, + { 112, 128, 144, "SlateGrey" }, + { 255, 250, 250, "snow" }, + { 255, 250, 250, "snow1" }, + { 238, 233, 233, "snow2" }, + { 205, 201, 201, "snow3" }, + { 139, 137, 137, "snow4" }, + { 0, 255, 127, "spring green" }, + { 0, 255, 127, "SpringGreen" }, + { 0, 255, 127, "SpringGreen1" }, + { 0, 238, 118, "SpringGreen2" }, + { 0, 205, 102, "SpringGreen3" }, + { 0, 139, 69, "SpringGreen4" }, + { 70, 130, 180, "steel blue" }, + { 70, 130, 180, "SteelBlue" }, + { 99, 184, 255, "SteelBlue1" }, + { 92, 172, 238, "SteelBlue2" }, + { 79, 148, 205, "SteelBlue3" }, + { 54, 100, 139, "SteelBlue4" }, + { 210, 180, 140, "tan" }, + { 255, 165, 79, "tan1" }, + { 238, 154, 73, "tan2" }, + { 205, 133, 63, "tan3" }, + { 139, 90, 43, "tan4" }, + { 0, 128, 128, "teal" }, + { 216, 191, 216, "thistle" }, + { 255, 225, 255, "thistle1" }, + { 238, 210, 238, "thistle2" }, + { 205, 181, 205, "thistle3" }, + { 139, 123, 139, "thistle4" }, + { 255, 99, 71, "tomato" }, + { 255, 99, 71, "tomato1" }, + { 238, 92, 66, "tomato2" }, + { 205, 79, 57, "tomato3" }, + { 139, 54, 38, "tomato4" }, + { 64, 224, 208, "turquoise" }, + { 0, 245, 255, "turquoise1" }, + { 0, 229, 238, "turquoise2" }, + { 0, 197, 205, "turquoise3" }, + { 0, 134, 139, "turquoise4" }, + { 238, 130, 238, "violet" }, + { 208, 32, 144, "violet red" }, + { 208, 32, 144, "VioletRed" }, + { 255, 62, 150, "VioletRed1" }, + { 238, 58, 140, "VioletRed2" }, + { 205, 50, 120, "VioletRed3" }, + { 139, 34, 82, "VioletRed4" }, + { 128, 128, 128, "web gray" }, + { 0, 128, 0, "web green" }, + { 128, 128, 128, "web grey" }, + { 128, 0, 0, "web maroon" }, + { 128, 0, 128, "web purple" }, + { 128, 128, 128, "WebGray" }, + { 0, 128, 0, "WebGreen" }, + { 128, 128, 128, "WebGrey" }, + { 128, 0, 0, "WebMaroon" }, + { 128, 0, 128, "WebPurple" }, + { 245, 222, 179, "wheat" }, + { 255, 231, 186, "wheat1" }, + { 238, 216, 174, "wheat2" }, + { 205, 186, 150, "wheat3" }, + { 139, 126, 102, "wheat4" }, + { 255, 255, 255, "white" }, + { 245, 245, 245, "white smoke" }, + { 245, 245, 245, "WhiteSmoke" }, + { 190, 190, 190, "x11 gray" }, + { 0, 255, 0, "x11 green" }, + { 190, 190, 190, "x11 grey" }, + { 176, 48, 96, "x11 maroon" }, + { 160, 32, 240, "x11 purple" }, + { 190, 190, 190, "X11Gray" }, + { 0, 255, 0, "X11Green" }, + { 190, 190, 190, "X11Grey" }, + { 176, 48, 96, "X11Maroon" }, + { 160, 32, 240, "X11Purple" }, + { 255, 255, 0, "yellow" }, + { 154, 205, 50, "yellow green" }, + { 255, 255, 0, "yellow1" }, + { 238, 238, 0, "yellow2" }, + { 205, 205, 0, "yellow3" }, + { 139, 139, 0, "yellow4" }, + { 154, 205, 50, "YellowGreen" }, +}; + +Bool +dixLookupBuiltinColor(int screen, + char *name, + unsigned int len, + unsigned short *pred, + unsigned short *pgreen, + unsigned short *pblue) +{ + int low = 0; + int high = ARRAY_SIZE(BuiltinColors) - 1; + + while (high >= low) { + int mid = (low + high) / 2; + const BuiltinColor *c = &BuiltinColors[mid]; + const int currentLen = strlen(c->name); + const int r = strncasecmp(c->name, name, min(len, currentLen)); + + if (r == 0) { + if (len == currentLen) { + *pred = c->red * 0x101; + *pgreen = c->green * 0x101; + *pblue = c->blue * 0x101; + return TRUE; + } else if (len > currentLen) { + low = mid + 1; + } else { + high = mid - 1; + } + } else if (r > 0) { + high = mid - 1; + } else { + low = mid + 1; + } + } + return FALSE; +} diff --git a/src/xpm/dix-config.h b/src/xpm/dix-config.h new file mode 100644 index 0000000..5137e8a --- /dev/null +++ b/src/xpm/dix-config.h @@ -0,0 +1 @@ +// Empty file making color.c able to compile without modifications. diff --git a/src/xpm/dix/dix_priv.h b/src/xpm/dix/dix_priv.h new file mode 100644 index 0000000..005da83 --- /dev/null +++ b/src/xpm/dix/dix_priv.h @@ -0,0 +1,14 @@ +#ifndef DIX_PRIV_H +#define DIX_PRIV_H + +#define TRUE 1 +#define FALSE 0 + +#define ARRAY_SIZE(a) (sizeof((a)) / sizeof((a)[0])) + +#include <string.h> +#include <strings.h> + +#define min(a, b) ((a) < (b) ? (a) : (b)) + +#endif // DIX_PRIV_H diff --git a/src/xpm/include/dix.h b/src/xpm/include/dix.h new file mode 100644 index 0000000..2a7125a --- /dev/null +++ b/src/xpm/include/dix.h @@ -0,0 +1,14 @@ +#ifndef DIX_H +#define DIX_H + +#include <X11/Xlib.h> + +Bool +dixLookupBuiltinColor(int screen, + char *name, + unsigned int len, + unsigned short *pred, + unsigned short *pgreen, + unsigned short *pblue); + +#endif // DIX_H diff --git a/subprojects/.gitignore b/subprojects/.gitignore new file mode 100644 index 0000000..7ba62ce --- /dev/null +++ b/subprojects/.gitignore @@ -0,0 +1,3 @@ +packagecache/ +googletest-1.17.0/ +.wraplock
\ No newline at end of file diff --git a/subprojects/gtest.wrap b/subprojects/gtest.wrap new file mode 100644 index 0000000..9902a4f --- /dev/null +++ b/subprojects/gtest.wrap @@ -0,0 +1,16 @@ +[wrap-file] +directory = googletest-1.17.0 +source_url = https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz +source_filename = googletest-1.17.0.tar.gz +source_hash = 65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c +patch_filename = gtest_1.17.0-4_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.17.0-4/get_patch +patch_hash = 3abf7662d09db706453a5b064a1e914678c74b9d9b0b19382747ca561d0d8750 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.17.0-4/googletest-1.17.0.tar.gz +wrapdb_version = 1.17.0-4 + +[provide] +gtest = gtest_dep +gtest_main = gtest_main_dep +gmock = gmock_dep +gmock_main = gmock_main_dep 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/cfg.cc b/test/cfg.cc new file mode 100644 index 0000000..0c27989 --- /dev/null +++ b/test/cfg.cc @@ -0,0 +1,190 @@ +#include "cfg.hh" + +#include "testdir.hh" +#include "testenv.hh" + +#include <fstream> +#include <gtest/gtest.h> +#include <string> +#include <vector> + +namespace { + +class ConfigTest : public TestEnv { + protected: + void SetUp() override { ASSERT_TRUE(dir_.good()); } + + TestDir dir_; +}; + +} // namespace + +TEST_F(ConfigTest, empty) { + auto does_not_exist = dir_.path() / "does-not-exist"; + std::vector<std::string> errors; + auto cfg = cfg::load_one(does_not_exist, errors); + ASSERT_TRUE(cfg); + EXPECT_EQ(1, errors.size()); + + EXPECT_FALSE(cfg->has("")); + EXPECT_FALSE(cfg->get("").has_value()); + + EXPECT_FALSE(cfg->has("foo")); + EXPECT_FALSE(cfg->get("foo").has_value()); +} + +TEST_F(ConfigTest, values) { + auto file = dir_.path() / "file"; + { + std::ofstream out(file); + out << "# Comment\n" + << "key=value\n" + << " foo = bar \n" + << "i1 = 12\n" + << "b1 = true\n" + << "b2=FaLSe\n" + << "i2 = -12313\n"; + } + std::vector<std::string> errors; + auto cfg = cfg::load_one(file, errors); + ASSERT_TRUE(cfg); + EXPECT_EQ(0, errors.size()); + + EXPECT_FALSE(cfg->has("")); + EXPECT_FALSE(cfg->get("").has_value()); + + EXPECT_TRUE(cfg->has("key")); + EXPECT_EQ("value", cfg->get("key").value_or("")); + + EXPECT_TRUE(cfg->has("foo")); + EXPECT_EQ("bar", cfg->get("foo").value_or("")); + EXPECT_FALSE(cfg->get_int64("foo").has_value()); + + EXPECT_TRUE(cfg->has("i1")); + EXPECT_EQ("12", cfg->get("i1").value_or("")); + EXPECT_EQ(12, cfg->get_int64("i1").value_or(0)); + EXPECT_EQ(12, cfg->get_uint64("i1").value_or(0)); + + EXPECT_TRUE(cfg->has("b1")); + EXPECT_EQ("true", cfg->get("b1").value_or("")); + EXPECT_TRUE(cfg->get_bool("b1").value_or(false)); + + EXPECT_TRUE(cfg->has("b2")); + EXPECT_EQ("FaLSe", cfg->get("b2").value_or("")); + EXPECT_FALSE(cfg->get_bool("b2").value_or(true)); + + EXPECT_TRUE(cfg->has("i2")); + EXPECT_EQ("-12313", cfg->get("i2").value_or("")); + EXPECT_EQ(-12313, cfg->get_int64("i2").value_or(0)); + EXPECT_EQ(0, cfg->get_uint64("i2").value_or(0)); +} + +TEST_F(ConfigTest, bools) { + auto file = dir_.path() / "file"; + { + std::ofstream out(file); + out << "key1=True\n" + << "key2=yES\n" + << "key3=false\n" + << "key4=NO\n" + << "key5=ja\n"; + } + std::vector<std::string> errors; + auto cfg = cfg::load_one(file, errors); + ASSERT_TRUE(cfg); + EXPECT_EQ(0, errors.size()); + + EXPECT_TRUE(cfg->get_bool("key1").value_or(false)); + EXPECT_TRUE(cfg->get_bool("key2").value_or(false)); + EXPECT_FALSE(cfg->get_bool("key3").value_or(true)); + EXPECT_FALSE(cfg->get_bool("key4").value_or(true)); + EXPECT_FALSE(cfg->get_bool("key5").has_value()); +} + +TEST_F(ConfigTest, errors) { + auto file = dir_.path() / "file"; + { + std::ofstream out(file); + out << "bad line\n" + << "key=value\n" + << "key=duplicate\n"; + } + std::vector<std::string> errors; + auto cfg = cfg::load_one(file, errors); + ASSERT_TRUE(cfg); + EXPECT_EQ(2, errors.size()); +} + +TEST_F(ConfigTest, merge) { + auto dir1 = dir_.path() / "dir1"; + auto dir2 = dir_.path() / "dir2"; + auto dir3 = dir_.path() / "dir3"; + std::filesystem::create_directory(dir1); + std::filesystem::create_directory(dir2); + std::filesystem::create_directory(dir3); + setenv("XDG_CONFIG_HOME", dir1); + setenv("XDG_CONFIG_DIRS", dir2.string() + ":" + dir3.string()); + auto file1 = dir1 / "file"; + auto file2 = dir2 / "file"; + { + std::ofstream out(file2); + out << "key1 = value1\n" + << "key2 = value2\n"; + } + { + std::ofstream out(file1); + out << "key1 = 12\n" + << "key3 = value3"; + } + std::vector<std::string> errors; + auto cfg = cfg::load_all("file", errors); + ASSERT_TRUE(cfg); + EXPECT_EQ(0, errors.size()); + + EXPECT_FALSE(cfg->has("")); + EXPECT_FALSE(cfg->get("").has_value()); + + EXPECT_TRUE(cfg->has("key1")); + EXPECT_EQ("12", cfg->get("key1").value_or("")); + + EXPECT_TRUE(cfg->has("key2")); + EXPECT_EQ("value2", cfg->get("key2").value_or("")); + + EXPECT_TRUE(cfg->has("key3")); + EXPECT_EQ("value3", cfg->get("key3").value_or("")); +} + +TEST_F(ConfigTest, merge_errors) { + auto dir1 = dir_.path() / "dir1"; + auto dir2 = dir_.path() / "dir2"; + std::filesystem::create_directory(dir1); + std::filesystem::create_directory(dir2); + setenv("XDG_CONFIG_HOME", dir1); + setenv("XDG_CONFIG_DIRS", dir2); + auto file1 = dir1 / "file"; + auto file2 = dir2 / "file"; + { + std::ofstream out(file2); + out << "key1 = value1\n" + << "key2 = value2\n"; + } + { + std::ofstream out(file1); + out << "invalid line"; + } + std::vector<std::string> errors; + auto cfg = cfg::load_all("file", errors); + ASSERT_TRUE(cfg); + EXPECT_EQ(1, errors.size()); + + EXPECT_FALSE(cfg->has("")); + EXPECT_FALSE(cfg->get("").has_value()); + + EXPECT_TRUE(cfg->has("key1")); + EXPECT_EQ("value1", cfg->get("key1").value_or("")); + + EXPECT_TRUE(cfg->has("key2")); + EXPECT_EQ("value2", cfg->get("key2").value_or("")); + + EXPECT_FALSE(cfg->has("key3")); +} diff --git a/test/data/test.jpeg b/test/data/test.jpeg Binary files differnew file mode 100644 index 0000000..0bc367a --- /dev/null +++ b/test/data/test.jpeg diff --git a/test/data/test.png b/test/data/test.png Binary files differnew file mode 100644 index 0000000..6c4877b --- /dev/null +++ b/test/data/test.png diff --git a/test/data/test.svg b/test/data/test.svg new file mode 100644 index 0000000..155a68a --- /dev/null +++ b/test/data/test.svg @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="10" + height="10" + viewBox="0 0 10 10" + version="1.1" + id="svg1" + inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" + sodipodi:docname="test.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:document-units="px" + inkscape:current-layer="layer1" /> + <defs + id="defs1" /> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <rect + style="fill:#ff0000;fill-rule:evenodd;" + id="rect1" + width="5" + height="5" + x="0" + y="0" /> + <rect + style="opacity:0.5;fill:#ff0000;fill-rule:evenodd" + id="rect1-36" + width="5" + height="5" + x="5" + y="5" /> + <rect + style="fill:#00ff00;fill-rule:evenodd" + id="rect1-3" + width="5" + height="5" + x="5" + y="0" /> + <rect + style="fill:#0000ff;fill-rule:evenodd" + id="rect1-3-1" + width="5" + height="5" + x="0" + y="5" /> + </g> +</svg> diff --git a/test/data/test.xpm b/test/data/test.xpm new file mode 100644 index 0000000..12eb337 --- /dev/null +++ b/test/data/test.xpm @@ -0,0 +1,17 @@ +/* XPM */ +static char * test_xpm[] = { +"10 10 4 1", +" c None", +". c #FF0000", +"+ c #00FF00", +"@ c #0000FF", +".....+++++", +".....+++++", +".....+++++", +".....+++++", +".....+++++", +"@@@@@ ", +"@@@@@ ", +"@@@@@ ", +"@@@@@ ", +"@@@@@ "}; diff --git a/test/image_processor.cc b/test/image_processor.cc new file mode 100644 index 0000000..c005f01 --- /dev/null +++ b/test/image_processor.cc @@ -0,0 +1,251 @@ +#include "image_processor.hh" + +#include "config.h" +#include "image.hh" +#include "image_loader.hh" +#include "spawner.hh" + +#include <bit> +#include <cmath> +#include <cstdint> +#include <filesystem> +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include <memory> +#include <string> +#include <utility> + +#ifndef TESTDIR +# define TESTDIR "." +#endif + +namespace { + +// JPEG compression at quality 90 +constexpr int kLossyError = 32; + +MATCHER_P(ColourLossyEq, expected, "") { + uint8_t a1 = arg >> 24; + uint8_t e1 = expected >> 24; + uint8_t a2 = arg >> 16; + uint8_t e2 = expected >> 16; + uint8_t a3 = arg >> 8; + uint8_t e3 = expected >> 8; + uint8_t a4 = arg; + uint8_t e4 = expected; + return abs(static_cast<int>(a1) - static_cast<int>(e1)) <= kLossyError && + abs(static_cast<int>(a2) - static_cast<int>(e2)) <= kLossyError && + abs(static_cast<int>(a3) - static_cast<int>(e3)) <= kLossyError && + abs(static_cast<int>(a4) - static_cast<int>(e4)) <= kLossyError; +} + +std::string error2str(ImageLoadError err) { + switch (err) { + case ImageLoadError::kNoSuchFile: + return "no such file"; + case ImageLoadError::kError: + return "error"; + case ImageLoadError::kUnsupportedFormat: + return "unsupported format"; + case ImageLoadError::kProcessError: + return "process error"; + } + std::unreachable(); +} + +class ImageProcessorTest : public testing::Test { + protected: + void SetUp() override { + { + auto spawner = Spawner::create(); + ASSERT_TRUE(spawner.has_value()); + spawner_ = std::move(spawner.value()); + } + + { + auto process = spawner_->run(Spawner::Exec::kImageProcessor); + ASSERT_TRUE(process.has_value()); + process_ = std::move(process.value()); + } + } + + [[nodiscard]] + Process& process() const { + return *process_; + } + + [[nodiscard]] + static std::filesystem::path testdir() { + return TESTDIR; + } + + void peek(std::string const& name) { + auto ret = image_processor::peek(process(), testdir() / name); + ASSERT_TRUE(ret.has_value()) << error2str(ret.error()); + EXPECT_EQ(10, ret->width); + EXPECT_EQ(10, ret->height); + } + + private: + std::unique_ptr<Spawner> spawner_; + std::unique_ptr<Process> process_; +}; + +enum class AlphaMode : uint8_t { + kFull, + kMono, + kNone, +}; + +class ImageProcessorTestWithFormat : public ImageProcessorTest, public testing::WithParamInterface<Image::Format> { + protected: + void load(std::string const& name, AlphaMode alpha = AlphaMode::kFull, bool lossy = false) { + auto ret = image_processor::load(process(), testdir() / name, GetParam()); + ASSERT_TRUE(ret.has_value()) << error2str(ret.error()); + EXPECT_EQ(10, ret.value()->width()); + EXPECT_EQ(10, ret.value()->height()); + EXPECT_EQ(GetParam(), ret.value()->format()); + + EXPECT_GE(4 * ret.value()->width(), ret.value()->scanline()); + if (lossy) { + for (uint32_t y = 0; y < ret.value()->height(); ++y) { + for (uint32_t x = 0; x < ret.value()->width(); ++x) { + EXPECT_THAT(reinterpret_cast<uint32_t const*>( + ret.value()->data() + (y * ret.value()->scanline()))[x], + ColourLossyEq(expected_color(x, y, alpha))) + << x << "x" << y; + } + } + } else { + for (uint32_t y = 0; y < ret.value()->height(); ++y) { + for (uint32_t x = 0; x < ret.value()->width(); ++x) { + EXPECT_EQ(expected_color(x, y, alpha), + reinterpret_cast<uint32_t const*>( + ret.value()->data() + (y * ret.value()->scanline()))[x]) + << x << "x" << y; + } + } + } + } + + private: + static uint32_t expected_argb(uint32_t x, uint32_t y) { + uint32_t argb; + if (x < 5) { + if (y < 5) { + argb = 0xffff0000; + } else { + argb = 0xff0000ff; + } + } else { + if (y < 5) { + argb = 0xff00ff00; + } else { + argb = 0x80ff0000; + } + } + return argb; + } + + static uint32_t argb_to_format(uint32_t argb) { + if constexpr (std::endian::native == std::endian::big) { + switch (GetParam()) { + case Image::Format::ARGB_8888: + return argb; + case Image::Format::BGRA_8888: + return std::byteswap(argb); + case Image::Format::RGBA_8888: + return std::rotl(argb, 8); + case Image::Format::ABGR_8888: + return std::byteswap(std::rotl(argb, 8)); + } + } else { + switch (GetParam()) { + case Image::Format::ARGB_8888: + return std::byteswap(argb); + case Image::Format::BGRA_8888: + return argb; + case Image::Format::RGBA_8888: + return std::byteswap(std::rotl(argb, 8)); + case Image::Format::ABGR_8888: + return std::rotl(argb, 8); + } + } + std::unreachable(); + } + + static uint32_t expected_color(uint32_t x, uint32_t y, AlphaMode alpha) { + uint32_t argb = expected_argb(x, y); + switch (alpha) { + case AlphaMode::kFull: + break; + case AlphaMode::kMono: + if (argb >> 24 != 0xff) + argb = 0; + break; + case AlphaMode::kNone: + argb |= 0xff000000; + break; + } + return argb_to_format(argb); + } + + static uint32_t expected_color_no_alpha(uint32_t x, uint32_t y) { + return argb_to_format(0xff000000 | expected_argb(x, y)); + } +}; + +} // namespace + +TEST_F(ImageProcessorTest, peek_no_such_file) { + auto ret = image_processor::peek(process(), testdir() / "does_not_exist.png"); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(ImageLoadError::kNoSuchFile, ret.error()); +} + +#if HAVE_PNG +TEST_F(ImageProcessorTest, peek_png) { + peek("test.png"); +} + +TEST_P(ImageProcessorTestWithFormat, load_png) { + load("test.png"); +} +#endif + +#if HAVE_JPEG +TEST_F(ImageProcessorTest, peek_jpeg) { + peek("test.jpeg"); +} + +TEST_P(ImageProcessorTestWithFormat, load_jpeg) { + load("test.jpeg", AlphaMode::kNone, /* lossy */ true); +} +#endif + +#if HAVE_XPM +TEST_F(ImageProcessorTest, peek_xpm) { + peek("test.xpm"); +} + +TEST_P(ImageProcessorTestWithFormat, load_xpm) { + load("test.xpm", AlphaMode::kMono); +} +#endif + +#if HAVE_RSVG +TEST_F(ImageProcessorTest, peek_svg) { + peek("test.svg"); +} + +TEST_P(ImageProcessorTestWithFormat, load_svg) { + load("test.svg"); +} +#endif + +INSTANTIATE_TEST_SUITE_P(ImageProcessor, + ImageProcessorTestWithFormat, + testing::Values(Image::Format::RGBA_8888, + Image::Format::ARGB_8888, + Image::Format::BGRA_8888, + Image::Format::ABGR_8888)); diff --git a/test/io.cc b/test/io.cc new file mode 100644 index 0000000..39d558b --- /dev/null +++ b/test/io.cc @@ -0,0 +1,274 @@ +#include "io.hh" + +#include "io_test_helper.hh" + +#include <cerrno> +#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 ret = io::createat(dirfd(), name, false); + ASSERT_TRUE(ret.has_value()); + auto ret2 = ret.value()->repeat_write(value.data(), value.size()); + EXPECT_TRUE(ret2.has_value()); + if (ret2.has_value()) { + EXPECT_EQ(ret2.value(), value.size()); + } + EXPECT_TRUE(ret.value()->close().has_value()); + } + + void cat(const std::string& name, const std::string& value) { + auto ret = io::openat(dirfd(), name); + ASSERT_TRUE(ret.has_value()); + std::string tmp(value.size(), ' '); + auto ret2 = ret.value()->repeat_read(tmp.data(), tmp.size()); + ASSERT_TRUE(ret2.has_value()); + EXPECT_EQ(value.size(), ret2.value()); + EXPECT_EQ(tmp, value); + ret2 = ret.value()->repeat_read(tmp.data(), tmp.size()); + ASSERT_FALSE(ret2.has_value()); + EXPECT_EQ(io::ReadError::Eof, ret2.error()); + } + + 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); +} + +TEST_F(IoTest, create) { + auto ret = io::createat(dirfd(), "test", false); + ASSERT_TRUE(ret.has_value()); + std::string tmp{"hello world"}; + auto ret2 = ret.value()->repeat_write(tmp.data(), tmp.size()); + ASSERT_TRUE(ret2.has_value()); + EXPECT_EQ(11, ret2.value()); + EXPECT_TRUE(ret.value()->close().has_value()); + + cat("test", "hello world"); +} + +TEST_F(IoTest, create_exists) { + touch("test"); + + auto ret = io::createat(dirfd(), "test", false); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(io::CreateError::Exists, ret.error()); +} + +TEST_F(IoTest, create_overwrite) { + touch("test"); + + auto ret = io::createat(dirfd(), "test", true); + ASSERT_TRUE(ret.has_value()); + std::string tmp{"hello world"}; + auto ret2 = ret.value()->repeat_write(tmp.data(), tmp.size()); + ASSERT_TRUE(ret2.has_value()); + EXPECT_EQ(11, ret2.value()); + EXPECT_TRUE(ret.value()->close().has_value()); + + cat("test", "hello world"); +} + +TEST_F(IoTest, write_block) { + auto ret = io::createat(dirfd(), "test", false); + ASSERT_TRUE(ret.has_value()); + auto ret2 = io_make_max_block(std::move(ret.value()), 2); + std::string tmp{"hello world"}; + auto ret3 = ret2->repeat_write(tmp.data(), tmp.size()); + ASSERT_TRUE(ret3.has_value()); + EXPECT_EQ(11, ret3.value()); + EXPECT_TRUE(ret2->close().has_value()); + + cat("test", "hello world"); +} + +TEST_F(IoTest, pipe) { + auto ret = io::pipe(); + ASSERT_TRUE(ret.has_value()); + + std::string tmp{"hello world"}; + auto ret2 = ret.value().second->repeat_write(tmp.data(), tmp.size()); + ASSERT_TRUE(ret2.has_value()); + EXPECT_EQ(11, ret2.value()); + ret.value().second.reset(); + + std::string tmp2(12, ' '); + auto ret3 = ret.value().first->repeat_read(tmp2.data(), tmp2.size()); + ASSERT_TRUE(ret3.has_value()); + EXPECT_EQ(11, ret3.value()); + tmp2.resize(ret3.value()); + EXPECT_EQ(tmp, tmp2); +} diff --git a/test/io_test_helper.cc b/test/io_test_helper.cc new file mode 100644 index 0000000..880a114 --- /dev/null +++ b/test/io_test_helper.cc @@ -0,0 +1,165 @@ +#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; + } + + [[nodiscard]] + int raw_fd() const override { + return reader_->raw_fd(); + } + + private: + std::unique_ptr<io::Reader> reader_; + size_t offset_; + io::ReadError const error_; +}; + +class BreakingWriter : public io::Writer { + public: + BreakingWriter(std::unique_ptr<io::Writer> writer, size_t offset, + io::WriteError error) + : writer_(std::move(writer)), offset_(offset), error_(error) {} + + [[nodiscard]] + std::expected<size_t, io::WriteError> write(void const* dst, + size_t size) override { + if (offset_ == 0) + return std::unexpected(error_); + size_t avail = std::min(offset_, size); + auto ret = writer_->write(dst, avail); + if (ret.has_value()) { + offset_ -= ret.value(); + } + return ret; + } + + [[nodiscard]] + std::expected<void, io::WriteError> close() override { + return writer_->close(); + } + + [[nodiscard]] + int raw_fd() const override { + return writer_->raw_fd(); + } + + private: + std::unique_ptr<io::Writer> writer_; + size_t offset_; + io::WriteError 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); + } + + [[nodiscard]] + int raw_fd() const override { + return reader_->raw_fd(); + } + + private: + std::unique_ptr<io::Reader> reader_; + size_t const max_block_size_; +}; + +class MaxBlockWriter : public io::Writer { + public: + MaxBlockWriter(std::unique_ptr<io::Writer> writer, size_t max_block_size) + : writer_(std::move(writer)), max_block_size_(max_block_size) {} + + [[nodiscard]] + std::expected<size_t, io::WriteError> write(void const* dst, + size_t size) override { + size_t avail = std::min(max_block_size_, size); + return writer_->write(dst, avail); + } + + [[nodiscard]] + std::expected<void, io::WriteError> close() override { + return writer_->close(); + } + + [[nodiscard]] + int raw_fd() const override { + return writer_->raw_fd(); + } + + private: + std::unique_ptr<io::Writer> writer_; + 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::Writer> io_make_breaking(std::unique_ptr<io::Writer> writer, + size_t offset, + io::WriteError error) { + return std::make_unique<BreakingWriter>(std::move(writer), 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); +} + +std::unique_ptr<io::Writer> io_make_max_block( + std::unique_ptr<io::Writer> writer, size_t max_block_size) { + return std::make_unique<MaxBlockWriter>(std::move(writer), max_block_size); +} diff --git a/test/io_test_helper.hh b/test/io_test_helper.hh new file mode 100644 index 0000000..eb7bbb3 --- /dev/null +++ b/test/io_test_helper.hh @@ -0,0 +1,27 @@ +#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::Writer> io_make_breaking( + std::unique_ptr<io::Writer> writer, size_t offset = 0, + io::WriteError error = io::WriteError::Error); + +[[nodiscard]] +std::unique_ptr<io::Reader> io_make_max_block( + std::unique_ptr<io::Reader> reader, size_t max_block_size); + +[[nodiscard]] +std::unique_ptr<io::Writer> io_make_max_block( + std::unique_ptr<io::Writer> writer, 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/paths.cc b/test/paths.cc new file mode 100644 index 0000000..b07e459 --- /dev/null +++ b/test/paths.cc @@ -0,0 +1,63 @@ +#include "paths.hh" + +#include "testenv.hh" + +#include <gtest/gtest.h> + +namespace { + +class PathsTest : public TestEnv {}; + +} // namespace + +TEST_F(PathsTest, home) { + setenv("HOME", "foo-bar"); + EXPECT_EQ("foo-bar", paths::home()); + unsetenv("HOME"); + EXPECT_NE("foo-bar", paths::home()); + setenv("HOME", ""); + EXPECT_NE("", paths::home()); +} + +TEST_F(PathsTest, config_dirs) { + setenv("XDG_CONFIG_HOME", "foo"); + setenv("XDG_CONFIG_DIRS", "bar:fum::foo"); + auto dirs = paths::config_dirs(); + EXPECT_EQ(3, dirs.size()); + if (dirs.size() >= 3) { + EXPECT_EQ("foo", dirs[0]); + EXPECT_EQ("bar", dirs[1]); + EXPECT_EQ("fum", dirs[2]); + } + setenv("HOME", "home"); + unsetenv("XDG_CONFIG_HOME"); + unsetenv("XDG_CONFIG_DIRS"); + dirs = paths::config_dirs(); + EXPECT_EQ(2, dirs.size()); + if (dirs.size() >= 2) { + EXPECT_EQ("home/.config", dirs[0]); + EXPECT_EQ("/etc/xdg", dirs[1]); + } +} + +TEST_F(PathsTest, data_dirs) { + setenv("XDG_DATA_HOME", "foo"); + setenv("XDG_DATA_DIRS", "bar:fum::foo"); + auto dirs = paths::data_dirs(); + EXPECT_EQ(3, dirs.size()); + if (dirs.size() >= 3) { + EXPECT_EQ("foo", dirs[0]); + EXPECT_EQ("bar", dirs[1]); + EXPECT_EQ("fum", dirs[2]); + } + setenv("HOME", "home"); + unsetenv("XDG_DATA_HOME"); + unsetenv("XDG_DATA_DIRS"); + dirs = paths::data_dirs(); + EXPECT_EQ(3, dirs.size()); + if (dirs.size() >= 3) { + EXPECT_EQ("home/.local/share", dirs[0]); + EXPECT_EQ("/usr/local/share/", dirs[1]); + EXPECT_EQ("/usr/share/", dirs[2]); + } +} diff --git a/test/str.cc b/test/str.cc new file mode 100644 index 0000000..0184ad5 --- /dev/null +++ b/test/str.cc @@ -0,0 +1,164 @@ +#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, split2) { + auto ret = str::split("", "\r\n"); + EXPECT_EQ(0, ret.size()); + + ret = str::split("", "\r\n", true); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ("", ret[0]); + + ret = str::split("\r\n", "\r\n"); + EXPECT_EQ(0, ret.size()); + + ret = str::split("\r\n", "\r\n", true); + ASSERT_EQ(2, ret.size()); + EXPECT_EQ("", ret[0]); + EXPECT_EQ("", ret[1]); + + ret = str::split("\r\na\r\nb\r\n", "\r\n"); + ASSERT_EQ(2, ret.size()); + EXPECT_EQ("a", ret[0]); + EXPECT_EQ("b", ret[1]); + + ret = str::split("\r\na\r\nb\r\n", "\r\n", 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("\r\na\r\nb", "\r\n", true); + ASSERT_EQ(3, ret.size()); + EXPECT_EQ("", ret[0]); + EXPECT_EQ("a", ret[1]); + EXPECT_EQ("b", ret[2]); + + ret = str::split("\ra\nb", "\r\n"); + ASSERT_EQ(1, ret.size()); + EXPECT_EQ("\ra\nb", ret[0]); +} + +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); +} + +TEST(str, ltrim) { + auto ret = str::ltrim(""); + EXPECT_EQ("", ret); + + ret = str::ltrim(" "); + EXPECT_EQ("", ret); + + ret = str::ltrim(" "); + EXPECT_EQ("", ret); + + ret = str::ltrim("foo"); + EXPECT_EQ("foo", ret); + + ret = str::ltrim(" foo"); + EXPECT_EQ("foo", ret); + + ret = str::ltrim(" foo "); + EXPECT_EQ("foo ", ret); + + ret = str::ltrim("foo "); + EXPECT_EQ("foo ", ret); + + ret = str::ltrim(" foo bar "); + EXPECT_EQ("foo bar ", ret); + + ret = str::ltrim("\tfoo bar\r\n"); + EXPECT_EQ("foo bar\r\n", ret); +} + +TEST(str, rtrim) { + auto ret = str::rtrim(""); + EXPECT_EQ("", ret); + + ret = str::rtrim(" "); + EXPECT_EQ("", ret); + + ret = str::rtrim(" "); + EXPECT_EQ("", ret); + + ret = str::rtrim("foo"); + EXPECT_EQ("foo", ret); + + ret = str::rtrim(" foo"); + EXPECT_EQ(" foo", ret); + + ret = str::rtrim(" foo "); + EXPECT_EQ(" foo", ret); + + ret = str::rtrim("foo "); + EXPECT_EQ("foo", ret); + + ret = str::rtrim(" foo bar "); + EXPECT_EQ(" foo bar", ret); + + ret = str::rtrim("\tfoo bar\r\n"); + EXPECT_EQ("\tfoo bar", ret); +} diff --git a/test/testdir.cc b/test/testdir.cc new file mode 100644 index 0000000..d5aef22 --- /dev/null +++ b/test/testdir.cc @@ -0,0 +1,37 @@ +#include "testdir.hh" + +#include <cstdlib> +#include <filesystem> +#include <format> +#include <gtest/gtest.h> +#include <system_error> + +namespace { + +std::filesystem::path mktmpdir() { + std::error_code ec; + auto base = std::filesystem::temp_directory_path(ec); + if (ec) + return {}; + for (int i = 0; i < 10; ++i) { + auto name = std::format( + "{}-{}", + ::testing::UnitTest::GetInstance()->current_test_info()->name(), + rand()); + auto tmpdir = base / name; + if (std::filesystem::exists(tmpdir)) + continue; + if (std::filesystem::create_directory(tmpdir, ec)) + return tmpdir; + } + return {}; +} + +} // namespace + +TestDir::TestDir() : path_(mktmpdir()) {} + +TestDir::~TestDir() { + if (!path_.empty()) + std::filesystem::remove_all(path_); +} diff --git a/test/testdir.hh b/test/testdir.hh new file mode 100644 index 0000000..b59e09a --- /dev/null +++ b/test/testdir.hh @@ -0,0 +1,22 @@ +#ifndef TESTDIR_HH +#define TESTDIR_HH + +#include <filesystem> // IWYU pragma: export + +class TestDir { + public: + TestDir(); + ~TestDir(); + + TestDir(TestDir const&) = delete; + TestDir& operator=(TestDir const&) = delete; + + bool good() const { return !path_.empty(); }; + + std::filesystem::path const& path() const { return path_; }; + + private: + std::filesystem::path path_; +}; + +#endif // TESTDIR_HH diff --git a/test/testenv.cc b/test/testenv.cc new file mode 100644 index 0000000..56701a4 --- /dev/null +++ b/test/testenv.cc @@ -0,0 +1,44 @@ +#include "testenv.hh" + +#include <cstdlib> +#include <optional> +#include <string> + +void TestEnv::setenv(std::string const& name, std::string const& value) { + saveenv(name); + + // NOLINTNEXTLINE(misc-include-cleaner) + ::setenv(name.c_str(), value.c_str(), 1); +} + +void TestEnv::unsetenv(std::string const& name) { + saveenv(name); + + // NOLINTNEXTLINE(misc-include-cleaner) + ::unsetenv(name.c_str()); +} + +void TestEnv::TearDown() { + for (auto const& pair : env_) { + if (pair.second.has_value()) { + // NOLINTNEXTLINE(misc-include-cleaner) + ::setenv(pair.first.c_str(), pair.second->c_str(), 1); + } else { + // NOLINTNEXTLINE(misc-include-cleaner) + ::unsetenv(pair.first.c_str()); + } + } +} + +void TestEnv::saveenv(std::string const& name) { + auto it = env_.find(name); + if (it != env_.end()) + return; + + auto* str = getenv(name.c_str()); + if (str == nullptr) { + env_.emplace(name, std::nullopt); + } else { + env_.emplace(name, str); + } +} diff --git a/test/testenv.hh b/test/testenv.hh new file mode 100644 index 0000000..abe0bc8 --- /dev/null +++ b/test/testenv.hh @@ -0,0 +1,22 @@ +#ifndef TESTENV_HH +#define TESTENV_HH + +#include <gtest/gtest.h> +#include <map> +#include <optional> +#include <string> + +class TestEnv : public testing::Test { + protected: + void setenv(std::string const& name, std::string const& value); + void unsetenv(std::string const& name); + + void TearDown() override; + + private: + void saveenv(std::string const& name); + + std::map<std::string, std::optional<std::string>> env_; +}; + +#endif // TESTENV_HH diff --git a/test/u8.cc b/test/u8.cc new file mode 100644 index 0000000..4378d9c --- /dev/null +++ b/test/u8.cc @@ -0,0 +1,276 @@ +#include "u8.hh" + +#include <gtest/gtest.h> +#include <iterator> +#include <vector> + +TEST(u8, empty) { + std::vector<uint8_t> empty; + auto it = empty.begin(); + auto ret = u8::read(it, empty.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::End, ret.error()); + + auto ret_replace = u8::read_replace(it, empty.end(), false); + ASSERT_FALSE(ret_replace.has_value()); + EXPECT_EQ(u::ReadErrorReplace::End, ret_replace.error()); + + EXPECT_FALSE(u8::write(it, empty.end(), 0x40)); + + EXPECT_FALSE(u8::skip(it, empty.end())); +} + +TEST(u8, examples) { + { + std::vector<uint8_t> literal{0x57}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_TRUE(ret.has_value()); + EXPECT_EQ(0x57, *ret); + EXPECT_EQ(it, literal.end()); + + it = literal.begin(); + EXPECT_TRUE(u8::skip(it, literal.end())); + EXPECT_EQ(it, literal.end()); + } + { + std::vector<uint8_t> literal{0xce, 0x92}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_TRUE(ret.has_value()); + EXPECT_EQ(0x392, *ret); + EXPECT_EQ(it, literal.end()); + + it = literal.begin(); + EXPECT_TRUE(u8::skip(it, literal.end())); + EXPECT_EQ(it, literal.end()); + } + { + std::vector<uint8_t> literal{0xec, 0x9c, 0x84}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_TRUE(ret.has_value()); + EXPECT_EQ(0xc704, *ret); + EXPECT_EQ(it, literal.end()); + + it = literal.begin(); + EXPECT_TRUE(u8::skip(it, literal.end())); + EXPECT_EQ(it, literal.end()); + } + { + std::vector<uint8_t> literal{0xf0, 0x90, 0x8d, 0x85}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_TRUE(ret.has_value()); + EXPECT_EQ(0x10345, *ret); + EXPECT_EQ(it, literal.end()); + + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0x10345, *ret_replace); + EXPECT_EQ(it, literal.end()); + + it = literal.begin(); + EXPECT_TRUE(u8::skip(it, literal.end())); + EXPECT_EQ(it, literal.end()); + } + { + std::vector<uint8_t> literal(1, 0x0); + auto it = literal.begin(); + EXPECT_TRUE(u8::write(it, literal.end(), 0x57)); + EXPECT_EQ(0x57, literal[0]); + EXPECT_EQ(it, literal.end()); + } + { + std::vector<uint8_t> literal(2, 0x0); + auto it = literal.begin(); + EXPECT_TRUE(u8::write(it, literal.end(), 0x392)); + EXPECT_EQ(0xce, literal[0]); + EXPECT_EQ(0x92, literal[1]); + EXPECT_EQ(it, literal.end()); + } + { + std::vector<uint8_t> literal(3, 0x0); + auto it = literal.begin(); + EXPECT_TRUE(u8::write(it, literal.end(), 0xc704)); + EXPECT_EQ(0xec, literal[0]); + EXPECT_EQ(0x9c, literal[1]); + EXPECT_EQ(0x84, literal[2]); + EXPECT_EQ(it, literal.end()); + } + { + std::vector<uint8_t> literal(4, 0x0); + auto it = literal.begin(); + EXPECT_TRUE(u8::write(it, literal.end(), 0x10345)); + EXPECT_EQ(0xf0, literal[0]); + EXPECT_EQ(0x90, literal[1]); + EXPECT_EQ(0x8d, literal[2]); + EXPECT_EQ(0x85, literal[3]); + EXPECT_EQ(it, literal.end()); + } +} + +TEST(u8, overlong) { + { + std::vector<uint8_t> literal{0xc0, 0x80}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Invalid, ret.error()); + } +} + +TEST(u8, incomplete) { + { + std::vector<uint8_t> literal{0xce}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Incomplete, ret.error()); + } + { + std::vector<uint8_t> literal{0xec}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Incomplete, ret.error()); + } + { + std::vector<uint8_t> literal{0xec, 0x9c}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Incomplete, ret.error()); + + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_FALSE(ret_replace.has_value()); + EXPECT_EQ(u::ReadErrorReplace::Incomplete, ret_replace.error()); + + it = literal.begin(); + ret_replace = u8::read_replace(it, literal.end(), true); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0xfffd, ret_replace.value()); + } + { + std::vector<uint8_t> literal{0xf0}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Incomplete, ret.error()); + } + { + std::vector<uint8_t> literal{0xf0, 0x90}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Incomplete, ret.error()); + } + { + std::vector<uint8_t> literal{0xf0, 0x90, 0x8d}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Incomplete, ret.error()); + } +} + +TEST(u8, invalid) { + { + std::vector<uint8_t> literal{0xf0, 0xf0, 0xf0, 0xf0}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Invalid, ret.error()); + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0xfffd, *ret_replace); + EXPECT_EQ(it, std::next(literal.begin())); + } + { + std::vector<uint8_t> literal{0xa0}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Invalid, ret.error()); + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0xfffd, *ret_replace); + EXPECT_EQ(it, std::next(literal.begin())); + } + { + std::vector<uint8_t> literal{0xce, 0xff}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Invalid, ret.error()); + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0xfffd, *ret_replace); + EXPECT_EQ(it, std::next(literal.begin())); + } + { + std::vector<uint8_t> literal{0xec, 0xff, 0x84}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Invalid, ret.error()); + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0xfffd, *ret_replace); + EXPECT_EQ(it, std::next(literal.begin())); + } + { + std::vector<uint8_t> literal{0xec, 0x9c, 0xff}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Invalid, ret.error()); + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0xfffd, *ret_replace); + EXPECT_EQ(it, std::next(literal.begin())); + } + { + std::vector<uint8_t> literal{0xf0, 0xff, 0x8d, 0x85}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Invalid, ret.error()); + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0xfffd, *ret_replace); + EXPECT_EQ(it, std::next(literal.begin())); + } + { + std::vector<uint8_t> literal{0xf0, 0x90, 0xff, 0x85}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Invalid, ret.error()); + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0xfffd, *ret_replace); + EXPECT_EQ(it, std::next(literal.begin())); + } + { + std::vector<uint8_t> literal{0xf0, 0x90, 0x8d, 0xff}; + auto it = literal.begin(); + auto ret = u8::read(it, literal.end()); + ASSERT_FALSE(ret.has_value()); + EXPECT_EQ(u::ReadError::Invalid, ret.error()); + it = literal.begin(); + auto ret_replace = u8::read_replace(it, literal.end(), false); + ASSERT_TRUE(ret_replace.has_value()); + EXPECT_EQ(0xfffd, *ret_replace); + EXPECT_EQ(it, std::next(literal.begin())); + } +} |
