pyOpenVBA 3.2.0__tar.gz → 3.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/PKG-INFO +8 -3
  2. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/README.md +7 -2
  3. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/pyproject.toml +1 -1
  4. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/__init__.py +1 -1
  5. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_host.py +1 -0
  6. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/__init__.py +168 -0
  7. pyopenvba-3.4.0/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  8. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/excel.py +6 -2
  9. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/vba.py +201 -28
  10. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/.gitignore +0 -0
  11. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/LICENSE.md +0 -0
  12. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/docs/architecture.md +0 -0
  13. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  14. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/docs/roadmap.md +0 -0
  15. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/__main__.py +0 -0
  16. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  17. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  18. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  19. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  20. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  21. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/access_read.py +0 -0
  22. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/cfb.py +0 -0
  23. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/exceptions.py +0 -0
  24. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/powerpoint.py +0 -0
  25. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/vba_pcode.py +0 -0
  26. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/word.py +0 -0
  27. {pyopenvba-3.2.0 → pyopenvba-3.4.0}/tests/fuzz_corpus/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyOpenVBA
3
- Version: 3.2.0
3
+ Version: 3.4.0
4
4
  Summary: Read and write VBA macros inside Excel, Word, and PowerPoint files in pure Python, no dependencies.
5
5
  Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyOpenVBA
6
6
  Project-URL: Repository, https://github.com/WilliamSmithEdward/pyOpenVBA
@@ -200,7 +200,8 @@ path controls the format:
200
200
  ```python
201
201
  from pyopenvba import ExcelFile, WordFile, PowerPointFile
202
202
 
203
- # Excel - macro-enabled workbook (.xlsm) or binary workbook (.xlsb)
203
+ # Excel - macro-enabled workbook (.xlsm), binary workbook (.xlsb),
204
+ # or add-in (.xlam)
204
205
  with ExcelFile.create_new("new_book.xlsm") as wb:
205
206
  wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlsm"\r\nEnd Sub\r\n')
206
207
  wb.save()
@@ -209,6 +210,10 @@ with ExcelFile.create_new("new_book.xlsb") as wb:
209
210
  wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlsb"\r\nEnd Sub\r\n')
210
211
  wb.save()
211
212
 
213
+ with ExcelFile.create_new("new_addin.xlam") as wb:
214
+ wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlam"\r\nEnd Sub\r\n')
215
+ wb.save()
216
+
212
217
  # Word - macro-enabled document (.docm)
213
218
  with WordFile.create_new("new_doc.docm") as doc:
214
219
  doc.set_module("Module1", 'Sub Hello()\r\n MsgBox "docm"\r\nEnd Sub\r\n')
@@ -329,7 +334,7 @@ modules, `.cls` for class modules and code-behind.
329
334
  |-----------|------------------------------|:----:|:-----:|:----------:|
330
335
  | `.xlsm` | Macro-enabled workbook | yes | yes | yes |
331
336
  | `.xlsb` | Binary workbook | yes | yes | yes |
332
- | `.xlam` | Macro-enabled add-in | yes | yes | no |
337
+ | `.xlam` | Macro-enabled add-in | yes | yes | yes |
333
338
  | `.xls` | Legacy (Excel 97-2003) | yes | yes | no |
334
339
 
335
340
  ### Word
@@ -164,7 +164,8 @@ path controls the format:
164
164
  ```python
165
165
  from pyopenvba import ExcelFile, WordFile, PowerPointFile
166
166
 
167
- # Excel - macro-enabled workbook (.xlsm) or binary workbook (.xlsb)
167
+ # Excel - macro-enabled workbook (.xlsm), binary workbook (.xlsb),
168
+ # or add-in (.xlam)
168
169
  with ExcelFile.create_new("new_book.xlsm") as wb:
169
170
  wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlsm"\r\nEnd Sub\r\n')
170
171
  wb.save()
@@ -173,6 +174,10 @@ with ExcelFile.create_new("new_book.xlsb") as wb:
173
174
  wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlsb"\r\nEnd Sub\r\n')
174
175
  wb.save()
175
176
 
177
+ with ExcelFile.create_new("new_addin.xlam") as wb:
178
+ wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlam"\r\nEnd Sub\r\n')
179
+ wb.save()
180
+
176
181
  # Word - macro-enabled document (.docm)
177
182
  with WordFile.create_new("new_doc.docm") as doc:
178
183
  doc.set_module("Module1", 'Sub Hello()\r\n MsgBox "docm"\r\nEnd Sub\r\n')
@@ -293,7 +298,7 @@ modules, `.cls` for class modules and code-behind.
293
298
  |-----------|------------------------------|:----:|:-----:|:----------:|
294
299
  | `.xlsm` | Macro-enabled workbook | yes | yes | yes |
295
300
  | `.xlsb` | Binary workbook | yes | yes | yes |
296
- | `.xlam` | Macro-enabled add-in | yes | yes | no |
301
+ | `.xlam` | Macro-enabled add-in | yes | yes | yes |
297
302
  | `.xls` | Legacy (Excel 97-2003) | yes | yes | no |
298
303
 
299
304
  ### Word
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyOpenVBA"
7
- version = "3.2.0"
7
+ version = "3.4.0"
8
8
  description = "Read and write VBA macros inside Excel, Word, and PowerPoint files in pure Python, no dependencies."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -204,4 +204,4 @@ __all__ = [
204
204
  "push_word",
205
205
  ]
206
206
 
207
- __version__ = "3.2.0"
207
+ __version__ = "3.4.0"
@@ -431,6 +431,7 @@ class VBAHostFile:
431
431
  rename_map,
432
432
  add_modules=add_modules_for_project,
433
433
  delete_names=delete_names,
434
+ code_page=project.code_page,
434
435
  )
435
436
  cfb.write_stream("PROJECT", new_project)
436
437
  # Rewrite PROJECTwm to enumerate the current module set in
@@ -1161,3 +1161,171 @@ _EMPTY_XLSB_B85 = (
1161
1161
 
1162
1162
  EMPTY_XLSB_BYTES: bytes = zlib.decompress(base64.b85decode(_EMPTY_XLSB_B85))
1163
1163
  # --- end XLSB template ---
1164
+
1165
+ # --- XLAM template (generated by bake_xlam_template.py) ---
1166
+ # Source bytes: 8400 | zlib: 7791 | base85: 9739
1167
+ _EMPTY_XLAM_B85 = (
1168
+ "c-nn=1yEhf(k>9(-Q8V7aJS$R+%-5GC%A9i-"
1169
+ "Gh4w9&AH!LU7j&!9755*gVcX_3HeG|Gu|Yt(mD=>+4n3Gu=J?wVL7^SX?M5D8!eC3hLp#PMi"
1170
+ "fa6qFb&6co<OjDZxu$=%Ay-9*#d*~-m`{gb0ZZOV5Q5-"
1171
+ "v>O*?a$pv^txU$v0JM?@0B19Na!*aMm;E@m<I65KWJ*6tBV3Vq#);Eh6}W&M54KyUAVcJay@"
1172
+ "~J$8G?l|r`#q?g@yCr&1=b8Mx9VQA@(f07Rva)5Hj05@=@M6T2aN_F~Qa&f>G@b!j?z}$I5o"
1173
+ "(kPBBsx6+v(VEc2hRiH6l&XQnm*QDF=sR{`J5byBy@T0B&EwIXMm;$yn_5xs4RVJv1wydr?F"
1174
+ "guXKRSnPWS%YHv%)fl-"
1175
+ "$oEajWE8%h9&&H2$ovqT7F>&4j&;oNFGs`t&V&J|bxaN(_#FqiPE@Y-"
1176
+ "QQ4^(S#XCPGLUkwi>#fH%1Vnx=6sq9%>2=9I{3y3BZ<qwh3E2nc(d24*D9DpVqxh3r^T+%dS"
1177
+ "?BoJX7?a?S@;z&;UOWsYa{ebl!C0*oTA#llO30Q@2hFN$RFcZ`egKfh!MAz8kFUdGNH#}!<O"
1178
+ "jh5h{cBB{wK!>xUq(tWP*5l@y@{)pgBu6?Uv2I5cZD}xnD84N8$w}iUe*Q@(%C06Ve8WK2!t"
1179
+ "sIPK&fS$rUTD1Q<mq&3`tY_6h*NrPJMwIibB@w063DY>e@;H8G*U42X*dLX~E8dZ+Zf^EOP!"
1180
+ ">Al^<&-|(sUD=H2?9c?6K3)5EIo6Nc;-yLbF6a<S0X4R`ZD^dVDEL9;m34E-"
1181
+ "8uVJxhm%@hXDi?5dy8L+?K{rSK#KlOxcIAn#O*fMCI=lR#b1VG1ZpBX!PW3AyG)AA@0obreg"
1182
+ "~Dy<c!K3OgM5m(<!D+zTdsfuj?0>ycbh#QhK!cm_U(Su9I}I5)wnPFADu{rO5p{edF^63W^X"
1183
+ "33JT+8=92@57r@ot900KY&n8Vb0Ia;j9Red93t{?MODm*0ASIy~EkG&jlo&Ta!^7wou~E-"
1184
+ "j(c-Zn-mf7rfoM<2W1BdVKl$AXwTdB`1Q6OGk+)43IZD1~=Y;KKTGn{Gf(dE<IYkQNsQZC?>"
1185
+ "78(xs$m~S3^=f3wfWF&tp&rh!m3zGmuLnqy)-"
1186
+ "6zLCK_v3;@)3(%0fC=Fo3$b}#2=@fc{UEVZ&Y*5k!N^P_@2B$e%0iVP`toLcDQ;=mlB<0*1<"
1187
+ "rxW!5JU1ZlExR%TBqd|u^)s?Lu~%4G4e^lr+w!|wbNdk~eoO*XWYiyTLa7N+e~3yGM?OeZ=;"
1188
+ "Kz+oeUb{I(DhvN8BT|AbQV)NOYx0vkg!T@b}^T@MRbkYUsp?-6$x-"
1189
+ "^6OnXD`}I<MkB}|p}ISXafd!?pQs^cprcAu%BjHxW>DoyP_B>#TB!LG+(@QVxqo$>pkiE3?s"
1190
+ "f=l=^7r&SDy4sSKFq`{49J$ooZ$LP7!fH)Dt9O?;;FkGFyEzs<7Yjgn^mr*XLuKj#q~f1)Lm"
1191
+ "g2|a&b5kr{_tOgS0H^ptn-N_v0Ey~YDhlG+q035grDUqAMTq)UEU|Qx)GAqUFED<kFuOc`p9"
1192
+ "<Hi<?;E$q97$`Nn6=kt2(Mhmv~Pe5d6vMVIsJy+&4A-B<1WEBHMdzGdZO#8=DChC&dxd-"
1193
+ "zmY!pu-vK?+>5b)cV@_LSPP%@lFbt#OAq5G3|VVx$0@CpwCQm3Yx3VVH^{=oK2*8a^qh?04x"
1194
+ "32*L>u#jJN6-"
1195
+ ")I=Vit=x0I_4i<QHt$!lVGCBB)xwNvXkK}C%H7Fho8#3al1xytG9Kr=`XC9H}cVNk6mF4BqQ"
1196
+ ";HY;-"
1197
+ "XiAn`@7%tN1ssRC`?hD=whO4rrxYAYQY=Fx8?D1*xAcjLZj2Hp50GBNbIJSf9qf3efU_Zdrp"
1198
+ "~sf(Gtax~%_JPbu0d|ITxd%hQi!Dt9{)o1&Qin$dM;)y0}tnVV0HPAI+G+ZyC%zdV+8fVW^V"
1199
+ "JtIh1aJTOB_H^yq%?F3_oBWXExbq`(cI~#c;9je>Abmky#)9>Iz>oABue-MKDUV#)y$3lyzA"
1200
+ "3TM<1;D5qd=1Tr=Aaz{>PSBuDM~<f}_?p`4$8M?y@);gGApOXL@$NeDVW#I@*!pW_5yKZ@f<"
1201
+ "UpRve~pTSULV+X{dOdftJdq?jL9ufX+ron%ZS?*p&<uAUA_-"
1202
+ "`})+kO2{WBuEeB}=IEabn8cykGsEE#jhqDOsVyhgc~T)(dTDot(LxKWw4k>MI@do+Uh5MyL3"
1203
+ "h;JIsnRVUQTJR*q=4o)ULw8ylfrBLg)wg^}_Xrn}`mJCjlle({}6QmC+ni<j97{6wPKZ9Y}*"
1204
+ "EpmowVWwj$W>1;`~9VG-"
1205
+ "{V)TIg77jOS??l@TD(N0^euhCEO!@3B!d}%BuRIXNM0nx}mC;qQbzpp^3S&5vWDe^#dgviH+"
1206
+ "Zd5-)ED=*}yp5^($}Fsxk03%`~0F$}PIMcQ86*6N;4=w)??S$6U!WoD-mU5JspJ+8@iOZ&*V"
1207
+ "(Q<k$4FUlT!rpZkDF0SifRiGq|0MwT{^i^L@oR22R#xtA9RKOK{?b|Aj3Qu#6BTOXA?OJq*h"
1208
+ "}ovE|yUyhgBDYB-"
1209
+ "&%eD9P3!l8!ienxF6*jKZQQNSQ&+7mY18mEQF2%wyDeN>BzUg<sw?`JOqgQsD8vE$TE#fJ2E"
1210
+ "%pL=*S68$uzK#UFD((8-FpPGeQ7gFN04r=*a2Q?Ie4-sVxjxyRPXXXtazXW57P-"
1211
+ "WVcFW%<S>odM#DEvynRZ*4N&pXm3yQLaAp#4c^khjXsT6r|VS$}a26hnKU@~K&?_F^aqW~Sj"
1212
+ "&6`EeXW0F)sT)1VIRo9pvKyJ@b5@N?tTS6z7mOyt9C}I)_5zb<eG^X_>Nr}UWsk7tn@nY_t$"
1213
+ "~LTdBqLdiROVRx-QiHLLl+@-"
1214
+ "ckIGrypu3>s0fN5Tyn=A<2zt&&#ApQHhV`=Tb|SX)ev@niCdm7fPh$_6EzDpd8bH|cnpds1}"
1215
+ "`Ea5f!c*k9;+d>(cxogiRrVE&_GY8PF=-"
1216
+ "VRliuorqpKqIE)Vm~1_&P}2{(65Iccr?vJ8eThFG3I)L=b%<9mYS8Wd*3|a&p#G;MR)V~W^P"
1217
+ "I2E-0{u2;aWVV*RbJ>9|($&zOs3%-"
1218
+ "V58QrP_p|)KFjSeV;trxncuD{d?uFO`9Zs!$Uz~QU87PdRcjQ8!JbvzuW&T`icHT!d43&h+z"
1219
+ "Mja{siM)&Rn#tGrRLbUc2H{;|_w`<8VI+lx_y@_>lsJIuiDtYR>X?UZ8hx+`KdyiE{n+n)qI"
1220
+ "%9wXgG|1`=tjq2n8L}KdxDQ5P9h~_--jd(lvL0+ARHy3dM56q(G3~%0U6FiiiB;xeDpsIOBC"
1221
+ ">*wZ37#oj=N}2dyEkuco!4lOpB9BZJ&nC98dWbhpDmn<A*RL1qoqOqN3_lRzg*YXoT3IvB*B"
1222
+ "&G8cvH{4MuvCQ~~DTn*gC349b%YWwt@rUbr02H4U_g(R=AFq7@Fq(QaC{pqpisAfVeCnIR2V"
1223
+ "|yhUPZ22=TLaiIQY`Yt`1{73A&eZnsNpxO#DFS@g0XxgyU4)Sc_+0XPThhh%LB6bwy4wes92"
1224
+ "f^To9*p)2zl&d%)0vH(d)nGdo(#gHdbgmi@KC!}UX6;lliHWKF}Cn?3#$)j<$Y=ku>=a32OK"
1225
+ "U=d*Z367av$oKZza`K9v*f<zF<X}g%edgp!kgh$1mC)NV>_&*JT!?+sK(n!>9a%IjVqPDagQ"
1226
+ "$R;S<6|%+-}4Jh>uy+LvERjWFIRdYwBn~q(-l1DwKaD7(e6Lc~~rrE{AA$T*QLwYt%-"
1227
+ "r7)EN%{guL6{DgSB0|4^Aysze~>kha)C<hon&k25ap+@VJ6W<a%dzc8ksS|rzzTX2w9-"
1228
+ "pkc`L<-"
1229
+ "pp07E&y&q1%fy14%mQS0bttS3=YZQSG2^^w4JqV<`9=E9F?{!tDnmxufK9@mH=7Qt76rt=<-"
1230
+ "|Hg7Z%ES<|2`m&MB<ZfM=^0$qQUvyx1*?hB-"
1231
+ "5LLO%kM<tobINTNW5gQhrHGQ7xKB7dIAqX+`FR?rcTu+k~?*!mX$ltxPY6G;NuW^%>h-"
1232
+ "I;rX6Y>`e9etC${K0suE$~uIt<cg~W7!=2zwb}MIg?tkiu|^rj8p#GE;BSUT#Fa%+%tD6r7T"
1233
+ "%mYDsp!a!;FcFml>Eko5VnglQJB`i@_U#G7>x($!U*GMWvK$?N>YE4+ERi7oad-"
1234
+ "8NZEflAXxVqe~|6F{2oE`ey~vOiBFHdqZy79zly~#*hOcS^!T-"
1235
+ "FgfZCO6>MY%at1`!!BHMVB>ZfTW}_bV!;x6*wU}(16v0=|GO{oo80s1*GQ7EU#KL9eIQ9l6W"
1236
+ "ky1pZF!-"
1237
+ "281bryixD<$5wEWEGydwF?Cs+=zN3CJ;f&ud>GDag3Ed^^a6Y;IkHA_Ei9P*WqTFb(#-"
1238
+ "P#`i+s+)ZtzYKIuU%H5V<Mbql=Zt!owp_K*uX=gC@u*gm0$kMOWr1V2%~dYCJ;@=Wr~rzZm#"
1239
+ "WDJPCQT%6|`X*N^y)={n%mMi@d*0`d;0phBgE_aGtMS6l27?{a-Y1OtcNV(SJklu-"
1240
+ ")4QOiyKYuT^pl;3kX_K{HVzwz1>9H*&~_eh#mI8-"
1241
+ "{XrB(X*k>v$DLa9iiYs}6fRB?akS>z#~;a9>K!d@p)mp#_AxwnEUYOpu-"
1242
+ "v$dUx=yjOAmB)iEx5X77-"
1243
+ "k)@9nel8!9?u_MA=~dXu2qPP`tgOw5^M)>XWGNt(Fp^y@27hTB@P)Jc>Grnn>bTh^7svX}BI"
1244
+ "_DMAVtWs!9)yRaK+DQa_SOpM7vtq$WsFRn6bVr@$$=xUNT-"
1245
+ "o{p53W|c@NJc)9or75?M69+5l=k9E;SD;iFOyl#j#Fj{{8K31tf@b2J;?YDsxtls8EjKpluD"
1246
+ "zvSotVE;Qk2%Yw;%OwWkXH(MGGb48W}Rn{RRl;~1BVdcVmia!5%6E@#}MfPmXGCmsm+YM)7H"
1247
+ "I7@KRgDqjk3$ZFB))7c7$WVwZmJk{D}!xh;MTCchY8mL_PE@a)g5M@#oBG2>$>`-"
1248
+ "0%E=?x`wjpkPEg%N|jR($sus|FdC^GlVV!SOIv4W;7KU=ayedCS{}(pP)U4Te9Mq{<Mm@fv-"
1249
+ "xM0pGCvbjT1k8z<rcj$~=#veO4)%pz!He>IyU+wlS%Qej^Hmj_MXY7FI3$v!!=A-O`1m#c-"
1250
+ "_hQ&RhcY^mGFu`DNU__d>o)X_#7KW$Us5k=%sP~@t6u9e1_D9Cl>k2?t|#X6ab+<5By0XV8M"
1251
+ "o&b8CSmiO}BrQEBZc3fGVK@+?ZW^zGDzCD!G|PxtlXsIr$%nZP`6(7#HUacwRnovoa&Jh*=A"
1252
+ "TFT;x#*=xO_1iUU(4Ll>u}V8?nyva9gOPvA_YtAC8;iaR&)K1p6XTuRWT2V>UF6%+^JfQCE9"
1253
+ "W8lL?Y!o-h<dB6euYUv8*vx^;cDxbO@%#*EKA$}`sY<-MI3O-"
1254
+ "OmsB<_{2wvO#B%F6|T+Bo4MeJz3!3W<rUomx{3|ybhYZ@Bgn4`z6=0?Z>E!O<l3Vf9Zt6nos"
1255
+ "r@x#dbzXub+TQ`v&E4C<%I&YvSegd5U*W`jN%$Tp!q&<3zMRlWXxnBJBO4Gkg?2Hw<VGGwB&"
1256
+ "E(SEk27{GPp<%y<5)MclWwqz43cbwUC!=I#i1zOMt0rj`;1165F5lAaMQ<8|2_ir3EG8F_dq"
1257
+ "6=*BY%6p@#{BNTkOzLQLqM$mlgt!`n4&709}QMPTTsSdGPKRcr8?YH$u^-g-"
1258
+ "J=+v=?1T>Uv_%U~@?kOV8uf11f)7056&FM;hKjgy&SMA=|JF*(c8~31>*T9gCJHiP1vQGP&%"
1259
+ "&8YO3}y|gG8ZS>604stxLVr)b$OFrb2{U~cyJjAy1cIAN^tc+IA(r{$%8g$;#Muo1ej8jd_s"
1260
+ "ZqyD}M6xVcjJbf*Z5TzcZMR7EA+)4CgchUbZ4j8jVIR9j_NgTV+NYrT<@fJ{_3Qmjbv#^+Re"
1261
+ "NRAH9MTJV;{0hWWJM#M2nuttHwpll;hZ^sZ(647eJ8-"
1262
+ "kIm(VkNPc*)cOXLhoOThsysWq{+Z6Xhhas+hAeB|op13oy~0Dat$b&_z#N;m_OF-viBRZ>(r"
1263
+ "`GHbDRGkf9x9_LKpIfUmAs}{BREd3v4KfqPZV=JC;6N3U#k0YXFKycelm3M$_ai91lPI^CUv"
1264
+ "fyXOq=nhQJxUK$0xUwG+(PzoPw>=2t%^@99W)am~?xlAiehWS|dzH)r&-"
1265
+ "d!^9^OW*Yn$yr1^Ogur#C_A=0c>ujQ9>0VaY%3A|(73x0Wk2MT%|CnAGaMc_CMGJcJxJK^WL"
1266
+ "fhwHrz2C@Q;CjVk!nOZPbjVnvd)nRcz*iiRp_6-eXb1Ls_2Y%N!@iIR=%ueW#-"
1267
+ "9cy>xfUNL$LfK8^#(#B!1kSi5oHW=K+4d*n{GwFa3zWYG5l#E{-XXePn`-"
1268
+ "oS;k@^1MEFGKp5Q{LYj*wfrh%@ts0W#P_lZtHZCeFng}AsaIB^2D~_3`^ujEaXK$(T+a2lM6"
1269
+ "9&&RE4@m<+R2mR-jFk$bBvd~9fG!Wb*dZ8m-"
1270
+ "JIYl=@fx=<ty2hS2N@Y3!os{r$8Je>2Qb}dbJ6qqyK+{k6`4iuR;Sn{(8M;pzyCJJLr<V}Or"
1271
+ "@-->yY(wEN65p3*y$}o>!y|PYE4aX+c-UR@N$Co-pQhqHZz{tGO8x=t%k(%Vemwd*r>BqY>^"
1272
+ ">ZfA=uv^(yi0sLw(6;tbt)>fa>Hc=E^2b_<~D(q<_oy3HCSo5~#7<ZZXWJZR~=FBR<++=nBx"
1273
+ "qScmS9)SSgW1cH}6_W&XZEuCx7WNGGxo@Kg)uqI~Q8bLXQZB~oxY*L~u|3&-"
1274
+ "L??Sj)I#veBkpzraL9eahZ@w}O0nH*qrb|x0-"
1275
+ "FboQ+c?LVe>sse%;yy3R&vR){$L$i@5N0@PBAVIMr^PS8NtE&!vFj3)J!OeRy<3*#Cid@6eH"
1276
+ "%liSD@DmRBC#|`(>l$nq?XI!1QHM*Rht@=unapR`D_@hGdW4x=1dTIrCawKhE6Ek_iyky5J{"
1277
+ "fTz!r-{eAo|PwWd+wBsFRk(8AInO-X0ke4EuDt#WOytdxKr-"
1278
+ "R^NE<UmtuA`MWrqaSt%X{qurqA_<8KT6+im!W$I;)rPM30vF@t)Znt>NwIw^^xsHLa4)gb`l"
1279
+ "#N0mLty*;XQbKgTZjE;^~2{-tt&A>$LnlQX~bu-"
1280
+ "D!vcNj&iq(>U3CIy|7)NIN}pmYpt6LKjoW_5_{gYhYQ}d)@9x`ght*qz;1E&slT@J&9>1RUq"
1281
+ "QN8M>{yNqvP~!GH`hAxk%G-"
1282
+ "?~?oH=+%9KKlSxDs?GX+aOtJYe)q+q`otmr^Gc54mWj!hnXqV-"
1283
+ "4{~i)ITg#O;W}=J{?2UR@Z-"
1284
+ "H%Chw&98??9oX@1j%;y!8V^&YK?GE$JIbG+lB9DQ@7i`>kI@hNcxGJ16RtNgNJnNNdVAIw#L"
1285
+ "OfT3XQFol7B6XN?F^}KI*!+;aGC$(DqbPP~28u$v*lY=H2xQS{u(UR)?YwK~><UBEW4*|vfz"
1286
+ "8}q;yXV!E9%Y$w_ztRnfV#l0l_qY)aBeiDg;yh3K`R-"
1287
+ "UH$!^oapVlw9L)_IE=hQ;R3k#TFBlk-O0BD5#mh2xkfF>L_*eMpAE&^O;HPkvvTh9>-"
1288
+ "m3<a0`^-$Ho_=^O-O!?!{H)(-"
1289
+ "*S1u&vf6pQ6a6MiPT64rRTT_K#TX$<AFrxKA;5KcAQbB{}m(b4sI($Cz<zhya(EZIwk@4Jym"
1290
+ "9a^*ZSJVM`{1^wUiP>U#`#-"
1291
+ "X3hARy#)fWSUxjD!bE<+mYyDSLq=q(7C7nLb8ad&Akz^v20x7~l(MT%Dj$aK6iPYcSc79~GR"
1292
+ "NXt_uaufLEJP>AK|f;3W>XBsBg(AlaEQ7p(kw}ad&(L_nrN+W&fwINe8RSxf!ed|t{jZi8<1"
1293
+ "lgLl9rxG!O}5JxNswwGS>KMI>kG$6())xqQRKFFd-"
1294
+ "2!jHv!$nR<?VIX?gjhIqpuW!aHx4D0o_bL+(@xIDXKBF@Rpi8_R0k*CSsp<~=8qYjuuJY3x("
1295
+ "sECw?N&0@m)@KItu1WD2KGR|aXE=u?Nm!;wHF`$|yS*a)?4}tJWr#}?BRpqO&99VKYlr*0@8"
1296
+ "|tJvQ<v>;eoI%I0VXYi^OVLTU;@^%vsp5EJgf4rVkXcS*$?eBk?Iz8N^@NG5FTHQlLrsGsv?"
1297
+ ">i_cuo7o!(e>{TNRRWmy;ur1)cfYU9VZ)tou?ZYkb8_{!ntBf4*iwQ$uXNDfuQ-HlWkwhm53"
1298
+ "mCw}E?v*FW)jv=*t16dRw3o@bceRJZKmyqC6i!t=kAT=m2T=#-"
1299
+ "Ha{_Qsv=+@LC=dpS`DFtL@T)?*#jGeSt;}9L4j?`NwaOb)cAA0zZ7&88Pc3-"
1300
+ "i%R>DXvk4m6bg#yXs~1sPIJVZHF?ozZ=`zymHrspzT>eX$GAQ6<taTzWAAPb>RwK0HCPi>8o"
1301
+ ">4qHIMm2>vIjCQgl05)mo)WYwS_>O&K$k$RRq{Xy)9&R)3swl|@9T;ou@3G6~L&mGkzU^TbU"
1302
+ "9A$;V}=$&o|JQI1G2iYH`!Bi_kAd{Pxax8dB-`dmI+V~(RyHnb-sm&HN4Ycz%M{dG5DGm>_S"
1303
+ "I?aF)jsTc*yY$%R{>8ks}~%rgnqqw&<uo|T0nu=;K4=zhcj^y&5$2QCzM~y7ya?%;^EM~8z_"
1304
+ "ieFnQ|}i@_6qIPX0m5b7Bx{#110IW(?YGoz`iz+xTNsb<($lyrU>(!BrYd99leo(nG=DD#Nx"
1305
+ "QnN&+iZv0g)DPz<^7Xqv#DeDQi2tdS@Z}V|gUc?p@QxwQ2Fo}xNhBv2`WQ;E@(Y0on(!8Rw<"
1306
+ "4<ulu?h97u$irMs<)bl`r|?*8CbuSg?OjOptC4HkV?JYkv^`BwG{QF#@(Y_9rBMC{kFB@kX}"
1307
+ "a9^y4RQT7SQHbLlw;>7z1@cqv`8=Sw9IrsY&`C(9nmV*KO3zN)Zg$022Wnu*5H^vC-"
1308
+ "Q1NXZ@f~1>AV3uFTT|>zS97e)Q;1x6bA0!Pwiv|x;f!W`MSKTk;Q19@V=tV1#`i+h*~9!D%q"
1309
+ ";;HFOl5Yjl%-qYW3HNG%nf5zLyghc!uzRL3iUgEhsHzU`ArBOt|lS%uXwZHcBAhG9M_W-"
1310
+ "+Z_`@CCdl%y34vQ!~eW2TxJLi}!;k1e<de*|r7G$6#y*NkP(5meUGN(*^^2?C_x@S?EV?gbs"
1311
+ "l0mXV6KR-"
1312
+ "0b!l({KpN<H=sIQSz~Bf3)c+@Q~x`EU)`eCU>81%@4w$O0`137_aRtdb;lzXI1!t(~LJCwPM"
1313
+ "p*M^g8LwNK&D{jN%o4_OkK}ovH*#^B2GD16=D^yO|NVa+ce%SV{|9Qi4b=kCg>#v|g4d)*sU"
1314
+ "v2!m(iP;(QcBZZbbiH@bQGCWvDYM$ApLTACV`-&4n-"
1315
+ "p%MV6o?lSk(!U+o`w^U8ThM7t=^GcjpfDqS-94~w-w>0DYDin8Ce)(<mf85638v<Kc?-"
1316
+ "Bbj=(z2O65@z6wma$&wP|W|SWiw~z|Inbx-"
1317
+ "<@7)P~h2{dvxGwT<#Y}ptv>{EsM|MS19?FM*5k0H9cJE`L8#BLLK`(I`VyJ0wI)QiJGlOTaE"
1318
+ "bG0;u7-b*@PgUv=A^nfv?BPcL$YBb6EBhM>atgZpbiw?)@TG_;=LdJDJW;T2h1vI-5nZ-"
1319
+ "Lr`RPJ$goV-BW@B`ZVs@!n0<qUY3(Q4uDq)DzY>>s22e7GNmlt&93^^3@E_s<vQC=2Iq2)i1"
1320
+ "AWbjX;OJQMn)?$Jv#v@khCzIqoAljj>@ifD-L({kj$KDE-"
1321
+ "Eu>UGyjhV<Gh5#+Y}>huGPu!9Zr8v<OdaY63q<%_$tHRM99^XrCL#-psFZ--KT5n%DanoeK*"
1322
+ "cj|&jlY5;?*mm;#`TQWB^t|yvXF49RNE}yweyv0Tt&g%#HB2EK3n|S>n~zx+sm~Q@c(2<i43"
1323
+ "j{tL?sxY;@0S|m72FVQ9?ROgF7>rX*-"
1324
+ "_r1GTnP0;D`q6|i{+ZwZn=!cNqRv}%{jrAW?tb*1LQN4G1{do8T|WN5R}eJRe=jD#7Vx^(`H"
1325
+ "z59=zmo{Ujtv)1OEY5!u+r5;A`;fD#brwHTeHu&EmD3*W~>_a)J^5-"
1326
+ "&Fo<DX)p(f28;#{a0G}wSd=&@;?G{F<+(4uc5C~lz*T`IR7PKc@2N<6#s!Ull&JK`5OM(+W&"
1327
+ "zklKyYoS5t(8|LZOC%kBU2LlN@7KK?(+6cj@"
1328
+ )
1329
+
1330
+ EMPTY_XLAM_BYTES: bytes = zlib.decompress(base64.b85decode(_EMPTY_XLAM_B85))
1331
+ # --- end XLAM template ---
@@ -55,10 +55,11 @@ class ExcelFile(VBAHostFile):
55
55
  VBA project (``ThisWorkbook``, ``Sheet1``, and a bare ``Module1``)
56
56
  and return an open :class:`ExcelFile` for it.
57
57
 
58
- Supported extensions: ``.xlsm`` (default) and ``.xlsb``.
58
+ Supported extensions: ``.xlsm`` (default), ``.xlsb``, and
59
+ ``.xlam`` (Excel add-in).
59
60
 
60
61
  The bytes are decoded from a baked-in template captured from a
61
- freshly Excel-authored workbook, so the resulting file opens
62
+ freshly Excel-authored file, so the resulting file opens
62
63
  cleanly in Excel without any "found a problem" repair prompt.
63
64
 
64
65
  ``path`` is overwritten if it already exists.
@@ -68,6 +69,9 @@ class ExcelFile(VBAHostFile):
68
69
  if suffix == ".xlsb":
69
70
  from pyopenvba._templates import EMPTY_XLSB_BYTES
70
71
  template = EMPTY_XLSB_BYTES
72
+ elif suffix == ".xlam":
73
+ from pyopenvba._templates import EMPTY_XLAM_BYTES
74
+ template = EMPTY_XLAM_BYTES
71
75
  else:
72
76
  from pyopenvba._templates import EMPTY_XLSM_BYTES
73
77
  template = EMPTY_XLSM_BYTES
@@ -24,7 +24,10 @@ Critical implementation traps (guide section 31)
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
+ import codecs
27
28
  import struct
29
+ import unicodedata
30
+ import warnings
28
31
  from collections.abc import Callable, Iterable
29
32
  from dataclasses import dataclass, field
30
33
  from enum import Enum
@@ -451,6 +454,22 @@ class VBAReference:
451
454
  libid_secondary: str = "" # twiddled / relative libid where applicable
452
455
 
453
456
 
457
+ def _prefer_unicode(ansi: str, unicode_value: str) -> str:
458
+ """Return the authoritative value of an ANSI / UTF-16 dir record pair.
459
+
460
+ The UTF-16 partner record is lossless by construction, while the ANSI
461
+ record is limited to the project's code page. When the two disagree
462
+ the ANSI side is the damaged one -- either its bytes were undecodable
463
+ (U+FFFD) or the writer had already substituted ``?`` for characters
464
+ outside the page -- so the Unicode record wins (GitHub issue #12).
465
+ Well-formed files carry identical values and are unaffected.
466
+ """
467
+ cleaned = unicode_value.rstrip("\x00")
468
+ if cleaned and cleaned != ansi:
469
+ return cleaned
470
+ return ansi
471
+
472
+
454
473
  def _parse_dir_stream(raw: bytes) -> tuple[_DirInfo, list[_ModuleInfo]]:
455
474
  """
456
475
  Parse a decompressed dir stream.
@@ -663,16 +682,28 @@ def _parse_dir_stream(raw: bytes) -> tuple[_DirInfo, list[_ModuleInfo]]:
663
682
  # Do not commit: REFERENCEORIGINAL is always followed by REFERENCECONTROL.
664
683
 
665
684
  # ------------- MODULE records -----------------------------
685
+ # Each MBCS record is followed by its UTF-16 partner. When the
686
+ # MBCS decode was lossy -- the project's code page could not
687
+ # represent the name, or the file declares a page we could not
688
+ # resolve -- the Unicode record is the authoritative lossless
689
+ # value, so prefer it (GitHub issue #12).
666
690
  elif record_id == 0x0047 and current is not None: # MODULENAMEUNICODE
667
691
  current.name_unicode = data.decode("utf-16-le", errors="replace")
692
+ current.name = _prefer_unicode(current.name, current.name_unicode)
668
693
  elif record_id == 0x001A and current is not None: # MODULESTREAMNAME
669
694
  current.stream_name = data.decode(_enc(), errors="replace")
670
695
  elif record_id == 0x0032 and current is not None: # MODULESTREAMNAME unicode
671
696
  current.stream_name_unicode = data.decode("utf-16-le", errors="replace")
697
+ current.stream_name = _prefer_unicode(
698
+ current.stream_name, current.stream_name_unicode
699
+ )
672
700
  elif record_id == 0x001C and current is not None: # MODULEDOCSTRING (MBCS)
673
701
  current.doc_string = data.decode(_enc(), errors="replace")
674
702
  elif record_id == 0x0048 and current is not None: # MODULEDOCSTRINGUNICODE
675
703
  current.doc_string_unicode = data.decode("utf-16-le", errors="replace")
704
+ current.doc_string = _prefer_unicode(
705
+ current.doc_string, current.doc_string_unicode
706
+ )
676
707
  elif record_id == 0x0031 and current is not None: # MODULEOFFSET
677
708
  if len(data) >= 4:
678
709
  current.text_offset = int(struct.unpack_from("<I", data, 0)[0])
@@ -1122,15 +1153,148 @@ class VBAProject:
1122
1153
  # Write-back helpers
1123
1154
  # ---------------------------------------------------------------------------
1124
1155
 
1156
+ # Windows code-page identifiers whose portable Python codec is not
1157
+ # spelled ``cp<N>``.
1158
+ #
1159
+ # These aliases are not merely a convenience: on Windows, CPython falls
1160
+ # through to the operating system's code-page registry, so ``cp28592``
1161
+ # and friends resolve there but raise LookupError on Linux and macOS.
1162
+ # Relying on the ``cp<N>`` spelling therefore makes a project's text
1163
+ # decode correctly on one platform and turn into latin-1 mojibake on
1164
+ # another -- a platform-dependent data bug (caught by the cross-OS
1165
+ # language-matrix CI job on its first run; see GitHub issue #13).
1166
+ # Consulting this table first keeps behavior identical everywhere.
1167
+ #
1168
+ # Pages VBA hosts write that Python does spell ``cp<N>`` portably (874,
1169
+ # 932, 936, 949, 950, 1250-1258, 1361, 65001) are deliberately absent.
1170
+ _CODEPAGE_ALIASES: dict[int, str] = {
1171
+ # Macintosh
1172
+ 10000: "mac_roman",
1173
+ 10004: "mac_arabic",
1174
+ 10006: "mac_greek",
1175
+ 10007: "mac_cyrillic",
1176
+ 10010: "mac_romanian",
1177
+ 10029: "mac_latin2",
1178
+ 10079: "mac_iceland",
1179
+ 10081: "mac_turkish",
1180
+ 10082: "mac_croatian",
1181
+ # KOI8
1182
+ 20866: "koi8_r",
1183
+ 21866: "koi8_u",
1184
+ # ISO 8859
1185
+ 28591: "iso8859_1",
1186
+ 28592: "iso8859_2",
1187
+ 28593: "iso8859_3",
1188
+ 28594: "iso8859_4",
1189
+ 28595: "iso8859_5",
1190
+ 28596: "iso8859_6",
1191
+ 28597: "iso8859_7",
1192
+ 28598: "iso8859_8",
1193
+ 28599: "iso8859_9",
1194
+ 28603: "iso8859_13",
1195
+ 28605: "iso8859_15",
1196
+ # ISO 2022 / EUC / GB / misc
1197
+ 50220: "iso2022_jp",
1198
+ 50225: "iso2022_kr",
1199
+ 51932: "euc_jp",
1200
+ 51936: "gb2312",
1201
+ 51949: "euc_kr",
1202
+ 52936: "hz",
1203
+ 54936: "gb18030",
1204
+ 65000: "utf_7",
1205
+ }
1206
+
1207
+
1125
1208
  def _encoding_for_codepage(code_page: int) -> str:
1209
+ """Map a PROJECTCODEPAGE value to a Python codec name.
1210
+
1211
+ Falls back to ``latin-1`` for pages Python cannot resolve, warning
1212
+ so the degradation is visible: silently decoding a foreign code page
1213
+ as latin-1 produces mojibake that survives round-trip checks (see
1214
+ GitHub issue #12).
1215
+ """
1216
+ alias = _CODEPAGE_ALIASES.get(code_page)
1217
+ if alias is not None:
1218
+ return alias
1219
+ encoding = f"cp{code_page}"
1126
1220
  try:
1127
- encoding = f"cp{code_page}"
1128
- "".encode(encoding)
1221
+ codecs.lookup(encoding)
1129
1222
  except LookupError:
1130
- encoding = "latin-1"
1223
+ warnings.warn(
1224
+ f"No Python codec for VBA code page {code_page}; falling back "
1225
+ "to latin-1. Text outside the Latin-1 range will be mojibake. "
1226
+ "Please report this code page at "
1227
+ "https://github.com/WilliamSmithEdward/pyOpenVBA/issues",
1228
+ UserWarning,
1229
+ stacklevel=3,
1230
+ )
1231
+ return "latin-1"
1131
1232
  return encoding
1132
1233
 
1133
1234
 
1235
+ def encode_mbcs(text: str, encoding: str) -> bytes:
1236
+ """Encode ``text`` to an MBCS code page, salvaging composed characters.
1237
+
1238
+ Equivalent to ``text.encode(encoding, errors="replace")`` except for
1239
+ characters the codec cannot represent directly but *can* represent as
1240
+ a base character plus combining marks. Python's charmap codecs do no
1241
+ composition, so Vietnamese text destined for cp1258 loses every
1242
+ stacked-diacritic character:
1243
+
1244
+ 'Tiếng Việt'.encode('cp1258', errors='replace')
1245
+ -> b'Ti?ng Vi?t'
1246
+
1247
+ cp1258 stores ``ệ`` as precomposed ``ê`` (0xEA) plus a combining
1248
+ dot-below byte (0xF2), which is *not* the character's canonical
1249
+ decomposition, so NFD alone does not help either. For each
1250
+ unmappable character this helper decomposes to NFD and tries folding
1251
+ each combining mark back into the base in turn, keeping the first
1252
+ combination the codec accepts and emitting the remaining marks as
1253
+ combining bytes. Decoding those bytes yields a canonically
1254
+ equivalent (NFD-ish) string, so compare with NFC normalization.
1255
+
1256
+ Characters that remain unmappable become ``?``, matching
1257
+ ``errors="replace"``. Text that the codec already encodes directly
1258
+ is returned byte-for-byte unchanged, so this never alters output for
1259
+ projects that work today.
1260
+ """
1261
+ try:
1262
+ return text.encode(encoding)
1263
+ except UnicodeEncodeError:
1264
+ pass
1265
+ out = bytearray()
1266
+ for ch in text:
1267
+ try:
1268
+ out += ch.encode(encoding)
1269
+ continue
1270
+ except UnicodeEncodeError:
1271
+ pass
1272
+ decomposed = unicodedata.normalize("NFD", ch)
1273
+ if len(decomposed) > 1:
1274
+ base, marks = decomposed[0], list(decomposed[1:])
1275
+ folded: bytes | None = None
1276
+ for i, mark in enumerate(marks):
1277
+ combined = unicodedata.normalize("NFC", base + mark)
1278
+ if len(combined) != 1:
1279
+ continue # this mark does not compose with the base
1280
+ rest = "".join(marks[:i] + marks[i + 1:])
1281
+ try:
1282
+ folded = combined.encode(encoding) + rest.encode(encoding)
1283
+ except UnicodeEncodeError:
1284
+ continue
1285
+ break
1286
+ if folded is not None:
1287
+ out += folded
1288
+ continue
1289
+ try:
1290
+ out += decomposed.encode(encoding)
1291
+ continue
1292
+ except UnicodeEncodeError:
1293
+ pass
1294
+ out += ch.encode(encoding, errors="replace")
1295
+ return bytes(out)
1296
+
1297
+
1134
1298
  def split_attribute_header(source: str) -> tuple[str, str]:
1135
1299
  """
1136
1300
  Split a VBA module source into ``(attribute_header, body)``.
@@ -1316,7 +1480,7 @@ def rebuild_module_stream(module: VBAModule, code_page: int) -> bytes:
1316
1480
  invalidation logic that Office performs on the prefix remains valid.
1317
1481
  """
1318
1482
  encoding = _encoding_for_codepage(code_page)
1319
- source_bytes = module.source.encode(encoding, errors="replace")
1483
+ source_bytes = encode_mbcs(module.source, encoding)
1320
1484
  compressed = compress(source_bytes)
1321
1485
  return module.prefix_bytes + compressed
1322
1486
 
@@ -1368,11 +1532,11 @@ def serialize_dir_modules_section(project: VBAProject) -> bytes:
1368
1532
  name_u = m.name_unicode or m.name
1369
1533
  stream = m.stream_name or m.name
1370
1534
  stream_u = m.stream_name_unicode or stream
1371
- out += _pack_record(0x0019, name.encode(enc, errors="replace"))
1535
+ out += _pack_record(0x0019, encode_mbcs(name, enc))
1372
1536
  out += _pack_record(0x0047, name_u.encode("utf-16-le"))
1373
- out += _pack_record(0x001A, stream.encode(enc, errors="replace"))
1537
+ out += _pack_record(0x001A, encode_mbcs(stream, enc))
1374
1538
  out += _pack_record(0x0032, stream_u.encode("utf-16-le"))
1375
- out += _pack_record(0x001C, m.doc_string.encode(enc, errors="replace"))
1539
+ out += _pack_record(0x001C, encode_mbcs(m.doc_string, enc))
1376
1540
  out += _pack_record(0x0048, m.doc_string_unicode.encode("utf-16-le"))
1377
1541
  out += _pack_record(0x0031, struct.pack("<I", m.text_offset))
1378
1542
  out += _pack_record(0x001E, struct.pack("<I", m.help_context))
@@ -1432,6 +1596,7 @@ def serialize_project_stream(
1432
1596
  *,
1433
1597
  add_modules: list[tuple[str, str]] | None = None,
1434
1598
  delete_names: set[str] | None = None,
1599
+ code_page: int = 1252,
1435
1600
  ) -> bytes:
1436
1601
  """
1437
1602
  Rewrite a PROJECT stream's plain-text body to apply pending mutations.
@@ -1454,7 +1619,13 @@ def serialize_project_stream(
1454
1619
  ``[Host Extender Info]``) is preserved byte-for-byte except for the
1455
1620
  targeted substitutions.
1456
1621
 
1457
- Returns cp1252-encoded bytes with CRLF line endings.
1622
+ ``code_page``:
1623
+ PROJECTCODEPAGE of the owning project. The PROJECT stream is
1624
+ code-page ANSI per [MS-OVBA] 2.3.1, so its ``Module=`` /
1625
+ ``Document=`` declarations carry module names in that page.
1626
+ Defaults to 1252 for callers that have no project handy.
1627
+
1628
+ Returns code-page-encoded bytes with CRLF line endings.
1458
1629
  """
1459
1630
  add_modules = add_modules or []
1460
1631
  delete_names = delete_names or set()
@@ -1462,10 +1633,8 @@ def serialize_project_stream(
1462
1633
  # callers may invoke this to run the dedup/normalization pass on a
1463
1634
  # PROJECT stream that was previously corrupted by buggy writes (e.g.
1464
1635
  # duplicate ``Module=NAME`` lines from a delete-then-readd flow).
1465
- try:
1466
- text = raw.decode("cp1252", errors="replace")
1467
- except LookupError:
1468
- text = raw.decode("latin-1", errors="replace")
1636
+ encoding = _encoding_for_codepage(code_page)
1637
+ text = raw.decode(encoding, errors="replace")
1469
1638
 
1470
1639
  rename_ci: dict[str, str] = {k.casefold(): v for k, v in rename_map.items()}
1471
1640
  delete_ci: set[str] = {n.casefold() for n in delete_names}
@@ -1579,7 +1748,7 @@ def serialize_project_stream(
1579
1748
  for name, _ in ws_new:
1580
1749
  seen_workspace.add(name.casefold())
1581
1750
 
1582
- return ("\r\n".join(out_lines) + "\r\n").encode("cp1252", errors="replace")
1751
+ return encode_mbcs("\r\n".join(out_lines) + "\r\n", encoding)
1583
1752
 
1584
1753
 
1585
1754
  def _project_section_end(lines: list[str]) -> int:
@@ -1725,7 +1894,7 @@ def parse_vba_project(cfb: CFB) -> VBAProject:
1725
1894
  project_stream_raw = None
1726
1895
  if project_stream_raw is not None:
1727
1896
  try:
1728
- ps = parse_project_stream(project_stream_raw)
1897
+ ps = parse_project_stream(project_stream_raw, code_page=code_page)
1729
1898
  project.protection = ps.protection
1730
1899
  except VBAProjectError:
1731
1900
  pass
@@ -1767,17 +1936,16 @@ class ProjectStream:
1767
1936
  host_extender_info: list[str] = field(default_factory=lambda: [])
1768
1937
 
1769
1938
 
1770
- def parse_project_stream(raw: bytes) -> ProjectStream:
1939
+ def parse_project_stream(raw: bytes, *, code_page: int = 1252) -> ProjectStream:
1771
1940
  """Parse the plain-text PROJECT stream.
1772
1941
 
1773
- The PROJECT stream is Windows-1252 encoded key=value text terminated
1774
- by CRLF. Empty lines separate the project information block, the
1775
- ``[Host Extender Info]`` block, and the ``[Workspace]`` block.
1942
+ The PROJECT stream is code-page ANSI key=value text terminated by
1943
+ CRLF ([MS-OVBA] 2.3.1); pass the project's PROJECTCODEPAGE as
1944
+ ``code_page`` so non-Latin module names decode correctly. Empty
1945
+ lines separate the project information block, the ``[Host Extender
1946
+ Info]`` block, and the ``[Workspace]`` block.
1776
1947
  """
1777
- try:
1778
- text = raw.decode("cp1252", errors="replace")
1779
- except LookupError:
1780
- text = raw.decode("latin-1", errors="replace")
1948
+ text = raw.decode(_encoding_for_codepage(code_page), errors="replace")
1781
1949
 
1782
1950
  out = ProjectStream()
1783
1951
  section = "project" # "project" | "host_extender" | "workspace"
@@ -1846,9 +2014,15 @@ def parse_project_stream(raw: bytes) -> ProjectStream:
1846
2014
  # PROJECTwm stream parser ([MS-OVBA] 2.3.4.4)
1847
2015
  # ---------------------------------------------------------------------------
1848
2016
 
1849
- def parse_projectwm(raw: bytes) -> list[tuple[str, str]]:
2017
+ def parse_projectwm(
2018
+ raw: bytes, *, code_page: int = 1252
2019
+ ) -> list[tuple[str, str]]:
1850
2020
  """Parse the PROJECTwm stream into (mbcs_name, unicode_name) pairs.
1851
2021
 
2022
+ ``code_page`` selects the MBCS codec for the first name in each pair;
2023
+ pass the project's PROJECTCODEPAGE so non-Latin names decode
2024
+ correctly.
2025
+
1852
2026
  Format:
1853
2027
  loop:
1854
2028
  ModuleName_MBCS : null-terminated bytes
@@ -1876,10 +2050,9 @@ def parse_projectwm(raw: bytes) -> list[tuple[str, str]]:
1876
2050
  pos += 2
1877
2051
  unicode_name = raw[start:pos].decode("utf-16-le", errors="replace")
1878
2052
  pos += 2 # skip the u16=0 terminator
1879
- try:
1880
- mbcs_name = mbcs.decode("cp1252", errors="replace")
1881
- except LookupError:
1882
- mbcs_name = mbcs.decode("latin-1", errors="replace")
2053
+ mbcs_name = mbcs.decode(
2054
+ _encoding_for_codepage(code_page), errors="replace"
2055
+ )
1883
2056
  out.append((mbcs_name, unicode_name))
1884
2057
  return out
1885
2058
 
@@ -1903,7 +2076,7 @@ def serialize_projectwm(
1903
2076
  if not mbcs:
1904
2077
  # An empty MBCS name would terminate the stream prematurely.
1905
2078
  continue
1906
- buf += mbcs.encode(enc, errors="replace")
2079
+ buf += encode_mbcs(mbcs, enc)
1907
2080
  buf += b"\x00"
1908
2081
  buf += unicode_name.encode("utf-16-le", errors="replace")
1909
2082
  buf += b"\x00\x00"
File without changes
File without changes
File without changes