PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/imh-python/lib/python3.9/site-packages/pylint/checkers
Viewing File: /opt/imh-python/lib/python3.9/site-packages/pylint/checkers/unicode.py
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Unicode and some other ASCII characters can be used to create programs that run much different compared to what a human reader would expect from them. PEP 672 lists some examples. See: https://www.python.org/dev/peps/pep-0672/ The following checkers are intended to make users are aware of these issues. """ from __future__ import annotations import codecs import contextlib import io import re from collections import OrderedDict from collections.abc import Iterable from functools import lru_cache from tokenize import detect_encoding from typing import NamedTuple, TypeVar from astroid import nodes import pylint.interfaces import pylint.lint from pylint import checkers _StrLike = TypeVar("_StrLike", str, bytes) # Based on: # https://golangexample.com/go-linter-which-checks-for-dangerous-unicode-character-sequences/ # We use '\u' because it doesn't require a map lookup and is therefore faster BIDI_UNICODE = [ "\u202A", # \N{LEFT-TO-RIGHT EMBEDDING} "\u202B", # \N{RIGHT-TO-LEFT EMBEDDING} "\u202C", # \N{POP DIRECTIONAL FORMATTING} "\u202D", # \N{LEFT-TO-RIGHT OVERRIDE} "\u202E", # \N{RIGHT-TO-LEFT OVERRIDE} "\u2066", # \N{LEFT-TO-RIGHT ISOLATE} "\u2067", # \N{RIGHT-TO-LEFT ISOLATE} "\u2068", # \N{FIRST STRONG ISOLATE} "\u2069", # \N{POP DIRECTIONAL ISOLATE} # The following was part of PEP 672: # https://www.python.org/dev/peps/pep-0672/ # so the list above might not be complete "\u200F", # \n{RIGHT-TO-LEFT MARK} # We don't use # "\u200E" # \n{LEFT-TO-RIGHT MARK} # as this is the default for latin files and can't be used # to hide code ] class _BadChar(NamedTuple): """Representation of an ASCII char considered bad.""" name: str unescaped: str escaped: str code: str help_text: str def description(self) -> str: """Used for the detailed error message description.""" return ( f"Invalid unescaped character {self.name}, " f'use "{self.escaped}" instead.' ) def human_code(self) -> str: """Used to generate the human readable error message.""" return f"invalid-character-{self.name}" # Based on https://www.python.org/dev/peps/pep-0672/ BAD_CHARS = [ _BadChar( "backspace", "\b", "\\b", "E2510", ( "Moves the cursor back, so the character after it will overwrite the " "character before." ), ), _BadChar( "carriage-return", "\r", "\\r", "E2511", ( "Moves the cursor to the start of line, subsequent characters overwrite " "the start of the line." ), ), _BadChar( "sub", "\x1A", "\\x1A", "E2512", ( 'Ctrl+Z "End of text" on Windows. Some programs (such as type) ignore ' "the rest of the file after it." ), ), _BadChar( "esc", "\x1B", "\\x1B", "E2513", ( "Commonly initiates escape codes which allow arbitrary control " "of the terminal." ), ), _BadChar( "nul", "\0", "\\0", "E2514", "Mostly end of input for python.", ), _BadChar( # Zero Width with Space. At the time of writing not accepted by Python. # But used in Trojan Source Examples, so still included and tested for. "zero-width-space", "\u200B", # \n{ZERO WIDTH SPACE} "\\u200B", "E2515", "Invisible space character could hide real code execution.", ), ] BAD_ASCII_SEARCH_DICT = {char.unescaped: char for char in BAD_CHARS} def _line_length(line: _StrLike, codec: str) -> int: """Get the length of a string like line as displayed in an editor.""" if isinstance(line, bytes): decoded = _remove_bom(line, codec).decode(codec, "replace") else: decoded = line stripped = decoded.rstrip("\n") if stripped != decoded: stripped = stripped.rstrip("\r") return len(stripped) def _map_positions_to_result( line: _StrLike, search_dict: dict[_StrLike, _BadChar], new_line: _StrLike, byte_str_length: int = 1, ) -> dict[int, _BadChar]: """Get all occurrences of search dict keys within line. Ignores Windows end of line and can handle bytes as well as string. Also takes care of encodings for which the length of an encoded code point does not default to 8 Bit. """ result: dict[int, _BadChar] = {} for search_for, char in search_dict.items(): if search_for not in line: continue # Special Handling for Windows '\r\n' if char.unescaped == "\r" and line.endswith(new_line): ignore_pos = len(line) - 2 * byte_str_length else: ignore_pos = None start = 0 pos = line.find(search_for, start) while pos > 0: if pos != ignore_pos: # Calculate the column col = int(pos / byte_str_length) result[col] = char start = pos + 1 pos = line.find(search_for, start) return result UNICODE_BOMS = { "utf-8": codecs.BOM_UTF8, "utf-16": codecs.BOM_UTF16, "utf-32": codecs.BOM_UTF32, "utf-16le": codecs.BOM_UTF16_LE, "utf-16be": codecs.BOM_UTF16_BE, "utf-32le": codecs.BOM_UTF32_LE, "utf-32be": codecs.BOM_UTF32_BE, } BOM_SORTED_TO_CODEC = OrderedDict( # Sorted by length of BOM of each codec (UNICODE_BOMS[codec], codec) for codec in ("utf-32le", "utf-32be", "utf-8", "utf-16le", "utf-16be") ) UTF_NAME_REGEX_COMPILED = re.compile( "utf[ -]?(8|16|32)[ -]?(le|be|)?(sig)?", flags=re.IGNORECASE ) def _normalize_codec_name(codec: str) -> str: """Make sure the codec name is always given as defined in the BOM dict.""" return UTF_NAME_REGEX_COMPILED.sub(r"utf-\1\2", codec).lower() def _remove_bom(encoded: bytes, encoding: str) -> bytes: """Remove the bom if given from a line.""" if encoding not in UNICODE_BOMS: return encoded bom = UNICODE_BOMS[encoding] if encoded.startswith(bom): return encoded[len(bom) :] return encoded def _encode_without_bom(string: str, encoding: str) -> bytes: """Encode a string but remove the BOM.""" return _remove_bom(string.encode(encoding), encoding) def _byte_to_str_length(codec: str) -> int: """Return how many byte are usually(!) a character point.""" if codec.startswith("utf-32"): return 4 if codec.startswith("utf-16"): return 2 return 1 @lru_cache(maxsize=1000) def _cached_encode_search(string: str, encoding: str) -> bytes: """A cached version of encode used for search pattern.""" return _encode_without_bom(string, encoding) def _fix_utf16_32_line_stream(steam: Iterable[bytes], codec: str) -> Iterable[bytes]: r"""Handle line ending for UTF16 and UTF32 correctly. Currently, Python simply strips the required zeros after \n after the line ending. Leading to lines that can't be decoded properly """ if not codec.startswith("utf-16") and not codec.startswith("utf-32"): yield from steam else: # First we get all the bytes in memory content = b"".join(line for line in steam) new_line = _cached_encode_search("\n", codec) # Now we split the line by the real new line in the correct encoding # we can't use split as it would strip the \n that we need start = 0 while True: pos = content.find(new_line, start) if pos >= 0: yield content[start : pos + len(new_line)] else: # Yield the rest and finish if content[start:]: yield content[start:] break start = pos + len(new_line) def extract_codec_from_bom(first_line: bytes) -> str: """Try to extract the codec (unicode only) by checking for the BOM. For details about BOM see https://unicode.org/faq/utf_bom.html#BOM Args: first_line: the first line of a file Returns: a codec name Raises: ValueError: if no codec was found """ for bom, codec in BOM_SORTED_TO_CODEC.items(): if first_line.startswith(bom): return codec raise ValueError("No BOM found. Could not detect Unicode codec.") class UnicodeChecker(checkers.BaseRawFileChecker): """Check characters that could be used to hide bad code to humans. This includes: - Bidirectional Unicode (see https://trojansource.codes/) - Bad ASCII characters (see PEP672) If a programmer requires to use such a character they should use the escaped version, that is also much easier to read and does not depend on the editor used. The Checker also includes a check that UTF-16 and UTF-32 are not used to encode Python files. At the time of writing Python supported only UTF-8. See https://stackoverflow.com/questions/69897842/ and https://bugs.python.org/issue1503789 for background. """ name = "unicode_checker" msgs = { "E2501": ( # This error will be only displayed to users once Python Supports # UTF-16/UTF-32 (if at all) "UTF-16 and UTF-32 aren't backward compatible. Use UTF-8 instead", "invalid-unicode-codec", ( "For compatibility use UTF-8 instead of UTF-16/UTF-32. " "See also https://bugs.python.org/issue1503789 for a history " "of this issue. And " "https://softwareengineering.stackexchange.com/questions/102205/ " "for some possible problems when using UTF-16 for instance." ), ), "E2502": ( ( "Contains control characters that can permit obfuscated code " "executed differently than displayed" ), "bidirectional-unicode", ( "bidirectional unicode are typically not displayed characters required " "to display right-to-left (RTL) script " "(i.e. Chinese, Japanese, Arabic, Hebrew, ...) correctly. " "So can you trust this code? " "Are you sure it displayed correctly in all editors? " "If you did not write it or your language is not RTL," " remove the special characters, as they could be used to trick you into " "executing code, " "that does something else than what it looks like.\n" "More Information:\n" "https://en.wikipedia.org/wiki/Bidirectional_text\n" "https://trojansource.codes/" ), ), "C2503": ( "PEP8 recommends UTF-8 as encoding for Python files", "bad-file-encoding", ( "PEP8 recommends UTF-8 default encoding for Python files. See " "https://peps.python.org/pep-0008/#source-file-encoding" ), ), **{ bad_char.code: ( bad_char.description(), bad_char.human_code(), bad_char.help_text, ) for bad_char in BAD_CHARS }, } @staticmethod def _is_invalid_codec(codec: str) -> bool: return codec.startswith(("utf-16", "utf-32")) @staticmethod def _is_unicode(codec: str) -> bool: return codec.startswith("utf") @classmethod def _find_line_matches(cls, line: bytes, codec: str) -> dict[int, _BadChar]: """Find all matches of BAD_CHARS within line. Args: line: the input codec: that will be used to convert line/or search string into Return: A dictionary with the column offset and the BadASCIIChar """ # We try to decode in Unicode to get the correct column offset # if we would use bytes, it could be off because UTF-8 has no fixed length try: line_search = line.decode(codec, errors="strict") search_dict = BAD_ASCII_SEARCH_DICT return _map_positions_to_result(line_search, search_dict, "\n") except UnicodeDecodeError: # If we can't decode properly, we simply use bytes, even so the column offsets # might be wrong a bit, but it is still better then nothing line_search_byte = line search_dict_byte: dict[bytes, _BadChar] = {} for char in BAD_CHARS: # Some characters might not exist in all encodings with contextlib.suppress(UnicodeDecodeError): search_dict_byte[_cached_encode_search(char.unescaped, codec)] = ( char ) return _map_positions_to_result( line_search_byte, search_dict_byte, _cached_encode_search("\n", codec), byte_str_length=_byte_to_str_length(codec), ) @staticmethod def _determine_codec(stream: io.BytesIO) -> tuple[str, int]: """Determine the codec from the given stream. first tries https://www.python.org/dev/peps/pep-0263/ and if this fails also checks for BOMs of UTF-16 and UTF-32 to be future-proof. Args: stream: The byte stream to analyse Returns: A tuple consisting of: - normalized codec name - the line in which the codec was found Raises: SyntaxError: if failing to detect codec """ try: # First try to detect encoding with PEP 263 # Doesn't work with UTF-16/32 at the time of writing # see https://bugs.python.org/issue1503789 codec, lines = detect_encoding(stream.readline) # lines are empty if UTF-8 BOM is found codec_definition_line = len(lines) or 1 except SyntaxError as e: # Codec could not be detected by Python, we try manually to check for # UTF 16/32 BOMs, which aren't supported by Python at the time of writing. # This is only included to be future save and handle these codecs as well stream.seek(0) try: codec = extract_codec_from_bom(stream.readline()) codec_definition_line = 1 except ValueError as ve: # Failed to detect codec, so the syntax error originated not from # UTF16/32 codec usage. So simply raise the error again. raise e from ve return _normalize_codec_name(codec), codec_definition_line def _check_codec(self, codec: str, codec_definition_line: int) -> None: """Check validity of the codec.""" if codec != "utf-8": msg = "bad-file-encoding" if self._is_invalid_codec(codec): msg = "invalid-unicode-codec" self.add_message( msg, # Currently Nodes will lead to crashes of pylint # node=node, line=codec_definition_line, end_lineno=codec_definition_line, confidence=pylint.interfaces.HIGH, col_offset=None, end_col_offset=None, ) def _check_invalid_chars(self, line: bytes, lineno: int, codec: str) -> None: """Look for chars considered bad.""" matches = self._find_line_matches(line, codec) for col, char in matches.items(): self.add_message( char.human_code(), # Currently Nodes will lead to crashes of pylint # node=node, line=lineno, end_lineno=lineno, confidence=pylint.interfaces.HIGH, col_offset=col + 1, end_col_offset=col + len(char.unescaped) + 1, ) def _check_bidi_chars(self, line: bytes, lineno: int, codec: str) -> None: """Look for Bidirectional Unicode, if we use unicode.""" if not self._is_unicode(codec): return for dangerous in BIDI_UNICODE: if _cached_encode_search(dangerous, codec) in line: # Note that we don't add a col_offset on purpose: # Using these unicode characters it depends on the editor # how it displays the location of characters in the line. # So we mark the complete line. self.add_message( "bidirectional-unicode", # Currently Nodes will lead to crashes of pylint # node=node, line=lineno, end_lineno=lineno, # We mark the complete line, as bidi controls make it hard # to determine the correct cursor position within an editor col_offset=0, end_col_offset=_line_length(line, codec), confidence=pylint.interfaces.HIGH, ) # We look for bidirectional unicode only once per line # as we mark the complete line anyway break def process_module(self, node: nodes.Module) -> None: """Perform the actual check by checking module stream.""" with node.stream() as stream: codec, codec_line = self._determine_codec(stream) self._check_codec(codec, codec_line) stream.seek(0) # Check for invalid content (controls/chars) for lineno, line in enumerate( _fix_utf16_32_line_stream(stream, codec), start=1 ): if lineno == 1: line = _remove_bom(line, codec) self._check_bidi_chars(line, lineno, codec) self._check_invalid_chars(line, lineno, codec) def register(linter: pylint.lint.PyLinter) -> None: linter.register_checker(UnicodeChecker(linter))