diff --git a/git/objects/commit.py b/git/objects/commit.py index 690a296a5..3239079c1 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -896,8 +896,19 @@ def _deserialize(self, stream: BytesIO) -> "Commit": enc = next_line buf = enc.strip() while buf: - if buf[0:10] == b"encoding ": - self.encoding = buf[buf.find(b" ") + 1 :].decode(self.encoding, "ignore") + if buf[0:9] == b"encoding ": + raw_encoding = buf[9:] + try: + # Codec names are ASCII, independent of the content encoding. + encoding = raw_encoding.decode("ascii") + # Probe one byte: decoding b"" skips the codec lookup entirely. + b" ".decode(encoding, "replace") + except (LookupError, ValueError): + # Reject malformed names and non-text codecs without changing + # the current encoding (the default unless a valid header was read). + _logger.error("Ignoring invalid commit encoding %r", raw_encoding) + else: + self.encoding = encoding elif buf[0:7] == b"gpgsig ": sig_lines = [buf[buf.find(b" ") + 1 :] + b"\n"] is_next_header = False diff --git a/test/test_commit.py b/test/test_commit.py index 379bc831b..a22daa87a 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -458,6 +458,61 @@ def test_invalid_commit(self): self.assertEqual(cmt.author.name, "E.Azer Ko�o�o�oculu", cmt.author.name) self.assertEqual(cmt.author.email, "azer@kodfabrik.com", cmt.author.email) + def test_deserialization_declared_encoding(self): + # The "encoding" header names the encoding of the identity and message bytes. + data = ( + b"tree 3385f7b1e5f91930a3bd3e07105e113fca29981d\n" + b"author Andr\xe9 1112591163 +0200\n" + b"committer Andr\xe9 1112591163 +0200\n" + b"encoding ISO-8859-1\n" + b"\n" + b"R\xe9sum\xe9\n" + ) + cmt = self.rorepo.commit() + cmt._deserialize(BytesIO(data)) + + self.assertEqual(cmt.encoding, "ISO-8859-1") + self.assertEqual(cmt.author.name, "André") + self.assertEqual(cmt.committer.name, "André") + self.assertEqual(cmt.message, "Résumé\n") + + def test_deserialization_unknown_declared_encoding(self): + # An encoding Python has no text codec for falls back to the default encoding. + data = ( + b"tree 3385f7b1e5f91930a3bd3e07105e113fca29981d\n" + b"author Andr\xe9 1112591163 +0200\n" + b"committer Andr\xe9 1112591163 +0200\n" + b"encoding pony-speak\n" + b"\n" + b"R\xe9sum\xe9\n" + ) + cmt = self.rorepo.commit() + cmt._deserialize(BytesIO(data)) + + self.assertEqual(cmt.encoding, cmt.default_encoding) + self.assertEqual(cmt.author.name, "Andr�") + self.assertEqual(cmt.message, "R�sum�\n") + + def test_deserialization_multiple_encoding_headers(self): + for headers in ( + b"encoding utf-16-be\nencoding ISO-8859-1\n", + b"encoding ISO-8859-1\nencoding utf-\xff8\n", + ): + with self.subTest(headers=headers): + data = ( + b"tree 3385f7b1e5f91930a3bd3e07105e113fca29981d\n" + b"author Andr\xe9 1112591163 +0200\n" + b"committer Andr\xe9 1112591163 +0200\n" + headers + b"\n" + b"R\xe9sum\xe9\n" + ) + cmt = self.rorepo.commit() + cmt._deserialize(BytesIO(data)) + + self.assertEqual(cmt.encoding, "ISO-8859-1") + self.assertEqual(cmt.author.name, "André") + self.assertEqual(cmt.committer.name, "André") + self.assertEqual(cmt.message, "Résumé\n") + @with_rw_directory def test_identity_cannot_alter_headers(self, rw_dir): """A name or email must not add header lines or present another identity."""