From 99c237bbca102c9b71da97ce655b1e71f78fc85d Mon Sep 17 00:00:00 2001 From: Alberto Spelta Date: Tue, 5 Mar 2024 00:00:56 +0100 Subject: [PATCH] Fix extension columns obfuscation (#15) * Remove DAX escape chars from obfuscation reserved * Add test for escaped quotation mark in string literal * Bump version 0.4-beta --- assets/ObfuscatorTest.pbix | Bin 15959 -> 0 bytes ...ModelDeobfuscator.DeobfuscateExpression.cs | 33 +++++---- .../DaxModelDeobfuscator.cs | 12 ++- .../DaxModelObfuscator.ObfuscateExpression.cs | 37 +++++---- src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs | 24 ++++-- src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs | 8 +- .../Extensions/DaxTokenExtensions.cs | 45 +++++------ .../Extensions/StringExtensions.cs | 69 ++++++++++++++++- src/Dax.Vpax.Obfuscator/version.json | 2 +- .../DaxModelDeobfuscatorTests.cs | 32 ++++++-- .../DaxModelObfuscatorTests.cs | 70 ++++++++++++++++-- .../DaxTextObfuscatorTests.cs | 8 -- tests/pbix/ObfuscatorTest.pbix | Bin 0 -> 19366 bytes 13 files changed, 248 insertions(+), 92 deletions(-) delete mode 100644 assets/ObfuscatorTest.pbix create mode 100644 tests/pbix/ObfuscatorTest.pbix diff --git a/assets/ObfuscatorTest.pbix b/assets/ObfuscatorTest.pbix deleted file mode 100644 index 35927d4798b29728bb3e5ca177f4a2c2ac162a9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15959 zcmbum1C(XU)+V~sW~FW0wkmDgHY&5yS!vt0ZCAR|wryLT=l=b7pL4t4eQ%5xV=n~0 zxguh&6)|J(_~fNPKv4hy03@K;)=(YSSXUeRUoijx0)PWhHF0#Zu(Kt|!vIj^{8jA# z49vz;Mhu2E`T&6Z{}Pq+CF-})qQu2laVlT{;Ma4yw%QcYJBMA0+x2NDBJM)U^>&oh~@PxM9U)s#X(tC*Oi5V@4S- zf@B{od|F5EK{Vhp-X_rcycV#1-1uFnY>(kKZG`}@p`GoN(fo5dck%&8l&lUaO0cQDvZU7u&TQa&uai^y3sN9_&y2koCS>zn{@Iz1nd|J`+eX+dH=anKA10PqF@0N}p5 zPQ=2%%+bI`+Q8k;#rZ#VT4LO&Y@YzE=sRVJRan}T;&1qC#1J}lrPMG$tUPO4#8B7bgNrFK?Q?=UUqMXmUYrBJ(N9O?_5iQpsF0u=aJmFaby41t zAm*Xs>4@R}H(VnWCZf3A^kLrO19~ggg!YPUwTX=tS`hSUwayvP%@C?rv(|wbr{lTK znxZw~o5|z4G9@!uX$9*vBg_>I2d^FSr=U9Wtm zXStSh9{w2RZP9uL{-?GsZ_@gszg}`@i2u@-qKUnoqci>g2W_dW%B~6^X3CY`{Fa(0 zD``Nri;-_c2AeC0rOJTV1vNV1sG^t0+TZ+na7OX~9$qH2)b7OVWXhurUh4 z9_dC1Luo?FDvLCshP?CsZ88O`u_Ni8pLSlhl4&j3^>Rf0!d`8%)pPZBaVab(j(lW% zHzp3W4lJ*s{#DhhehjHo2tN57r5Ty+n*^GO!XGWnKM3mGAW4^ZKZzgvV>P_-ue)62 z*qHLyaatsoHYEOd%zdCeCUCNIb9aggBr=G%gJ-+*SJ%b&<@+8kg-=@J+>a1KyG3GC zENmscJRWr3L#DCGK6y}Dx&BZ+(#X#(Gw~^Sc)(NheX-RsZA#vf#z&}Y%LjhM?)D@- z_>Z%Y)x>W|NaTgrQdZpLL2<%a)kmptWkFrx)1AVMKn&!@xih2~kwMvz+i!PBM7-4x zQ-&1Ts9vm!$BddvKm9(%5%MRRC$=!D#wfpEjkSoj*#}iP22u!LWJl#(?UiH*}}swYYK3Y)9@4R&-&*^gi*z&A4>l@5|lc zJ9pn`Xy&yUoJmIXNS@J$;gFQ!|EJ#d9zE-9&07KbrI}Sp((;m;k6@B>1E~4)!EUg4 z)jkUDoz|+Yg!f%|Ax)?F=PCU^-)af#J;m^^;T!pD9D@8BgOp61oh@w5oc`1B;}ty$ z{hI+%bh6o)Hm?w}Z6Qz!zvU5$MZS^cSV;17J>9Pj$z*tJZ(`eYW}9QnA_O#x$5~IF zNpW4Wx{#H`^(3Asq$O-zS=??dXaUFH@lBBHp=0krV`a^WjjX$6hSM=G&V!L}g!U)A zmU0?`^IlPv>G*s7x=--sCnxlvbfQ@qnA3jfobEOFKN~|_0uN&Q)tH{IcW1R16R%n)Qw!`A9|mUyiBc6bJ9*FeW;!^fyWqp1DPM>9l_=)jN=a>ioB z0MtSS;H9V&4nCL8>{`2%buWneFDGupoD z@^!c0+=Cr!kXOha~%=w4qI)dx94-MNntXH)_;JJAt`t^PdVFXv(@uaQNJaP z*5z1QGzF4iK#bJds7YIFT!j5(3>B-P&VTl_A|B|TxZ}*8V5z!Sq<7)Asx+)mgNuCRn=0&50_+wgOviQcC{D zt(=6Pv7;vGq-2(lkPR!Qq|5zX%a5)K>@=)xV^+j00&Q?t+?d>&YI)=kf&5$3Rw~MX zgN(na^cf|QI4W+uolX&9C2 zfqh}NxxiIEKf;2MQK0u>bQ0!2~y_jLD+ zImPy7YbeZ7OHrQK9J?~hS@9j#ap5A;I(R;*pTkfDJaJ=GMeLm9#4@_>QL57dGj>xi zuY8BrVCKLq#11?&XM2v#Rr5}$3|C2vKl<|A#+-OO21wjub;st01LvI88N zM!FkhX%4iW-wdz(qJPYDH?ar!@JfK4$po=ok`aTz?+~u;LJ$s))?Y@+n|c;nMLy?# znobVj)tC+5x{sV-jn3(pJn51=#?@5COa9*OGl9a>#&QYeU16}X5eJ6CscIjCq_ZPm z%9NGc8fFUK4(;HZRus;wxED%_^cH*8cC7eQ}x$16Q%JVg4mMxz;c8)kQBPGET56T6GlzPa>*9o=g4)HDZf zsO?~Enr@}JHpOH%Aaa#aY6iJLakXa{pw%wQy9*XTQv;)qtHkEdO^&{1ipFM#N(-$k z>(}dxOlRye&P>;$OJ=`Dt$7MVO6^2$oC&(-Zwq$P@011vE*oa5z*il}Q_9`>qj*GA zDIk9A>MBwotJfRgX&_~rCpefxZv>m344WZld zz!j%$d^0cT(mqx$tjIIE$K1ZY&29VG&xGMh7dEsZf8J{OQ+@n)BtrG*{$>TZ9XUg(Rveq69*SiavpA}QKxXZvz-y;lL*xH)HB6=oy?n`YnYRgmT z#ueqfVrR;N^q*Gu%#IGzTP0<(%P8c}LuQ?GGLQFifbBTG_hS=3^2*R_mfkvbqtumb zr{+7%Kc}vuwd5f4uQ}NC>p=Y1JZ$9RXyNQGWMOOk|27{R8SCq}Off(5E!! zd;}MDn}UYvY(IJQ^6a!&MXv|Xd9B)#00&~E2!I9%V8sBa6ab3k&YS$L#y6ZDy*t~N zGkb64^lEpWb{idZgx|&9mtq5Uq6JT$u?RE;PY$n+Cu{$hhchk{ds(|q7A1`xdVF>k zNn~UxoTViE_)}#MEI&RHW3)+M^?2fj;Y|^0zRD2D?ZkH1S%8T8~*L{^#uYFxyk!vt6JKnl$T+JyTNKIWgS#1zUCi=AP7*8A zK-)tTIa#ZfS!Pz_E}hqz(rq+$mKL+-uK4dscPo(pTKN98Y6bxQTKKa5f#>w40Ra9w zpuRLF@?~hq*cqGr$K|dvz#L!!Z~_nj3;_lJBY+ja1z`UbbNh<@m1+cV1eko?JAa*x zzv3=msTKfRfZ5l*IzawkxlUhcoc}6o2eA1<|AqJ~%>rQeRg&(n`b~iXkiV)$wHPG( z*L|cu5a50U{I49qE+_2bU)Ov^uYLf40KG%^%kFU8;}b0 z5j64+GM022r30Cpu1W|ND3#Y1`cFXSngzk0z``Kf&!u@EI=1ODV=TIhTJ-~i)m}qt z7@1izn}34%fqs_5#yzFG()$c3_ubt|Xm(46$xc_F@|^+KFN0$Xt0Vj{G7|Ejgk>XZ zD$aDlHCmD##WOT2T7=oB!yW?v4uVZC7%oO2sw+-hAYs|M9!^3s;(;)=SrdyQ zU(mQigG{8THi>l}GO_s;^r+OS!2rH(f8Lbu`8}f5qIILKc(co^)mHB-kK{(sZqy57 z*tQ7tO(6|^{qbNqLcRl|AB{2HhE-WD>e-mwobzr4GTHmVh2R2XTVMc^1MUq#hJmP;{J_Bn1u@EUA%B$k`$K485+Di!>7A%zi1D0~4^h@ePQIEU$TT$$Yquvc3U zpkU&wKWkC7349rb+;Ge{ftzwtLJ#vmOZ+TjTq%ALOu-1?kvH^LB&Kfr<{UtEbUxV| zfMF{Ovx4TDW>J976RZh)VohDgBmp%DK~nhv;U@?Hfa@DT`v5?l0{WIfN4}uQ6g9rJ zg0oBss`kKe$WvQDXlyz;B7RWPL#YV&7J)rN_E~1Ri09vLAQTEfJ+CHweSx6XwtfNI zzuBNH^@7XR00GG|1b{gU0G79a(Gm5*dFAz&)?uT58iHWA2*Air2^sWRKdg3eIPqku z)eLL%6mclZ%#sYBZU|b$+t~%qn6y;s9;iWCUU&eW31OxCAae*Cq$_0w=+T}PWtvL3 zLh#FR!DeUu*59>B!*fz8!+)$q%fW(iI@DNJkfR6fup zRu)e;t16Q;Nm&-l$Ldg}zg4wKR1g;*5w$C`J?q@Zs*B4^i@TOu-?YcdkngQie-!|? znPpx_33#|jl<`(GB57g}`&WJ%QSAiJ0LpS<-N>b7R6p9mFx_7Tq(~Mtiq!ulZhg2Y zYGxz0ES2F387=FRz zNf?!I7jo8g6m<84U*c?RN#AJ5C0T|DRQwwIA16X$Gbnl-EBbG={pP<*HAJH+__=`M z3W0IW_)7#WAVtW^ee)o;Af!PHBcl-`Hkmn%96n^$Vy#b_Kexu@ZLaS*K}4h_>`sOw zRLB#a2TMqr`KF1ROWIlB8neJ6Q9W5qktthTZ}|Xhg+wbk#3!!$Y5~^epUdPzFttMjZ!+_HWvKax`^C6>F8`f z(Xd%jSWl}!wS%e2^^(b`Di=wSD=+8^A2wXJJPCsZvrIr`C|W9zeu-#VR=8b4wmIae zI^JvJLKKU%bg~>()Z<}l6Ohpri}UlD!SNMFx?n@qoaG3rUgwbLj*VC}MMo22o2fk_ zq0g8&GNG@`9i?fdtPE6{^-`HC2f(esyct~%J*!gUJC_C7v{?zyhf&a-nPVpc#D$Xp zKX*w$+_=JNFC|@(uO533rr<~ND^|tHPAl9(LFo4z00AEjW6#wRx}p$xl0d<@wM@}( zA567OBgeHwLl31v5HA${h-y!>->o3z?db;>hM1JrOP*Z`bVma{PC=j{$+Wj--r>Hc z4F?&{-j?uuL;uSX0rfme?lpzF02b^@pBD^pxmphAwFroUQ>>g7N%WpelCXm4R$pwNUsDsO}XyhqccvWl9j2m7{P`WQR${czYG&RfNSgr=wiT~rvkvD38yZ1{7LdQJY0m2JtapgCF zInbA@cmV)_1sF@U^SR&b6`Bmq0A@e{S3rPOxF?(;wko-gkjH1G?)f)9cQqHc)_F7U z9u>YPQ+(*1NV`Wo4ff4B)+go`t8(($H+;Iijq~-VNqm>_H7nanAGg=0hsIM}REtr| zPm&%ixG8(sjz@HA&S3@UrQ|asxo;h_k##w5Vj49M@K5j-N;B9Mdj!#kQ5JAM*&{yB zRg4cmz+=ID8yyj%j=kxnu+6F`kgwGj`BLi!lW|hiHVh62k6q{L^)4b=^)I!~q>B2D zY2De}y#F|6l zGHpI&%-<6KV7xQ~IcvS)6d9El5ZGBVU7BHXtF5$V;ag+n65n?%vriw&9h_I|5(<)D zZfAeM!=|U;NV_rSk;^dhUL*Z&o+3 zlevdl=(A#e{W!=WRUUw0{WSKt_WY>O!Wn#d4+($7Uf*fmU$)^WTb`wjf`kaTg~-~* zF)nXG+$rE_;D{Be?^V<_j(+!qx${>-ndU>WMsp25qH;!;<3Py+b5%0tCv-Nn6-vsu zalT-Xb^K(85DD#3t<}a^3M>@!Xw%$|bKr^Vaa55*XoxIc24k?TT528H^$@IET-$ay zaN;MkjKKR2$*ia@5W1eZe>hmnAfXl-yl4NbOpaG!Q0@mJw?-|O#9{{^)6D@2m>z{# zL0kV#IqKV^B$zkPsrc{EbS1f9R}|%8h7CKN3wedJu3T;iUl zD)K_W;!-6fSBauDUzOi0EYISxEOMdU->P$8o>uqZ8HL$|B_)li^l(n1Oq4t;%go>K zkss`SH{v3ljxOwmxo~|zAE`$=0E(kE4kgU`<#HXdPrlTz(9n|wGl@+1^u{X*=K6N) z#u@(W+djIf$Mm<|2cH+beggiT6i_Fc{Q#QVPFGy#7$~{ScR181`+H72hTa-{RA^6g zpwnp?!|4N&bi=g#@|f;HUN>G^DlR)nPpJa~9@#2_j;Q0y=K!_M&mcpTSRlkIazp~?oc)6znN8k_YNq$Ie9TWanQN5jGV0v(oDuE zJ>lsIH(;P;RZoF*BxL)7w!dni_$1)sZ9u#G37UQI=pEzaVDfeBRZg|ya;4caDhHLh z=ZcOu)1!&+!w}UP^B{vRM91x&2=DIhheGZeSmZMxTgpDucpK1VMcw^cb#&G!%TWUH zDo+wI*pVLz8x6ut`mrdYn$XoqQAD0J1B?rsM?f7U85>j3i)V2ln7!Ntx=rexnQ(30 zieLt8SU}{Vt=%t1tCuI_TAOn|i(wr!k^lm(-+_+f0!?7fZe|W!l=o>mqDS&Hzrm79!%=Z zrOY=73Xntql;Hs6AAo%6My&{>7HD75rbu6@Vmc!Y<96avE-RNb8C4lsUW1l>SYD~Z zK5E>2sTHigZ&KtBA6%ogcd!+YTRFkjM`D~%(NNw-;BDI{prE&R>L!0%<@-@dz2~DU zI>|cAx-_;+iK%g5AL*FrzD7zs!&K``s%pL3f4(M97|gm~pYPrH;{3>`*}jm$XAl2O z8|^y25f<&sS0B(QOG_zf!T3ksHW%Mwq0Ufu)gmA>x7E` z$PKQomL!XZ)V2N^xk*f%8I#<~vk}LXJg)nL#^rqCnCw~FwAo$zArE=T{xKvMuX_Ac z(G%}3j=y!2nO7%ZoGcoSr)Q(}r{;^mS&T;8=NK57#wa?$t82~={sB1DchtO{%sRVb z2C9#oom*5jl{L%itp(il4nqvxW1R7O_yvW83PhDf<1u{5kf5HQFA`D{^KnM z_{7SuBHd)Wg({@Xyr>BFd2%u+&k~Ui${?Zn6IucqX6g1;9kI?Q=Q{}_)Iru89^Y*#CZ5~P$FiOnp&72n~c(@4HR_q z@qm}_5IkWsL>dT0^e6)DTdufi97DP^+V8DmShO$_IJC^`@);CIdF}6n(O|%K8@%W> ze7**N8|hXa=5O(%j5PJpI0R0|YsoWS#UY*~#-`oY7^Jfv(z1PzM%AAU&lhl8UHX-@ zL)u6YWWOl51dt*Y3<>VUxCEZ0!Q*l;0(7lR&Et_Q$f+EGj1HetzrxL+G;9EdY-Gaq zH?d#Bd?bho(-IoeXxxHnrcQxmyasf0sgU`R4Xb0C0Ry%q%pTIZ^kM^rw#8o{BdlKE~1DQwfjV62H?Zc`))T(kMbms$uBt8S~2YOESm)t zj7Yw}q!EQ?d6OBkEldpp*rZ_2MVCo%RIbp~Gp~oktN7E0uobS3^z5oBBG$=;;Kox> zU(b#LIfJZNetSg_cO?mhA91)R!8#9lNQa}fg`7rFdW{%o-mw+{ngm~$PUQ1pje+ke zxT=&pr~#jCq80|ATTzL5((h0#lRq3MepKTeoY6+T+1*I9EbtKeX@Wu81C!f#@e>K@A=!WZorS7dFHR>hAvBo##zoc88Ai8wUxCQsy@^O4>*n zMgH&Yf252KtJXs_8FV zLxB=+K~qR#;L8@sgLhKaw_!_#)_Fpbad0Mczd7J zx*Tx3C<1nwx8*q0Z`UeU?a9?!xh5Ff?$f-1`~HFOQ8O`c*q$mTg0_g0@OQjX)y-&~ z$>D36)h-0VW^*GF*Ei-dLz1h!A7nE$YKg{8^rQ9KP=^JD1q`}D{;Z_hn9u~s4@kEHUOAFcj&Mr||nj3GYI?$4@@9iG(8x?S@Lpii$=zYFYVfdeEL0F@zT zpp4tk{MUE~(1nlzJTuMot}0Ru31|hPI~Va|%xgFyBkZLa(6>lFsX!f76ym!ht7G-wf?-JKQ^d+(|SLsBD?gX-y06@4PXa zmIyd)6TGk04xOLz@Y9LF>}Hjy>o*Gx?!0D zsZ5eGSvN0r-My%LZ$gwu8dV9@P%umlm(&mbqiLr6Q`tP*`KSZ@$-zZE_NrIdIt9s=m7jo5-*U4nOK-EdRUMTxvo>QQdO>nOfp0Z zo{gR`YX4I3hg1eBrbZfE9S?Ch`v~VJ<4T6C`1k4I9i%Lft!ldG{8q-RZ5 z@?ko9Bk%>YILh>?sIJ0jTo%BwXq&vQ-i`0^?| z(QeTP6kg$=Z6_oV&BF+1jI{|3_A_ZJYO9)z+)vH*L$k+RlTm zUkRU@7T0Y@Z8m*YPwjpN*znU>1ysPX4gY2zUJ-~_Ik<_U!FHfZP^#YFiHeIQ?l2+6 z{^^L;semAggM38Lh4YEtRr%Dw--`F8q2~1l#ONif0 zjhS8~n8%+YQ(LESYY2UHERIq`=Fu#F0R-<{A8N|c}UwG02Wqgz_PIqRV- z!+C^H+KF)ir#6xa$tj8b&bzgODri2EVKHG#hgEJKEq+Jq(e|BhBZw+!6f>&s^W`Et zHwXTMj*oM=rKuhH^(hiMT9t%^P72gom!@Mpw` z{H0sYj)Vpi2su-LkOSCXj3-$(QF^)>Uen%ON{AZAS^>EE;!35fd%Q;5o;@?-w~`6J z(vnr`%3-*81Wj4V0R0|20{%5At_Y-AUVI0u!@cMH9;KcbdtZ(Bo#qptTBI4b%uYi2 zwp|y-yoW7Jx)fLP0l1k59~c}2x8$_)6m=Q0hO5Y?ilii> zAkfu}38DDLex2v&TJA<_Bn*PqMch(+9oMB8hvwnHN_1Vp4S9&8cGLx%@58O@b4Yhm z?)eQUenA4`t{`bD9w_b#EG7!_h;mThbWH3U6Nr8`N~KGG(+KX)agHK`wS z8ulsj$nMk7gHmuXlif1})()!Exk-pF60UyZfWp}X31dgvd#Rv85HP03D6qQjGHe9G z{;@t-mv{Y>PX;0_e1;t!)xoJNxovv54gbX;zER5{{>UN(FgWi_8dN5wKFJ+gd|(b7 zIIiF`h~^)2hGSTX#~FKjz*Gwn3J!n?#UkfPIP6jN3OYS6nur$dtb8n9(9{%PRLrDs zaX%RpdDVL@vf3Nw*;nc@0Vt?65p(&t(ACWZ6S=P!hAsEDdRz2+bVs8pR-K*Lqbz;0 z8huVMk3`+#8+!7Ueex=CTxzMR*;o=CpiB~!ANh~U=RBUO3eJY}Iu>i27&-FLchCFD zoiJs-@0ho=36y%_hw?9;w1rt3;>s)hK}Rbb&KFK>$0owV*xfsVTJDN~!>b*6wzuxxqU-z%wffdSdz zVRGSMW<&bxG?>OzUv_fh^(LLvwVr2vbWxWjK1&hsZ=jwPhiOw`EM&*y4eeihCJ58+s{t=>&|AO4c8ZzG=cFhv2d=KS|o)_cFmU1M7Kph)J-e zL|O8AG=31k3<^8Y04*weXrS)n=`1G0*uqa*9G_OiUxzUsE?s*tQH0~q^Zw4Ijb`wf zj^h~TYm~2SIW1X+zw+!JYz+N63b;TRV9SrXUGZ%`5SZVD3dysC)8?L6JQwgG2}8*o zO~+%YOsrWva;y)^$In;8b|<&Ht|@;IZ-lV#=zTpTh@48Sir%W?ygRGz-tx5RC;Hj0 zK&|(~!Mg3#J=slkoK37lpe(!GXHSEcK0btI%S6=Dd}08Ek=B0KgWTc4oB2+lP_wBh z&pw?C0ykwwcwrDMU++QpLUd2TZJs9=;X89 zysyF?dnKJ`g~pPNSmo5A9`Y)X4l~nz@0}Y~+JO*me~?6#FrzNaNsv$tc2(Ninx!pj zMTDl=z**`m(}gmQh$Yjho1vO%ceyu70v`(At(08I`$#RzC=+|(w7K)9^|fq}TPCo#5YGlJS*DA}GlN|SzD(pX7OuCIPwH_L#M9$%x zrSUtX|26S^>BtpI(s(8!7^*vSAu89Q3a->W`27ONX>txdBmd77nu zyas_oOiNigkh!Dc*W;wjtvL0~=v}2niCI(dle#jOy$o)xU5h!}-4qhv%ykJKQ0p9UD>MvA^wxbw{Uv47H+62dX zmFAX`yW1#^^>MW^WC&wM9i4GUl|myck)+%Je#cz#0{kDj@;So8*@n+nioqz0VXzp! z^X2)~T_qdQR>eKdX%XNO`-T0k4`Tjx$*tnG=w*_thl^v{;;Xdmg zg()>7MHq`|Y0zx(BQEuQ%GaC~xGhCA_xU5wlgg!feErElfOr8$N;Cmn?$i7PkR)ok zc%$XdXK?4Cg%2itimJk^0srL6A9_9D#)hdFUyh7#eOv~PtvuhPUeoq{1s}5o{onU8 znPz9IT3+{@OQh00TU}m&$^Jy&spr4ZkCVBob~u}TR<63arm}H*RgTcZoqz*6nXwr; z$v*9XV>G-<`M8b3Ccy>{FIpjc!}ea2HeBLWS|?wwTcPwwFtK zZ@d~|Y_$8xF5X|9yLde6e2(&AqhwbefcW;79_&Y)@W(tz}NX2PsiI_FAe`d zo9YUGyzZyakM&@NrDb(sf41I|hiF!P>!>wO$Zeeo4#5i6>FBhNI8{Ti1yOAB_LR5l4so?M_ z+{swRHa+J$8#$}A!q)8l1nce*$=z2x9ZhI$Rex_eB0?{efUGT#QGWEXh1OPtaQDx+ z#nrz}68K(?;4C#ECt>_TX!M$1BU9b?BFchQVLKd!uDD{_|6+sIS<^S)v}=vN-M~5; zc?~J3?{XKlVfqWmCMn~?hLTA`fBzswe<|9!SAVtERen?A^Fw_-h}(OeZryZh?-ZHK zbjfD+_L5kYJoCn%9hD3*vwi9T;Ar(w<>D~iKBF$hG zo5H4bqm5}lhHz=2FCUkzbIE%7axWH5*unT1;}=O0sE4W|ZEbG6fG+=&D|o^WGUtVF z=(Y+Bu))(i)P!c_EgBceo7+dB%m?>3 zsbk~=RJFDV# z9Bf$k^)~0!jQcK)%Vk>J96GH}4a5I=@1319Zsu60nr*{9 zeoLw<)1Nk>ho@H0&+(rioGOfu2tRSR%KktE(5N*sjy^v_m9Jl?2B#{L)O37)vaD}o z00knFPxq=a<4I~V?-1sYAR@-zsYyng^pD)%ZvY%*Hb@Km9!3@2D55|9e0{#@?voIf z5lYqp&9g)+)E8TDzMrB(xwLi*aOA|TtxHJ*(|X4XyGim#*tRdCF7v`2k#*&>NxbEw zHteOMx4f*+7vzwAmIc)DQ*b6Le`y(8JbN!2Xs|_4=2N7RdqboG&6AI^Z^eoT1VT(Q z@0QJOO#l4_E6N9N43~|ohAZ-&xmdroYz}N8Yb+WAR$b@N92-0>_72xx;woA^uPMv& z^v;pUkU{^x?Z{FjZZ@KTJ8Qw8cyJ^eY4PQj2&UWCH8{NL6+P+G4DhUme^+}nT&v|t zw_f{51c>3w-SwUmA6{ZGN-aL2V>jQY_Zj7OR*$f*-#2-%W<=SI;)6f8-oq?1;i0bt zujG%aAs~=_X~p>P;|%_>aChlkOv)t!qsV-QEupIxAed3=%y5*S>R=LrwkobW+%TN5 zzpsvq?ImTN$UZcAQ5>(MDP}gjY}b^)78M>``r_SM7j@4VwT(FS2D2p0=Zf#5QAPQ@~Q`w^1>u(Z+YR(s=q0?6-%75k$D zoMkqFQIbcy#G2)F_CJz+a?W^}gPiFuod@n{(>{_)14f4vux&%;sp%c>h$rN__Y01+ zUY420J%sf}kL?8GiSl@{g@`#H;8Z`@6!tKaKFXAj8{EPRfwA)fKU<5bTp%T?*NulI zuRYge%+qbsXki}z>YvM;P%DYm>m<4u%0(UY@=HuF%Oh|50nC>CJYWgb(kEqRKe@&? z?@RsE8ROqgI9Ogz5zj zXfUdFpPzwUj8OcIL`yFb|i5e<(-1m~nR2IWdLLn=WI?$qipwwQe>jh^dw%fu`J#wD0@^d~X#cjv4ZeIn|x1a01P+>kY38G)h8r?~{u$zHMkTz^zgx_`v(u&aEdOoep;w1NUKJG8#PNxq!JC6U-llPb?5{CJL1l$ro)VDTWcLq<98=aG5E$RlO0#8hmf zA0qoSwi!Ch;#iDD&>V)~f7%f~zWmk5r*fh^KwHM*<|QsdhO#8<&)|zTCg6+f4ENy9 z2ch1xrVql*;b&RnhbtW}-|ou=*KFl>N_vwta{Lw^rG)l|=m-zNO}>C4{LFwPgvc|l zAheBBu*Xw!&VqnO0SdF^mUh0}3(m&i3!+WmBToHIDLiS5+^c(1SAWtm3>4((a71E? zwm$3FJ*tVK@k5h@9BCE?^XarW!yK86vevW|tG*hCer|5Hjc){ElWCcBum>$Jj0UAv zJ_T-=bZe|BQy&~#{h~+?2WU1kXcQKlz%8!_mMn)(_Kjp(fEZokES4hEB5p6GNJn5+ zcXbdlX~_&$GkGS^LggCKRj(6A5%gmXnYM&nBJmjB@=Jb{ckAA>OC zZq5WgtkI&tHF{J^%d68Q;0*eU&^eA7e$D|ICM9VaLeIY*4AwUQ5BjjQC^gqUw1opB z`G<|3sVFUm>2&K+zPzxrCST1~aF2!Zu`&MBfPj|ouiu&4QZ25&P|n<#kB#SG4c2Yi zWx%&EGBxt0$NWoVw0EJL(x!rMC=v32w+92x?5X;6p^EdXPUn_g)WS{w00J* zJad)?2d5FWMN(dqIb`%QDr{J3f#zKh)`2T5lw`PRlQTk0^wQJ2Ftf~@t#{*w4al%r zh=W9NGQ%afdcEaf4_kwfpt0d|mP{N${11-X-o(?N34ZSr{=f?Gu>;b}vY$%|b0lQM z#^?o_5M9gA>x`*_KN~fkz58R2M@V}p=a`l9VpX}%gH1}Gm1;7;O2kSWR!N|HgXpzI zv!GBx5O}=x3AmnwVFgp=9*=-gL$4E5)~78jtOa8$o;`O-j;KLZK}&>fv@A-V@3Req zmWZpfd`M(f9(htFg$p8FiT(xLxnS5}iRZbX@)^(!5?hzOsh|p7I*aLS8Ze?yMmbY3 zpGKEkrMOXqBmmP0QPCKJRu~DUstJ{3PIzx`Fz=b%!;C9wW3mB;kX1}Af6yY8({Qj9 z)^KD)Mr%UZ@MY5FubVP+Xv<|!rNXxTP|jr@)IAv|3s=A{hqtSJvqw!|weg7WEodAs zp5Y#63m?z-XXZVe|bap^hlUy{F#v{Gg zxZzZXEIHCSHsG_=Wm!hS|TP{>kk6J1Y2#KKlQY=k@o8{Y4}DCwJiQ z4~v8Qt6~3S68snX|7jol-_arnU&{FJUG4HxVE@`c4)wL!o$HGR3-M(h00933$0QxM diff --git a/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.DeobfuscateExpression.cs b/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.DeobfuscateExpression.cs index f597444..48ab9cc 100644 --- a/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.DeobfuscateExpression.cs +++ b/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.DeobfuscateExpression.cs @@ -35,12 +35,9 @@ internal string DeobfuscateExpression(string expression) case DaxToken.DELIMITED_COMMENT: tokenText = _dictionary.GetValue(tokenText); break; - case DaxToken.COLUMN_OR_MEASURE when token.IsReservedExtensionColumn(): + case DaxToken.COLUMN_OR_MEASURE when token.IsReservedTokenName(): tokenText = token.Replace(expression, tokenText); break; - case DaxToken.STRING_LITERAL when token.IsExtensionColumnName(): - tokenText = ReplaceExtensionColumnName(token); - break; case DaxToken.TABLE_OR_VARIABLE when token.IsVariable(): case DaxToken.TABLE: case DaxToken.COLUMN_OR_MEASURE: @@ -48,7 +45,18 @@ internal string DeobfuscateExpression(string expression) case DaxToken.UNTERMINATED_COLREF: case DaxToken.UNTERMINATED_TABLEREF: case DaxToken.UNTERMINATED_STRING: - tokenText = token.Replace(expression, _dictionary.GetValue(tokenText)); + { + if (token.Text.IsFullyQualifiedColumnName()) + { + var value = DeobfuscateFullyQualifiedColumnName(tokenText).EscapeDax(token.Type); + tokenText = token.Replace(expression, value); + } + else + { + var value = _dictionary.GetValue(tokenText).EscapeDax(token.Type); + tokenText = token.Replace(expression, value); + } + } break; } @@ -56,15 +64,14 @@ internal string DeobfuscateExpression(string expression) } return builder.ToString(); + } - string ReplaceExtensionColumnName(DaxToken token) - { - var (tableName, columnName) = token.GetExtensionColumnNameParts(); - tableName = _dictionary.GetValue(tableName); - columnName = _dictionary.GetValue(columnName); + internal string DeobfuscateFullyQualifiedColumnName(string value) + { + var (table, column) = value.GetFullyQualifiedColumnNameParts(); + var tableName = _dictionary.GetValue(table); + var columnName = _dictionary.GetValue(column); - var value = $"{tableName.DaxEscape()}[{columnName.DaxEscape()}]"; - return token.Replace(expression, value, escape: true); - } + return $"{tableName}[{columnName}]"; } } diff --git a/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.cs b/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.cs index 531001e..eae2562 100644 --- a/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.cs +++ b/src/Dax.Vpax.Obfuscator/DaxModelDeobfuscator.cs @@ -113,8 +113,16 @@ private void Deobfuscate(DaxName name) { if (string.IsNullOrWhiteSpace(name?.Name)) return; - var value = _dictionary.GetValue(name!.Name); - name.Name = value; + if (name!.Name.IsFullyQualifiedColumnName()) + { + var value = DeobfuscateFullyQualifiedColumnName(name!.Name); + name.Name = value; + } + else + { + var value = _dictionary.GetValue(name!.Name); + name.Name = value; + } } private void Deobfuscate(DaxNote note) diff --git a/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.ObfuscateExpression.cs b/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.ObfuscateExpression.cs index 3fd3fa3..0aa7414 100644 --- a/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.ObfuscateExpression.cs +++ b/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.ObfuscateExpression.cs @@ -1,6 +1,6 @@ -using Dax.Vpax.Obfuscator.Extensions; -using System.Text; +using System.Text; using Dax.Tokenizer; +using Dax.Vpax.Obfuscator.Extensions; namespace Dax.Vpax.Obfuscator; @@ -35,12 +35,9 @@ internal string ObfuscateExpression(string expression) case DaxToken.DELIMITED_COMMENT: tokenText = ObfuscateText(new DaxText(tokenText)).ObfuscatedValue; break; - case DaxToken.COLUMN_OR_MEASURE when token.IsReservedExtensionColumn(): + case DaxToken.COLUMN_OR_MEASURE when token.IsReservedTokenName(): tokenText = token.Replace(expression, tokenText); break; - case DaxToken.STRING_LITERAL when token.IsExtensionColumnName(): - tokenText = ReplaceExtensionColumnName(token); - break; case DaxToken.TABLE_OR_VARIABLE when token.IsVariable(): case DaxToken.TABLE: case DaxToken.COLUMN_OR_MEASURE: @@ -48,7 +45,18 @@ internal string ObfuscateExpression(string expression) case DaxToken.UNTERMINATED_COLREF: case DaxToken.UNTERMINATED_TABLEREF: case DaxToken.UNTERMINATED_STRING: - tokenText = token.Replace(expression, ObfuscateText(new DaxText(tokenText))); + { + if (token.Text.IsFullyQualifiedColumnName()) + { + var value = ObfuscateFullyQualifiedColumnName(tokenText).EscapeDax(token.Type); + tokenText = token.Replace(expression, value); + } + else + { + var value = ObfuscateText(new DaxText(tokenText)).ObfuscatedValue.EscapeDax(token.Type); + tokenText = token.Replace(expression, value); + } + } break; } @@ -56,15 +64,14 @@ internal string ObfuscateExpression(string expression) } return builder.ToString(); + } - string ReplaceExtensionColumnName(DaxToken token) - { - var (tableName, columnName) = token.GetExtensionColumnNameParts(); - var tableText = ObfuscateText(new DaxText(tableName)); - var columnText = ObfuscateText(new DaxText(columnName)); + internal string ObfuscateFullyQualifiedColumnName(string value) + { + var (table, column) = value.GetFullyQualifiedColumnNameParts(obfuscating: true); + var tableName = ObfuscateText(new DaxText(table)).ObfuscatedValue; + var columnName = ObfuscateText(new DaxText(column)).ObfuscatedValue; - var value = $"{tableText.ObfuscatedValue.DaxEscape()}[{columnText.ObfuscatedValue.DaxEscape()}]"; - return token.Replace(expression, value, escape: true); - } + return $"{tableName}[{columnName}]"; } } diff --git a/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs b/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs index dd548a7..4291b53 100644 --- a/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs +++ b/src/Dax.Vpax.Obfuscator/DaxModelObfuscator.cs @@ -55,7 +55,8 @@ private void ObfuscateIdentifiers(Column column) private void ObfuscateIdentifiers(Measure measure) { - var measureText = Obfuscate(measure.MeasureName) ?? throw new InvalidOperationException($"The measure name is not valid [{measure.MeasureName}]."); + var name = measure.MeasureName.Name; + var obfuscatedName = Obfuscate(measure.MeasureName) ?? throw new InvalidOperationException($"The measure name is not valid [{name}]."); CreateKpiMeasure(measure.KpiTargetExpression, "Goal"); CreateKpiMeasure(measure.KpiStatusExpression, "Status"); CreateKpiMeasure(measure.KpiTrendExpression, "Trend"); @@ -64,8 +65,8 @@ void CreateKpiMeasure(DaxExpression kpi, string type) { if (string.IsNullOrWhiteSpace(kpi?.Expression)) return; - var text = new DaxText($"_{measureText.Value} {type}"); - text.ObfuscatedValue = $"_{measureText.ObfuscatedValue} {type}"; + var text = new DaxText($"_{name} {type}"); + text.ObfuscatedValue = $"_{obfuscatedName} {type}"; // It may already exist in case of incremental obfuscation if (Texts.IsIncrementalObfuscation && Texts.Contains(text)) @@ -151,13 +152,22 @@ private void Obfuscate(TablePermission tablePermission) Obfuscate(tablePermission.FilterExpression); } - private DaxText? Obfuscate(DaxName name) + private string? Obfuscate(DaxName name) { if (string.IsNullOrWhiteSpace(name?.Name)) return null; - var text = ObfuscateText(new DaxText(name!.Name)); - name.Name = text.ObfuscatedValue; - return text; + if (name!.Name.IsFullyQualifiedColumnName()) + { + var value = ObfuscateFullyQualifiedColumnName(name!.Name); + name.Name = value; + } + else + { + var text = ObfuscateText(new DaxText(name!.Name)); + name.Name = text.ObfuscatedValue; + } + + return name.Name; } private void Obfuscate(DaxNote note) diff --git a/src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs b/src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs index 5d81823..857d245 100644 --- a/src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs +++ b/src/Dax.Vpax.Obfuscator/DaxTextObfuscator.cs @@ -65,12 +65,11 @@ private static bool IsReservedChar(char @char) { // Reserved characters are preserved during obfuscation - switch (@char) { - case '-': // single-line comment char + switch (@char) + { + case ReservedChar_Minus: // single-line comment char case '/': // multi-line comment char case '*': // multi-line comment char - case ']': // square bracket escape char e.g. Sales[Rate[%]]] - case '"': // quotation mark escape char e.g. VAR __quotationMarkChar = """" case '\n': // line feed char e.g. in multi-line comments case '\r': // carriage return char e.g. in multi-line comments return true; @@ -79,6 +78,7 @@ private static bool IsReservedChar(char @char) return false; } + internal const char ReservedChar_Minus = '-'; /// /// CALENDAR() [Date] extension column. /// diff --git a/src/Dax.Vpax.Obfuscator/Extensions/DaxTokenExtensions.cs b/src/Dax.Vpax.Obfuscator/Extensions/DaxTokenExtensions.cs index 0377fe2..ebe2573 100644 --- a/src/Dax.Vpax.Obfuscator/Extensions/DaxTokenExtensions.cs +++ b/src/Dax.Vpax.Obfuscator/Extensions/DaxTokenExtensions.cs @@ -5,15 +5,15 @@ namespace Dax.Vpax.Obfuscator.Extensions; internal static class DaxTokenExtensions { - public static bool IsExtensionColumnName(this DaxToken token) - => token.Type == DaxToken.STRING_LITERAL && token.Text.EndsWith("]") && token.Text.IndexOf('[') > 0; - public static bool IsVariable(this DaxToken token) - => token.Type == DaxToken.TABLE_OR_VARIABLE && !IsFunction(token); + { + Debug.Assert(token.Type == DaxToken.TABLE_OR_VARIABLE); + return token.Type == DaxToken.TABLE_OR_VARIABLE && !IsFunction(token); + } public static bool IsFunction(this DaxToken token) { - if (token.Type != DaxToken.TABLE_OR_VARIABLE) return false; + Debug.Assert(token.Type == DaxToken.TABLE_OR_VARIABLE); var current = token.Next; while (current != null && current.CommentOrWhitespace) @@ -22,9 +22,9 @@ public static bool IsFunction(this DaxToken token) return current != null && current.Type == DaxToken.OPEN_PARENS; } - public static bool IsReservedExtensionColumn(this DaxToken token) + public static bool IsReservedTokenName(this DaxToken token) { - if (token.Type != DaxToken.COLUMN_OR_MEASURE) return false; + Debug.Assert(token.Type == DaxToken.COLUMN_OR_MEASURE); if (token.Text.StartsWith(DaxTextObfuscator.ReservedToken_Value, StringComparison.OrdinalIgnoreCase)) { @@ -44,28 +44,21 @@ public static bool IsReservedExtensionColumn(this DaxToken token) return false; } - public static (string tableName, string columnName) GetExtensionColumnNameParts(this DaxToken token) - { - Debug.Assert(token.IsExtensionColumnName()); - - var openIndex = token.Text.IndexOf('['); - var closeIndex = token.Text.LastIndexOf(']'); - var tableName = token.Text.Substring(0, openIndex); - var columnName = token.Text.Substring(openIndex + 1, closeIndex - openIndex - 1); - return (tableName, columnName); - } - - public static string Replace(this DaxToken token, string expression, DaxText text) - => Replace(token, expression, text.ObfuscatedValue); - - public static string Replace(this DaxToken token, string expression, string value, bool escape = false) + public static string Replace(this DaxToken token, string expression, string value) { var substring = expression.Substring(token.StartIndex, token.StopIndex - token.StartIndex + 1); - var tokenText = escape ? token.Text.DaxEscape() : token.Text; - if (substring.IndexOf(tokenText, StringComparison.Ordinal) == -1) - throw new InvalidOperationException($"Failed to replace token >> {token.Type} | {substring} | {tokenText} | {value}"); + switch (token.Type) + { + case DaxToken.TABLE: + case DaxToken.STRING_LITERAL: + case DaxToken.COLUMN_OR_MEASURE: + return string.Concat(substring[0], value, substring[substring.Length - 1]); + case DaxToken.UNTERMINATED_TABLEREF: + case DaxToken.UNTERMINATED_COLREF: + return string.Concat(substring[0], value); + } - return substring.Replace(tokenText, value); + return substring.Replace(token.Text, value); } } diff --git a/src/Dax.Vpax.Obfuscator/Extensions/StringExtensions.cs b/src/Dax.Vpax.Obfuscator/Extensions/StringExtensions.cs index 10a8ccc..f8323bb 100644 --- a/src/Dax.Vpax.Obfuscator/Extensions/StringExtensions.cs +++ b/src/Dax.Vpax.Obfuscator/Extensions/StringExtensions.cs @@ -1,7 +1,70 @@ -namespace Dax.Vpax.Obfuscator.Extensions; +using System.Diagnostics; +using Dax.Tokenizer; + +namespace Dax.Vpax.Obfuscator.Extensions; internal static class StringExtensions { - public static string DaxEscape(this string value) - => value.Replace("\"", "\"\"").Replace("'", "''"); + public static bool IsFullyQualifiedColumnName(this string value) + => value.TrimEnd().EndsWith("]") && value.IndexOf('[') > 0; + + public static (string table, string column) GetFullyQualifiedColumnNameParts(this string value, bool obfuscating = false) + { + Debug.Assert(IsFullyQualifiedColumnName(value)); + + var openIndex = value.IndexOf('['); + var closeIndex = value.LastIndexOf(']'); + var table = value.Substring(0, openIndex); + var column = value.Substring(openIndex + 1, closeIndex - openIndex - 1); + + if (obfuscating) + { + table = table.Trim(); // remove any leading or trailing whitespace first + + if (IsSquareBraketsRequired(table, column)) + { + // Since the plaintext value contains at least one character that results in a fully qualified + // column name that requires square brackets, then, in order to preserve the same semantics + // we must add at least a single char of the same type to the obfuscated value as well. + table = $"{DaxTextObfuscator.ReservedChar_Minus}{table}"; + } + } + + return (table, column); + + static bool IsSquareBraketsRequired(string table, string column) + { + if (table.Length > 0) + { + if ("012345679".Contains(table[0])) + return true; // Table name start with a digit + + if (table.Any((c) => c != '_' && !DaxTextObfuscator.CharSet.Contains(c))) + return true; // Table name contains any non-alphabetic characters except for the underscore + } + + return column.Contains(']'); + } + } + + public static string EscapeDax(this string value, int tokenType) + { + // See _action() methods in Dax.Tokenizer.DaxLexer class + + switch (tokenType) + { + case DaxToken.TABLE: + case DaxToken.UNTERMINATED_TABLEREF: + return value.Replace("'", "''"); + + case DaxToken.STRING_LITERAL: + return value.Replace("\"", "\"\""); + + case DaxToken.COLUMN_OR_MEASURE: + case DaxToken.UNTERMINATED_COLREF: + return value.Replace("]", "]]"); + } + + return value; + } } diff --git a/src/Dax.Vpax.Obfuscator/version.json b/src/Dax.Vpax.Obfuscator/version.json index 1d8af53..77031d7 100644 --- a/src/Dax.Vpax.Obfuscator/version.json +++ b/src/Dax.Vpax.Obfuscator/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.3-beta", + "version": "0.4-beta", "nugetPackageVersion": { "semVer": 2.0 }, diff --git a/tests/Dax.Vpax.Obfuscator.Tests/DaxModelDeobfuscatorTests.cs b/tests/Dax.Vpax.Obfuscator.Tests/DaxModelDeobfuscatorTests.cs index 035ad1b..2116aaa 100644 --- a/tests/Dax.Vpax.Obfuscator.Tests/DaxModelDeobfuscatorTests.cs +++ b/tests/Dax.Vpax.Obfuscator.Tests/DaxModelDeobfuscatorTests.cs @@ -108,15 +108,15 @@ public void DeobfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsDeobf } [Fact] - public void DeobfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsDeobfuscatedColumnNamePartsPreservingQuotationMarkEscapeChar() + public void DeobfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsDeobfuscatedColumnNamePartsWithoutPreservingQuotationMarkEscapeChar() { - var expression = """ SELECTCOLUMNS(ADDCOLUMNS({}, "XXX[Y""Y]", 1), XXX[Y"Y]) """; + var expression = """ SELECTCOLUMNS(ADDCOLUMNS({}, "XXX[YYY]", 1), XXX[YYY]) """; var expected = """ SELECTCOLUMNS(ADDCOLUMNS({}, "aaa[b""c]", 1), aaa[b"c]) """; var (_, _, deobfuscator) = CreateTest( [ new ObfuscationText("aaa", "XXX"), - new ObfuscationText("b\"c", "Y\"Y"), + new ObfuscationText("b\"c", "YYY"), ]); var actual = deobfuscator.DeobfuscateExpression(expression); @@ -141,13 +141,14 @@ public void DeobfuscateExpression_TableNameMultipleReferencesWithDifferentCasing [Fact] public void DeobfuscateExpression_ColumnName_ReturnsDeobfuscatedValuePreservingSquareBracketEscapeChar() { - var expression = "RELATED( XXXXX[YYYYYY]]] )"; + var expression = "RELATED( XXXXX[YYYY[Z]]] )"; var expected = "RELATED( Sales[Rate[%]]] )"; var (_, _, deobfuscator) = CreateTest( [ new ObfuscationText("Sales", "XXXXX"), - new ObfuscationText("Rate[%]", "YYYYYY]") + new ObfuscationText("Rate", "YYYY"), + new ObfuscationText("%", "Z") ]); var actual = deobfuscator.DeobfuscateExpression(expression); @@ -170,7 +171,7 @@ public void DeobfuscateExpression_VariableNameMultipleReferencesWithDifferentCas } [Fact] - public void ObfuscateExpression_ValueExtensionColumnName_IsNotObfuscated() + public void DebfuscateExpression_ValueExtensionColumnName_IsNotDeobfuscatedBecauseItIsNotObfuscated() { var expression = """ SELECTCOLUMNS({0}, "XXXXXXXXXX", ''[Value]) """; var expected = """ SELECTCOLUMNS({0}, "__Measures", ''[Value]) """; @@ -185,7 +186,7 @@ public void ObfuscateExpression_ValueExtensionColumnName_IsNotObfuscated() } [Fact] - public void DeobfuscateExpression_EmptyStringLiteral_IsNotDeobfuscatedBecauseItIsNotObfuscated() + public void DeobfuscateExpression_StringLiteralEmpty_IsNotDeobfuscatedBecauseItIsNotObfuscated() { var expected = """ IF("" = "", "", "") """; @@ -195,6 +196,23 @@ public void DeobfuscateExpression_EmptyStringLiteral_IsNotDeobfuscatedBecauseItI Assert.Equal(expected, actual); } + [Fact] + public void ObfuscateExpression_StringLiteralWithEscapedQuotationMark_IsObfuscated() + { + var expression = """"" "X" & "Y" & "Z" """""; + var expected = """"" "A" & """" & "B" """""; + + var (_, _, deobfuscator) = CreateTest( + [ + new ObfuscationText("A", "X"), + new ObfuscationText("\"", "Y"), + new ObfuscationText("B", "Z"), + ]); + var actual = deobfuscator.DeobfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + private (Model model, ObfuscationDictionary dictionary, DaxModelDeobfuscator deobfuscator) CreateTest(ObfuscationText[] texts) { var dictionary = new ObfuscationDictionary(id: Guid.NewGuid().ToString("D"), texts); diff --git a/tests/Dax.Vpax.Obfuscator.Tests/DaxModelObfuscatorTests.cs b/tests/Dax.Vpax.Obfuscator.Tests/DaxModelObfuscatorTests.cs index 4977c40..ee61bf2 100644 --- a/tests/Dax.Vpax.Obfuscator.Tests/DaxModelObfuscatorTests.cs +++ b/tests/Dax.Vpax.Obfuscator.Tests/DaxModelObfuscatorTests.cs @@ -95,14 +95,56 @@ public void ObfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsObfusca } [Fact] - public void ObfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsObfuscatedColumnNamePartsPreservingQuotationMarkEscapeChar() + public void ObfuscateExpression_ExtensionColumnNameFullyQualified_ReturnsObfuscatedColumnNamePartsWithoutPreservingQuotationMarkEscapeChar() { var expression = """ SELECTCOLUMNS(ADDCOLUMNS({}, "aaa[b""c]", 1), aaa[b"c]) """; - var expected = """ SELECTCOLUMNS(ADDCOLUMNS({}, "XXX[Y""Y]", 1), XXX[Y"Y]) """; + var expected = """ SELECTCOLUMNS(ADDCOLUMNS({}, "XXX[YYY]", 1), XXX[YYY]) """; var obfuscator = new DaxModelObfuscator(new Model()); obfuscator.Texts.Add(new DaxText("aaa", "XXX")); - obfuscator.Texts.Add(new DaxText("b\"c", "Y\"Y")); + obfuscator.Texts.Add(new DaxText("b\"c", "YYY")); + var actual = obfuscator.ObfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ObfuscateExpression_ExtensionColumnNameFullyQualifiedWithSquareBracket_Test1() + { + var expression = """ SUMX(ADDCOLUMNS({}, "@rate[%]", 1), [@rate[%]]]) """; + var expected = """ SUMX(ADDCOLUMNS({}, "-XXXXX[Y]", 1), [-XXXXX[Y]]]) """; + + var obfuscator = new DaxModelObfuscator(new Model()); + obfuscator.Texts.Add(new DaxText("-@rate", "-XXXXX")); + obfuscator.Texts.Add(new DaxText("%", "Y")); + var actual = obfuscator.ObfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ObfuscateExpression_ExtensionColumnNameFullyQualifiedWithSquareBracket_Test2() + { + var expression = """ SUMX(ADDCOLUMNS({}, " col11 [ a] b ] ", 1), [ col11 [ a]] b ]] ]) """; + var expected = """ SUMX(ADDCOLUMNS({}, "-XXXXX[YYYYYY]", 1), [-XXXXX[YYYYYY]]]) """; + + var obfuscator = new DaxModelObfuscator(new Model()); + obfuscator.Texts.Add(new DaxText("-col11", "-XXXXX")); + obfuscator.Texts.Add(new DaxText(" a] b ", "YYYYYY")); + var actual = obfuscator.ObfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ObfuscateExpression_ExtensionColumnNameFullyQualifiedWithSquareBracket_Test3() + { + var expression = """ SUMX(ADDCOLUMNS({}, " col15 [ a ""' b ] ", 1), col15[ a "' b ]) """; + var expected = """ SUMX(ADDCOLUMNS({}, "XXXXX[YYYYYYYY]", 1), XXXXX[YYYYYYYY]) """; + + var obfuscator = new DaxModelObfuscator(new Model()); + obfuscator.Texts.Add(new DaxText("col15", "XXXXX")); + obfuscator.Texts.Add(new DaxText(" a \"' b ", "YYYYYYYY")); var actual = obfuscator.ObfuscateExpression(expression); Assert.Equal(expected, actual); @@ -125,11 +167,12 @@ public void ObfuscateExpression_TableNameWithDifferentCasings_ReturnsSameObfusca public void ObfuscateExpression_ColumnName_ReturnsObfuscatedValuePreservingSquareBracketEscapeChar() { var expression = "RELATED( Sales[Rate[%]]] )"; - var expected = "RELATED( XXXXX[YYYYYY]]] )"; + var expected = "RELATED( XXXXX[YYYY[Z]]] )"; var obfuscator = new DaxModelObfuscator(new Model()); obfuscator.Texts.Add(new DaxText("Sales", "XXXXX")); - obfuscator.Texts.Add(new DaxText("Rate[%]", "YYYYYY]")); + obfuscator.Texts.Add(new DaxText("Rate", "YYYY")); + obfuscator.Texts.Add(new DaxText("%", "Z")); var actual = obfuscator.ObfuscateExpression(expression); Assert.Equal(expected, actual); @@ -162,7 +205,7 @@ public void ObfuscateExpression_ValueExtensionColumnName_IsNotObfuscated() } [Fact] - public void ObfuscateExpression_EmptyStringLiteral_IsNotObfuscated() + public void ObfuscateExpression_StringLiteralEmpty_IsNotObfuscated() { var expected = """ IF("" = "", "", "") """; @@ -172,6 +215,21 @@ public void ObfuscateExpression_EmptyStringLiteral_IsNotObfuscated() Assert.Equal(expected, actual); } + [Fact] + public void ObfuscateExpression_StringLiteralWithEscapedQuotationMark_IsObfuscated() + { + var expression = """"" "A" & """" & "B" """""; + var expected = """"" "X" & "Y" & "Z" """""; + + var obfuscator = new DaxModelObfuscator(new Model()); + obfuscator.Texts.Add(new DaxText("A", "X")); + obfuscator.Texts.Add(new DaxText("\"", "Y")); + obfuscator.Texts.Add(new DaxText("B", "Z")); + var actual = obfuscator.ObfuscateExpression(expression); + + Assert.Equal(expected, actual); + } + [Theory] [InlineData(nameof(DaxToken.DISPLAYFOLDER))] [InlineData(nameof(DaxToken.FORMATSTRING))] diff --git a/tests/Dax.Vpax.Obfuscator.Tests/DaxTextObfuscatorTests.cs b/tests/Dax.Vpax.Obfuscator.Tests/DaxTextObfuscatorTests.cs index 5d0e85a..b017b52 100644 --- a/tests/Dax.Vpax.Obfuscator.Tests/DaxTextObfuscatorTests.cs +++ b/tests/Dax.Vpax.Obfuscator.Tests/DaxTextObfuscatorTests.cs @@ -28,14 +28,6 @@ public void Obfuscate_SameValueUsingDifferentDaxTextObfuscatorInstances_ReturnsD Assert.NotEqual(text1.ObfuscatedValue, text2.ObfuscatedValue); } - [Fact] - public void Obfuscate_EscapedQuotationMarkInStringLiteral_ReturnsUnobfuscatedQuotationMark() - { - var value = "\"\"\"\""; // e.g. VAR __quotationMarkChar = """" - var text = _obfuscator.Obfuscate(new DaxText(value)); - Assert.Equal(value, text.ObfuscatedValue); - } - [Theory] [InlineData(DaxTextObfuscator.CharSet)] [InlineData("Sales Amount")] diff --git a/tests/pbix/ObfuscatorTest.pbix b/tests/pbix/ObfuscatorTest.pbix new file mode 100644 index 0000000000000000000000000000000000000000..795682fdce3d2008e7be4c8d7c4a1f8c947696e0 GIT binary patch literal 19366 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 XceLuiy;_Y02YA;)9SF1U=h1%wpF%*M literal 0 HcmV?d00001