From 5be02a65966479073c9a0164ffe9c1a51b5441b5 Mon Sep 17 00:00:00 2001 From: igor-ctrl Date: Tue, 11 Aug 2026 17:12:14 -0500 Subject: [PATCH 1/2] fix(cli): accept a UTF-8 BOM in an @file --data payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows PowerShell 5.1 writes a UTF-8 BOM for `Set-Content -Encoding utf8`, and `utf8NoBOM` only exists in PowerShell 6+. Reading the file as plain utf-8 made json.loads reject the payload with "Unexpected UTF-8 BOM" before any request went out — so the documented `-d @payload.json` workaround, which is the *recommended* way to pass a body on PowerShell because inline JSON gets mangled, failed on the exact platform it exists for. Reading as utf-8-sig strips a BOM when present and is identical to utf-8 when it is not, so this only ever accepts more input. Malformed JSON behind a BOM still reports cleanly. --- pyproject.toml | 2 +- src/bcli_cli/_data_arg.py | 6 +++++- tests/test_cli/test_data_arg.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0601bc8..65d79c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ build-backend = "hatchling.build" # installed CLI binary (`bcli`) are unaffected — only `pip install` / # `uv tool install` use this name. name = "bc-cli" -version = "0.8.1" +version = "0.8.2" description = "Python SDK and CLI for Microsoft Dynamics 365 Business Central APIs" readme = "README.md" license = "Apache-2.0" diff --git a/src/bcli_cli/_data_arg.py b/src/bcli_cli/_data_arg.py index 1e6593b..7725a8a 100644 --- a/src/bcli_cli/_data_arg.py +++ b/src/bcli_cli/_data_arg.py @@ -102,7 +102,11 @@ def parse_data_argument(data: str) -> dict: if not is_file: raise typer.BadParameter(f"File not found: {path}") try: - return json.loads(path.read_text(encoding="utf-8")) + # utf-8-sig, not utf-8: Windows PowerShell 5.1 writes a UTF-8 BOM for + # `Set-Content -Encoding utf8`, and json.loads rejects the BOM + # outright. utf-8-sig strips one if present and is identical to + # utf-8 when it is not, so this only ever accepts more. + return json.loads(path.read_text(encoding="utf-8-sig")) except OSError as e: raise typer.BadParameter(f"Could not read {path}: {e.strerror or e}") from e except json.JSONDecodeError as e: diff --git a/tests/test_cli/test_data_arg.py b/tests/test_cli/test_data_arg.py index b9bfd3e..fce4c66 100644 --- a/tests/test_cli/test_data_arg.py +++ b/tests/test_cli/test_data_arg.py @@ -194,3 +194,35 @@ def test_long_or_nul_payload_still_raises_bad_parameter(self, payload): def test_long_at_path_reports_not_found_not_oserror(self): with pytest.raises(typer.BadParameter, match="File not found"): parse_data_argument("@" + "z" * 300) + + +class TestBomTolerantAtFile: + """A UTF-8 BOM in an @file must not be a hard failure. + + Windows PowerShell 5.1 — the runtime install-real.ps1 targets — writes a + BOM for `Set-Content -Encoding utf8`, and `utf8NoBOM` only exists in + PowerShell 6+. Reading as plain utf-8 made json.loads reject the payload + with "Unexpected UTF-8 BOM" before any request went out, which broke the + documented Windows workflow for -d. + """ + + def test_bom_prefixed_file_parses(self, tmp_path): + p = tmp_path / "payload.json" + p.write_bytes(b'\xef\xbb\xbf{"bladeType": "FAN BLADE"}') + assert parse_data_argument(f"@{p}") == {"bladeType": "FAN BLADE"} + + def test_plain_utf8_file_still_parses(self, tmp_path): + p = tmp_path / "payload.json" + p.write_bytes(b'{"bladeType": "HPT BLADE"}') + assert parse_data_argument(f"@{p}") == {"bladeType": "HPT BLADE"} + + def test_non_ascii_without_bom_still_parses(self, tmp_path): + p = tmp_path / "payload.json" + p.write_text('{"note": "Grüße"}', encoding="utf-8") + assert parse_data_argument(f"@{p}") == {"note": "Grüße"} + + def test_malformed_bom_file_still_reports_cleanly(self, tmp_path): + p = tmp_path / "payload.json" + p.write_bytes(b"\xef\xbb\xbf{not valid") + with pytest.raises(typer.BadParameter, match="is not valid JSON"): + parse_data_argument(f"@{p}") From 916e3961ba55c941df58846469cebee60bd37424 Mon Sep 17 00:00:00 2001 From: igor-ctrl Date: Wed, 12 Aug 2026 07:28:00 -0500 Subject: [PATCH 2/2] chore: sync uv.lock with pyproject.toml (bc-cli 0.8.1 -> 0.8.2) uv sync --locked in CI failed before ruff or pytest ever ran because the version bump in this PR's pyproject.toml was never reflected in uv.lock, which pins the same version for the editable root package. --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 82086d7..9929b4b 100644 --- a/uv.lock +++ b/uv.lock @@ -321,7 +321,7 @@ wheels = [ [[package]] name = "bc-cli" -version = "0.8.1" +version = "0.8.2" source = { editable = "." } dependencies = [ { name = "httpx" },