Skip to content

fix(cpp): replace malformed UTF-8 in CLI text output - #997

Merged
ColinLeeo merged 2 commits into
apache:developfrom
ColinLeeo:colin/fix-cli-invalid-utf8-identifiers
Oct 8, 2026
Merged

ColinLeeo merged 2 commits into
apache:developfrom
ColinLeeo:colin/fix-cli-invalid-utf8-identifiers

Conversation

@ColinLeeo

@ColinLeeo ColinLeeo commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

TsFile files written through the SDK can contain malformed UTF-8 identifiers. The CLI currently copies those bytes into CSV, NDJSON and table output, so a name such as bad\xff produces invalid text instead of bad� (TsFile-162).

Replace each maximal ill-formed UTF-8 subpart with U+FFFD at the text output boundary. Share the conversion across the three formatters, export manifests and sketch output. Preserve valid Unicode, raw identifiers used for SDK lookup, and BLOB hexadecimal output. The replacement follows Unicode section 3.9.6.

NDJSON validates the rendered column names before writing rows. If distinct identifiers such as bad\xff and bad\xfe both become bad�, the command reports the duplicate key and exits with code 3 instead of producing JSON that loses a column when parsed. This also covers table statistics TAG keys. Failed exports leave no new target and preserve an existing target with --force.

JSON and table escaping consume the shared UTF-8 recovery stream directly, avoiding an intermediate sanitized string. NDJSON keys are escaped once per result and reused across rows. Sketch skips text conversion when a failed command will not publish output. All changes retain C++11 compatibility.

Validation:

  • 171 CLI, input/output format, row writer and tree/table fixture tests passed.
  • New regressions cover isolated continuation bytes, overlong sequences, surrogates, out-of-range code points, truncated sequences, following delimiters, valid Chinese/emoji and BLOB bytes.
  • SDK-generated malformed device/measurement names remain queryable by their original names; metadata, row output, exports, manifests and sketch produce valid UTF-8.
  • Collision regressions cover two malformed names, a malformed name colliding with a valid U+FFFD name, zero-row results, both data models, table statistics and atomic export failure.
  • 280,481 byte sequences match Python UTF-8 replacement decoding after the streaming refactor, including exhaustive one/two-byte inputs, three/four-byte boundary cases and 100,000 seeded random inputs.
  • The original malformed-identifier fixture now passes strict UTF-8 decoding and JSON parsing.
  • Scoped Maven Spotless check, clang-format 17.0.6 checks for changed code and git diff --check passed.

Built in Release with LZ4 enabled and other optional compression libraries, ANTLR4 and SIMD disabled.

@ColinLeeo
ColinLeeo requested a balanced review from Copilot October 8, 2026 08:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

4 open findings
What changed in this PR

Updates the C++ CLI text output pipeline to sanitize malformed UTF-8 identifiers/strings by replacing ill-formed UTF-8 subparts with U+FFFD at output boundaries, ensuring CSV/NDJSON/table output (plus sketch and export manifests) remains valid UTF-8.

Changes:

  • Introduces a shared replace_invalid_utf8() helper and applies it in CSV/JSON/table escaping.
  • Sanitizes sketch output and reuses shared JSON escaping for export manifests.
  • Adds regression and e2e tests covering invalid UTF-8 sequences while preserving valid UTF-8 and BLOB hex output.
File Description
cpp/​tools/​format/​output_format.h Declares shared UTF-8 sanitization helper for text output formatting.
cpp/​tools/​format/​output_format.cc Implements UTF-8 sanitization and routes CSV/JSON/table escaping through it.
cpp/​tools/​commands/​cmd_sketch.cc Sanitizes sketch output before printing/writing.
cpp/​tools/​commands/​cmd_export.cc Removes local JSON escaping and uses shared formatter utilities for manifests.
cpp/​tools/​README.md Documents UTF-8 replacement behavior for CLI text outputs and manifests.
cpp/​test/​tools/​output_format_test.cc Adds unit tests for valid/invalid UTF-8 handling across formatters and RowWriter.
cpp/​test/​tools/​command_e2e_test.cc Adds end-to-end coverage ensuring invalid identifiers remain queryable but outputs are valid UTF-8.

🧠 Review effort: Lite


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment on lines 587 to +600
int code = printer.run(args, content, err);
const std::string text = replace_invalid_utf8(content.str());
if (code != kExitOk) {
if (args.output.empty()) {
out << content.str();
out << text;
out.flush();
return out.good() ? code : kExitRuntime;
}
return code;
}
if (!args.output.empty()) {
return write_atomic_text(args.output, content.str(), args.file,
args.force, err);
return write_atomic_text(args.output, text, args.file, args.force, err);
}
out << content.str();
out << text;
Comment thread cpp/tools/format/output_format.cc Outdated
std::string out;
out.reserve(s.size() + 2);
for (unsigned char c : s) {
for (unsigned char c : replace_invalid_utf8(s)) {
Comment thread cpp/tools/format/output_format.cc Outdated
std::string out;
out.reserve(s.size() + 2);
for (unsigned char c : s) {
for (unsigned char c : replace_invalid_utf8(s)) {
Comment thread cpp/tools/format/output_format.cc Outdated
if (consumed == length) {
out.append(s, i, consumed);
} else {
out += "\xef\xbf\xbd";

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!keys.insert(key).second) {
error_ =
"duplicate NDJSON column name after UTF-8 replacement: \"" +
key + "\"";
Comment on lines 92 to +98
RowWriter writer(out, fmt, header, types, no_header);
if (!writer.error().empty()) {
if (output_error != nullptr) {
*output_error = writer.error();
}
return common::E_INVALID_ARG;
}
Comment on lines +249 to +250
for_each_utf8_byte(
s, [&out](unsigned char c) { out += static_cast<char>(c); });
table_widths_(header_.size(), 0) {
if (!no_header_) {
if (fmt_ == OutputFormat::kJson) {
std::set<std::string> keys;
@ColinLeeo
ColinLeeo merged commit 133c9b6 into apache:develop Oct 8, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants