NAME
App::karr::Encoding - The character/octet boundary for karr
VERSION
version 0.500
SYNOPSIS
use App::karr::Encoding qw( decode_argv enable_std_utf8 yaml_dump );
enable_std_utf8();
decode_argv();
print yaml_dump( { title => "Fix \x{fc}nicode \x{2014} \x{e4}rger" } );
DESCRIPTION
karr holds one rule: everything inside the program is a Perl character string, and bytes exist only at the outer edges. This module is the only place that crosses that line, so every edge crosses it the same way.
The edges, and who guards them:
@ARGV— "decode_argv", called from bin/karr and bin/karr-foundation.STDOUT/STDERR— "enable_std_utf8", likewise called from the two scripts. An in-process caller that captures output (a test, say) has to put the same layer on its capture handle, because reopeningSTDOUTdrops the layer the script installed.Git refs — "write_ref" in App::karr::Git and "read_ref" in App::karr::Git call "to_octets" and "from_octets". Blobs hold UTF-8 octets; everything above
read_refsees characters.Files — Path::Tiny's
slurp_utf8/spew_utf8, which are already character-level. Nothing extra is needed, and nothing extra may be added: anEncode::encodein front of aspew_utf8is a double encode.YAML — "yaml_dump" and "yaml_load".
YAML::XS::Dumpemits octets andYAML::XS::Loadexpects them, which is the opposite of the rule above, so those two functions are never called directly. (DumpFileandLoadFileare character-level and are used unwrapped.)JSON — "json_encode" and "json_decode". The
encode_jsonanddecode_jsonfunctions are octet-level for the same reason and are likewise not used directly.%ENV— "to_octets_for_env" and "from_octets_from_env". Perl's%ENVis a byte boundary: assigning a character string warnsWide character in setenv, and$ENV{NAME}reads back whatever bytes were stored. The crossing is named here so neither side is handled ad hoc at the call site.
Legacy boards
karr up to and including 0.402 mixed the two levels, and every board written by those versions has UTF-8 octets encoded a second time in its task frontmatter, its config, and its activity log. Task bodies are unaffected: they never passed through Dump. "repair_mojibake" undoes exactly that second encoding, and "board_encoding_version" in App::karr::Git decides when to apply it — see App::karr::Cmd::Repair for the migration.
SEE ALSO
karr, App::karr, App::karr::Git, App::karr::Task, App::karr::Cmd::Repair
BOARD_ENCODING_VERSION
The encoding contract version this code writes, stored per board in refs/karr/meta/encoding. A board without that ref predates the contract and is read through "repair_mojibake".
to_octets
my $bytes = to_octets($characters);
Encodes a character string to UTF-8 octets. undef passes through. This is the character-to-octet edge: the only place karr calls Encode::encode directly, so nothing outside this module ever needs to.
from_octets
my $characters = from_octets($bytes);
Decodes UTF-8 octets to a character string -- the octet-to-character edge, and the only place karr calls Encode::decode directly. A payload that is not valid UTF-8 is returned unchanged rather than being lossily substituted: karr ref blobs and command-line arguments are UTF-8 by contract, and passing a non-conforming payload through keeps the byte-in/byte-out behaviour karr had before this boundary existed, instead of quietly replacing bytes with U+FFFD.
to_octets_for_env
my $bytes = to_octets_for_env($characters);
The character-to-octet edge for %ENV. undef passes through. Assigning a character string to $ENV{NAME} makes Perl emit Wide character in setenv and emit bytes whose encoding depends on the IO layers in scope; encoding explicitly here makes the warning impossible to emit and the bytes the child process receives well-defined. Equivalent to "to_octets"; the dedicated name marks the call site as an ENV crossing, the way "decode_argv" marks argv.
from_octets_from_env
my $characters = from_octets_from_env($bytes);
The octet-to-character edge for %ENV. %ENV stores bytes; reading a non-ASCII value back through $ENV{NAME} gives octets, not characters. A payload that is not valid UTF-8 is returned unchanged, for the same reason "from_octets" does. Equivalent to "from_octets"; the dedicated name marks the call site as an ENV crossing.
decode_argv
decode_argv();
Decodes @ARGV in place from UTF-8. Arguments that are not valid UTF-8 are left as they arrived.
enable_std_utf8
enable_std_utf8();
Puts a :encoding(UTF-8) layer on STDOUT and STDERR so command bodies can print character strings.
STDIN is deliberately left alone. Only App::karr::Cmd::Restore reads it, and it decodes its own payload, so a layer here would decode it twice.
yaml_dump
my $characters = yaml_dump($data);
YAML::XS::Dump at the character level, and the only place karr calls it directly: Dump itself emits octets, which is why the result is passed through "from_octets" before any caller sees it.
yaml_load
my $data = yaml_load($characters);
YAML::XS::Load at the character level, and the only place karr calls it directly: Load expects octets, which is why the character string is turned to them with "to_octets" first.
json_encode
my $characters = json_encode($data);
encode_json at the character level, and the only place karr calls it directly, with canonical key ordering so the --json payload an agent parses is byte-stable across runs.
json_decode
my $data = json_decode($characters);
decode_json at the character level, and the only place karr calls it directly.
repair_mojibake
my $fixed = repair_mojibake($data);
Undoes one round of UTF-8 double encoding, walking hashes and arrays and returning a fresh structure. Blessed references and hash keys are passed through untouched.
A string is only rewritten when it cannot be anything but double-encoded:
pure ASCII is never touched, so an ASCII board is bit-identical afterwards and the repair is safe to run over a whole board;
a string containing a codepoint above U+00FF cannot be a byte string misread as characters, so it is already correct and is left alone;
what remains must additionally form valid UTF-8 when read back as bytes. Ordinary Latin-1 text almost never does —
"\x{fc}ber"isfc 62, which is not valid UTF-8 — whereas"\x{c3}\x{bc}ber"isc3 bc 65 72, which decodes to"\x{fc}ber".
The residual ambiguity is a string whose non-ASCII characters happen to spell valid UTF-8, such as a literal "\x{c3}\x{a9}". That is why the repair is bounded to boards that predate refs/karr/meta/encoding instead of running forever.
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/karr/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.