diff --git a/.gitignore b/.gitignore index 5e2968ce87d..2cf7d3fcba3 100644 --- a/.gitignore +++ b/.gitignore @@ -77,7 +77,7 @@ Makefile.in /src/common/fail.html.c /src/systemd/cockpit*.service /src/systemd/cockpit*.socket -/src/systemd/cockpit-tempfiles.conf +/src/systemd/tmpfiles.d/cockpit-ws.conf /src/tls/cockpit-certificate-helper /src/ws/cockpit-desktop /src/ws/cockpit.appdata.xml diff --git a/containers/ws/cockpit-auth-ssh-key b/containers/ws/cockpit-auth-ssh-key index d77fc2cd496..76efbe3c0e5 100755 --- a/containers/ws/cockpit-auth-ssh-key +++ b/containers/ws/cockpit-auth-ssh-key @@ -42,8 +42,8 @@ def usage(): def send_frame(content): - data = json.dumps(content).encode('utf-8') - os.write(1, str(len(data) + 1).encode('utf-8')) + data = json.dumps(content).encode() + os.write(1, str(len(data) + 1).encode()) os.write(1, b"\n\n") os.write(1, data) @@ -109,7 +109,7 @@ def read_frame(fd): size = size - len(d) data += d - return data.decode("UTF-8") + return data.decode() def read_auth_reply(): @@ -129,7 +129,7 @@ def decode_basic_header(response): assert response assert response.startswith(starts), response - val = base64.b64decode(response[len(starts):].encode('utf-8')).decode("utf-8") + val = base64.b64decode(response[len(starts):].encode()).decode() user, password = val.split(':', 1) return user, password @@ -150,7 +150,7 @@ cat /run/password""") pass_fd = os.open("/run/password", os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_CLOEXEC, mode=0o600) try: - os.write(pass_fd, password.encode("UTF-8")) + os.write(pass_fd, password.encode()) os.close(pass_fd) p = subprocess.run(["ssh-add", "-t", "30", fname], diff --git a/pkg/base1/test-http.js b/pkg/base1/test-http.js index 4dc70219177..db508143560 100644 --- a/pkg/base1/test-http.js +++ b/pkg/base1/test-http.js @@ -131,6 +131,35 @@ QUnit.test("streaming", assert => { }); }); +QUnit.test("split UTF8 frames", assert => { + const done = assert.async(); + assert.expect(1); + + cockpit.http(test_server) + .get("/mock/split-utf8") + .then(resp => assert.equal(resp, "initialfirst half é second halffinal", "correct response")) + .finally(done); +}); + +QUnit.test("truncated UTF8 frame", assert => { + const done = assert.async(); + assert.expect(3); + let received = ""; + + cockpit.http(test_server) + .get("/mock/truncated-utf8") + .stream(block => { received += block }) + .then(() => assert.ok(false, "should not have succeeded")) + // does not include the first byte of é + .catch(ex => { + // does not include the first byte of é + assert.equal(received, "initialfirst half ", "received expected data"); + assert.equal(ex.problem, "protocol-error", "error code"); + assert.ok(ex.message.includes("unexpected end of data"), ex.message); + }) + .finally(done); +}); + QUnit.test("close", assert => { const done = assert.async(); assert.expect(3); diff --git a/pkg/lib/cockpit.js b/pkg/lib/cockpit.js index 7c582fcf118..4c84815f25d 100644 --- a/pkg/lib/cockpit.js +++ b/pkg/lib/cockpit.js @@ -3896,7 +3896,7 @@ function factory() { if (options.problem) { http_debug("http problem: ", options.problem); - dfd.reject(new BasicError(options.problem)); + dfd.reject(new BasicError(options.problem, options.message)); } else { const body = buffer.squash(); diff --git a/pkg/storaged/crypto/luksmeta-monitor-hack.py b/pkg/storaged/crypto/luksmeta-monitor-hack.py index 36c50fb8722..770f64d984a 100755 --- a/pkg/storaged/crypto/luksmeta-monitor-hack.py +++ b/pkg/storaged/crypto/luksmeta-monitor-hack.py @@ -21,7 +21,7 @@ def b64_decode(data): def get_clevis_config_from_protected_header(protected_header): - header = b64_decode(protected_header).decode("utf-8") + header = b64_decode(protected_header).decode() header_object = json.loads(header) clevis = header_object.get("clevis", None) if clevis is None: @@ -85,7 +85,7 @@ def info(dev): "-u", "cb6e8904-81ff-40da-a84a-07ab9ab5715e"], stderr=subprocess.PIPE) entry["ClevisConfig"] = { - "v": json.dumps(get_clevis_config_from_jwe(luksmeta.decode("utf-8"))) + "v": json.dumps(get_clevis_config_from_jwe(luksmeta.decode())) } except (subprocess.CalledProcessError, FileNotFoundError): pass @@ -98,7 +98,7 @@ def info(dev): try: token = subprocess.check_output(["cryptsetup", "token", "export", dev, "--token-id", match.group(1)], stderr=subprocess.PIPE) - token_object = json.loads(token.decode("utf-8")) + token_object = json.loads(token.decode()) if token_object.get("type") == "clevis": config = json.dumps(get_clevis_config_from_protected_header(token_object["jwe"]["protected"])) for slot_str in token_object.get("keyslots", []): diff --git a/src/build_backend.py b/src/build_backend.py index 89fa465c1c3..1930e7aff7a 100644 --- a/src/build_backend.py +++ b/src/build_backend.py @@ -87,7 +87,7 @@ def write(filename: str, data: AnyStr) -> None: def beipack_self(main: str, args: str = '') -> bytes: from cockpit._vendor.bei import beipack contents = {name: wheel.read(name) for name in wheel.namelist()} - pack = beipack.pack(contents, main, args=args).encode('utf-8') + pack = beipack.pack(contents, main, args=args).encode() return lzma.compress(pack, preset=lzma.PRESET_EXTREME) def write_distinfo(filename: str, lines: Iterable[str]) -> None: diff --git a/src/cockpit/channel.py b/src/cockpit/channel.py index 28d05fe1101..29069d9695c 100644 --- a/src/cockpit/channel.py +++ b/src/cockpit/channel.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import asyncio +import codecs import json import logging import traceback @@ -111,6 +112,8 @@ class Channel(Endpoint): # These get filled in from .do_open() channel = '' group = '' + is_binary: bool + decoder: 'codecs.IncrementalDecoder | None' # input def do_control(self, command: str, message: JsonObject) -> None: @@ -124,6 +127,8 @@ def do_control(self, command: str, message: JsonObject) -> None: self._send_pings = True self._ack_bytes = get_enum(message, 'send-acks', ['bytes'], None) is not None self.group = get_str(message, 'group', 'default') + self.is_binary = get_enum(message, 'binary', ['raw'], None) is not None + self.decoder = None self.freeze_endpoint() self.do_open(message) elif command == 'ready': @@ -217,7 +222,17 @@ def ready(self, **kwargs: JsonValue) -> None: self.thaw_endpoint() self.send_control(command='ready', **kwargs) + def __decode_frame(self, data: bytes, *, final: bool = False) -> str: + assert self.decoder is not None + try: + return self.decoder.decode(data, final=final) + except UnicodeDecodeError as exc: + raise ChannelError('protocol-error', message=str(exc)) from exc + def done(self) -> None: + # any residue from partial send_data() frames? this is invalid, fail the channel + if self.decoder is not None: + self.__decode_frame(b'', final=True) self.send_control(command='done') # tasks and close management @@ -263,8 +278,8 @@ def close(self, close_args: 'JsonObject | None' = None) -> None: if not self._tasks: self._close_now() - def send_data(self, data: bytes) -> bool: - """Send data and handle book-keeping for flow control. + def send_bytes(self, data: bytes) -> bool: + """Send binary data and handle book-keeping for flow control. The flow control is "advisory". The data is sent immediately, even if it's larger than the window. In general you should try to send packets @@ -273,6 +288,10 @@ def send_data(self, data: bytes) -> bool: Returns True if there is still room in the window, or False if you should stop writing for now. In that case, `.do_resume_send()` will be called later when there is more room. + + Be careful with text channels (i.e. without binary="raw"): you are responsible + for ensuring that @data is valid UTF-8. This isn't validated here for + efficiency reasons. """ self.send_channel_data(self.channel, data) @@ -284,6 +303,34 @@ def send_data(self, data: bytes) -> bool: return self._out_sequence < self._out_window + def send_data(self, data: bytes) -> bool: + """Send data and transparently handle UTF-8 for text channels + + Use this for channels which can be text, but are not guaranteed to get + valid UTF-8 frames -- i.e. multi-byte characters may be split across + frames. This is expensive, so prefer send_text() or send_bytes() wherever + possible. + """ + if self.is_binary: + return self.send_bytes(data) + + # for text channels we must avoid splitting UTF-8 multi-byte characters, + # as these can't be sent to a WebSocket (and are often confusing for text streams as well) + if self.decoder is None: + self.decoder = codecs.getincrementaldecoder('utf-8')(errors='strict') + return self.send_text(self.__decode_frame(data)) + + def send_text(self, data: str) -> bool: + """Send UTF-8 string data and handle book-keeping for flow control. + + Similar to `send_bytes`, but for text data. The data is sent as UTF-8 encoded bytes. + """ + return self.send_bytes(data.encode()) + + def send_json(self, _msg: 'JsonObject | None' = None, **kwargs: JsonValue) -> bool: + pretty = self.json_encoder.encode(create_object(_msg, kwargs)) + '\n' + return self.send_text(pretty) + def do_pong(self, message): if not self._send_pings: # huh? logger.warning("Got wild pong on channel %s", self.channel) @@ -299,10 +346,6 @@ def do_resume_send(self) -> None: json_encoder: 'ClassVar[json.JSONEncoder]' = json.JSONEncoder(indent=2) - def send_json(self, _msg: 'JsonObject | None' = None, **kwargs: JsonValue) -> bool: - pretty = self.json_encoder.encode(create_object(_msg, kwargs)) + '\n' - return self.send_data(pretty.encode()) - def send_control(self, command: str, **kwargs: JsonValue) -> None: self.send_channel_control(self.channel, command, None, **kwargs) diff --git a/src/cockpit/channels/filesystem.py b/src/cockpit/channels/filesystem.py index 8d23bd81ee1..6871cad0142 100644 --- a/src/cockpit/channels/filesystem.py +++ b/src/cockpit/channels/filesystem.py @@ -42,7 +42,6 @@ JsonError, JsonObject, get_bool, - get_enum, get_int, get_str, get_strv, @@ -123,7 +122,6 @@ class FsReadChannel(GeneratorChannel): def do_yield_data(self, options: JsonObject) -> Generator[bytes, None, JsonObject]: path = get_str(options, 'path') - binary = get_enum(options, 'binary', ['raw'], None) is not None max_read_size = get_int(options, 'max_read_size', None) logger.debug('Opening file "%s" for reading', path) @@ -134,7 +132,7 @@ def do_yield_data(self, options: JsonObject) -> Generator[bytes, None, JsonObjec if max_read_size is not None and buf.st_size > max_read_size: raise ChannelError('too-large') - if binary and stat.S_ISREG(buf.st_mode): + if self.is_binary and stat.S_ISREG(buf.st_mode): self.ready(size_hint=buf.st_size) else: self.ready() @@ -144,8 +142,8 @@ def do_yield_data(self, options: JsonObject) -> Generator[bytes, None, JsonObjec if data == b'': break logger.debug(' ...sending %d bytes', len(data)) - if not binary: - data = data.replace(b'\0', b'').decode('utf-8', errors='ignore').encode('utf-8') + if not self.is_binary: + data = data.replace(b'\0', b'').decode(errors='ignore').encode() yield data return {'tag': tag_from_stat(buf)} diff --git a/src/cockpit/channels/http.py b/src/cockpit/channels/http.py index 509e273ff10..5fa0b91ada3 100644 --- a/src/cockpit/channels/http.py +++ b/src/cockpit/channels/http.py @@ -21,7 +21,7 @@ import ssl from ..channel import AsyncChannel, ChannelError -from ..jsonutil import JsonObject, get_dict, get_enum, get_int, get_object, get_str, typechecked +from ..jsonutil import JsonObject, get_dict, get_int, get_object, get_str, typechecked logger = logging.getLogger(__name__) @@ -97,7 +97,6 @@ def request( async def run(self, options: JsonObject) -> None: logger.debug('open %s', options) - binary = get_enum(options, 'binary', ['raw'], None) is not None method = get_str(options, 'method') path = get_str(options, 'path') headers = get_object(options, 'headers', lambda d: {k: typechecked(v, str) for k, v in d.items()}, None) @@ -133,7 +132,7 @@ async def run(self, options: JsonObject) -> None: self.send_control(command='response', status=response.status, reason=response.reason, - headers=self.get_headers(response, binary=binary)) + headers=self.get_headers(response, binary=self.is_binary)) # Receive the body and finish up try: diff --git a/src/cockpit/channels/metrics.py b/src/cockpit/channels/metrics.py index f11c7d39d13..02744e1c42a 100644 --- a/src/cockpit/channels/metrics.py +++ b/src/cockpit/channels/metrics.py @@ -160,7 +160,7 @@ def send_updates(self, samples: Samples, last_samples: Samples): self.send_meta(samples, timestamp) self.last_timestamp = self.next_timestamp - self.send_data(json.dumps([data]).encode()) + self.send_text(json.dumps([data])) async def run(self, options): self.metrics = [] diff --git a/src/cockpit/channels/packages.py b/src/cockpit/channels/packages.py index be3703718fc..d3569a14b03 100644 --- a/src/cockpit/channels/packages.py +++ b/src/cockpit/channels/packages.py @@ -36,7 +36,7 @@ class PackagesChannel(AsyncChannel): def http_error(self, status: int, message: str) -> None: template = read_cockpit_data_file('fail.html') self.send_json(status=status, reason='ERROR', headers={'Content-Type': 'text/html; charset=utf-8'}) - self.send_data(template.replace(b'@@message@@', message.encode('utf-8'))) + self.send_data(template.replace(b'@@message@@', message.encode())) self.done() self.close() diff --git a/src/cockpit/channels/trivial.py b/src/cockpit/channels/trivial.py index 221dcbc4707..740584c576d 100644 --- a/src/cockpit/channels/trivial.py +++ b/src/cockpit/channels/trivial.py @@ -28,8 +28,8 @@ class EchoChannel(Channel): def do_open(self, options): self.ready() - def do_data(self, data): - self.send_data(data) + def do_data(self, data: bytes) -> None: + self.send_bytes(data) def do_done(self): self.done() diff --git a/src/cockpit/misc/print.py b/src/cockpit/misc/print.py index 17e5fb2ba09..c0241e0c3d9 100644 --- a/src/cockpit/misc/print.py +++ b/src/cockpit/misc/print.py @@ -44,7 +44,7 @@ def data(self, channel: str, /, data: bytes) -> None: def json(self, channel: str, /, **kwargs: object) -> None: """Send a json message (built from **kwargs) on a channel""" - self.data(channel, json.dumps(kwargs, indent=2).encode('utf-8') + b'\n') + self.data(channel, json.dumps(kwargs, indent=2).encode() + b'\n') def control(self, command: str, **kwargs: Any) -> None: """Send a control message, build from **kwargs""" diff --git a/src/ws/test-server.c b/src/ws/test-server.c index 850e33e6228..c242d6e16e9 100644 --- a/src/ws/test-server.c +++ b/src/ws/test-server.c @@ -152,7 +152,7 @@ mock_http_qs (CockpitWebRequest *request, } static gboolean -on_timeout_send (gpointer data) +send_numbers (gpointer data) { CockpitWebResponse *response = data; gint *at = g_object_get_data (data, "at"); @@ -175,12 +175,62 @@ on_timeout_send (gpointer data) return TRUE; } +static const char* SPLIT_UTF8_FRAMES[] = { + "initial", + /* split an é in the middle */ + "first half \xc3", + "\xa9 second half", + "final", + NULL, +}; + +static gboolean +send_split_utf8 (gpointer data) +{ + CockpitWebResponse *response = data; + gint *at = g_object_get_data (data, "at"); + const char *frame = SPLIT_UTF8_FRAMES[*at]; + + if (frame == NULL) + { + cockpit_web_response_complete (response); + return FALSE; + } + + (*at) += 1; + + g_autoptr(GBytes) bytes = g_bytes_new_static (frame, strlen (frame)); + cockpit_web_response_queue (response, bytes); + return TRUE; +} + +static gboolean +send_truncated_utf8 (gpointer data) +{ + CockpitWebResponse *response = data; + gint *at = g_object_get_data (data, "at"); + const char *frame = SPLIT_UTF8_FRAMES[*at]; + + /* only send the first two frames */ + if (*at == 2) + { + cockpit_web_response_complete (response); + return FALSE; + } + + (*at) += 1; + + g_autoptr(GBytes) bytes = g_bytes_new_static (frame, strlen (frame)); + cockpit_web_response_queue (response, bytes); + return TRUE; +} + static gboolean -mock_http_stream (CockpitWebResponse *response) +mock_http_stream (CockpitWebResponse *response, GSourceFunc func) { cockpit_web_response_headers (response, 200, "OK", -1, NULL); g_object_set_data_full (G_OBJECT (response), "at", g_new0 (gint, 1), g_free); - g_timeout_add_full (G_PRIORITY_DEFAULT, 100, on_timeout_send, + g_timeout_add_full (G_PRIORITY_DEFAULT, 100, func, g_object_ref (response), g_object_unref); return TRUE; @@ -301,7 +351,11 @@ on_handle_mock (CockpitWebServer *server, if (g_str_equal (path, "/qs")) return mock_http_qs (request, response); if (g_str_equal (path, "/stream")) - return mock_http_stream (response); + return mock_http_stream (response, send_numbers); + if (g_str_equal (path, "/split-utf8")) + return mock_http_stream (response, send_split_utf8); + if (g_str_equal (path, "/truncated-utf8")) + return mock_http_stream (response, send_truncated_utf8); if (g_str_equal (path, "/headers")) return mock_http_headers (response, headers); if (g_str_equal (path, "/host")) diff --git a/test/common/cdp.py b/test/common/cdp.py index a796a786712..19a52ef8e4f 100644 --- a/test/common/cdp.py +++ b/test/common/cdp.py @@ -253,10 +253,10 @@ def command(self, cmd: str) -> Any: assert self._driver.stdin is not None assert self._driver.stdout is not None - self._driver.stdin.write(cmd.encode("UTF-8")) + self._driver.stdin.write(cmd.encode()) self._driver.stdin.write(b"\n") self._driver.stdin.flush() - line = self._driver.stdout.readline().decode("UTF-8") + line = self._driver.stdout.readline().decode() if not line: self.kill() raise RuntimeError("CDP broken") diff --git a/test/common/run-tests b/test/common/run-tests index 5a2246e2054..15b83fbe4f3 100755 --- a/test/common/run-tests +++ b/test/common/run-tests @@ -96,7 +96,7 @@ class Test: # If test is being skipped pick up the reason if self.returncode == 77: lines = self.output.splitlines() - skip_reason = lines[-1].strip().decode("utf-8") + skip_reason = lines[-1].strip().decode() self.output = b"\n".join(lines[:-1]) self._print_test(print_tap, skip_reason=skip_reason) return None, 0 @@ -138,7 +138,7 @@ class Test: return None, 1 if proc.returncode == 1: - retry_reason = reason.decode("utf-8") + retry_reason = reason.decode() except OSError as ex: if ex.errno != errno.ENOENT: @@ -279,7 +279,7 @@ def get_affected_tests(test_dir, base_branch, test_files): diff_out = subprocess.check_output(["git", "diff", "--name-only", "origin/" + base_branch, test_dir]) # Never consider 'test/verify/check-example' to be affected - our tests for tests count on that # This file provides only examples, there is no place for it being flaky, no need to retry - changed_tests = [test.decode("utf-8") for test in diff_out.strip().splitlines() if not test.endswith(b"check-example")] + changed_tests = [test.decode() for test in diff_out.strip().splitlines() if not test.endswith(b"check-example")] # If more than 3 test files were changed don't consider any of them as affected # as it might be a PR that changes more unrelated things. @@ -297,7 +297,7 @@ def get_affected_tests(test_dir, base_branch, test_files): diff_out = subprocess.check_output(["git", "diff", "--name-only", "origin/" + base_branch, "--", "pkg/"]) # Drop changes in css files - this does not affect tests thus no reason to retry - files = [f.decode("utf-8") for f in diff_out.strip().splitlines() if not f.endswith(b"css")] + files = [f.decode() for f in diff_out.strip().splitlines() if not f.endswith(b"css")] changed_pkgs = {"check-" + pkg.split('/')[1] for pkg in files} changed_tests.extend([test for test in test_files if any(pkg in test for pkg in changed_pkgs)]) diff --git a/test/common/testlib.py b/test/common/testlib.py index 5966fd11a6a..46ac2cdec2a 100644 --- a/test/common/testlib.py +++ b/test/common/testlib.py @@ -1015,7 +1015,7 @@ def snapshot(self, title: str, label: str | None = None) -> None: html = self.cdp.invoke("Runtime.evaluate", expression="document.documentElement.outerHTML", no_trace=True)["result"]["value"] with open(filename, 'wb') as f: - f.write(html.encode('UTF-8')) + f.write(html.encode()) attach(filename, move=True) print("Wrote HTML dump to " + filename) @@ -1303,7 +1303,7 @@ def copy_js_log(self, title: str, label: str | None = None) -> None: if logs: filename = unique_filename(f"{label or self.label}-{title}", "js.log") with open(filename, 'wb') as f: - f.write('\n'.join(logs).encode('UTF-8')) + f.write('\n'.join(logs).encode()) attach(filename, move=True) print("Wrote JS log to " + filename) @@ -1856,9 +1856,6 @@ def add_machine( # timedatex.service shuts down after timeout, runs into race condition with property watching ".*org.freedesktop.timedate1: couldn't get all properties.*Error:org.freedesktop.DBus.Error.NoReply.*", - - # https://github.com/cockpit-project/cockpit/issues/19235 - "invalid non-UTF8 @data passed as text to web_socket_connection_send.*", ] default_allowed_messages += os.environ.get("TEST_ALLOW_JOURNAL_MESSAGES", "").split(",") diff --git a/test/pytest/pseudo.py b/test/pytest/pseudo.py index 6eaae70fc0a..be58111a3d8 100644 --- a/test/pytest/pseudo.py +++ b/test/pytest/pseudo.py @@ -10,7 +10,7 @@ interaction_client.askpass(2, writer, ['-', 'can haz pw?'], {}) os.close(writer) - response = os.read(reader, 1024).decode('utf-8').strip() + response = os.read(reader, 1024).decode().strip() if response != pw: sys.stderr.write('pseudo says: Bad password\n') sys.exit(1) diff --git a/test/verify/files/mock-faf-server.py b/test/verify/files/mock-faf-server.py index 789de292957..2eb9b129189 100755 --- a/test/verify/files/mock-faf-server.py +++ b/test/verify/files/mock-faf-server.py @@ -11,7 +11,7 @@ class Handler(BaseHTTPRequestHandler): def do_POST_attach(self): - self.wfile.write(json.dumps({'result': True}).encode("UTF-8")) + self.wfile.write(json.dumps({'result': True}).encode()) def do_POST_new(self): response = { @@ -31,7 +31,7 @@ def do_POST_new(self): ], 'result': False } - self.wfile.write(json.dumps(response, indent=2).encode('UTF-8')) + self.wfile.write(json.dumps(response, indent=2).encode()) def do_POST(self): content_length = int(self.headers.get('content-length', 0)) @@ -39,8 +39,8 @@ def do_POST(self): # Without the cgi module, we need to massage the data to form a valid message p = email.parser.BytesFeedParser(policy=email.policy.HTTP) - p.feed('Content-Type: {}\r\n'.format(self.headers.get('content-type', '')).encode('utf-8')) - p.feed('\r\n'.encode('utf-8')) + p.feed('Content-Type: {}\r\n'.format(self.headers.get('content-type', '')).encode()) + p.feed('\r\n'.encode()) p.feed(data) m = p.close() diff --git a/test/verify/files/mock-insights b/test/verify/files/mock-insights index e0792076aaf..7aa85e7bf6d 100755 --- a/test/verify/files/mock-insights +++ b/test/verify/files/mock-insights @@ -55,7 +55,7 @@ class handler(BaseHTTPRequestHandler): self.send_response(200) self.end_headers() if machine_id in systems: - self.wfile.write(json.dumps(systems[machine_id]).encode('utf-8') + b"\n") + self.wfile.write(json.dumps(systems[machine_id]).encode() + b"\n") else: self.wfile.write(b"{ }\n") return diff --git a/tools/build-debian-copyright b/tools/build-debian-copyright index f91486d1b96..c5f193abad7 100755 --- a/tools/build-debian-copyright +++ b/tools/build-debian-copyright @@ -139,7 +139,7 @@ for dirglob in sorted(dist_copyrights): # force UTF-8 output, even when running in C locale for line in template.splitlines(): if '#NPM' in line: - sys.stdout.buffer.write('\n\n'.join(paragraphs).encode('UTF-8')) + sys.stdout.buffer.write('\n\n'.join(paragraphs).encode()) else: - sys.stdout.buffer.write(line.encode('UTF-8')) + sys.stdout.buffer.write(line.encode()) sys.stdout.buffer.write(b'\n')