@@ -594,6 +594,34 @@ def _(item):
594594 applyFunctionRecursively (value , _ )
595595 return retVal [0 ]
596596
597+ def _looksLikeMisdecodedUtf8 (value ):
598+ """
599+ True if a retrieved value looks like UTF-8 data shown through a single-byte (ISO-8859-1) charset -
600+ the '<page charset too WIDE>' mismatch that leaves NO undecodable bytes (latin-1 accepts every byte),
601+ so _pageCharsetCorrupted() cannot see it, yet the data is silently mojibake (a UTF-8 accent shows as
602+ two spurious latin-1 chars).
603+
604+ Precise by construction: real mojibake re-encodes to latin-1 and decodes back to a DIFFERENT valid
605+ UTF-8 string, whereas correctly-decoded UTF-8 ('cafe' with U+00E9) and genuine latin-1 text ('resume')
606+ do not (a lone accent is a UTF-8 lead byte with no continuation -> decode fails). So it fires only on
607+ the corruption, not on clean data. Complements _pageCharsetCorrupted() (the too-NARROW direction).
608+ """
609+
610+ retVal = [False ]
611+
612+ def _ (item ):
613+ if not retVal [0 ] and isinstance (item , six .string_types ) and any (0x80 <= ord (_ ) <= 0xFF for _ in item ):
614+ try :
615+ decoded = item .encode ("latin-1" ).decode ("utf-8" )
616+ except (UnicodeEncodeError , UnicodeDecodeError ):
617+ return item
618+ if decoded != item and any (ord (_ ) > 0x7F for _ in decoded ):
619+ retVal [0 ] = True
620+ return item
621+
622+ applyFunctionRecursively (value , _ )
623+ return retVal [0 ]
624+
597625@lockedmethod
598626@stackedmethod
599627def getValue (expression , blind = True , union = True , error = True , time = True , fromUser = False , expected = None , batch = False , unpack = True , resumeValue = True , charsetType = None , firstChar = None , lastChar = None , dump = False , suppressOutput = None , expectingNone = False , safeCharEncode = True ):
@@ -699,7 +727,7 @@ def getValue(expression, blind=True, union=True, error=True, time=True, fromUser
699727 if (found and not conf .hexConvert and not conf .binaryFields and expected not in (EXPECTED .BOOL , EXPECTED .INT )
700728 and getTechnique () in (PAYLOAD .TECHNIQUE .UNION , PAYLOAD .TECHNIQUE .ERROR , PAYLOAD .TECHNIQUE .QUERY )
701729 and Backend .getIdentifiedDbms () and hasattr (queries [Backend .getIdentifiedDbms ()], "hex" )
702- and _pageCharsetCorrupted (value )):
730+ and ( _pageCharsetCorrupted (value ) or _looksLikeMisdecodedUtf8 ( value ) )):
703731 warnMsg = "retrieved data appears corrupted because of a charset mismatch between the "
704732 warnMsg += "DBMS and the web page. Re-fetching using hexadecimal encoding"
705733 singleTimeWarnMessage (warnMsg )
@@ -710,7 +738,10 @@ def getValue(expression, blind=True, union=True, error=True, time=True, fromUser
710738 finally :
711739 conf .hexConvert = False
712740
713- if _value is not None :
741+ # only adopt the hex re-fetch if it is actually cleaner: a rare mis-trigger (or a
742+ # column whose real charset the hex decode still cannot resolve) must never replace
743+ # the original with equal-or-worse data
744+ if _value is not None and not _pageCharsetCorrupted (_value ) and not _looksLikeMisdecodedUtf8 (_value ):
714745 value = _value
715746
716747 if found and conf .dnsDomain :
0 commit comments