88
99from __future__ import annotations
1010
11- __version__ = "3.1.0 "
11+ __version__ = "3.1.1 "
1212
1313import abc
1414import array
@@ -251,6 +251,38 @@ def __call__(
251251 ) -> str : ...
252252
253253
254+ def _warn_if_string_ends_with_decoded_pad_bytes (
255+ s : str ,
256+ pad_byte : bytes ,
257+ encoding : str = "utf-8" ,
258+ encodingErrors : str = "strict" ,
259+ ) -> None :
260+ """Warns if e.g. the encoding is utf-16-le, and the
261+ decoded text ends in "†", which encodes to a pair of
262+ ascii spaces (b" ", the pad byte for C and M fields).
263+ """
264+ # Max code unit size under UTF-8, UTF-16, and UTF-32 is 4 bytes.
265+ for n in range (1 , 5 ):
266+ # TODO: test for encodings ending in a null terminator preceded
267+ # by pad bytes, that are exactly the field's size (length).
268+ pad_bytes = pad_byte * n
269+ try :
270+ decoded_pad_bytes : str = pad_bytes .decode (encoding , encodingErrors )
271+ except UnicodeDecodeError :
272+ continue
273+ if s .endswith (decoded_pad_bytes ):
274+ msg = (
275+ f"Under the given encoding: { encoding } , "
276+ f" the text (field name or 'C' or 'M' field): { s !r} "
277+ f" ends with { decoded_pad_bytes !r} , which coincidentally"
278+ f"encodes to the pad bytes: { pad_bytes !r} . "
279+ "The real end of the actual data may be earlier. "
280+ )
281+
282+ warnings .warn (msg , category = PossibleDataLoss )
283+ break
284+
285+
254286def _encode_dbf_string (
255287 s : str ,
256288 size : int ,
@@ -273,6 +305,8 @@ def _encode_dbf_string(
273305 N = len (s )
274306 trimmed : str
275307 encoded : bytes
308+
309+ # i - num of characters to keep. Starts by trying to keep all N.
276310 for i in reversed (range (0 , N + 1 )):
277311 trimmed = s [:i ]
278312 encoded = trimmed .encode (encoding , encodingErrors )
@@ -300,16 +334,27 @@ def _encode_dbf_string(
300334 f"to a short enough byte string, using { encoding = } , { encodingErrors = } "
301335 )
302336
337+ if pad_byte is not None :
338+ _warn_if_string_ends_with_decoded_pad_bytes (
339+ s = trimmed ,
340+ pad_byte = pad_byte ,
341+ encoding = encoding ,
342+ encodingErrors = encodingErrors ,
343+ )
344+
303345 if len (encoded ) < size and pad_byte is not None :
304346 padded = encoded .ljust (size , pad_byte )
305347 else :
306348 padded = encoded
307349
308- decoded = decode (
309- b = padded ,
310- encoding = encoding ,
311- encodingErrors = encodingErrors ,
312- )
350+ with warnings .catch_warnings ():
351+ warnings .simplefilter ("ignore" )
352+ decoded = decode (
353+ b = padded ,
354+ encoding = encoding ,
355+ encodingErrors = encodingErrors ,
356+ )
357+
313358 if decoded != trimmed :
314359 msg = f"Padded value: { padded !r} does not decode to { trimmed !r} using PyShp's decoder: { decode .__name__ } "
315360 if len (trimmed ) < len (s ):
@@ -324,20 +369,67 @@ def _encode_dbf_string(
324369 return padded , trimmed
325370
326371
372+ def _try_to_decode_dbf_name_or_text_field (
373+ b : bytes ,
374+ pad_bytes : bytes , # Pad bytes will be trimmed (from the R of b) in their order in the byte-string
375+ encoding : str = "utf8" ,
376+ encodingErrors : str = "strict" ,
377+ ) -> str :
378+ N = len (b )
379+ decoded : str
380+ trimmed = b
381+ num_trailing_pad_bytes = N - len (b .rstrip (pad_bytes ))
382+
383+ # Test if we need to restore any pad_bytes to
384+ # correctly decode the remaining bytes to a string.
385+ # num_to_trim starts from num_trailing_pad_bytes
386+ # - initially trimming all trailing pad bytes
387+ for num_to_trim in reversed (range (num_trailing_pad_bytes + 1 )):
388+ i = N - num_to_trim
389+ trimmed = b [:i ]
390+ try :
391+ decoded = trimmed .decode (encoding , encodingErrors )
392+ except UnicodeDecodeError :
393+ continue
394+ if num_to_trim < num_trailing_pad_bytes :
395+ warnings .warn (
396+ f"Used { num_trailing_pad_bytes - num_to_trim } pad bytes ({ pad_bytes !r} ) "
397+ f"from padding to decode raw field: { b !r} "
398+ f"to: { decoded !r} ({ encoding = } , { encodingErrors = } ) " ,
399+ category = PossibleDataLoss ,
400+ )
401+ return decoded
402+
403+ raise dbfFileException (
404+ f"Could not decode field name or text/memo field: { b !r} using { encoding = } and { encodingErrors = } "
405+ " no matter how many trailing pad bytes (if any) ({pad_bytes!r}) were used. "
406+ )
407+
408+
327409def _decode_C_or_M_field (
328410 b : bytes ,
329411 encoding : str = "utf8" ,
330412 encodingErrors : str = "strict" ,
331413 strict : bool = True ,
332414) -> str :
333- retval = b .decode (encoding , encodingErrors ).rstrip ("\x00 " ).rstrip (" " )
334- if retval .rstrip ("\x00 " ) != retval and strict :
415+ retval = _try_to_decode_dbf_name_or_text_field (
416+ b = b ,
417+ pad_bytes = b" \x00 " ,
418+ encoding = encoding ,
419+ encodingErrors = encodingErrors ,
420+ )
421+
422+ if not strict :
423+ return retval
424+
425+ if retval .rstrip ("\x00 " ) != retval :
335426 msg = (
336- f"More Trailing Null chars in: { b !r} "
337- " after removing trailing null chars and ascii spaces"
338- f", resulting in { retval !r} "
427+ f"More trailing null chars in: { retval !r} "
428+ " after removing one trailing null char and ascii spaces"
429+ f" from { b !r} , and decoding (codec: { encoding } , errors: { encodingErrors } ). "
339430 )
340431 warnings .warn (msg , category = PossibleDataLoss )
432+
341433 return retval
342434
343435
@@ -360,34 +452,15 @@ def decode_name(
360452 encodingErrors : str = "strict" ,
361453 strict : bool = True ,
362454 ) -> str :
363- N = len (b )
364- decoded : str
365- num_trailing_null_bytes = N - len (b .rstrip (b"\x00 " ))
366-
367- # Test if we need to restore any of those null bytes to
368- # correctly decode the remaining bytes to a string.
369- for num_to_trim in reversed (range (num_trailing_null_bytes + 1 )):
370- i = N - num_to_trim
371- trimmed = b [:i ]
372- try :
373- decoded = trimmed .decode (encoding , encodingErrors )
374- except UnicodeDecodeError :
375- continue
376- if strict and num_to_trim < num_trailing_null_bytes :
377- warnings .warn (
378- f"Used { num_trailing_null_bytes - num_to_trim } null bytes "
379- f"from padding to decode { b !r} "
380- f"to: { decoded !r} ({ encoding = } , { encodingErrors = } ) " ,
381- category = PossibleDataLoss ,
382- )
383- if not strict :
384- decoded = decoded .lstrip ()
385- return decoded
386-
387- raise dbfFileException (
388- f"Could not decode field name: { b !r} using { encoding = } and { encodingErrors = } "
389- " no matter how many trailing null-bytes (if any) were used. "
455+ decoded = _try_to_decode_dbf_name_or_text_field (
456+ b = b ,
457+ pad_bytes = b"\x00 " ,
458+ encoding = encoding ,
459+ encodingErrors = encodingErrors ,
390460 )
461+ if not strict :
462+ decoded = decoded .lstrip ()
463+ return decoded
391464
392465 @classmethod
393466 def from_byte_stream (
@@ -445,6 +518,14 @@ def from_unchecked(
445518 size = 1
446519 decimal = 0
447520
521+ if not strict and " " in name :
522+ warnings .warn (
523+ f"Replacing ascii spaces (0x20, ' 's) with underscores ('_'s) in { name !r} . "
524+ "Use a Writer(file, strict=True) to preserve the field name as it is. " ,
525+ category = PossibleDataLoss ,
526+ )
527+ name = name .replace (" " , "_" )
528+
448529 # Only use the portion of the name that we are able to encode to
449530 # 10 bytes or less.
450531 _encoded_name , trimmed_name = cls .trim_name_until_encodable (
@@ -502,13 +583,6 @@ def encode_field_descriptor(
502583 encodingErrors = encodingErrors ,
503584 strict = strict ,
504585 )
505- if not strict and b" " in encoded_name :
506- warnings .warn (
507- "Replacing ascii spaces (0x20) with underscores "
508- f"in encoded bytes: { encoded_name !r} " ,
509- category = PossibleDataLoss ,
510- )
511- encoded_name = encoded_name .replace (b" " , b"_" )
512586
513587 encoded_field_type = self .field_type .encode ("ascii" )
514588 return self .get_struct ().pack (
0 commit comments