Skip to content

Commit c2c8dff

Browse files
committed
Raise error when strings end in entire code points that encode to pad bytes
1 parent 9649c5d commit c2c8dff

3 files changed

Lines changed: 120 additions & 6 deletions

File tree

README.md

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The Python Shapefile Library (PyShp) reads and writes ESRI Shapefiles in pure Py
88

99
- **Author**: [Joel Lawhead](https://github.com/GeospatialPython)
1010
- **Maintainers**: [James Parrott](https://github.com/JamesParrott) & [Karim Bahgat](https://github.com/karimbahgat)
11-
- **Version**: 3.1.1
11+
- **Version**: 3.1.2
1212
- **Date**: 24th June 2026
1313
- **License**: [MIT](https://github.com/GeospatialPython/pyshp/blob/master/LICENSE.TXT)
1414

@@ -93,6 +93,10 @@ part of your geospatial project.
9393

9494
# Version Changes
9595

96+
## 3.1.2
97+
- Raise error in strict mode when creating field with a name, or writing strings, that ends with whole code points
98+
that whose encoding is pad bytes.
99+
96100
## 3.1.1
97101
### Unicode support made even more robust and yet another encoding bug fixed!
98102
- When reading, only use minimum number of pad bytes to decode text successfully (fixes issue 423).
@@ -1334,8 +1338,14 @@ All logging happens under the namespace `shapefile`. So another way to suppress
13341338

13351339
PyShp supports reading and writing shapefiles in any language or character encoding, and provides several options for decoding and encoding text.
13361340
Most shapefiles are written in UTF-8 encoding, PyShp's default encoding, so in most cases you don't have to specify the encoding.
1341+
#### Encoding errors
13371342
If you encounter an encoding error when reading a shapefile, this means the shapefile was likely written in a non-utf8 encoding.
13381343
For instance, when working with English language shapefiles, a common reason for encoding errors is that the shapefile was written in Latin-1 encoding.
1344+
#### Recommendation
1345+
*We recommend avoiding writing Shapefiles with unicode encoded as UTF-16 and UTF-32, especially UTF-16-LE (if at all possible).*
1346+
When using other encodings, especially UTF-32 or UTF-16, a whole host of other known (and unknown) unicode related issues can be encountered. See ## Specialist Unicode Handling below for more details.
1347+
1348+
#### Non-utf8 encodings
13391349
For reading shapefiles in any non-utf8 encoding, such as Latin-1, just
13401350
supply the encoding option when creating the Reader class.
13411351

@@ -1362,7 +1372,10 @@ should give you the same unicode string you started with.
13621372
>>> r.close()
13631373

13641374
If you supply the wrong encoding and the string is unable to be decoded, PyShp will by default raise an
1365-
exception. If however, on rare occasion, you are unable to find the correct encoding and want to ignore
1375+
exception.
1376+
1377+
#### Custom handling of encoding errors.
1378+
If however, on rare occasion, you are unable to find the correct encoding and want to ignore
13661379
or replace encoding errors, you can specify the "encodingErrors" to be used by the decode method. This
13671380
applies to both reading and writing.
13681381

@@ -1378,6 +1391,95 @@ in [CPython 3.9 - 3.14](https://docs.python.org/3/library/stdtypes.html#bytes.de
13781391
('xmlcharrefreplace' and 'backslashreplace' are only supported by
13791392
[`str.encode`](https://docs.python.org/3/library/stdtypes.html#str.encode)).
13801393

1394+
## Specialist Unicode handling.
1395+
### Summary
1396+
If at all possible use UTF-8. Or at the very least avoid UTF-16 and UTF-32
1397+
### Background
1398+
Unicode is one of humanity's greatest achievements. GIS applications support it within Shapefiles.
1399+
But most unicode encodings (e.g. UTF-8, UTF-16) assume
1400+
data will be stored in pure binary form, as bytes (8-bit octets). The Shapefile spec however, defers to
1401+
a long lost dbf spec (the URL link to a former company's website is now dead) which like most other dbf
1402+
specifications, will require text to be stored as Ascii.
1403+
The fields are some known width, and Extended Ascii character sets (with 256 entries, not only the first
1404+
128 common to them all) are essentially one-to-one mappings to bytes - so this would not be a
1405+
problem in isolation.
1406+
### Padding bytes
1407+
Dbf field names are post padded with null bytes, and dbf text fields ("C" and "M") are post
1408+
padded with Ascii spaces (the byte 0x20). Example shapefiles exist where text fields also end in numerous
1409+
null bytes, essentially also as padding.
1410+
#### Problem 1.
1411+
Encodings of some unicode strings may contain these exact padding bytes as part of their data (in particular
1412+
the first null byte is not necessarily a null terminator).
1413+
1414+
>>> "囊萤映雪".encode("utf-16-be")
1415+
b'V\xca\x84$f \x96\xea'
1416+
1417+
#### Problem 2.
1418+
Some unicode strings encode to byte sequences under some non-UTF-8 codecs, that contain and even
1419+
trail a null byte as part of their data.
1420+
1421+
>>> "ABC".encode("utf-16-le")
1422+
b'A\x00B\x00C\x00'
1423+
1424+
#### Problem 3.
1425+
Some unicode strings encode to byte sequences under some non-UTF-8 codecs, that
1426+
are entirely dbf padding bytes, e.g.:
1427+
1428+
>>> '††††'.encode("utf-16-le")
1429+
b' '
1430+
1431+
#### Problem 4.
1432+
If encodings of even only moderately long unicode strings (especially the 10 byte field names) are truncated at byte
1433+
boundaries (i.e. not truncated correctly at one of the string's code-point boundaries), their data is corrupted, and
1434+
the encoding of the final code point whose bytes were chopped is corrupted.
1435+
1436+
>>> try:
1437+
... "ÀÀÀÀ०".encode()[:10].decode()
1438+
... except UnicodeDecodeError:
1439+
... print("Failed to decode")
1440+
...
1441+
Failed to decode
1442+
1443+
#### Problem 5.
1444+
Historically, PyShp has attempted to prettify dbf string data. E.g. stripping whitespace.
1445+
In particular, ascii spaces (`b" "`) in a dbf name were transformed to underscores (`b"_"`) - but
1446+
crucially, only after encoding to bytes, whether the codec was ascii or not. This can result in silent
1447+
changes to the user's data, and since version 3.1 PyShp no longer does this.
1448+
1449+
>>> s = "囊萤映雪"
1450+
>>> s2 = s.encode("utf-16-be").replace(b" ",b"_").decode("utf-16-be")
1451+
>>> print(s2)
1452+
囊萤晟雪
1453+
>>> s == s2
1454+
False
1455+
1456+
### PyShp's approach
1457+
The simplest most robust solution to support arbitrary codec encoding as ascii bytes, is probably to encode those
1458+
bytes as Base64 (with an alphabet excluding dbf pad bytes), and then encode that Base64 string a third time as bytes.
1459+
PyShp tries to be a little easier to use than this. The intention is to adhere to [Postel's Law](https://en.wikipedia.org/wiki/Robustness_principle) so hopefully PyShp is still fairly lenient when decoding and reading Shapefiles, but either informative or
1460+
stricter, when the user attempts to encoding or write invalid files.
1461+
#### Warnings
1462+
When possible data corruption is detected when reading or writing a Shapefile (or dbf file), by default
1463+
PyShp will raise a warning. Supported situations are listed below under Decoding and Encoding.
1464+
#### Strict mode
1465+
If a Writer (or dbf version) is created with `strict=True` then an exception is raised instead of any of the above warnings
1466+
(hopefully before corrupt data can be written at all).
1467+
#### Decoding
1468+
- PyShp first removes all padding bytes. If decoding fails, those padding bytes are restored one by one to the data
1469+
until decoding succeeds. A warning is raised if any padding bytes were required, but an exception is not raised for this
1470+
in strict mode (those pad bytes might just have been part of a valid encoding).
1471+
- PyShp warns if there is a null character in a decoded string (but silently accepts null bytes in encoded strings). This
1472+
warning does not become an exception in strict mode (there might simply be null characters in the data).
1473+
#### Encoding
1474+
- PyShp warns when trying to write data to a text field or field name that PyShp itself would not be able to decode correctly.
1475+
- PyShp now truncates text fields the dbf field `size` or 10 byte limit for names, by if necessary, successively
1476+
truncating the actual string, one code point at a time, until the number of encoded bytes is less than or equal
1477+
to the maximum. If truncation was necessary, a warning is raised.
1478+
- When trying to write text data, that ends with code points that would encode
1479+
to pad bytes (these would be lost on decoding, otherwise all fields stored as utf-16 under the limit will end
1480+
in ††††††††††††...).
1481+
- PyShp warns if a field name is written containing a null character (before encoding). For users who wish to steer well clear of potential bugs.
1482+
13811483
## Reading Large Shapefiles
13821484

13831485
Despite being a lightweight library, PyShp is designed to be able to read shapefiles of any size, allowing you to work with hundreds of thousands or even millions

changelog.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1+
VERSION 3.1.2
2+
3+
2026-06-24
4+
* Raise error in strict mode when creating field with a name, or writing strings, that ends with whole code points
5+
that whose encoding is pad bytes.
6+
* Document handling of unicode.
7+
18
VERSION 3.1.1
9+
10+
2026-06-24
211
Unicode support made even more robust and yet another encoding bug fixed!
312
* When reading, only use minimum number of pad bytes to decode text successfully (fixes issue 423).
413
* When writing, warn (or raise in strict mode) if the text's encoding ends in pad bytes.

src/shapefile.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from __future__ import annotations
1010

11-
__version__ = "3.1.1"
11+
__version__ = "3.1.2"
1212

1313
import abc
1414
import array
@@ -251,11 +251,12 @@ def __call__(
251251
) -> str: ...
252252

253253

254-
def _warn_if_string_ends_with_decoded_pad_bytes(
254+
def _check_if_string_ends_with_decoded_pad_bytes(
255255
s: str,
256256
pad_byte: bytes,
257257
encoding: str = "utf-8",
258258
encodingErrors: str = "strict",
259+
strict: bool = True,
259260
) -> None:
260261
"""Warns if e.g. the encoding is utf-16-le, and the
261262
decoded text ends in "†", which encodes to a pair of
@@ -278,7 +279,8 @@ def _warn_if_string_ends_with_decoded_pad_bytes(
278279
f"encodes to the pad bytes: {pad_bytes!r}. "
279280
"The real end of the actual data may be earlier. "
280281
)
281-
282+
if strict:
283+
raise DbfStringDataLoss(msg)
282284
warnings.warn(msg, category=PossibleDataLoss)
283285
break
284286

@@ -335,11 +337,12 @@ def _encode_dbf_string(
335337
)
336338

337339
if pad_byte is not None:
338-
_warn_if_string_ends_with_decoded_pad_bytes(
340+
_check_if_string_ends_with_decoded_pad_bytes(
339341
s=trimmed,
340342
pad_byte=pad_byte,
341343
encoding=encoding,
342344
encodingErrors=encodingErrors,
345+
strict=strict,
343346
)
344347

345348
if len(encoded) < size and pad_byte is not None:

0 commit comments

Comments
 (0)