[
  {
    "path": ".gitignore",
    "content": "# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n## User settings\nxcuserdata/\n\n## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)\n*.xcscmblueprint\n*.xccheckout\n\n## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)\nrelease/\nreleases/\n\nDerivedData/\n*.moved-aside\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\n\n## Obj-C/Swift specific\n*.hmap\n\n## App packaging\n*.ipa\n*.dSYM.zip\n*.dSYM\n\n# CocoaPods\n#\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n#\n# Pods/\n#\n# Add this line if you want to avoid checking in source code from the Xcode workspace\n# *.xcworkspace\n\n# Carthage\n#\n# Add this line if you want to avoid checking in source code from Carthage dependencies.\n# Carthage/Checkouts\n\nCarthage/Build/\n\n# fastlane\n#\n# It is recommended to not store the screenshots in the git repo.\n# Instead, use fastlane to re-generate the screenshots whenever they are needed.\n# For more information about the recommended setup visit:\n# https://docs.fastlane.tools/best-practices/source-control/#source-control\n\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots/**/*.png\nfastlane/test_output\n\n# Code Injection\n#\n# After new code Injection tools there's a generated folder /iOSInjectionProject\n# https://github.com/johnno1962/injectionforxcode\n\niOSInjectionProject/\n.DS_Store\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "README.md",
    "content": "# WechatExporter\n  \n \n> **BUG!!! 1.8.0.7以前的版本异步加载方式存在一个比较严重的小白bug：当设置为滚动到页面底部异步加载时，越靠后面的页码，加载的消息数量越少；设置为页面打开全部消息异步加载时，消息只能加载到一半。如果iTunes备份还存在，请使用版本1.8.0.8重新导出一遍。如果过往的备份已经清除了，可以下载补丁程序[Win64版本](https://github.com/BlueMatthew/WechatExporter/releases/download/v1.8.0.8/patch_x64_win.zip)/[MacOS 64版本](https://github.com/BlueMatthew/WechatExporter/releases/download/v1.8.0.8/patch_x64_macos.zip)并解压，把wxexpatch.exe/wxexppatch拷贝到导出目录，并执行，来修复已经导出的页面（补丁修复的文件清单可查看日志文件 patch.log）。**    \n\n本程序参考 https://github.com/stomakun/WechatExport-iOS 修改成C++来实现，便于在各个平台以更少依赖运行。同时增加了聊天群名称的解析支持和更多消息类型的导出支持。导出支持Text、HTML、PDF三种格式。  \n  \n- 导出的聊天记录页面可以设置为打开时一次性加载完成（默认方式）、打开时异步加载、页面滑动到底部时加载更多三种方式，可以在菜单“选项”中修改加载方式。  \n\n- 可以在导出的页面增加过滤功能，功能也需要在菜单“选项”中设置。\n   \n- PDF格式，实质是导出打开时一次性加载完成的HTML页面，然后通过Google Chrome或者Microsoft Edge浏览器的功能转成PDF文件，转PDF文件耗时较长，请不要关闭自动弹出的命令行窗口。  \n  \n- 增量导出：菜单“选项”中，如果设置了增量导出，则会仅仅导出上一次导出的最后一条消息之后的部分，通过此功能，再一次备份之后，微信中的聊天记录可以删除，下一次导出，可以把同一个聊天群的消息合并在一起。  \n  \n## 操作步骤：\n1. 通过iTunes将手机备份到电脑上(备份时不要选择设置口令)，Windows操作系统一般位于目录：C:\\用户[用户名]\\AppData\\Roaming\\Apple Computer\\MobileSync\\Backup\\。Android手机可以找一个iPad/iPhone设备，把聊天记录迁移到iPad/iPhone设备上，然后通过iTunes备份到电脑上。\n![iTunesBackup-960](https://user-images.githubusercontent.com/37573096/125906418-090d4ac8-a2ba-4a26-9db2-c6dbed4b0a3c.png)\n  \n2. 下载本代码的执行文件：[Windows x64版本](https://github.com/BlueMatthew/WechatExporter/releases/download/v1.8.0.10/v1.8.0.10_x64_win.zip) 或者 [MacOS x64版本](https://github.com/BlueMatthew/WechatExporter/releases/download/v1.8.0.10/v1.8.0.10_x64_macos.zip)，然后解压压缩文件\n\n3. 执行解压出来的WechatExport.exe/WechatExporter (Windows下如果运行报缺少必须的dll文件，请安装[Visual C++ 2017 redist](https://aka.ms/vs/16/release/vc_redist.x64.exe)后再尝试运行)\n\n4. 按界面提示进行操作。  \n![Windows界面截屏](https://src.wakin.org/github/wxexp/screenshots/win.png) ![MacOS界面截屏](https://src.wakin.org/github/wxexp/screenshots/mac.png###)\n\n5. 导出后的页面示例：  \n![导出后的页面示例截屏](https://src.wakin.org/github/wxexp/demo/demo.png)\n  \n[点击链接可打开网页：https://src.wakin.org/github/wxexp/demo/](https://src.wakin.org/github/wxexp/demo/)\n\n## 模版修改\n解压目录下的res\\templates(MacOS版本位于Contents\\Resources\\res)子目录里存放了输出聊天记录的html页面模版，其中通过两个%包含起来的字符串，譬如，%%NAME%%，不要修改之外，其它页面内容和格式都可以自行调整。  \n  \n特别感谢Chao.M帮忙优化当前的模版。  \n  \n## 系统依赖：\nWindows版本：Windows 7+(XP不支持), [Visual C++ 2017 redist](https://aka.ms/vs/16/release/vc_redist.x64.exe) at [The latest supported Visual C++ downloads](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads)  \nMacOS版本：MacOS 10.10(Yosemite)+\n\n\n## 程序编译\n程序依赖如下第三方库：\n- libxml2: http://www.xmlsoft.org/  \n- libcurl: https://curl.se/libcurl/  \n- libsqlite3: https://www.sqlite.org/index.html   \n- libprotobuf: https://github.com/protocolbuffers/protobuf  \n- libjsoncpp: https://github.com/open-source-parsers/jsoncpp  \n- lame: http://lame.sourceforge.net/ \n- silk: https://github.com/collects/silk (也参考了： https://github.com/kn007/silk-v3-decoder)  \n- libplist: https://github.com/libimobiledevice/libplist  https://github.com/libimobiledevice-win32/libplist  \n- libiconv(windows only): https://www.gnu.org/software/libiconv/  \n- openssl(windows only)：https://github.com/openssl/openssl   \n- WTL (windows only)：https://sourceforge.net/projects/wtl/  \n\nMacOS下，libxml2,libcurl,libsqlite3直接使用了Xcode自带的库，其它第三方库需自行编译。  \nlibmp3lame需手动删除文件include/libmp3lame.sym中的行：lame_init_old  \n\nWindows环境下，silk自带Visual Studio工程文件，可以直接利用Visual Studio编译，其余除了libplist之外，都通过vcpkg可以编译。libplist在vcpkg中也存在，但是在编译x64-windows-static target的时候报了错，于是直接通过Visual Studio建了工程进行编译。\n\nhttps://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x64-windows-static.zip\nhttps://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x86-windows-static.zip\nhttps://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x64-windows-static-dbg.zip\nhttps://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x86-windows-static-dbg.zip\nhttps://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x64-macos-static.zip  \n  \n已测试iTunes和微信版本  \niTunes 12.3.3.17 + 微信6.5.9  \niTunes 12.5.1.21 + 微信6.3.30  \niTunes 12.10.10.2 + 微信7.0.2  \niTunes 12.10.9.3 + 微信 7.0.15  \niTunes 12.9.5.5 + 微信 7.0.2  \nWindows 10 + iTunes 12.11.0.26(Microsoft Store) + 微信 7.0.2  \nWindows 10 + iTunes 12.11.0.26(Microsoft Store) + 微信 8.0.1  \nMac Catalina (Embedded iTunes) + 微信 8.0.1/8.0.2  \nWindows 7 + iTunes 12.10.9.3 + 微信版本 8.0.2  \nWindows 10 + iTunes 12.11.3.17 + 微信 8.0.7  \nWindows 7 + iTunes 12.10.9.3/Mac Catalina (Embedded iTunes) + 微信 7.0.2 + iOS 9.3.5  \nWindows + iTunes 12.10.3.1+ 微信 7.0.10 + iOS 13.3 (@lazybug163)  \nMacOS 11.6（Embedded iTunes）+ iOS Version: 15.0 + 微信 8.0.9  \n\n"
  },
  {
    "path": "WechatExporter/AppConfiguration.h",
    "content": "//\n//  AppConfiguration.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/3/18.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#import <Foundation/Foundation.h>\n\nNS_ASSUME_NONNULL_BEGIN\n\n\ntypedef NS_ENUM(NSInteger, OUTPUT_FORMAT)\n{\n    OUTPUT_FORMAT_HTML = 0,\n    OUTPUT_FORMAT_TEXT,\n    OUTPUT_FORMAT_PDF,\n    OUTPUT_FORMAT_LAST\n};\n\n@interface AppConfiguration : NSObject\n\n+ (void)setDescOrder:(BOOL)descOrder;\n+ (BOOL)getDescOrder;\n+ (BOOL)IsPdfSupported;\n+ (NSInteger)getOutputFormat;\n+ (BOOL)isHtmlMode;\n+ (BOOL)isTextMode;\n+ (BOOL)isPdfMode;\n+ (void)setOutputFormat:(NSInteger)outputFormat;\n+ (void)setSavingInSession:(BOOL)savingInSession;\n+ (BOOL)getSavingInSession;\n\n+ (void)setSyncLoading;\n+ (BOOL)getSyncLoading;\n\n+ (void)setIncrementalExporting:(BOOL)incrementalExp;\n+ (BOOL)getIncrementalExporting;\n   \n+ (void)setLastOutputDir:(NSString *)outputDir;\n+ (NSString *)getLastOrDefaultOutputDir;\n+ (NSString *)getDefaultOutputDir;\n\n+ (void)setLastBackupDir:(NSString *)backupDir;\n+ (NSString *)getLastBackupDir;\n+ (NSString *)getDefaultBackupDir:(BOOL)checkExistence; // YES\n\n+ (NSInteger)getLastCheckUpdateTime;\n+ (void)setLastCheckUpdateTime;\n+ (void)setLastCheckUpdateTime:(NSInteger)lastCheckUpdateTime;\n\n+ (void)setCheckingUpdateDisabled:(BOOL)disabled;\n+ (BOOL)isCheckingUpdateDisabled;\n\n+ (void)setLoadingDataOnScroll;\n+ (BOOL)getLoadingDataOnScroll;\n\n+ (void)setNormalPagination;\n+ (BOOL)getNormalPagination;\n+ (void)setPaginationOnYear;\n+ (BOOL)getPaginationOnYear;\n+ (void)setPaginationOnMonth;\n+ (BOOL)getPaginationOnMonth;\n\n+ (void)setSupportingFilter:(BOOL)supportingFilter;\n+ (BOOL)getSupportingFilter;\n\n+ (void)setOutputDebugLogs:(BOOL)dbgLogs;\n+ (BOOL)outputDebugLogs;\n\n+ (void)setIncludingSubscriptions:(BOOL)includingSubscriptions;\n+ (BOOL)includeSubscriptions;\n\n+ (void)setOpenningFolderAfterExp:(BOOL)openningFolderAfterExp;\n+ (BOOL)getOpenningFolderAfterExp;\n\n+ (void)setSkipGuide:(BOOL)skipGuide;\n+ (BOOL)getSkipGuide;\n\n+ (void)upgrade;\n+ (uint64_t)buildOptions;\n\n\n@end\n\nNS_ASSUME_NONNULL_END\n"
  },
  {
    "path": "WechatExporter/AppConfiguration.mm",
    "content": "//\n//  AppConfiguration.m\n//  WechatExporter\n//\n//  Created by Matthew on 2021/3/18.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#import \"AppConfiguration.h\"\n#include \"Utils.h\"\n#include \"PdfConverterImpl.h\"\n#include \"ExportOption.h\"\n\n#define ASYNC_NONE              0\n#define ASYNC_ONSCROLL          1\n#define ASYNC_PAGER_NORMAL      2\n#define ASYNC_PAGER_ON_YEAR     3\n#define ASYNC_PAGER_ON_MONTH    4\n\n@implementation AppConfiguration\n\n+ (NSInteger)getAsyncLoadingValue\n{\n    NSObject *obj = [[NSUserDefaults standardUserDefaults] objectForKey:@\"AsyncLoading\"];\n    if (nil == obj)\n    {\n        return ASYNC_ONSCROLL;\n    }\n    return [[NSUserDefaults standardUserDefaults] integerForKey:@\"AsyncLoading\"];\n}\n\n+ (void)setDescOrder:(BOOL)descOrder\n{\n    [[NSUserDefaults standardUserDefaults] setBool:descOrder forKey:@\"DescOrder\"];\n}\n\n+ (BOOL)getDescOrder\n{\n    return [[NSUserDefaults standardUserDefaults] boolForKey:@\"DescOrder\"];\n}\n\n+ (BOOL)IsPdfSupported\n{\n    PdfConverterImpl converter(NULL);\n    return converter.isPdfSupported() ? YES : NO;\n}\n\n+ (NSInteger)getOutputFormat\n{\n    return [[NSUserDefaults standardUserDefaults] integerForKey:@\"OutputFormat\"];\n}\n\n+ (BOOL)isHtmlMode\n{\n    return [self getOutputFormat] == OUTPUT_FORMAT_HTML;\n}\n\n+ (BOOL)isTextMode\n{\n    return [self getOutputFormat] == OUTPUT_FORMAT_TEXT;\n}\n\n+ (BOOL)isPdfMode\n{\n    return [self getOutputFormat] == OUTPUT_FORMAT_PDF;\n}\n\n+ (void)setOutputFormat:(NSInteger)outputFormat\n{\n    [[NSUserDefaults standardUserDefaults] setInteger:outputFormat forKey:@\"OutputFormat\"];\n}\n\n+ (void)setSavingInSession:(BOOL)savingInSession\n{\n    [[NSUserDefaults standardUserDefaults] setBool:(!savingInSession) forKey:@\"UniversalFolder\"];\n}\n\n+ (BOOL)getSavingInSession\n{\n    return ![[NSUserDefaults standardUserDefaults] boolForKey:@\"UniversalFolder\"];\n}\n\n+ (void)setSyncLoading\n{\n    [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_NONE forKey:@\"AsyncLoading\"];\n}\n\n+ (BOOL)getSyncLoading\n{\n    return [AppConfiguration getAsyncLoadingValue] == ASYNC_NONE;\n}\n\n+ (void)setIncrementalExporting:(BOOL)incrementalExp\n{\n    [[NSUserDefaults standardUserDefaults] setBool:incrementalExp forKey:@\"IncrementalExp\"];\n}\n\n+ (BOOL)getIncrementalExporting\n{\n    return [[NSUserDefaults standardUserDefaults] boolForKey:@\"IncrementalExp\"];\n}\n   \n+ (void)setLastOutputDir:(NSString *)outputDir\n{\n    [[NSUserDefaults standardUserDefaults] setObject:outputDir forKey:@\"OutputDir\"];\n}\n\n+ (NSString *)getLastOrDefaultOutputDir\n{\n    NSString *outputDir = [[NSUserDefaults standardUserDefaults] stringForKey:@\"OutputDir\"];\n    if (nil != outputDir && outputDir.length > 0)\n    {\n        return outputDir;\n    }\n\n    return [self getDefaultOutputDir];\n}\n\n+ (NSString *)getDefaultOutputDir\n{\n    NSMutableArray *components = [NSMutableArray array];\n    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);\n    if (nil == paths && paths.count > 0)\n    {\n        [components addObject:[paths objectAtIndex:0]];\n    }\n    else\n    {\n        [components addObject:NSHomeDirectory()];\n        [components addObject:@\"Documents\"];\n    }\n    [components addObject:@\"WechatHistory\"];\n    \n    return [NSString pathWithComponents:components];\n}\n\n+ (void)setLastBackupDir:(NSString *)backupDir\n{\n    [[NSUserDefaults standardUserDefaults] setObject:backupDir forKey:@\"BackupDir\"];\n}\n\n+ (NSString *)getLastBackupDir\n{\n    return [[NSUserDefaults standardUserDefaults] stringForKey:@\"BackupDir\"];\n}\n\n+ (NSString *)getDefaultBackupDir:(BOOL)checkExistence // YES\n{\n    NSFileManager *fileManager = [NSFileManager defaultManager];\n    NSURL *appSupport = [fileManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];\n    \n    NSArray *components = @[[appSupport path], @\"MobileSync\", @\"Backup\"];\n    NSString *backupDir = [NSString pathWithComponents:components];\n    if (!checkExistence)\n    {\n        return backupDir;\n    }\n    \n    BOOL isDir = NO;\n    if ([fileManager fileExistsAtPath:backupDir isDirectory:&isDir] && isDir)\n    {\n        return backupDir;\n    }\n    \n    return nil;\n}\n\n+ (NSInteger)getLastCheckUpdateTime\n{\n    return [[NSUserDefaults standardUserDefaults] integerForKey:@\"LastChkUpdateTime\"];\n}\n\n+ (void)setLastCheckUpdateTime\n{\n    [self setLastCheckUpdateTime:0];\n}\n\n+ (void)setLastCheckUpdateTime:(NSInteger)lastCheckUpdateTime\n{\n    if (0 == lastCheckUpdateTime)\n    {\n        lastCheckUpdateTime = static_cast<NSInteger>(getUnixTimeStamp());\n    }\n    [[NSUserDefaults standardUserDefaults] setInteger:lastCheckUpdateTime forKey:@\"LastChkUpdateTime\"];\n}\n\n+ (void)setCheckingUpdateDisabled:(BOOL)disabled\n{\n    [[NSUserDefaults standardUserDefaults] setBool:disabled forKey:@\"ChkUpdateDisabled\"];\n}\n\n+ (BOOL)isCheckingUpdateDisabled\n{\n    return [[NSUserDefaults standardUserDefaults] boolForKey:@\"ChkUpdateDisabled\"];\n}\n\n+ (void)setLoadingDataOnScroll\n{\n    [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_ONSCROLL forKey:@\"AsyncLoading\"];\n}\n\n+ (BOOL)getLoadingDataOnScroll\n{\n    return [AppConfiguration getAsyncLoadingValue] == ASYNC_ONSCROLL;\n}\n\n+ (void)setNormalPagination\n{\n    [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_PAGER_NORMAL forKey:@\"AsyncLoading\"];\n}\n\n+ (BOOL)getNormalPagination\n{\n    return [AppConfiguration getAsyncLoadingValue] == ASYNC_PAGER_NORMAL;\n}\n\n+ (void)setPaginationOnYear\n{\n    [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_PAGER_ON_YEAR forKey:@\"AsyncLoading\"];\n}\n\n+ (BOOL)getPaginationOnYear\n{\n    return [AppConfiguration getAsyncLoadingValue] == ASYNC_PAGER_ON_YEAR;\n}\n\n+ (void)setPaginationOnMonth\n{\n    [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_PAGER_ON_MONTH forKey:@\"AsyncLoading\"];\n}\n\n+ (BOOL)getPaginationOnMonth\n{\n    return [AppConfiguration getAsyncLoadingValue] == ASYNC_PAGER_ON_MONTH;\n}\n\n+ (void)setSupportingFilter:(BOOL)supportingFilter\n{\n    [[NSUserDefaults standardUserDefaults] setBool:(!supportingFilter) forKey:@\"NoFilter\"];\n}\n\n+ (BOOL)getSupportingFilter\n{\n    return ![[NSUserDefaults standardUserDefaults] boolForKey:@\"NoFilter\"];\n}\n\n+ (void)setOutputDebugLogs:(BOOL)dbgLogs\n{\n    [[NSUserDefaults standardUserDefaults] setBool:dbgLogs forKey:@\"OutputDebugLogs\"];\n}\n\n+ (BOOL)outputDebugLogs\n{\n    return [[NSUserDefaults standardUserDefaults] boolForKey:@\"OutputDebugLogs\"];\n}\n\n+ (void)setIncludingSubscriptions:(BOOL)includingSubscriptions\n{\n    [[NSUserDefaults standardUserDefaults] setBool:includingSubscriptions forKey:@\"IncludingSubscriptions\"];\n}\n\n+ (BOOL)includeSubscriptions\n{\n    return [[NSUserDefaults standardUserDefaults] boolForKey:@\"IncludingSubscriptions\"];\n}\n\n+ (void)setOpenningFolderAfterExp:(BOOL)openningFolderAfterExp\n{\n    [[NSUserDefaults standardUserDefaults] setBool:openningFolderAfterExp forKey:@\"OpenningFolderAfterExp\"];\n}\n\n+ (BOOL)getOpenningFolderAfterExp\n{\n    NSObject *obj = [[NSUserDefaults standardUserDefaults] objectForKey:@\"OpenningFolderAfterExp\"];\n    return (nil == obj) ? YES : [[NSUserDefaults standardUserDefaults] boolForKey:@\"OpenningFolderAfterExp\"];\n}\n\n+ (void)setSkipGuide:(BOOL)skipGuide\n{\n    [[NSUserDefaults standardUserDefaults] setBool:skipGuide forKey:@\"SkipGuide\"];\n}\n\n+ (BOOL)getSkipGuide\n{\n    return [[NSUserDefaults standardUserDefaults] boolForKey:@\"SkipGuide\"];\n}\n\n+ (void)upgrade\n{\n    NSObject *obj = [[NSUserDefaults standardUserDefaults] objectForKey:@\"SyncLoading\"];\n    if (obj != nil)\n    {\n        BOOL val = [[NSUserDefaults standardUserDefaults] boolForKey:@\"SyncLoading\"];\n        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@\"SyncLoading\"];\n        if (val)\n        {\n            [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_NONE forKey:@\"AsyncLoading\"];\n        }\n    }\n    \n    obj = [[NSUserDefaults standardUserDefaults] objectForKey:@\"LoadingOnScroll\"];\n    if (obj != nil)\n    {\n        BOOL val = [[NSUserDefaults standardUserDefaults] boolForKey:@\"LoadingOnScroll\"];\n        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@\"LoadingOnScroll\"];\n        if (val)\n        {\n            [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_ONSCROLL forKey:@\"AsyncLoading\"];\n        }\n    }\n    \n}\n\n+ (uint64_t)buildOptions\n{\n    ExportOption options;\n\n    if ([AppConfiguration isTextMode])\n    {\n        options.setTextMode();\n    }\n    if ([AppConfiguration isPdfMode])\n    {\n        options.setPdfMode();\n    }\n    \n    options.setOrder(![AppConfiguration getDescOrder]);\n\n    // getSavingInSession\n    \n    if ([AppConfiguration getSyncLoading])\n    {\n        options.setSyncLoading();\n    }\n    else\n    {\n        // options.setSyncLoading(false);\n        if ([AppConfiguration getLoadingDataOnScroll])\n        {\n            options.setLoadingDataOnScroll([AppConfiguration getLoadingDataOnScroll]);\n        }\n        if ([AppConfiguration getNormalPagination])\n        {\n            options.setPager();\n        }\n        if ([AppConfiguration getPaginationOnYear])\n        {\n            options.setPagerByYear();\n        }\n        if ([AppConfiguration getPaginationOnMonth])\n        {\n            options.setPagerByMonth();\n        }\n        // options.set([AppConfiguration getLoadingDataOnScroll]);\n    }\n    \n    options.setIncrementalExporting([AppConfiguration getIncrementalExporting]);\n    options.supportsFilter([AppConfiguration getSupportingFilter]);\n    \n    options.outputDebugLogs([AppConfiguration outputDebugLogs]);\n    if ([AppConfiguration includeSubscriptions])\n    {\n        options.includesSubscription();\n    }\n\n    return (uint64_t)options;\n}\n\n\n@end\n"
  },
  {
    "path": "WechatExporter/AppDelegate.h",
    "content": "//\n//  AppDelegate.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/29.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#import <Cocoa/Cocoa.h>\n\n@interface AppDelegate : NSObject <NSApplicationDelegate>\n- (IBAction)fileMenuItemClick:(NSMenuItem *)sender;\n- (IBAction)formatMenuItemClick:(NSMenuItem *)sender;\n- (IBAction)optionsMenuItemClick:(NSMenuItem *)sender;\n\n@end\n\n"
  },
  {
    "path": "WechatExporter/AppDelegate.mm",
    "content": "//\r\n//  AppDelegate.m\r\n//  WechatExporter\r\n//\r\n//  Created by Matthew on 2020/9/29.\r\n//  Copyright © 2020 Matthew. All rights reserved.\r\n//\r\n\r\n#import \"AppDelegate.h\"\r\n#include \"Exporter.h\"\r\n#import \"AppConfiguration.h\"\r\n\r\n@interface AppDelegate ()\r\n{\r\n    BOOL m_pdfSupported;\r\n}\r\n@end\r\n\r\n@implementation AppDelegate\r\n\r\n- (void)applicationWillBecomeActive:(NSNotification *)notification\r\n{\r\n    \r\n}\r\n\r\n- (void)applicationDidFinishLaunching:(NSNotification *)notification\r\n{\r\n    [AppConfiguration upgrade];\r\n    \r\n    Exporter::initializeExporter();\r\n    \r\n    m_pdfSupported = [AppConfiguration IsPdfSupported];\r\n    \r\n    NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];\r\n    NSMenuItem *fileMenu = [mainMenu itemAtIndex:1];\r\n    for (NSMenuItem *menuItem in fileMenu.submenu.itemArray)\r\n    {\r\n        if ([@\"updater\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration isCheckingUpdateDisabled] ? NSControlStateValueOff : NSControlStateValueOn;\r\n        }\r\n        else if ([@\"openFolder\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration getOpenningFolderAfterExp] ? NSControlStateValueOn : NSControlStateValueOff;\r\n        }\r\n        else if ([@\"outputDbgLogs\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration outputDebugLogs] ? NSControlStateValueOn : NSControlStateValueOff;\r\n        }\r\n    }\r\n    \r\n    BOOL htmlMode = [AppConfiguration isHtmlMode];\r\n    NSMenuItem *formatMenu = [mainMenu itemAtIndex:2];\r\n    for (NSMenuItem *menuItem in formatMenu.submenu.itemArray)\r\n    {\r\n        if ([@\"htmlMode\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = htmlMode ? NSControlStateValueOn : NSControlStateValueOff;\r\n        }\r\n        else if ([@\"textMode\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration isTextMode] ? NSControlStateValueOn : NSControlStateValueOff;\r\n        }\r\n        else if ([@\"pdfMode\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration isPdfMode] && m_pdfSupported ? NSControlStateValueOn : NSControlStateValueOff;\r\n        }\r\n    }\r\n    \r\n    NSMenuItem *optionsMenu = [mainMenu itemAtIndex:3];\r\n    for (NSMenuItem *menuItem in optionsMenu.submenu.itemArray)\r\n    {\r\n        if ([@\"descOrder\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration getDescOrder] ? NSControlStateValueOn : NSControlStateValueOff;\r\n        }\r\n        else if ([@\"asyncLoadingOnScroll\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration getLoadingDataOnScroll] ? NSControlStateValueOn : NSControlStateValueOff;\r\n            menuItem.enabled = htmlMode;\r\n        }\r\n        else if ([@\"asyncPagerNormal\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration getNormalPagination] ? NSControlStateValueOn : NSControlStateValueOff;\r\n            menuItem.enabled = htmlMode;\r\n        }\r\n        else if ([@\"asyncPagerYear\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration getPaginationOnYear] ? NSControlStateValueOn : NSControlStateValueOff;\r\n            menuItem.enabled = htmlMode;\r\n        }\r\n        else if ([@\"asyncPagerMonth\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration getPaginationOnMonth] ? NSControlStateValueOn : NSControlStateValueOff;\r\n            menuItem.enabled = htmlMode;\r\n        }\r\n        else if ([@\"filter\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration getSupportingFilter] ? NSControlStateValueOn : NSControlStateValueOff;\r\n            menuItem.enabled = htmlMode;\r\n        }\r\n        else if ([@\"incrementalExp\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration getIncrementalExporting] ? NSControlStateValueOn : NSControlStateValueOff;\r\n        }\r\n        else if ([@\"includingSubscriptions\" isEqual:menuItem.identifier])\r\n        {\r\n            menuItem.state = [AppConfiguration includeSubscriptions] ? NSControlStateValueOn : NSControlStateValueOff;\r\n#ifndef NDEBUG\r\n            menuItem.hidden = NO;\r\n#endif\r\n        }\r\n        else\r\n        {\r\n            \r\n        }\r\n    }\r\n}\r\n\r\n- (void)applicationWillTerminate:(NSNotification *)aNotification\r\n{\r\n    // Insert code here to tear down your application\r\n    Exporter::uninitializeExporter();\r\n}\r\n\r\n- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication\r\n{\r\n    return YES;\r\n}\r\n\r\n- (IBAction)fileMenuItemClick:(NSMenuItem *)sender\r\n{\r\n    if ([sender.identifier isEqualToString:@\"openFolder\"])\r\n    {\r\n        [AppConfiguration setOpenningFolderAfterExp:(sender.state == NSControlStateValueOff)];\r\n        sender.state = (sender.state == NSControlStateValueOff) ? NSControlStateValueOn : NSControlStateValueOff;\r\n    }\r\n    else if ([sender.identifier isEqualToString:@\"outputDbgLogs\"])\r\n    {\r\n        [AppConfiguration setOutputDebugLogs:(sender.state == NSControlStateValueOff)];\r\n        sender.state = (sender.state == NSControlStateValueOff) ? NSControlStateValueOn : NSControlStateValueOff;\r\n    }\r\n    else if ([sender.identifier isEqualToString:@\"updater\"])\r\n    {\r\n        [AppConfiguration setCheckingUpdateDisabled:(sender.state == NSControlStateValueOn)];\r\n        sender.state = (sender.state == NSControlStateValueOff) ? NSControlStateValueOn : NSControlStateValueOff;\r\n    }\r\n}\r\n\r\n- (IBAction)formatMenuItemClick:(NSMenuItem *)sender\r\n{\r\n    if ([sender.identifier isEqualToString:@\"htmlMode\"] || [sender.identifier isEqualToString:@\"textMode\"] || [sender.identifier isEqualToString:@\"pdfMode\"])\r\n    {\r\n        if (sender.state == NSControlStateValueOff)\r\n        {\r\n            NSInteger outputFormat = [sender.identifier isEqualToString:@\"htmlMode\"] ? OUTPUT_FORMAT_HTML : ([sender.identifier isEqualToString:@\"pdfMode\"] ? OUTPUT_FORMAT_PDF : OUTPUT_FORMAT_TEXT);\r\n            [AppConfiguration setOutputFormat:outputFormat];\r\n            BOOL htmlMode = [AppConfiguration isHtmlMode];\r\n            \r\n            NSMenuItem *menuItem = nil;\r\n            NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];\r\n\r\n            NSMenuItem *formatMenu = [mainMenu itemAtIndex:2];\r\n            for (NSInteger idx = 0; idx < 3; idx++)\r\n            {\r\n                menuItem = [formatMenu.submenu itemAtIndex:idx];\r\n                menuItem.state = [menuItem.identifier isEqualToString:sender.identifier] ? NSControlStateValueOn : NSControlStateValueOff;\r\n            }\r\n\r\n            NSMenuItem *optionsMenu = [mainMenu itemAtIndex:3];\r\n            for (NSMenuItem *menuItem in optionsMenu.submenu.itemArray)\r\n            {\r\n                if ([menuItem.identifier hasPrefix:@\"async\"])\r\n                {\r\n                    menuItem.enabled = htmlMode;\r\n                }\r\n                else if ([@\"filter\" isEqual:menuItem.identifier])\r\n                {\r\n                    menuItem.enabled = htmlMode;\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n- (IBAction)optionsMenuItemClick:(NSMenuItem *)sender\r\n{\r\n    BOOL newValue = sender.state == NSControlStateValueOff;\r\n    // if (![sender.identifier hasPrefix:@\"pager\"])\r\n    {\r\n        sender.state = newValue ? NSControlStateValueOn : NSControlStateValueOff;\r\n    }\r\n    \r\n    if ([sender.identifier isEqualToString:@\"includingSubscriptions\"])\r\n    {\r\n        [AppConfiguration setIncludingSubscriptions:newValue];\r\n    }\r\n    else if ([sender.identifier isEqualToString:@\"descOrder\"])\r\n    {\r\n        [AppConfiguration setDescOrder:newValue];\r\n    }\r\n    else if ([sender.identifier isEqualToString:@\"savingInSessionFolder\"])\r\n    {\r\n        [AppConfiguration setSavingInSession:newValue];\r\n    }\r\n    /*\r\n    else if ([sender.identifier isEqualToString:@\"asyncLoading\"])\r\n    {\r\n        [AppConfiguration setAsyncLoading:newValue];\r\n\r\n        NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];\r\n        NSMenuItem *optionsMenu = [mainMenu itemAtIndex:3];\r\n        \r\n        for (NSMenuItem *menuItem in optionsMenu.submenu.itemArray)\r\n        {\r\n            if ([@\"loadingOnScroll\" isEqual:menuItem.identifier])\r\n            {\r\n                menuItem.enabled = [AppConfiguration isHtmlMode] && [AppConfiguration getAsyncLoading];\r\n            }\r\n        }\r\n    }\r\n     \r\n    else if ([sender.identifier isEqualToString:@\"asyncLoadingOnScroll\"])\r\n    {\r\n        [AppConfiguration setLoadingDataOnScroll:newValue];\r\n    }\r\n     */\r\n    else if ([sender.identifier isEqualToString:@\"filter\"])\r\n    {\r\n        [AppConfiguration setSupportingFilter:newValue];\r\n    }\r\n    else if ([sender.identifier isEqualToString:@\"incrementalExp\"])\r\n    {\r\n        [AppConfiguration setIncrementalExporting:newValue];\r\n    }\r\n    else if ([sender.identifier hasPrefix:@\"async\"])\r\n    {\r\n        if (newValue)\r\n        {\r\n            for (NSMenuItem *menuItem in sender.parentItem.submenu.itemArray)\r\n            {\r\n                if ((menuItem != sender) && [menuItem.identifier hasPrefix:@\"async\"])\r\n                {\r\n                    if (menuItem.state == NSControlStateValueOn)\r\n                    {\r\n                        menuItem.state = NSControlStateValueOff;\r\n                    }\r\n                }\r\n            }\r\n            \r\n            if ([sender.identifier isEqualToString:@\"asyncLoadingOnScroll\"])\r\n            {\r\n                [AppConfiguration setLoadingDataOnScroll];\r\n            }\r\n            else if ([sender.identifier isEqualToString:@\"asyncPagerNormal\"])\r\n            {\r\n                [AppConfiguration setNormalPagination];\r\n            }\r\n            else if ([sender.identifier isEqualToString:@\"asyncPagerYear\"])\r\n            {\r\n                [AppConfiguration setPaginationOnYear];\r\n            }\r\n            else if ([sender.identifier isEqualToString:@\"asyncPagerMonth\"])\r\n            {\r\n                [AppConfiguration setPaginationOnMonth];\r\n            }\r\n        }\r\n        else\r\n        {\r\n            [AppConfiguration setSyncLoading];\r\n        }\r\n    }\r\n}\r\n\r\n\r\n@end\r\n"
  },
  {
    "path": "WechatExporter/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\r\n  \"images\" : [\r\n    {\r\n      \"filename\" : \"16.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"1x\",\r\n      \"size\" : \"16x16\"\r\n    },\r\n    {\r\n      \"filename\" : \"32.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"2x\",\r\n      \"size\" : \"16x16\"\r\n    },\r\n    {\r\n      \"filename\" : \"32.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"1x\",\r\n      \"size\" : \"32x32\"\r\n    },\r\n    {\r\n      \"filename\" : \"64.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"2x\",\r\n      \"size\" : \"32x32\"\r\n    },\r\n    {\r\n      \"filename\" : \"128.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"1x\",\r\n      \"size\" : \"128x128\"\r\n    },\r\n    {\r\n      \"filename\" : \"256.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"2x\",\r\n      \"size\" : \"128x128\"\r\n    },\r\n    {\r\n      \"filename\" : \"256.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"1x\",\r\n      \"size\" : \"256x256\"\r\n    },\r\n    {\r\n      \"filename\" : \"512.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"2x\",\r\n      \"size\" : \"256x256\"\r\n    },\r\n    {\r\n      \"filename\" : \"512.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"1x\",\r\n      \"size\" : \"512x512\"\r\n    },\r\n    {\r\n      \"filename\" : \"1024.png\",\r\n      \"idiom\" : \"mac\",\r\n      \"scale\" : \"2x\",\r\n      \"size\" : \"512x512\"\r\n    }\r\n  ],\r\n  \"info\" : {\r\n    \"author\" : \"xcode\",\r\n    \"version\" : 1\r\n  }\r\n}\r\n"
  },
  {
    "path": "WechatExporter/Assets.xcassets/Contents.json",
    "content": "{\r\n  \"info\" : {\r\n    \"author\" : \"xcode\",\r\n    \"version\" : 1\r\n  }\r\n}\r\n"
  },
  {
    "path": "WechatExporter/Assets.xcassets/MainMenuCN.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"main-menu-cn@1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"main-menu-cn@2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"main-menu-cn@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "WechatExporter/Assets.xcassets/MainMenuEN.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"main-menu-en@1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"main-menu-en@2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"main-menu-en@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "WechatExporter/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"17156\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" initialViewController=\"B8D-0N-5wS\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"17156\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--Application-->\n        <scene sceneID=\"JPo-4y-FX3\">\n            <objects>\n                <application id=\"hnw-xV-0zn\" sceneMemberID=\"viewController\">\n                    <menu key=\"mainMenu\" title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n                        <items>\n                            <menuItem title=\"WechatExporter\" id=\"1Xt-HY-uBw\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"WechatExporter\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                                    <items>\n                                        <menuItem title=\"About WechatExporter\" id=\"5kV-Vb-QxS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontStandardAboutPanel:\" target=\"Ady-hI-5gd\" id=\"Exp-CZ-Vem\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                                        <menuItem title=\"Hide WechatExporter\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                            <connections>\n                                                <action selector=\"hide:\" target=\"Ady-hI-5gd\" id=\"PnN-Uc-m68\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"hideOtherApplications:\" target=\"Ady-hI-5gd\" id=\"VT4-aY-XCT\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                                        <menuItem title=\"Quit WechatExporter\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                            <connections>\n                                                <action selector=\"terminate:\" target=\"Ady-hI-5gd\" id=\"Te7-pn-YzF\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"File\" id=\"UCE-Dj-bc1\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"File\" autoenablesItems=\"NO\" id=\"yFn-8R-v2s\">\n                                    <items>\n                                        <menuItem title=\"Check Update Automatically\" state=\"on\" identifier=\"updater\" id=\"0eV-Ry-IfP\" userLabel=\"Update\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"fileMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"xct-X6-Tm8\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"4FA-3H-oyN\"/>\n                                        <menuItem title=\"Open the Folder After Exporting\" identifier=\"openFolder\" id=\"TWh-AV-CCF\" userLabel=\"OpenFolder\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"fileMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"ogW-Y1-xTE\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Output Detailed Logs\" identifier=\"outputDbgLogs\" id=\"Pyu-qm-bT0\" userLabel=\"Logs\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"fileMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"JLp-9y-Hr3\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Format\" id=\"v5F-yk-5ct\" userLabel=\"Format\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Format\" autoenablesItems=\"NO\" id=\"or1-Xl-Us0\">\n                                    <items>\n                                        <menuItem title=\"HTML\" state=\"on\" identifier=\"htmlMode\" id=\"cWo-5k-XRR\" userLabel=\"HTML\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"formatMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"NHl-5W-ye0\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Text\" identifier=\"textMode\" id=\"GeM-zb-UkW\" userLabel=\"TEXT\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"formatMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"5kY-da-x9v\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"PDF\" identifier=\"pdfMode\" id=\"T2A-kG-daZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"formatMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"g68-A8-wGQ\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Options\" id=\"fod-ax-X6A\" userLabel=\"Options\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Options\" autoenablesItems=\"NO\" id=\"uG2-8c-wjP\">\n                                    <items>\n                                        <menuItem title=\"From Newer To Earlier\" identifier=\"descOrder\" id=\"eJH-AO-QUD\" userLabel=\"DescOrder\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"bmE-bp-yvx\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Save Avatar/Emoji in Chat Folder\" state=\"on\" hidden=\"YES\" identifier=\"savingInSessionFolder\" id=\"tGU-e5-isp\" userLabel=\"SavingInSession\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"Ak2-Wp-Nxc\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"cxg-sZ-JjI\"/>\n                                        <menuItem title=\"Asynchronous Loading for HTML  Format\" hidden=\"YES\" id=\"WOF-ft-ZWq\" userLabel=\"AsyncLoading\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"2yw-ca-llR\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Load Messages on Scrolling\" state=\"on\" identifier=\"asyncLoadingOnScroll\" id=\"c4Q-qP-ppM\" userLabel=\"LoadOnScroll\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"zuW-td-SIh\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Normal Pagination (Every 1000 Messages)\" identifier=\"asyncPagerNormal\" id=\"Ygm-65-QSG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"MG1-Vl-fgK\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Pagination Based on Year\" identifier=\"asyncPagerYear\" id=\"91m-8a-U1P\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"aU3-uy-zps\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Pagination Based on Month\" identifier=\"asyncPagerMonth\" id=\"QlU-BX-D1D\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"295-GE-eQy\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"bTu-p0-ZCF\"/>\n                                        <menuItem title=\"Show Message Filter\" identifier=\"filter\" id=\"HWn-ip-mii\" userLabel=\"Filter\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"0Pi-LY-fNC\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"BZ6-lW-JdL\"/>\n                                        <menuItem title=\"Incremental Exporting\" identifier=\"incrementalExp\" id=\"5c2-wM-XIf\" userLabel=\"IncrementalExp\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"HQe-Oy-Na5\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Including Subscriptions\" hidden=\"YES\" identifier=\"includingSubscriptions\" id=\"GbO-lL-BLb\" userLabel=\"Subscriptions\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"optionsMenuItemClick:\" target=\"Voe-Tx-rLC\" id=\"MEP-ZQ-Zdq\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Help\" id=\"k5c-yf-BQl\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Help\" systemMenu=\"help\" id=\"pOf-8G-Ji8\">\n                                    <items>\n                                        <menuItem title=\"WechatExporter Help\" keyEquivalent=\"?\" id=\"vp6-q8-ljb\">\n                                            <connections>\n                                                <action selector=\"showHelp:\" target=\"Ady-hI-5gd\" id=\"pmH-Nb-8ng\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                    <connections>\n                        <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"PrD-fu-P6m\"/>\n                    </connections>\n                </application>\n                <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\"/>\n                <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n                <customObject id=\"Ady-hI-5gd\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"75\" y=\"0.0\"/>\n        </scene>\n        <!--Window Controller-->\n        <scene sceneID=\"R2V-B0-nI4\">\n            <objects>\n                <windowController id=\"B8D-0N-5wS\" sceneMemberID=\"viewController\">\n                    <window key=\"window\" title=\"Wechat Exporter\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" visibleAtLaunch=\"NO\" animationBehavior=\"default\" id=\"IQv-IB-iLA\">\n                        <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n                        <rect key=\"contentRect\" x=\"196\" y=\"240\" width=\"1024\" height=\"640\"/>\n                        <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"1680\" height=\"1027\"/>\n                        <value key=\"minSize\" type=\"size\" width=\"1024\" height=\"640\"/>\n                        <connections>\n                            <outlet property=\"delegate\" destination=\"B8D-0N-5wS\" id=\"98r-iN-zZc\"/>\n                        </connections>\n                    </window>\n                    <connections>\n                        <segue destination=\"XfG-lQ-9wD\" kind=\"relationship\" relationship=\"window.shadowedContentViewController\" id=\"cq2-FE-JQM\"/>\n                    </connections>\n                </windowController>\n                <customObject id=\"Oky-zY-oP4\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"75\" y=\"250\"/>\n        </scene>\n        <!--View Controller-->\n        <scene sceneID=\"hIz-AP-VOD\">\n            <objects>\n                <viewController id=\"XfG-lQ-9wD\" customClass=\"ViewController\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" id=\"m2S-Jp-Qdl\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"642\" height=\"400\"/>\n                        <autoresizingMask key=\"autoresizingMask\"/>\n                        <subviews>\n                            <textField horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Kpd-TD-eLh\">\n                                <rect key=\"frame\" x=\"472\" y=\"376\" width=\"160\" height=\"16\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <textFieldCell key=\"cell\" lineBreakMode=\"clipping\" alignment=\"right\" id=\"I2S-Gi-YO0\">\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                    <color key=\"textColor\" name=\"labelColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                    <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                </textFieldCell>\n                            </textField>\n                            <textField horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"OAQ-rY-5Vw\">\n                                <rect key=\"frame\" x=\"12\" y=\"376\" width=\"575\" height=\"16\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <textFieldCell key=\"cell\" lineBreakMode=\"clipping\" title=\"iTunes Backup Directory:\" id=\"BqF-Du-vMt\">\n                                    <font key=\"font\" usesAppearanceFont=\"YES\"/>\n                                    <color key=\"textColor\" name=\"labelColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                    <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                </textFieldCell>\n                            </textField>\n                            <popUpButton verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"FYs-pu-LFe\">\n                                <rect key=\"frame\" x=\"10\" y=\"343\" width=\"599\" height=\"25\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <popUpButtonCell key=\"cell\" type=\"push\" bezelStyle=\"rounded\" alignment=\"left\" lineBreakMode=\"truncatingTail\" borderStyle=\"borderAndBezel\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"gUM-SE-fVj\">\n                                    <behavior key=\"behavior\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"menu\"/>\n                                    <menu key=\"menu\" id=\"8a7-Ad-bIv\"/>\n                                </popUpButtonCell>\n                            </popUpButton>\n                            <button verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"8gl-oS-xn6\">\n                                <rect key=\"frame\" x=\"603\" y=\"339\" width=\"33\" height=\"32\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"...\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"LFI-6K-mnE\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                            </button>\n                            <textField horizontalHuggingPriority=\"249\" verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"az7-4y-9PZ\">\n                                <rect key=\"frame\" x=\"12\" y=\"292\" width=\"594\" height=\"21\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <textFieldCell key=\"cell\" scrollable=\"YES\" lineBreakMode=\"clipping\" selectable=\"YES\" sendsActionOnEndEditing=\"YES\" borderStyle=\"bezel\" drawsBackground=\"YES\" usesSingleLineMode=\"YES\" id=\"Ka2-DD-4h3\">\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                    <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                    <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                </textFieldCell>\n                            </textField>\n                            <button verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"c7I-ig-uay\">\n                                <rect key=\"frame\" x=\"542\" y=\"7\" width=\"96\" height=\"32\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"Export\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"wX7-rb-1cu\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                            </button>\n                            <textField horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"9La-2q-J8z\">\n                                <rect key=\"frame\" x=\"12\" y=\"322\" width=\"596\" height=\"16\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <textFieldCell key=\"cell\" lineBreakMode=\"clipping\" title=\"Output Directory:\" id=\"uh4-ix-NCq\">\n                                    <font key=\"font\" usesAppearanceFont=\"YES\"/>\n                                    <color key=\"textColor\" name=\"labelColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                    <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                </textFieldCell>\n                            </textField>\n                            <popUpButton verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"mwI-Gk-UmA\">\n                                <rect key=\"frame\" x=\"10\" y=\"259\" width=\"418\" height=\"25\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <popUpButtonCell key=\"cell\" type=\"push\" bezelStyle=\"rounded\" alignment=\"left\" lineBreakMode=\"truncatingTail\" borderStyle=\"borderAndBezel\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"uSN-2a-KEP\">\n                                    <behavior key=\"behavior\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"menu\"/>\n                                    <menu key=\"menu\" id=\"YIH-W8-iNB\"/>\n                                </popUpButtonCell>\n                            </popUpButton>\n                            <scrollView fixedFrame=\"YES\" autohidesScrollers=\"YES\" horizontalLineScroll=\"19\" horizontalPageScroll=\"10\" verticalLineScroll=\"19\" verticalPageScroll=\"10\" usesPredominantAxisScrolling=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"pql-rp-pLF\">\n                                <rect key=\"frame\" x=\"12\" y=\"48\" width=\"618\" height=\"206\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <clipView key=\"contentView\" drawsBackground=\"NO\" copiesOnScroll=\"NO\" id=\"zyz-bc-zu0\">\n                                    <rect key=\"frame\" x=\"1\" y=\"0.0\" width=\"616\" height=\"190\"/>\n                                    <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                    <subviews>\n                                        <tableView verticalHuggingPriority=\"750\" allowsExpansionToolTips=\"YES\" columnAutoresizingStyle=\"lastColumnOnly\" alternatingRowBackgroundColors=\"YES\" columnSelection=\"YES\" multipleSelection=\"NO\" autosaveColumns=\"NO\" rowSizeStyle=\"automatic\" headerView=\"wdC-HO-Vvc\" viewBased=\"YES\" id=\"J2V-fs-LwZ\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"899\" height=\"165\"/>\n                                            <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                            <size key=\"intercellSpacing\" width=\"3\" height=\"2\"/>\n                                            <color key=\"backgroundColor\" name=\"controlBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                            <tableViewGridLines key=\"gridStyleMask\" vertical=\"YES\" horizontal=\"YES\"/>\n                                            <color key=\"gridColor\" name=\"gridColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                            <tableColumns>\n                                                <tableColumn identifier=\"columnCheck\" editable=\"NO\" width=\"20\" minWidth=\"20\" maxWidth=\"20\" id=\"wOB-II-58U\">\n                                                    <tableHeaderCell key=\"headerCell\" lineBreakMode=\"truncatingTail\" borderStyle=\"border\">\n                                                        <color key=\"textColor\" name=\"headerTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" name=\"headerColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                    </tableHeaderCell>\n                                                    <textFieldCell key=\"dataCell\" lineBreakMode=\"truncatingTail\" selectable=\"YES\" editable=\"YES\" title=\"Text Cell\" id=\"gtr-oV-Pno\">\n                                                        <font key=\"font\" metaFont=\"system\"/>\n                                                        <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" name=\"controlBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                    </textFieldCell>\n                                                    <prototypeCellViews>\n                                                        <tableCellView id=\"QOc-nX-BuW\">\n                                                            <rect key=\"frame\" x=\"1\" y=\"1\" width=\"20\" height=\"17\"/>\n                                                            <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                                            <subviews>\n                                                                <button verticalHuggingPriority=\"750\" id=\"asV-0M-pRJ\">\n                                                                    <rect key=\"frame\" x=\"1\" y=\"1\" width=\"20\" height=\"18\"/>\n                                                                    <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                                                    <buttonCell key=\"cell\" type=\"check\" title=\"Check\" bezelStyle=\"regularSquare\" imagePosition=\"left\" state=\"on\" inset=\"2\" id=\"58n-zQ-LqF\">\n                                                                        <behavior key=\"behavior\" changeContents=\"YES\" doesNotDimImage=\"YES\" lightByContents=\"YES\"/>\n                                                                        <font key=\"font\" metaFont=\"system\"/>\n                                                                    </buttonCell>\n                                                                </button>\n                                                            </subviews>\n                                                        </tableCellView>\n                                                    </prototypeCellViews>\n                                                </tableColumn>\n                                                <tableColumn identifier=\"columnName\" editable=\"NO\" width=\"256\" minWidth=\"40\" maxWidth=\"1000\" id=\"8FB-hv-fkB\">\n                                                    <tableHeaderCell key=\"headerCell\" lineBreakMode=\"truncatingTail\" borderStyle=\"border\" title=\"Name\">\n                                                        <color key=\"textColor\" name=\"headerTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" name=\"headerColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                    </tableHeaderCell>\n                                                    <textFieldCell key=\"dataCell\" lineBreakMode=\"truncatingTail\" selectable=\"YES\" editable=\"YES\" title=\"Text Cell\" id=\"X6v-WO-gkU\">\n                                                        <font key=\"font\" metaFont=\"system\"/>\n                                                        <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" name=\"controlBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                    </textFieldCell>\n                                                    <tableColumnResizingMask key=\"resizingMask\" resizeWithTable=\"YES\" userResizable=\"YES\"/>\n                                                    <prototypeCellViews>\n                                                        <tableCellView id=\"lR5-LQ-1Qd\">\n                                                            <rect key=\"frame\" x=\"24\" y=\"1\" width=\"256\" height=\"17\"/>\n                                                            <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                                            <subviews>\n                                                                <textField horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"750\" horizontalCompressionResistancePriority=\"250\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Jim-uO-w4w\">\n                                                                    <rect key=\"frame\" x=\"0.0\" y=\"1\" width=\"256\" height=\"16\"/>\n                                                                    <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" flexibleMinY=\"YES\" flexibleMaxY=\"YES\"/>\n                                                                    <textFieldCell key=\"cell\" lineBreakMode=\"truncatingTail\" sendsActionOnEndEditing=\"YES\" title=\"Table View Cell\" id=\"fos-aD-v3q\">\n                                                                        <font key=\"font\" usesAppearanceFont=\"YES\"/>\n                                                                        <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                                        <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                                    </textFieldCell>\n                                                                </textField>\n                                                            </subviews>\n                                                            <connections>\n                                                                <outlet property=\"textField\" destination=\"Jim-uO-w4w\" id=\"Kfv-s2-e0D\"/>\n                                                            </connections>\n                                                        </tableCellView>\n                                                    </prototypeCellViews>\n                                                </tableColumn>\n                                                <tableColumn identifier=\"columnRecordCount\" width=\"96\" minWidth=\"10\" maxWidth=\"3.4028234663852886e+38\" id=\"bsY-Rr-JHi\">\n                                                    <tableHeaderCell key=\"headerCell\" lineBreakMode=\"truncatingTail\" borderStyle=\"border\" alignment=\"left\" title=\"Number of Msgs\">\n                                                        <color key=\"textColor\" name=\"headerTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" white=\"0.0\" alpha=\"0.0\" colorSpace=\"custom\" customColorSpace=\"genericGamma22GrayColorSpace\"/>\n                                                    </tableHeaderCell>\n                                                    <textFieldCell key=\"dataCell\" lineBreakMode=\"truncatingTail\" selectable=\"YES\" editable=\"YES\" alignment=\"left\" title=\"Text Cell\" id=\"Q3f-RG-97F\">\n                                                        <font key=\"font\" metaFont=\"system\"/>\n                                                        <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" name=\"controlBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                    </textFieldCell>\n                                                    <tableColumnResizingMask key=\"resizingMask\" resizeWithTable=\"YES\" userResizable=\"YES\"/>\n                                                    <prototypeCellViews>\n                                                        <tableCellView id=\"qEb-Tp-Qka\">\n                                                            <rect key=\"frame\" x=\"283\" y=\"1\" width=\"96\" height=\"17\"/>\n                                                            <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                                            <subviews>\n                                                                <textField horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"750\" horizontalCompressionResistancePriority=\"250\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"lYR-ar-9XO\">\n                                                                    <rect key=\"frame\" x=\"0.0\" y=\"1\" width=\"96\" height=\"16\"/>\n                                                                    <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" flexibleMinY=\"YES\" flexibleMaxY=\"YES\"/>\n                                                                    <textFieldCell key=\"cell\" lineBreakMode=\"truncatingTail\" sendsActionOnEndEditing=\"YES\" title=\"Table View Cell\" id=\"qJ3-t0-qJh\">\n                                                                        <font key=\"font\" usesAppearanceFont=\"YES\"/>\n                                                                        <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                                        <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                                    </textFieldCell>\n                                                                </textField>\n                                                            </subviews>\n                                                            <connections>\n                                                                <outlet property=\"textField\" destination=\"lYR-ar-9XO\" id=\"J96-Vh-lc5\"/>\n                                                            </connections>\n                                                        </tableCellView>\n                                                    </prototypeCellViews>\n                                                </tableColumn>\n                                                <tableColumn identifier=\"columnLastMsg\" width=\"384\" minWidth=\"40\" maxWidth=\"3.4028234663852886e+38\" id=\"hIY-8j-rpO\" userLabel=\"Last Msg\">\n                                                    <tableHeaderCell key=\"headerCell\" lineBreakMode=\"truncatingTail\" borderStyle=\"border\" alignment=\"left\" title=\"Last Message\">\n                                                        <color key=\"textColor\" name=\"headerTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" white=\"0.0\" alpha=\"0.0\" colorSpace=\"custom\" customColorSpace=\"genericGamma22GrayColorSpace\"/>\n                                                    </tableHeaderCell>\n                                                    <textFieldCell key=\"dataCell\" lineBreakMode=\"truncatingTail\" selectable=\"YES\" editable=\"YES\" alignment=\"left\" title=\"Text Cell\" id=\"CrY-ui-CpL\">\n                                                        <font key=\"font\" metaFont=\"system\"/>\n                                                        <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" name=\"controlBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                    </textFieldCell>\n                                                    <tableColumnResizingMask key=\"resizingMask\" resizeWithTable=\"YES\" userResizable=\"YES\"/>\n                                                    <prototypeCellViews>\n                                                        <tableCellView id=\"9Mg-vi-1eQ\">\n                                                            <rect key=\"frame\" x=\"382\" y=\"1\" width=\"384\" height=\"17\"/>\n                                                            <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                                            <subviews>\n                                                                <textField horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"750\" horizontalCompressionResistancePriority=\"250\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"xHM-Sg-n34\">\n                                                                    <rect key=\"frame\" x=\"0.0\" y=\"1\" width=\"384\" height=\"16\"/>\n                                                                    <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" flexibleMinY=\"YES\" flexibleMaxY=\"YES\"/>\n                                                                    <textFieldCell key=\"cell\" lineBreakMode=\"truncatingTail\" sendsActionOnEndEditing=\"YES\" title=\"Table View Cell\" id=\"sWr-dF-dlL\">\n                                                                        <font key=\"font\" usesAppearanceFont=\"YES\"/>\n                                                                        <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                                        <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                                    </textFieldCell>\n                                                                </textField>\n                                                            </subviews>\n                                                            <connections>\n                                                                <outlet property=\"textField\" destination=\"xHM-Sg-n34\" id=\"5GB-En-lll\"/>\n                                                            </connections>\n                                                        </tableCellView>\n                                                    </prototypeCellViews>\n                                                </tableColumn>\n                                                <tableColumn identifier=\"columnUser\" width=\"128\" minWidth=\"10\" maxWidth=\"3.4028234663852886e+38\" id=\"mqg-xB-lDI\">\n                                                    <tableHeaderCell key=\"headerCell\" lineBreakMode=\"truncatingTail\" borderStyle=\"border\" alignment=\"left\" title=\"Wechat Account\">\n                                                        <color key=\"textColor\" name=\"headerTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" white=\"0.0\" alpha=\"0.0\" colorSpace=\"custom\" customColorSpace=\"genericGamma22GrayColorSpace\"/>\n                                                    </tableHeaderCell>\n                                                    <textFieldCell key=\"dataCell\" lineBreakMode=\"truncatingTail\" selectable=\"YES\" editable=\"YES\" alignment=\"left\" title=\"Text Cell\" id=\"O0i-cF-8Ix\">\n                                                        <font key=\"font\" metaFont=\"system\"/>\n                                                        <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                        <color key=\"backgroundColor\" name=\"controlBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                    </textFieldCell>\n                                                    <tableColumnResizingMask key=\"resizingMask\" resizeWithTable=\"YES\" userResizable=\"YES\"/>\n                                                    <prototypeCellViews>\n                                                        <tableCellView id=\"BcU-k8-Igt\">\n                                                            <rect key=\"frame\" x=\"769\" y=\"1\" width=\"128\" height=\"17\"/>\n                                                            <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                                            <subviews>\n                                                                <textField horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"750\" horizontalCompressionResistancePriority=\"250\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"DFJ-Vc-mZN\">\n                                                                    <rect key=\"frame\" x=\"0.0\" y=\"1\" width=\"128\" height=\"16\"/>\n                                                                    <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" flexibleMinY=\"YES\" flexibleMaxY=\"YES\"/>\n                                                                    <textFieldCell key=\"cell\" lineBreakMode=\"truncatingTail\" sendsActionOnEndEditing=\"YES\" title=\"Table View Cell\" id=\"5Mi-xr-dCf\">\n                                                                        <font key=\"font\" usesAppearanceFont=\"YES\"/>\n                                                                        <color key=\"textColor\" name=\"controlTextColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                                        <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                                                    </textFieldCell>\n                                                                </textField>\n                                                            </subviews>\n                                                            <connections>\n                                                                <outlet property=\"textField\" destination=\"DFJ-Vc-mZN\" id=\"epd-Xc-GX3\"/>\n                                                            </connections>\n                                                        </tableCellView>\n                                                    </prototypeCellViews>\n                                                </tableColumn>\n                                            </tableColumns>\n                                        </tableView>\n                                    </subviews>\n                                    <nil key=\"backgroundColor\"/>\n                                </clipView>\n                                <scroller key=\"horizontalScroller\" wantsLayer=\"YES\" verticalHuggingPriority=\"750\" horizontal=\"YES\" id=\"adY-Ee-7bA\">\n                                    <rect key=\"frame\" x=\"1\" y=\"190\" width=\"616\" height=\"15\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                </scroller>\n                                <scroller key=\"verticalScroller\" hidden=\"YES\" wantsLayer=\"YES\" verticalHuggingPriority=\"750\" doubleValue=\"1\" horizontal=\"NO\" id=\"OzV-b0-sAV\">\n                                    <rect key=\"frame\" x=\"224\" y=\"17\" width=\"15\" height=\"102\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                </scroller>\n                                <tableHeaderView key=\"headerView\" wantsLayer=\"YES\" id=\"wdC-HO-Vvc\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"899\" height=\"25\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                </tableHeaderView>\n                            </scrollView>\n                            <progressIndicator wantsLayer=\"YES\" fixedFrame=\"YES\" maxValue=\"100\" indeterminate=\"YES\" style=\"bar\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"VMi-pK-9Z0\">\n                                <rect key=\"frame\" x=\"12\" y=\"13\" width=\"391\" height=\"20\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                            </progressIndicator>\n                            <button verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"ybQ-bI-p2r\">\n                                <rect key=\"frame\" x=\"603\" y=\"285\" width=\"33\" height=\"32\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"...\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"1GT-FK-TVG\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                            </button>\n                            <button hidden=\"YES\" verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"kaB-gl-s4B\">\n                                <rect key=\"frame\" x=\"446\" y=\"7\" width=\"96\" height=\"32\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"Cancel\" bezelStyle=\"rounded\" alignment=\"center\" enabled=\"NO\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"CHb-bh-1kG\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                    <string key=\"keyEquivalent\" base64-UTF8=\"YES\">\nGw\n</string>\n                                </buttonCell>\n                            </button>\n                            <button verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"yOo-KU-sM8\">\n                                <rect key=\"frame\" x=\"-2\" y=\"222\" width=\"22\" height=\"18\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <buttonCell key=\"cell\" type=\"check\" bezelStyle=\"regularSquare\" imagePosition=\"left\" state=\"on\" allowsMixedState=\"YES\" inset=\"2\" id=\"xAr-eh-ypW\">\n                                    <behavior key=\"behavior\" changeContents=\"YES\" doesNotDimImage=\"YES\" lightByContents=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                            </button>\n                            <button verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"ZAV-Dz-fdD\">\n                                <rect key=\"frame\" x=\"446\" y=\"7\" width=\"96\" height=\"32\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"Close\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"dkl-Nk-ye1\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                            </button>\n                            <scrollView hidden=\"YES\" fixedFrame=\"YES\" borderType=\"none\" autohidesScrollers=\"YES\" horizontalLineScroll=\"10\" horizontalPageScroll=\"10\" verticalLineScroll=\"10\" verticalPageScroll=\"10\" hasHorizontalScroller=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Mkn-sk-kD7\">\n                                <rect key=\"frame\" x=\"12\" y=\"48\" width=\"618\" height=\"206\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <clipView key=\"contentView\" drawsBackground=\"NO\" copiesOnScroll=\"NO\" id=\"jiT-mv-qJD\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"618\" height=\"206\"/>\n                                    <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                    <subviews>\n                                        <textView editable=\"NO\" importsGraphics=\"NO\" richText=\"NO\" verticallyResizable=\"YES\" textCompletion=\"NO\" id=\"09Z-23-gCi\">\n                                            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"618\" height=\"206\"/>\n                                            <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                                            <color key=\"textColor\" name=\"textColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                            <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                            <size key=\"minSize\" width=\"618\" height=\"206\"/>\n                                            <size key=\"maxSize\" width=\"618\" height=\"10000000\"/>\n                                            <color key=\"insertionPointColor\" name=\"textColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                        </textView>\n                                    </subviews>\n                                </clipView>\n                                <scroller key=\"horizontalScroller\" hidden=\"YES\" verticalHuggingPriority=\"750\" horizontal=\"YES\" id=\"F2k-Dr-bCj\">\n                                    <rect key=\"frame\" x=\"-100\" y=\"-100\" width=\"225\" height=\"15\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                </scroller>\n                                <scroller key=\"verticalScroller\" hidden=\"YES\" verticalHuggingPriority=\"750\" horizontal=\"NO\" id=\"5aF-5y-0Jo\">\n                                    <rect key=\"frame\" x=\"603\" y=\"0.0\" width=\"15\" height=\"241\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                </scroller>\n                            </scrollView>\n                            <button verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Kgc-dQ-c8D\">\n                                <rect key=\"frame\" x=\"542\" y=\"255\" width=\"96\" height=\"32\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"Show Logs\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"WmO-MK-b5O\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                            </button>\n                            <button hidden=\"YES\" verticalHuggingPriority=\"750\" fixedFrame=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"8bu-eG-BOn\">\n                                <rect key=\"frame\" x=\"312\" y=\"7\" width=\"130\" height=\"32\"/>\n                                <autoresizingMask key=\"autoresizingMask\" flexibleMaxX=\"YES\" flexibleMinY=\"YES\"/>\n                                <buttonCell key=\"cell\" type=\"push\" title=\"Backup Device\" bezelStyle=\"rounded\" alignment=\"center\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"3sn-TB-2UJ\">\n                                    <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                    <font key=\"font\" metaFont=\"system\"/>\n                                </buttonCell>\n                            </button>\n                        </subviews>\n                    </view>\n                    <connections>\n                        <outlet property=\"btnBackup\" destination=\"8gl-oS-xn6\" id=\"ykX-mi-M09\"/>\n                        <outlet property=\"btnBackupDevice\" destination=\"8bu-eG-BOn\" id=\"Cxy-vH-TgJ\"/>\n                        <outlet property=\"btnCancel\" destination=\"kaB-gl-s4B\" id=\"pBJ-1v-c4B\"/>\n                        <outlet property=\"btnExport\" destination=\"c7I-ig-uay\" id=\"L8d-sU-cTy\"/>\n                        <outlet property=\"btnOutput\" destination=\"ybQ-bI-p2r\" id=\"PLg-jN-ozw\"/>\n                        <outlet property=\"btnQuit\" destination=\"ZAV-Dz-fdD\" id=\"Kfh-6w-xj4\"/>\n                        <outlet property=\"btnShowLogs\" destination=\"Kgc-dQ-c8D\" id=\"PBX-v1-ITi\"/>\n                        <outlet property=\"btnToggleAll\" destination=\"yOo-KU-sM8\" id=\"RX5-S3-gcr\"/>\n                        <outlet property=\"lblITunes\" destination=\"Kpd-TD-eLh\" id=\"uWI-3i-BEz\"/>\n                        <outlet property=\"popupBackup\" destination=\"FYs-pu-LFe\" id=\"Z34-x5-0Me\"/>\n                        <outlet property=\"popupUsers\" destination=\"mwI-Gk-UmA\" id=\"cu6-0h-a2o\"/>\n                        <outlet property=\"progressBar\" destination=\"VMi-pK-9Z0\" id=\"O1p-nX-RQW\"/>\n                        <outlet property=\"sclSessions\" destination=\"pql-rp-pLF\" id=\"trV-0t-r84\"/>\n                        <outlet property=\"sclViewLogs\" destination=\"Mkn-sk-kD7\" id=\"42K-bF-yhL\"/>\n                        <outlet property=\"tblSessions\" destination=\"J2V-fs-LwZ\" id=\"vR2-aN-XfD\"/>\n                        <outlet property=\"txtViewLogs\" destination=\"09Z-23-gCi\" id=\"8iS-At-7Jk\"/>\n                        <outlet property=\"txtboxOutput\" destination=\"az7-4y-9PZ\" id=\"H1v-yu-igB\"/>\n                    </connections>\n                </viewController>\n                <customObject id=\"rPt-NT-nkU\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"155\" y=\"720\"/>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "WechatExporter/ExportNotifierImpl.h",
    "content": "#pragma once\n\n#include \"ExportNotifier.h\"\n#include \"ViewController.h\"\n\nclass ExportNotifierImpl : public ExportNotifier\n{\nprotected:\n    __weak ViewController *m_viewController;\n\t\n\npublic:\n\tExportNotifierImpl(ViewController* viewController)\n\t{\n        m_viewController = viewController;\n\t}\n\n\t~ExportNotifierImpl()\n\t{\n        m_viewController = nil;\n\t}\n    \n    void onStart() const\n    {\n        __block __weak ViewController* viewController = m_viewController;\n        dispatch_async(dispatch_get_main_queue(), ^{\n            __strong __typeof(viewController)strongVC = viewController;\n            if (strongVC)\n            {\n                [strongVC onStart];\n                strongVC = nil;\n            }\n        });\n    }\n    \n\tvoid onProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const\n\t{\n\t}\n\t\n\tvoid onComplete(bool cancelled) const\n\t{\n        __block __weak ViewController* viewController = m_viewController;\n        __block BOOL localCancelled = cancelled;\n        \n        dispatch_async(dispatch_get_main_queue(), ^{\n            __strong __typeof(viewController)strongVC = viewController;\n            if (strongVC)\n            {\n                [strongVC onComplete:localCancelled];\n                strongVC = nil;\n            }\n        });\n\t}\n    \n    void onUserSessionStart(const std::string& usrName, uint32_t numberOfSessions) const\n    {\n        \n    }\n    \n    void onUserSessionComplete(const std::string& usrName) const\n    {\n        \n    }\n\n    void onSessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages) const\n    {\n        __block __weak ViewController* viewController = m_viewController;\n        __block NSInteger row = (NSInteger)sessionData;\n        __block NSString *usrName = [NSString stringWithUTF8String:sessionUsrName.c_str()];\n        dispatch_async(dispatch_get_main_queue(), ^{\n            __strong __typeof(viewController)strongVC = viewController;\n            if (strongVC)\n            {\n                [strongVC onSessionStart:usrName row:row];\n                strongVC = nil;\n            }\n        });\n    }\n    \n    void onSessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const\n    {\n        __block __weak ViewController* viewController = m_viewController;\n        __block NSInteger row = (NSInteger)sessionData;\n        __block NSUInteger numberOfMsgs = (NSUInteger)numberOfMessages;\n        __block NSUInteger numberOfTotalMsgs = (NSUInteger)numberOfTotalMessages;\n        __block NSString *usrName = [NSString stringWithUTF8String:sessionUsrName.c_str()];\n        dispatch_async(dispatch_get_main_queue(), ^{\n            __strong __typeof(viewController)strongVC = viewController;\n            if (strongVC)\n            {\n                [strongVC onSessionProgress:usrName row:row numberOfMessages:numberOfMsgs numberOfTotalMessages:numberOfTotalMsgs];\n                strongVC = nil;\n            }\n        });\n    }\n    \n    void onSessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled) const\n    {\n        __block __weak ViewController* viewController = m_viewController;\n        // __block BOOL localCancelled = cancelled;\n        __block NSString *usrName = [NSString stringWithUTF8String:sessionUsrName.c_str()];\n        dispatch_async(dispatch_get_main_queue(), ^{\n            __strong __typeof(viewController)strongVC = viewController;\n            if (strongVC)\n            {\n                [strongVC onSessionComplete:usrName];\n                strongVC = nil;\n            }\n        });\n    }\n    \n    void onTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks) const\n    {\n        \n    }\n    \n    void onTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalMessages) const\n    {\n        \n    }\n    \n    void onTasksComplete(const std::string& usrName, bool cancelled) const\n    {\n        \n    }\n\t\n};\n\n"
  },
  {
    "path": "WechatExporter/HttpHelper.h",
    "content": "//\n//  HttpHelper.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/3/9.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef HttpHelper_h\n#define HttpHelper_h\n\n#import <Foundation/Foundation.h>\n\n@interface HttpHelper : NSObject\n\n+ (NSString *)standardUserAgent;\n\n@end\n\n#endif /* HttpHelper_h */\n"
  },
  {
    "path": "WechatExporter/HttpHelper.mm",
    "content": "//\n//  HttpHelper.m\n//  WechatExporter\n//\n//  Created by Matthew on 2021/3/9.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#import \"HttpHelper.h\"\n\n#if defined(__ppc__) || defined(__ppc64__)\n#define PROCESSOR \"PPC\"\n#elif defined(__i386__) || defined(__x86_64__)\n#define PROCESSOR \"Intel\"\n#else\n#error Unknown architecture\n#endif\n\n@implementation HttpHelper\n\nstatic inline int callGestalt(OSType selector)\n{\n    SInt32 value = 0;\n    Gestalt(selector, &value);\n    return value;\n}\n\n// Uses underscores instead of dots because if \"4.\" ever appears in a user agent string, old DHTML libraries treat it as Netscape 4.\n+ (NSString *)macOSXVersionString\n{\n    // Can't use -[NSProcessInfo operatingSystemVersionString] because it has too much stuff we don't want.\n    int major = callGestalt(gestaltSystemVersionMajor);\n    // ASSERT(major);\n\n    int minor = callGestalt(gestaltSystemVersionMinor);\n    int bugFix = callGestalt(gestaltSystemVersionBugFix);\n    if (bugFix)\n        return [NSString stringWithFormat:@\"%d_%d_%d\", major, minor, bugFix];\n    if (minor)\n        return [NSString stringWithFormat:@\"%d_%d\", major, minor];\n    return [NSString stringWithFormat:@\"%d\", major];\n}\n\n+ (NSString *)userVisibleWebKitVersionString\n{\n    // If the version is 4 digits long or longer, then the first digit represents\n    // the version of the OS. Our user agent string should not include this first digit,\n    // so strip it off and report the rest as the version. <rdar://problem/4997547>\n    NSString *fullVersion = [[NSBundle bundleForClass:NSClassFromString(@\"WKView\")] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];\n    NSRange nonDigitRange = [fullVersion rangeOfCharacterFromSet:[[NSCharacterSet decimalDigitCharacterSet] invertedSet]];\n    if (nonDigitRange.location == NSNotFound && [fullVersion length] >= 4)\n        return [fullVersion substringFromIndex:1];\n    if (nonDigitRange.location != NSNotFound && nonDigitRange.location >= 4)\n        return [fullVersion substringFromIndex:1];\n    return fullVersion;\n}\n\n+ (NSString *)standardUserAgent\n{\n    // https://opensource.apple.com/source/WebKit2/WebKit2-7536.26.14/UIProcess/mac/WebPageProxyMac.mm.auto.html\n    NSString *osVersion = [self macOSXVersionString];\n    NSString *webKitVersion = [self userVisibleWebKitVersionString];\n    \n    return [NSString stringWithFormat:@\"Mozilla/5.0 (Macintosh; %@ Mac OS X %@) AppleWebKit/%@ (KHTML, like Gecko) \", @PROCESSOR, osVersion, webKitVersion];\n}\n\n\n@end\n"
  },
  {
    "path": "WechatExporter/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(MARKETING_VERSION)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(CURRENT_PROJECT_VERSION)</string>\n\t<key>LSApplicationCategoryType</key>\n\t<string>public.app-category.utilities</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>Copyright © 2020 Matthew. All rights reserved.</string>\n\t<key>NSMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n\t<key>NSSupportsAutomaticTermination</key>\n\t<true/>\n\t<key>NSSupportsSuddenTermination</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "WechatExporter/LICENSES/jsoncpp.LICENSE",
    "content": "The JsonCpp library's source code, including accompanying documentation, \ntests and demonstration applications, are licensed under the following\nconditions...\n\nBaptiste Lepilleur and The JsonCpp Authors explicitly disclaim copyright in all \njurisdictions which recognize such a disclaimer. In such jurisdictions, \nthis software is released into the Public Domain.\n\nIn jurisdictions which do not recognize Public Domain property (e.g. Germany as of\n2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur and\nThe JsonCpp Authors, and is released under the terms of the MIT License (see below).\n\nIn jurisdictions which recognize Public Domain property, the user of this \nsoftware may choose to accept it either as 1) Public Domain, 2) under the \nconditions of the MIT License (see below), or 3) under the terms of dual \nPublic Domain/MIT License conditions described here, as they choose.\n\nThe MIT License is about as close to Public Domain as a license can get, and is\ndescribed in clear, concise terms at:\n\n   http://en.wikipedia.org/wiki/MIT_License\n   \nThe full text of the MIT License follows:\n\n========================================================================\nCopyright (c) 2007-2010 Baptiste Lepilleur and The JsonCpp Authors\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use, copy,\nmodify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS\nBE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\nACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n========================================================================\n(END LICENSE TEXT)\n\nThe MIT license is compatible with both the GPL and commercial\nsoftware, affording one all of the rights of Public Domain with the\nminor nuisance of being required to keep the above copyright notice\nand license text in the source code. Note also that by accepting the\nPublic Domain \"license\" you can re-license your copy using whatever\nlicense you like.\n"
  },
  {
    "path": "WechatExporter/LICENSES/lame.LICENSE",
    "content": "Can I use LAME in my commercial program?  \n\nYes, you can, under the restrictions of the LGPL (see COPYING\nin this folder). The easiest way to do this is to:\n\n1. Link to LAME as separate library (libmp3lame.a on unix or \n   lame_enc.dll or libmp3lame.dll on windows)\n\n2. Fully acknowledge that you are using LAME, and give a link\n   to our web site, www.mp3dev.org\n\n3. If you make modifications to LAME, you *must* release these\n   modifications back to the LAME project, under the LGPL.\n"
  },
  {
    "path": "WechatExporter/LICENSES/libcurl.COPYING",
    "content": "COPYRIGHT AND PERMISSION NOTICE\n\nCopyright (c) 1996 - 2020, Daniel Stenberg, <daniel@haxx.se>, and many\ncontributors, see the THANKS file.\n\nAll rights reserved.\n\nPermission to use, copy, modify, and distribute this software for any purpose\nwith or without fee is hereby granted, provided that the above copyright\nnotice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN\nNO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE\nOR OTHER DEALINGS IN THE SOFTWARE.\n\nExcept as contained in this notice, the name of a copyright holder shall not\nbe used in advertising or otherwise to promote the sale, use or other dealings\nin this Software without prior written authorization of the copyright holder.\n"
  },
  {
    "path": "WechatExporter/LICENSES/libimobiledevice-glue.COPYING",
    "content": "\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n\t\t       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n\t\t\t    Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\f\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\f\n\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n  \n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\f\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\f\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\f\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\f\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\f\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\f\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n\t\t\t    NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n\t\t     END OF TERMS AND CONDITIONS\n\f\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n"
  },
  {
    "path": "WechatExporter/LICENSES/libimobiledevice.COPYING.LESSER",
    "content": "\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n\t\t       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n\t\t\t    Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\f\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\f\n\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n  \n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\f\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\f\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\f\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\f\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\f\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\f\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n\t\t\t    NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n\t\t     END OF TERMS AND CONDITIONS\n\f\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n"
  },
  {
    "path": "WechatExporter/LICENSES/libplist.COPYING.LESSER",
    "content": "\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n\t\t       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n\t\t\t    Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\f\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\f\n\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n  \n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\f\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\f\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\f\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\f\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\f\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\f\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n\t\t\t    NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n\t\t     END OF TERMS AND CONDITIONS\n\f\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n"
  },
  {
    "path": "WechatExporter/LICENSES/libusbmuxd.COPYING",
    "content": "\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n\t\t       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n\t\t\t    Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\f\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\f\n\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n  \n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\f\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\f\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\f\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\f\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\f\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\f\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n\t\t\t    NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n\t\t     END OF TERMS AND CONDITIONS\n\f\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n"
  },
  {
    "path": "WechatExporter/LICENSES/libxml2.Copyright",
    "content": "Except where otherwise noted in the source code (e.g. the files hash.c,\nlist.c and the trio files, which are covered by a similar licence but\nwith different Copyright notices) all the files are:\n\n Copyright (C) 1998-2012 Daniel Veillard.  All Rights Reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is fur-\nnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FIT-\nNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "WechatExporter/LICENSES/opencore-amr.LICENSE",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the\ncopyright owner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other\nentities that control, are controlled by, or are under common control with\nthat entity. For the purposes of this definition, \"control\" means (i) the\npower, direct or indirect, to cause the direction or management of such\nentity, whether by contract or otherwise, or (ii) ownership of fifty\npercent (50%) or more of the outstanding shares, or (iii) beneficial\nownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications,\nincluding but not limited to software source code, documentation source,\nand configuration files.\n\n\"Object\" form shall mean any form resulting from mechanical transformation\nor translation of a Source form, including but not limited to compiled\nobject code, generated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form,\nmade available under the License, as indicated by a copyright notice that\nis included in or attached to the work (an example is provided in the\nAppendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form,\nthat is based on (or derived from) the Work and for which the editorial\nrevisions, annotations, elaborations, or other modifications represent, as\na whole, an original work of authorship. For the purposes of this License,\nDerivative Works shall not include works that remain separable from, or\nmerely link (or bind by name) to the interfaces of, the Work and Derivative\nWorks thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original\nversion of the Work and any modifications or additions to that Work or\nDerivative Works thereof, that is intentionally submitted to Licensor for\ninclusion in the Work by the copyright owner or by an individual or Legal\nEntity authorized to submit on behalf of the copyright owner. For the\npurposes of this definition, \"submitted\" means any form of electronic,\nverbal, or written communication sent to the Licensor or its\nrepresentatives, including but not limited to communication on electronic\nmailing lists, source code control systems, and issue tracking systems that\nare managed by, or on behalf of, the Licensor for the purpose of discussing\nand improving the Work, but excluding communication that is conspicuously\nmarked or otherwise designated in writing by the copyright owner as \"Not a\nContribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on\nbehalf of whom a Contribution has been received by Licensor and\nsubsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of this\nLicense, each Contributor hereby grants to You a perpetual, worldwide,\nnon-exclusive, no-charge, royalty-free, irrevocable copyright license to\nreproduce, prepare Derivative Works of, publicly display, publicly perform,\nsublicense, and distribute the Work and such Derivative Works in Source or\nObject form.\n\n3. Grant of Patent License. Subject to the terms and conditions of this\nLicense, each Contributor hereby grants to You a perpetual, worldwide,\nnon-exclusive, no-charge, royalty-free, irrevocable (except as stated in\nthis section) patent license to make, have made, use, offer to sell, sell,\nimport, and otherwise transfer the Work, where such license applies only to\nthose patent claims licensable by such Contributor that are necessarily\ninfringed by their Contribution(s) alone or by combination of their\nContribution(s) with the Work to which such Contribution(s) was submitted.\nIf You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or\ncontributory patent infringement, then any patent licenses granted to You\nunder this License for that Work shall terminate as of the date such\nlitigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the Work or\nDerivative Works thereof in any medium, with or without modifications, and\nin Source or Object form, provided that You meet the following conditions:\n\n   1. You must give any other recipients of the Work or Derivative Works a\ncopy of this License; and\n\n   2. You must cause any modified files to carry prominent notices stating\nthat You changed the files; and\n\n   3. You must retain, in the Source form of any Derivative Works that You\ndistribute, all copyright, patent, trademark, and attribution notices from\nthe Source form of the Work, excluding those notices that do not pertain to\nany part of the Derivative Works; and\n\n   4. If the Work includes a \"NOTICE\" text file as part of its\ndistribution, then any Derivative Works that You distribute must include a\nreadable copy of the attribution notices contained within such NOTICE file,\nexcluding those notices that do not pertain to any part of the Derivative\nWorks, in at least one of the following places: within a NOTICE text file\ndistributed as part of the Derivative Works; within the Source form or\ndocumentation, if provided along with the Derivative Works; or, within a\ndisplay generated by the Derivative Works, if and wherever such third-party\nnotices normally appear. The contents of the NOTICE file are for\ninformational purposes only and do not modify the License. You may add Your\nown attribution notices within Derivative Works that You distribute,\nalongside or as an addendum to the NOTICE text from the Work, provided that\nsuch additional attribution notices cannot be construed as modifying the\nLicense.\n\nYou may add Your own copyright statement to Your modifications and may\nprovide additional or different license terms and conditions for use,\nreproduction, or distribution of Your modifications, or for any such\nDerivative Works as a whole, provided Your use, reproduction, and\ndistribution of the Work otherwise complies with the conditions stated in\nthis License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise, any\nContribution intentionally submitted for inclusion in the Work by You to\nthe Licensor shall be under the terms and conditions of this License,\nwithout any additional terms or conditions. Notwithstanding the above,\nnothing herein shall supersede or modify the terms of any separate license\nagreement you may have executed with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\nnames, trademarks, service marks, or product names of the Licensor, except\nas required for reasonable and customary use in describing the origin of\nthe Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed to\nin writing, Licensor provides the Work (and each Contributor provides its\nContributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied, including, without limitation, any\nwarranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for\ndetermining the appropriateness of using or redistributing the Work and\nassume any risks associated with Your exercise of permissions under this\nLicense.\n\n8. Limitation of Liability. In no event and under no legal theory, whether\nin tort (including negligence), contract, or otherwise, unless required by\napplicable law (such as deliberate and grossly negligent acts) or agreed to\nin writing, shall any Contributor be liable to You for damages, including\nany direct, indirect, special, incidental, or consequential damages of any\ncharacter arising as a result of this License or out of the use or\ninability to use the Work (including but not limited to damages for loss of\ngoodwill, work stoppage, computer failure or malfunction, or any and all\nother commercial damages or losses), even if such Contributor has been\nadvised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing the\nWork or Derivative Works thereof, You may choose to offer, and charge a fee\nfor, acceptance of support, warranty, indemnity, or other liability\nobligations and/or rights consistent with this License. However, in\naccepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if\nYou agree to indemnify, defend, and hold each Contributor harmless for any\nliability incurred by, or claims asserted against, such Contributor by\nreason of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included\non the same \"printed page\" as the copyright notice for easier\nidentification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\"); you may\n not use this file except in compliance with the License. You may obtain a\n copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable\n law or agreed to in writing, software distributed under the License is\n distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n KIND, either express or implied. See the License for the specific language\n governing permissions and limitations under the License.\n"
  },
  {
    "path": "WechatExporter/LICENSES/openssl.LICENSE",
    "content": "\n  LICENSE ISSUES\n  ==============\n\n  The OpenSSL toolkit stays under a double license, i.e. both the conditions of\n  the OpenSSL License and the original SSLeay license apply to the toolkit.\n  See below for the actual license texts.\n\n  OpenSSL License\n  ---------------\n\n/* ====================================================================\n * Copyright (c) 1998-2019 The OpenSSL Project.  All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n *\n * 1. Redistributions of source code must retain the above copyright\n *    notice, this list of conditions and the following disclaimer.\n *\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in\n *    the documentation and/or other materials provided with the\n *    distribution.\n *\n * 3. All advertising materials mentioning features or use of this\n *    software must display the following acknowledgment:\n *    \"This product includes software developed by the OpenSSL Project\n *    for use in the OpenSSL Toolkit. (http://www.openssl.org/)\"\n *\n * 4. The names \"OpenSSL Toolkit\" and \"OpenSSL Project\" must not be used to\n *    endorse or promote products derived from this software without\n *    prior written permission. For written permission, please contact\n *    openssl-core@openssl.org.\n *\n * 5. Products derived from this software may not be called \"OpenSSL\"\n *    nor may \"OpenSSL\" appear in their names without prior written\n *    permission of the OpenSSL Project.\n *\n * 6. Redistributions of any form whatsoever must retain the following\n *    acknowledgment:\n *    \"This product includes software developed by the OpenSSL Project\n *    for use in the OpenSSL Toolkit (http://www.openssl.org/)\"\n *\n * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY\n * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE OpenSSL PROJECT OR\n * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED\n * OF THE POSSIBILITY OF SUCH DAMAGE.\n * ====================================================================\n *\n * This product includes cryptographic software written by Eric Young\n * (eay@cryptsoft.com).  This product includes software written by Tim\n * Hudson (tjh@cryptsoft.com).\n *\n */\n\n Original SSLeay License\n -----------------------\n\n/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)\n * All rights reserved.\n *\n * This package is an SSL implementation written\n * by Eric Young (eay@cryptsoft.com).\n * The implementation was written so as to conform with Netscapes SSL.\n *\n * This library is free for commercial and non-commercial use as long as\n * the following conditions are aheared to.  The following conditions\n * apply to all code found in this distribution, be it the RC4, RSA,\n * lhash, DES, etc., code; not just the SSL code.  The SSL documentation\n * included with this distribution is covered by the same copyright terms\n * except that the holder is Tim Hudson (tjh@cryptsoft.com).\n *\n * Copyright remains Eric Young's, and as such any Copyright notices in\n * the code are not to be removed.\n * If this package is used in a product, Eric Young should be given attribution\n * as the author of the parts of the library used.\n * This can be in the form of a textual message at program startup or\n * in documentation (online or textual) provided with the package.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the copyright\n *    notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in the\n *    documentation and/or other materials provided with the distribution.\n * 3. All advertising materials mentioning features or use of this software\n *    must display the following acknowledgement:\n *    \"This product includes cryptographic software written by\n *     Eric Young (eay@cryptsoft.com)\"\n *    The word 'cryptographic' can be left out if the rouines from the library\n *    being used are not cryptographic related :-).\n * 4. If you include any Windows specific code (or a derivative thereof) from\n *    the apps directory (application code) you must include an acknowledgement:\n *    \"This product includes software written by Tim Hudson (tjh@cryptsoft.com)\"\n *\n * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\n * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\n * SUCH DAMAGE.\n *\n * The licence and distribution terms for any publically available version or\n * derivative of this code cannot be changed.  i.e. this code cannot simply be\n * copied and put under another distribution licence\n * [including the GNU Public Licence.]\n */\n\n"
  },
  {
    "path": "WechatExporter/LICENSES/protobuf.LICENSE",
    "content": "Copyright 2008 Google Inc.  All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n    * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nCode generated by the Protocol Buffer compiler is owned by the owner\nof the input file used when generating it.  This code is not\nstandalone and requires a support library to be linked with it.  This\nsupport library is itself covered by the above license.\n"
  },
  {
    "path": "WechatExporter/LoggerImpl.h",
    "content": "//\n//  LoggerImpl.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/10/1.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include \"core/Logger.h\"\n#include <ctime>\n#include \"ViewController.h\"\n\n#ifndef LoggerImpl_h\n#define LoggerImpl_h\n\nclass LoggerImpl : public Logger\n{\nprotected:\n    __weak ViewController *m_viewController;\n    NSLock *m_lock;\n    char m_logFile[1024];\n    \npublic:\n    LoggerImpl(ViewController* viewController)\n    {\n        m_viewController = viewController;\n        m_lock = [[NSLock alloc] init];\n    }\n    \n    ~LoggerImpl()\n    {\n        m_viewController = nil;\n        m_lock = nil;\n    }\n    \n    void setLogPath(const char* logPath)\n    {\n#if !defined(NDEBUG) || defined(DBG_PERF)\n        [m_lock lock];\n        strcpy(m_logFile, logPath);\n        if (!endsWith(logPath, DIR_SEP_STR))\n        {\n            strcat(m_logFile, DIR_SEP_STR);\n        }\n        strcat(m_logFile, \"log.txt\");\n        FILE *file = fopen(m_logFile, \"w\");\n        if (NULL != file)\n        {\n            fclose(file);\n        }\n        [m_lock unlock];\n#endif\n    }\n    \n    void write(const std::string& log)\n    {\n#if !defined(NDEBUG) || defined(DBG_PERF)\n        std::string timeString = getTimestampString(false, true) + \": \";\n#else\n        std::string timeString = getTimestampString() + \": \";\n#endif\n        \n#if !defined(NDEBUG) || defined(DBG_PERF)\n        [m_lock lock];\n        if (strlen(m_logFile) > 0)\n        {\n            FILE *file = fopen(m_logFile, \"a\");\n            if (NULL != file)\n            {\n                fputs(timeString.c_str(), file);\n                fputs(log.c_str(), file);\n                fputs(\"\\r\", file);\n                fclose(file);\n            }\n        }\n        [m_lock unlock];\n#endif\n\n        __block NSString *logString = [NSString stringWithUTF8String:(timeString + log).c_str()];\n        __block __weak ViewController* viewController = m_viewController;\n        dispatch_async(dispatch_get_main_queue(), ^{\n            __strong __typeof(viewController)strongVC = viewController;\n            if (strongVC)\n            {\n                [strongVC writeLog:logString];\n                strongVC = nil;\n            }\n        });\n    }\n    \n    void debug(const std::string& log)\n    {\n        write(log);\n#if !defined(NDEBUG) || defined(DBG_PERF)\n        NSString *logString = [NSString stringWithUTF8String:log.c_str()];\n        NSLog(@\"%@\", logString);\n#endif\n    }\n    \n};\n\n#endif /* LoggerImpl_h */\n"
  },
  {
    "path": "WechatExporter/PdfConverterImpl.h",
    "content": "//\n//  PdfConverterImpl.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/4/22.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#include \"PdfConverter.h\"\n#import <Cocoa/Cocoa.h>\n#include \"FileSystem.h\"\n#include \"Utils.h\"\n\n#ifndef PdfConverterImpl_h\n#define PdfConverterImpl_h\n\nclass PdfConverterImpl : public PdfConverter\n{\npublic:\n    PdfConverterImpl(const char *outputDir) : m_pdfSupported(false)\n    {\n        if (detectChromeInstalled())\n        {\n            m_pdfSupported = true;\n            m_param = @[@\"--headless\", @\"--disable-extensions\", @\"--disable-gpu\", @\"--print-to-pdf-no-header\"];\n            // m_param = \"--headless --disable-extensions --disable-gpu --print-to-pdf=\\\"%%DEST%%\\\" --print-to-pdf-no-header \\\"file://%%SRC%%\\\"\";\n        }\n        else if (detectEdgeInstalled())\n        {\n            m_pdfSupported = true;\n            // m_param = @[@\"--headless\", @\"--disable-extensions\", @\"--disable-gpu\", @\"--print-to-pdf-no-header\"];\n            m_param = @[@\"--headless\", @\"--print-to-pdf-no-header\"];\n        }\n        \n        if (NULL != outputDir)\n        {\n            initShellFile(outputDir);\n        }\n    }\n    \n    bool isPdfSupported() const\n    {\n        return m_pdfSupported;\n    }\n\n    ~PdfConverterImpl()\n    {\n    }\n    \n    void executeCommand()\n    {\n        NSString *shellPathString = [NSString stringWithUTF8String:m_shellPath.c_str()];\n        if (![[NSFileManager defaultManager] fileExistsAtPath:shellPathString])\n        {\n            return;\n        }\n\n        NSURL *terminalUrl = [[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:@\"com.apple.Terminal\"];\n        if (nil == terminalUrl)\n        {\n            return;\n        }\n        \n        [[NSWorkspace sharedWorkspace] openFile:shellPathString withApplication:terminalUrl.path];\n    }\n    \n    void setWorkDir(NSString* workDir)\n    {\n        m_workDir = [NSString stringWithString:workDir];\n    }\n    \n    bool makeUserDirectory(const std::string& dirName)\n    {\n        // std::string command = replaceAll(dirName, \" \", \"\\\\ \");\n        std::string command = NEW_LINE + \"[ -d \\\"pdf/\" + dirName + \"\\\" ] || mkdir \\\"pdf/\" + dirName + \"\\\"\" + NEW_LINE;\n        \n        appendFile(m_shellPath, reinterpret_cast<const unsigned char *>(command.c_str()), command.size());\n        \n        return true;\n    }\n    \n    bool convert(const std::string& htmlPath, const std::string& pdfPath)\n    {\n        if (!m_pdfSupported)\n        {\n            return false;\n        }\n        \n        NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:htmlPath.c_str()]];\n     \n        std::string command = \"chrome \";\n        command += \"--headless --disable-gpu --disable-extensions --print-to-pdf-no-header --print-to-pdf=\\\"\";\n        command += pdfPath;\n        command += \"\\\" \\\"\";\n        command += [[url absoluteString] UTF8String];\n        command += \"\\\" >/dev/null 2>&1\" + NEW_LINE;\n        command += \"echo \\\"\" + pdfPath.substr(m_output.size()) + \"\\\"\" + NEW_LINE;\n                \n        appendFile(m_shellPath, reinterpret_cast<const unsigned char *>(command.c_str()), command.size());\n        \n        return true;\n        \n        /*\n        std::string command = [m_assemblyPath UTF8String];\n        command += \" \";\n        \n        NSString *src = [NSString stringWithUTF8String:htmlPath.c_str()];\n        NSString *dest = [NSString stringWithUTF8String:pdfPath.c_str()];\n        NSURL *url = [NSURL fileURLWithPath:src];\n        \n        NSMutableArray *params = [NSMutableArray arrayWithArray:m_param];\n        [params addObject:[NSString stringWithFormat:@\"--print-to-pdf=\\\"%@\\\"\", dest]];\n        [params addObject:[url absoluteString]];\n        \n        NSString* args = [params componentsJoinedByString:@\" \"];\n        NSString* arg1 = [NSString stringWithFormat:@\"--print-to-pdf=\\\"%@\\\"\", dest];\n        \n        command += [args UTF8String];\n        \n        // system(command.c_str());\n        \n        execlp([m_assemblyPath UTF8String], [m_assemblyPath UTF8String], \"--headless\", \"--print-to-pdf-no-header\", [arg1 UTF8String], [[url absoluteString] UTF8String], NULL);\n        */\n        \n        /*\n        // std::string param = m_param;\n        // replaceAll(param, \"%%SRC%%\", htmlPath);\n        // replaceAll(param, \"%%DEST%%\", pdfPath);\n\n        NSTask *task = [[NSTask alloc] init];\n        task.launchPath = m_assemblyPath;\n        task.arguments = params;\n        task.currentDirectoryPath = m_workDir;\n        // task.standardOutput = pipe;\n        NSPipe *pipe = [NSPipe pipe];\n        NSFileHandle *file = pipe.fileHandleForReading;\n        \n        task.standardOutput = pipe;\n        \n        [task launch];\n\n        NSData *data = [file readDataToEndOfFile];\n        [file closeFile];\n        \n        // [[outputPipe fileHandleForReading] readToEndOfFileInBackgroundAndNotify];\n        \n        [task waitUntilExit];\n        int status = [task terminationStatus];\n        \n        return status == 0;\n         */\n    }\n\nprotected:\n    bool detectChromeInstalled()\n    {\n        NSString *appPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@\"com.google.Chrome\"];\n        if (nil != appPath)\n        {\n            appPath = [appPath stringByAppendingPathComponent:@\"Contents/MacOS/Google Chrome\"];\n            if ([[NSFileManager defaultManager] fileExistsAtPath:appPath])\n            {\n                m_assemblyPath = [NSString stringWithString:appPath];\n                return true;\n            }\n        }\n        \n        return false;\n    }\n\n    bool detectEdgeInstalled()\n    {\n        NSString *appPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@\"com.microsoft.edgemac\"];\n        if (nil != appPath)\n        {\n            appPath = [appPath stringByAppendingPathComponent:@\"Contents/MacOS/Microsoft Edge\"];\n            if ([[NSFileManager defaultManager] fileExistsAtPath:appPath])\n            {\n                m_assemblyPath = [NSString stringWithString:appPath];\n                return true;\n            }\n        }\n        \n        return false;\n    }\n    \n    void initShellFile(const char *outputDir)\n    {\n        m_output = outputDir;\n        if (!endsWith(m_output, \"/\"))\n        {\n            m_output += \"/\";\n        }\n        m_shellPath = combinePath(m_output, \"pdf.sh\");\n        deleteFile(m_shellPath);\n\n        std::string aliasCmd = [m_assemblyPath UTF8String];\n        replaceAll(aliasCmd, \" \", \"\\\\ \");\n        aliasCmd = \"#!/bin/sh\" + NEW_LINE + NEW_LINE + \"alias chrome=\\\"\" + aliasCmd + \"\\\"\" + NEW_LINE + NEW_LINE;\n        \n        std::string output = m_output;\n        // replaceAll(output, \" \", \"\\\\ \");\n        \n        aliasCmd += \"cd \\\"\" + output + \"\\\"\" + NEW_LINE;\n        aliasCmd += \"[ -d pdf ] || mkdir pdf\" + NEW_LINE;\n        \n        NSDictionary<NSFileAttributeKey, id> *attributes = @{NSFilePosixPermissions : [NSNumber numberWithShort:0777]};\n        NSData *contents = [NSData dataWithBytes:aliasCmd.c_str() length:aliasCmd.size()];\n        NSString *shellPath = [NSString stringWithUTF8String:m_shellPath.c_str()];\n        [[NSFileManager defaultManager] createFileAtPath:shellPath contents:contents attributes:attributes];\n        \n        // appendFile(m_shellPath, reinterpret_cast<const unsigned char *>(aliasCmd.c_str()), aliasCmd.size());\n    }\n\nprivate:\n    bool m_pdfSupported;\n    NSString *m_assemblyPath;\n    NSArray *m_param;\n    std::string m_output;\n    std::string m_shellPath;\n    NSString *m_workDir;\n    \n    const std::string NEW_LINE = \"\\n\";\n};\n\n\n#endif /* PdfConverterImpl_h */\n"
  },
  {
    "path": "WechatExporter/SessionDataSource.h",
    "content": "//\n//  SessionDataSource.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/2/1.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef SessionDataSource_h\n#define SessionDataSource_h\n\n#import <Cocoa/Cocoa.h>\n#include <vector>\n#include <set>\n#include <utility>\n\n#import \"WechatObjects.h\"\n\n@interface SessionItem : NSObject\n\n@property (assign) NSInteger orgIndex;\n@property (assign) NSInteger userIndex;\n@property (assign) BOOL checked;\n@property (strong) NSString *sessionUsrName;\n@property (strong) NSString *displayName;\n@property (assign) NSInteger recordCount;\n@property (strong) NSString *userDisplayName;\n@property (strong) NSString *usrName;\n@property (strong) NSString *lastMessage;\n#ifndef NDEBUG\n@property (assign) NSUInteger lastMessageTime;\n#endif\n// @property (assign) NSInteger userPointer;\n// @property (assign) NSInteger sessionPointer;\n\n// - (NSComparisonResult)orgIndexCompare:(SessionItem *)sessionItem:(BOOL)ascending;\n- (NSComparisonResult)displayNameCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending;\n- (NSComparisonResult)recordCountCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending;\n- (NSComparisonResult)userIndexCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending;\n\n\n@end\n\n@interface SessionDataSource : NSObject<NSTableViewDataSource>\n\n@property (nonatomic, assign) NSInteger rowInProgress;\n@property (nonatomic, assign) NSUInteger numberOfMsgExported;\n\n- (void)loadData:(const std::vector<std::pair<Friend, std::vector<Session>>> *)usersAndSessions withAllUsers:(BOOL)allUsers indexOfSelectedUser:(NSInteger)indexOfSelectedUser includesSubscription:(BOOL)includesSubscriptions;\n- (void)getSelectedUserAndSessions:(std::map<std::string, std::map<std::string, void *>>&)usersAndSessions;\n\n- (void)bindCellView:(NSTableCellView *)cellView atRow:(NSInteger)row andColumnId:(NSString *)identifier;\n- (NSControlStateValue)updateCheckStateAtRow:(NSInteger)row;\n- (void)checkAllSessions:(BOOL)checked;\n\n@end\n\n#endif /* SessionDataSource_h */\n"
  },
  {
    "path": "WechatExporter/SessionDataSource.mm",
    "content": "//\n//  SessionDataSource.m\n//  WechatExporter\n//\n//  Created by Matthew on 2021/2/1.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#import \"SessionDataSource.h\"\n\n@implementation SessionItem\n\n- (NSComparisonResult)orgIndexCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending\n{\n    if (self.orgIndex < sessionItem.orgIndex)\n        return NSOrderedAscending;\n    else if (self.orgIndex > sessionItem.orgIndex)\n        return NSOrderedDescending;\n    \n    return NSOrderedSame;\n}\n\n- (NSComparisonResult)displayNameCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending\n{\n    NSComparisonResult result = [self.displayName caseInsensitiveCompare:sessionItem.displayName];\n    if (result == NSOrderedSame)\n    {\n        result = [self orgIndexCompare:sessionItem ascending:ascending];\n    }\n    else\n    {\n        if (!ascending)\n        {\n            result = (result == NSOrderedAscending) ? NSOrderedDescending : NSOrderedAscending;\n        }\n    }\n    \n    return result;\n}\n\n- (NSComparisonResult)recordCountCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending\n{\n    if (self.recordCount < sessionItem.recordCount)\n        return ascending ? NSOrderedAscending : NSOrderedDescending;\n    else if (self.recordCount > sessionItem.recordCount)\n        return ascending ? NSOrderedDescending : NSOrderedAscending;\n    \n    return [self orgIndexCompare:sessionItem ascending:ascending];\n}\n\n- (NSComparisonResult)userIndexCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending\n{\n    if (self.userIndex < sessionItem.userIndex)\n        return ascending ? NSOrderedAscending : NSOrderedDescending;\n    else if (self.userIndex > sessionItem.userIndex)\n        return ascending ? NSOrderedDescending : NSOrderedAscending;\n    \n    return [self orgIndexCompare:sessionItem ascending:ascending];\n}\n\n@end\n\n@interface SessionDataSource()\n{\n    const std::vector<std::pair<Friend, std::vector<Session>>> *m_usersAndSessions;\n    NSInteger m_indexOfSelectedUser;\n    NSArray<SessionItem *> *m_sessions;\n}\n@end\n\n@implementation SessionDataSource\n@synthesize rowInProgress = m_rowInProgress;\n@synthesize numberOfMsgExported = m_numberOfMsgExported;\n\n- (instancetype)init\n{\n    if (self = [super init])\n    {\n        m_rowInProgress = -1;\n        m_numberOfMsgExported = 0;\n        m_usersAndSessions = NULL;\n        m_indexOfSelectedUser = -1;\n        m_sessions = nil;\n    }\n    \n    return self;\n}\n\n- (void)setRowInProgress:(NSInteger)rowInProgress\n{\n    if (m_rowInProgress != rowInProgress)\n    {\n        m_numberOfMsgExported = 0;\n        m_rowInProgress = rowInProgress;\n    }\n}\n\n- (void)getSelectedUserAndSessions:(std::map<std::string, std::map<std::string, void *>>&)usersAndSessions\n{\n    NSInteger row = 0;\n    for (SessionItem *sessionItem in m_sessions)\n    {\n        if (sessionItem.checked)\n        {\n            std::string usrName = [sessionItem.usrName UTF8String];\n            std::string sessionUsrName = [sessionItem.sessionUsrName UTF8String];\n            \n            std::map<std::string, std::map<std::string, void *>>::iterator it = usersAndSessions.find(usrName);\n            if (it == usersAndSessions.end())\n            {\n                it = usersAndSessions.insert(usersAndSessions.end(), std::pair<std::string, std::map<std::string, void *>>(usrName, std::map<std::string, void *>()));\n            }\n            \n            it->second.insert(std::pair<std::string, void *>(sessionUsrName, (void *)row));\n        }\n        \n        ++row;\n    }\n}\n\n- (void)loadData:(const std::vector<std::pair<Friend, std::vector<Session>>> *)usersAndSessions withAllUsers:(BOOL)allUsers indexOfSelectedUser:(NSInteger)indexOfSelectedUser includesSubscription:(BOOL)includesSubscriptions\n{\n    m_usersAndSessions = usersAndSessions;\n    m_indexOfSelectedUser = indexOfSelectedUser;\n    m_sessions = nil;\n    \n    if (!allUsers && indexOfSelectedUser == -1)\n    {\n        return;\n    }\n    \n    NSInteger orgIndex = 0;\n    NSInteger userIndex = 0;\n    \n    NSMutableArray<SessionItem *> *sessions = [NSMutableArray<SessionItem *> array];\n    for (std::vector<std::pair<Friend, std::vector<Session>>>::const_iterator it = usersAndSessions->cbegin(); it != usersAndSessions->cend(); ++it, ++userIndex)\n    {\n        if (!allUsers)\n        {\n            if (userIndex != indexOfSelectedUser)\n            {\n                continue;\n            }\n        }\n\n        for (std::vector<Session>::const_iterator it2 = it->second.cbegin(); it2 != it->second.cend(); ++it2, ++orgIndex)\n        {\n            if (!includesSubscriptions && it2->isSubscription())\n            {\n                continue;\n            }\n\n            SessionItem *sessionItem = [[SessionItem alloc] init];\n            sessionItem.orgIndex = orgIndex;\n            sessionItem.userIndex = userIndex;\n            sessionItem.checked = YES;\n            sessionItem.displayName = [NSString stringWithUTF8String:it2->getDisplayName().c_str()];\n            if (it2->isDeleted())\n            {\n                sessionItem.displayName = [sessionItem.displayName stringByAppendingString:NSLocalizedString(@\"session-deleted\", comment: \"\")];\n            }\n            sessionItem.sessionUsrName = [NSString stringWithUTF8String:it2->getUsrName().c_str()];\n            sessionItem.recordCount = it2->getRecordCount();\n            sessionItem.usrName = [NSString stringWithUTF8String:it->first.getUsrName().c_str()];\n            sessionItem.userDisplayName = [NSString stringWithUTF8String:it->first.getDisplayName().c_str()];\n#ifndef NDEBUG\n            sessionItem.lastMessageTime = it2->getLastMessageTime();\n#endif\n            \n            NSString *displayMsg = nil;\n            std::string msg = it2->getLastMessage();\n            if (it2->isTextMessage())\n            {\n                if (it2->hasLastMessageUserDisplayName())\n                {\n                    msg = it2->getLastMessageUserDisplayName() + \": \" + msg;\n                }\n                displayMsg = [NSString stringWithUTF8String:msg.c_str()];\n            }\n            else\n            {\n                displayMsg = NSLocalizedString(@\"not-text-msg\", comment: \"\");\n            }\n            // NSLocalizedString(@\"err-failed-to-parse-backup\", comment: \"\")\n            \n            sessionItem.lastMessage = displayMsg;\n            \n            [sessions addObject:sessionItem];\n        }\n    }\n    \n    m_sessions = sessions;\n}\n\n- (void)checkAllSessions:(BOOL)checked\n{\n    for (NSInteger idx = 0; idx < m_sessions.count; ++idx)\n    {\n        SessionItem *sessionItem = [m_sessions objectAtIndex:idx];\n        sessionItem.checked = checked;\n    }\n}\n\n- (NSControlStateValue)updateCheckStateAtRow:(NSInteger)row\n{\n    if (row < m_sessions.count)\n    {\n        SessionItem *sessionItem = [m_sessions objectAtIndex:row];\n        if (sessionItem != nil)\n        {\n            sessionItem.checked = !sessionItem.checked;\n        }\n        \n        BOOL checked = sessionItem.checked;\n        BOOL allSame = YES;\n        \n        for (NSInteger idx = 0; idx < m_sessions.count; ++idx)\n        {\n            SessionItem *sessionItem = [m_sessions objectAtIndex:idx];\n            if (sessionItem.checked != checked)\n            {\n                allSame = NO;\n                break;\n            }\n        }\n        \n        return allSame ? (checked ? NSControlStateValueOn : NSControlStateValueOff) : NSControlStateValueMixed;\n    }\n    \n    return NSControlStateValueMixed;\n}\n\n- (void)clearSort\n{\n    m_sessions = [m_sessions sortedArrayUsingComparator: ^(SessionItem *item1, SessionItem *item2) {\n        return [item1 orgIndexCompare:item2 ascending:YES];\n    }];\n}\n\n- (void)sortOnDisplayName:(BOOL)ascending\n{\n    __block BOOL localAsc = ascending;\n    \n    m_sessions = [m_sessions sortedArrayUsingComparator: ^(SessionItem *item1, SessionItem *item2) {\n        return [item1 displayNameCompare:item2 ascending:localAsc];\n    }];\n}\n\n- (void)sortOnRecordCount:(BOOL)ascending\n{\n    __block BOOL localAsc = ascending;\n    \n    m_sessions = [m_sessions sortedArrayUsingComparator: ^(SessionItem *item1, SessionItem *item2) {\n        return [item1 recordCountCompare:item2 ascending:localAsc];\n    }];\n}\n\n- (void)sortOnUserName:(BOOL)ascending\n{\n    __block BOOL localAsc = ascending;\n    \n    m_sessions = [m_sessions sortedArrayUsingComparator: ^(SessionItem *item1, SessionItem *item2) {\n        return [item1 userIndexCompare:item2 ascending:localAsc];\n    }];\n}\n\n- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView\n{\n    return m_sessions.count;\n}\n\n/*\n- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row\n{\n    return nil;\n}\n*/\n\n- (void)tableView:(NSTableView *)tableView sortDescriptorsDidChange:(NSArray<NSSortDescriptor *> *)oldDescriptors\n{\n    if (tableView.sortDescriptors.count > 0)\n    {\n        NSSortDescriptor *sortDescriptor = [tableView.sortDescriptors objectAtIndex:0];\n        if ([sortDescriptor.key isEqualToString:@\"columnName\"])\n        {\n            [self sortOnDisplayName:sortDescriptor.ascending];\n        }\n        else if ([sortDescriptor.key isEqualToString:@\"columnRecordCount\"])\n        {\n            [self sortOnRecordCount:sortDescriptor.ascending];\n        }\n        else if ([sortDescriptor.key isEqualToString:@\"columnUser\"])\n        {\n            [self sortOnUserName:sortDescriptor.ascending];\n        }\n    }\n    else\n    {\n        [self clearSort];\n    }\n    \n    [tableView reloadData];\n}\n\n- (void)bindCellView:(NSTableCellView *)cellView atRow:(NSInteger)row andColumnId:(NSString *)identifier\n{\n    SessionItem *sessionItem = [m_sessions objectAtIndex:row];\n    \n    if ([identifier isEqualToString:@\"columnCheck\"])\n    {\n        NSButton *btn = (NSButton *)cellView.subviews.firstObject;\n        if (btn)\n        {\n            btn.state = sessionItem.checked;\n        }\n    }\n    else if([identifier isEqualToString:@\"columnName\"])\n    {\n        cellView.textField.stringValue = sessionItem.displayName;\n    }\n    else if([identifier isEqualToString:@\"columnRecordCount\"])\n    {\n        if (row == m_rowInProgress)\n        {\n            cellView.textField.stringValue = [NSString stringWithFormat:@\"%ld / %ld\", (long)m_numberOfMsgExported, (long)sessionItem.recordCount];\n        }\n        else\n        {\n            cellView.textField.stringValue = [NSString stringWithFormat:@\"%ld\", (long)sessionItem.recordCount];\n        }\n    }\n    else if([identifier isEqualToString:@\"columnUser\"])\n    {\n        cellView.textField.stringValue = sessionItem.userDisplayName;\n    }\n    else if([identifier isEqualToString:@\"columnLastMsg\"])\n    {\n        cellView.textField.stringValue = sessionItem.lastMessage;\n    }\n}\n\n\n@end\n"
  },
  {
    "path": "WechatExporter/ViewController copy.mm",
    "content": "//\r\n//  ViewController.m\r\n//  WechatExporter\r\n//\r\n//  Created by Matthew on 2020/9/29.\r\n//  Copyright © 2020 Matthew. All rights reserved.\r\n//\r\n\r\n#import \"ViewController.h\"\r\n#include \"ITunesParser.h\"\r\n\r\n#include \"LoggerImpl.h\"\r\n#include \"ShellImpl.h\"\r\n#include \"ExportNotifierImpl.h\"\r\n#include \"RawMessage.h\"\r\n#include \"Utils.h\"\r\n#include \"Exporter.h\"\r\n\r\n#include <sqlite3.h>\r\n#include <fstream>\r\n\r\nvoid errorLogCallback(void *pArg, int iErrCode, const char *zMsg)\r\n{\r\n    NSString *log = [NSString stringWithUTF8String:zMsg];\r\n    \r\n    NSLog(@\"SQLITE3: %@\", log);\r\n}\r\n\r\n\r\n@interface ViewController() <NSComboBoxDelegate, NSTableViewDataSource, NSTableViewDelegate>\r\n{\r\n    ShellImpl* m_shell;\r\n    LoggerImpl* m_logger;\r\n    ExportNotifierImpl *m_notifier;\r\n    Exporter* m_exporter;\r\n    \r\n    std::vector<BackupManifest> m_manifests;\r\n    NSInteger m_selectedIndex;\r\n    \r\n}\r\n@end\r\n\r\n@implementation ViewController\r\n\r\n-(void)dealloc\r\n{\r\n    [self stopExporting];\r\n}\r\n\r\n- (void)stopExporting\r\n{\r\n    if (NULL != m_exporter)\r\n    {\r\n        m_exporter->cancel();\r\n        m_exporter->waitForComplition();\r\n        delete m_exporter;\r\n        m_exporter = NULL;\r\n    }\r\n    if (NULL != m_notifier)\r\n    {\r\n        delete m_notifier;\r\n        m_notifier = NULL;\r\n    }\r\n    if (NULL != m_logger)\r\n    {\r\n        delete m_logger;\r\n        m_logger = NULL;\r\n    }\r\n    if (NULL != m_shell)\r\n    {\r\n        delete m_shell;\r\n        m_shell = NULL;\r\n    }\r\n    \r\n    [self.btnBackup setAction:nil];\r\n    [self.btnOutput setAction:nil];\r\n    [self.btnExport setAction:nil];\r\n    [self.btnCancel setAction:nil];\r\n    [self.chkboxDesc setAction:nil];\r\n    [self.chkboxNoAudio setAction:nil];\r\n    // self.popupBackup.delegate = nil;\r\n}\r\n\r\n- (void)viewDidLoad {\r\n    [super viewDidLoad];\r\n    \r\n#ifndef NDEBUG\r\n    self.chkboxNoAudio.hidden = NO;\r\n#endif\r\n    sqlite3_config(SQLITE_CONFIG_LOG, errorLogCallback, NULL);\r\n\r\n    [self.view.window center];\r\n    \r\n    // self.txtboxLogs.\r\n    \r\n    m_selectedIndex = 0;\r\n    m_shell = new ShellImpl();\r\n    m_logger = new LoggerImpl(self);\r\n    m_notifier = new ExportNotifierImpl(self);\r\n    m_exporter = NULL;\r\n    \r\n    [self.btnBackup setAction:@selector(btnBackupClicked:)];\r\n    [self.btnOutput setAction:@selector(btnOutputClicked:)];\r\n    [self.btnExport setAction:@selector(btnExportClicked:)];\r\n    [self.btnCancel setAction:@selector(btnCancelClicked:)];\r\n    [self.chkboxDesc setAction:@selector(btnDescClicked:)];\r\n    [self.chkboxNoAudio setAction:@selector(btnIgnoreAudioClicked:)];\r\n    // self.popupBackup.delegate = self;\r\n    \r\n    BOOL descOrder = [[NSUserDefaults standardUserDefaults] boolForKey:@\"Desc\"];\r\n    self.chkboxDesc.state = descOrder ? NSOnState : NSOffState;\r\n    \r\n    BOOL ignoreAudio = [[NSUserDefaults standardUserDefaults] boolForKey:@\"IgnoreAudio\"];\r\n    self.chkboxNoAudio.state = ignoreAudio ? NSOnState : NSOffState;\r\n\r\n    NSFileManager *fileManager = [NSFileManager defaultManager];\r\n    \r\n    NSString *outputDir = [[NSUserDefaults standardUserDefaults] objectForKey:@\"OutputDir\"];\r\n    if (nil == outputDir || [outputDir isEqualToString:@\"\"])\r\n    {\r\n        NSMutableArray *components = [NSMutableArray array];\r\n        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);\r\n        if (nil == paths && paths.count > 0)\r\n        {\r\n            [components addObject:[paths objectAtIndex:0]];\r\n        }\r\n        else\r\n        {\r\n            [components addObject:NSHomeDirectory()];\r\n            [components addObject:@\"Documents\"];\r\n        }\r\n        [components addObject:@\"WechatHistory\"];\r\n        \r\n        outputDir = [NSString pathWithComponents:components];\r\n    }\r\n    \r\n    self.txtboxOutput.stringValue = outputDir;\r\n    \r\n    NSURL *appSupport = [fileManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];\r\n    \r\n    NSArray *components = @[[appSupport path], @\"MobileSync\", @\"Backup\"];\r\n    NSString *backupDir = [NSString pathWithComponents:components];\r\n    BOOL isDir = NO;\r\n    if ([fileManager fileExistsAtPath:backupDir isDirectory:&isDir] && isDir)\r\n    {\r\n        NSString *backupDir = [NSString pathWithComponents:components];\r\n\r\n        ManifestParser parser([backupDir UTF8String], m_shell);\r\n        std::vector<BackupManifest> manifests;\r\n        if (parser.parse(manifests))\r\n        {\r\n            NSString *previoudBackupDir = [[NSUserDefaults standardUserDefaults] objectForKey:@\"BackupDir\"];\r\n            [self updateBackups:manifests withPreviousPath:previoudBackupDir];\r\n        }\r\n    }\r\n    \r\n    self.popupBackup.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable;\r\n    self.btnBackup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;\r\n    \r\n    self.txtboxOutput.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable;\r\n    self.btnOutput.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;\r\n    \r\n    self.popupUsers.autoresizingMask = NSViewMinYMargin;\r\n    self.sclSessions.autoresizingMask = NSViewMinYMargin | NSViewHeightSizable;\r\n    \r\n    self.sclViewLogs.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;\r\n    self.progressBar.autoresizingMask = NSViewMaxYMargin;\r\n    self.btnCancel.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin;\r\n    self.btnExport.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin;\r\n};\r\n\r\n- (void)updateBackups:(const std::vector<BackupManifest>&) manifests withPreviousPath:(NSString *)previousPath\r\n{\r\n    if (manifests.empty())\r\n    {\r\n        return;\r\n    }\r\n    \r\n    size_t selectedIndex = (size_t)self.popupBackup.indexOfSelectedItem;\r\n    for (std::vector<BackupManifest>::const_iterator it = manifests.cbegin(); it != manifests.cend(); ++it)\r\n    {\r\n        std::vector<BackupManifest>::const_iterator it2 = std::find(m_manifests.cbegin(), m_manifests.cend(), *it);\r\n        if (it2 == m_manifests.cend())\r\n        {\r\n            m_manifests.push_back(*it);\r\n        }\r\n    }\r\n    \r\n    // update\r\n    [self.popupBackup removeAllItems];\r\n    for (std::vector<BackupManifest>::const_iterator it = m_manifests.cbegin(); it != m_manifests.cend(); ++it)\r\n    {\r\n        std::string itemTitle = it->toString();\r\n        NSString* item = [NSString stringWithUTF8String:itemTitle.c_str()];\r\n        [self.popupBackup addItemWithTitle:item];\r\n        \r\n        if (selectedIndex == -1)\r\n        {\r\n            if (nil != previousPath && ![previousPath isEqualToString:@\"\"])\r\n            {\r\n                NSString* itemPath = [NSString stringWithUTF8String:it->getPath().c_str()];\r\n                if ([previousPath isEqualToString:itemPath])\r\n                {\r\n                    selectedIndex = std::distance(m_manifests.cbegin(), it);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    if (selectedIndex == -1 && self.popupBackup.numberOfItems > 0)\r\n    {\r\n        selectedIndex = 0;\r\n    }\r\n    if (selectedIndex != -1 && selectedIndex < self.popupBackup.numberOfItems)\r\n    {\r\n        [self.popupBackup selectItemAtIndex:selectedIndex];\r\n    }\r\n}\r\n\r\n-(void)comboBoxSelectionDidChange:(NSNotification *)notification\r\n{\r\n#ifndef NDEBUG\r\n    if ([notification.name isEqualToString:NSComboBoxSelectionDidChangeNotification])\r\n    {\r\n        NSComboBox *cmb = (NSComboBox *)notification.object;\r\n        if (cmb == self.popupBackup)\r\n        {\r\n            if (self.popupBackup.indexOfSelectedItem != -1 && self.popupBackup.indexOfSelectedItem < m_manifests.size())\r\n            {\r\n                const BackupManifest& manifest = m_manifests[self.popupBackup.indexOfSelectedItem];\r\n                std::string backup = manifest.getPath();\r\n                NSString *backupPath = [NSString stringWithUTF8String:backup.c_str()];\r\n                [[NSUserDefaults standardUserDefaults] setObject:backupPath forKey:@\"BackupDir\"];\r\n            }\r\n        }\r\n    }\r\n#endif\r\n}\r\n\r\n- (void)btnBackupClicked:(id)sender\r\n{\r\n    NSOpenPanel *panel = [NSOpenPanel openPanel];\r\n    panel.canChooseFiles = NO;\r\n    panel.canChooseDirectories = YES;\r\n    panel.allowsMultipleSelection = NO;\r\n    panel.canCreateDirectories = NO;\r\n    panel.showsHiddenFiles = YES;\r\n    \r\n    [panel setDirectoryURL:[NSURL URLWithString:NSHomeDirectory()]]; // Set panel's default directory.\r\n    // [panel setDirectoryURL:[NSURL URLWithString:@\"/Users/matthew/Documents/reebes/Backup\"]]; // Set panel's default directory.\r\n    \r\n    [panel beginSheetModalForWindow:[self.view window] completionHandler: (^(NSInteger result) {\r\n        if (result == NSOKButton)\r\n        {\r\n            NSURL *backupUrl = panel.directoryURL;\r\n            \r\n            ManifestParser parser([backupUrl.path UTF8String], self->m_shell);\r\n            std::vector<BackupManifest> manifests;\r\n            if (parser.parse(manifests) && !manifests.empty())\r\n            {\r\n                [self updateBackups:manifests withPreviousPath:nil];\r\n            }\r\n            else\r\n            {\r\n                [self msgBox:@\"解析iTunes Backup文件失败。\"];\r\n            }\r\n        }\r\n    })];\r\n}\r\n\r\n- (void)btnOutputClicked:(id)sender\r\n{\r\n    NSOpenPanel *panel = [NSOpenPanel openPanel];\r\n    panel.canChooseFiles = NO;\r\n    panel.canChooseDirectories = YES;\r\n    panel.allowsMultipleSelection = NO;\r\n    panel.canCreateDirectories = YES;\r\n    panel.showsHiddenFiles = NO;\r\n    \r\n    NSString *outputPath = self.txtboxOutput.stringValue;\r\n    if (nil == outputPath || [outputPath isEqualToString:@\"\"])\r\n    {\r\n        [panel setDirectoryURL:[NSURL URLWithString:NSHomeDirectory()]]; // Set panel's default directory.\r\n    }\r\n    else\r\n    {\r\n        [panel setDirectoryURL:[NSURL fileURLWithPath:outputPath]];\r\n    }\r\n    \r\n    [panel beginSheetModalForWindow:[self.view window] completionHandler: (^(NSInteger result){\r\n        if (result == NSOKButton)\r\n        {\r\n            NSURL *url = panel.directoryURL;\r\n            [[NSUserDefaults standardUserDefaults] setObject:url.path forKey:@\"OutputDir\"];\r\n            \r\n            self.txtboxOutput.stringValue = url.path;\r\n        }\r\n    })];\r\n}\r\n\r\n- (void)btnExportClicked:(id)sender\r\n{\r\n    if (NULL != m_exporter)\r\n    {\r\n        [self msgBox:@\"导出已经在执行。\"];\r\n        return;\r\n    }\r\n    \r\n    if (self.popupBackup.indexOfSelectedItem == -1 || self.popupBackup.indexOfSelectedItem >= m_manifests.size())\r\n    {\r\n        [self msgBox:@\"请选择iTunes备份目录。\"];\r\n        return;\r\n    }\r\n    \r\n    const BackupManifest& manifest = m_manifests[self.popupBackup.indexOfSelectedItem];\r\n    if (manifest.isEncrypted())\r\n    {\r\n        [self msgBox:@\"不支持加密的iTunes Backup。请使用不加密形式备份iPhone/iPad设备。\"];\r\n        return;\r\n    }\r\n    \r\n    std::string backup = manifest.getPath();\r\n    NSString *backupPath = [NSString stringWithUTF8String:backup.c_str()];\r\n    BOOL isDir = NO;\r\n    if (![[NSFileManager defaultManager] fileExistsAtPath:backupPath isDirectory:&isDir] || !isDir)\r\n    {\r\n        [self msgBox:@\"iTunes备份目录不存在。\"];\r\n        return;\r\n    }\r\n    \r\n    NSString *outputPath = self.txtboxOutput.stringValue;\r\n    if (nil == outputPath || [outputPath isEqualToString:@\"\"])\r\n    {\r\n        [self msgBox:@\"请选择输出目录。\"];\r\n        return;\r\n    }\r\n    \r\n    if (![[NSFileManager defaultManager] fileExistsAtPath:outputPath isDirectory:&isDir] || !isDir)\r\n    {\r\n        [self msgBox:@\"输出目录不存在。\"];\r\n        // self.txtboxOutput focus\r\n        return;\r\n    }\r\n\r\n    BOOL descOrder = (self.chkboxDesc.state == NSOnState);\r\n    BOOL ignoreAudio = (self.chkboxNoAudio.state == NSOnState);\r\n    BOOL saveFilesInSessionFolder = (self.chkboxSaveFilesInSessionFolder.state == NSOnState);\r\n    \r\n    m_logger->write(\"iTunes Backup:\" + manifest.getPath());\r\n    m_logger->write(\"iTunes Version:\" + manifest.getITunesVersion());\r\n    \r\n    self.txtViewLogs.string = @\"\";\r\n    [self onStart];\r\n    NSDictionary *dict = @{@\"backup\": backupPath, @\"output\": outputPath, @\"descOrder\": @(descOrder), @\"ignoreAudio\": @(ignoreAudio), @\"saveFilesInSessionFolder\": @(saveFilesInSessionFolder)};\r\n    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:dict];\r\n}\r\n\r\n- (void)btnCancelClicked:(id)sender\r\n{\r\n    if (NULL == m_exporter)\r\n    {\r\n        // [self msgBox:@\"当前未执行导出。\"];\r\n        return;\r\n    }\r\n    \r\n    m_exporter->cancel();\r\n    [self.btnCancel setEnabled:NO];\r\n}\r\n\r\n- (void)btnDescClicked:(id)sender\r\n{\r\n    BOOL descOrder = (self.chkboxDesc.state == NSOnState);\r\n    [[NSUserDefaults standardUserDefaults] setBool:descOrder forKey:@\"Desc\"];\r\n}\r\n\r\n- (void)btnIgnoreAudioClicked:(id)sender\r\n{\r\n    BOOL ignoreAudio = (self.chkboxNoAudio.state == NSOnState);\r\n    [[NSUserDefaults standardUserDefaults] setBool:ignoreAudio forKey:@\"IgnoreAudio\"];\r\n}\r\n\r\n- (void)run:(NSDictionary *)dict\r\n{\r\n    NSString *backup = [dict objectForKey:@\"backup\"];\r\n    NSString *output = [dict objectForKey:@\"output\"];\r\n\r\n    if (backup == nil || output == nil)\r\n    {\r\n        [self msgBox:@\"参数错误。\"];\r\n        return;\r\n    }\r\n    \r\n    NSString *iTunesVersion = [dict objectForKey:@\"iTunesVersion\"];\r\n    NSNumber *ignoreAudio = [dict objectForKey:@\"ignoreAudio\"];\r\n    NSNumber *descOrder = [dict objectForKey:@\"descOrder\"];\r\n    NSNumber *saveFilesInSessionFolder = [dict objectForKey:@\"saveFilesInSessionFolder\"];\r\n    \r\n    NSString *workDir = [[NSFileManager defaultManager] currentDirectoryPath];\r\n    \r\n    workDir = [[NSBundle mainBundle] resourcePath];\r\n    \r\n    m_exporter = new Exporter([workDir UTF8String], [backup UTF8String], [output UTF8String], m_shell, m_logger);\r\n    if (nil != ignoreAudio && [ignoreAudio boolValue])\r\n    {\r\n        m_exporter->ignoreAudio();\r\n    }\r\n    if (nil != descOrder && [descOrder boolValue])\r\n    {\r\n        m_exporter->setOrder(false);\r\n    }\r\n    if (nil != saveFilesInSessionFolder && [saveFilesInSessionFolder boolValue])\r\n    {\r\n        m_exporter->saveFilesInSessionFolder();\r\n    }\r\n\r\n    m_exporter->setNotifier(m_notifier);\r\n    \r\n    m_exporter->run();\r\n}\r\n\r\n- (void)msgBox:(NSString *)msg\r\n{\r\n    __block NSString *localMsg = [NSString stringWithString:msg];\r\n    __block NSString *title = [NSRunningApplication currentApplication].localizedName;\r\n    dispatch_async(dispatch_get_main_queue(), ^{\r\n        NSAlert *alert = [[NSAlert alloc] init];\r\n        alert.messageText = localMsg;\r\n        alert.window.title = title;\r\n        [alert runModal];\r\n    });\r\n}\r\n\r\n- (void)onStart\r\n{\r\n    self.view.window.styleMask &= ~NSClosableWindowMask;\r\n    [self.popupBackup setEnabled:NO];\r\n    [self.btnOutput setEnabled:NO];\r\n    [self.btnBackup setEnabled:NO];\r\n    [self.btnExport setEnabled:NO];\r\n    [self.btnCancel setEnabled:YES];\r\n    [self.chkboxDesc setEnabled:NO];\r\n    [self.chkboxNoAudio setEnabled:NO];\r\n    [self.chkboxSaveFilesInSessionFolder setEnabled:NO];\r\n    [self.progressBar startAnimation:nil];\r\n}\r\n\r\n- (void)onComplete:(BOOL)cancelled\r\n{\r\n    self.view.window.styleMask |= NSClosableWindowMask;\r\n    [self.btnExport setEnabled:YES];\r\n    [self.btnCancel setEnabled:NO];\r\n    [self.popupBackup setEnabled:YES];\r\n    [self.btnOutput setEnabled:YES];\r\n    [self.btnBackup setEnabled:YES];\r\n    [self.chkboxDesc setEnabled:YES];\r\n    [self.chkboxNoAudio setEnabled:YES];\r\n    [self.chkboxSaveFilesInSessionFolder setEnabled:YES];\r\n    [self.progressBar stopAnimation:nil];\r\n    \r\n    if (m_exporter)\r\n    {\r\n        m_exporter->waitForComplition();\r\n        delete m_exporter;\r\n        m_exporter = NULL;\r\n    }\r\n}\r\n\r\n- (void)writeLog:(NSString *)log\r\n{\r\n    NSString *newLog = nil;\r\n   \r\n    if (nil == self.txtViewLogs.string || self.txtViewLogs.string.length == 0)\r\n    {\r\n        self.txtViewLogs.string = [log copy];\r\n    }\r\n    else\r\n    {\r\n        newLog = [NSString stringWithFormat:@\"%@\\n%@\", self.txtViewLogs.string, log];\r\n        self.txtViewLogs.string = newLog;\r\n    }\r\n\r\n    NSPoint newScrollOrigin;\r\n    // assume that the scrollview is an existing variable\r\n    if ([[self.sclViewLogs documentView] isFlipped])\r\n    {\r\n        newScrollOrigin = NSMakePoint(0.0, NSMaxY([[self.sclViewLogs documentView] frame])\r\n                                       -NSHeight([[self.sclViewLogs contentView] bounds]));\r\n    }\r\n    else\r\n    {\r\n        newScrollOrigin = NSMakePoint(0.0,0.0);\r\n    }\r\n\r\n    [[self.sclViewLogs documentView] scrollPoint:newScrollOrigin];\r\n}\r\n\r\n\r\n- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView\r\n{\r\n    return 0;\r\n}\r\n\r\n@end\r\n"
  },
  {
    "path": "WechatExporter/ViewController.h",
    "content": "//\n//  ViewController.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/29.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#import <Cocoa/Cocoa.h>\n\n@interface ViewController : NSViewController\n\n@property (weak) IBOutlet NSTextField *lblITunes;\n@property (weak) IBOutlet NSButton *btnExport;\n@property (weak) IBOutlet NSButton *btnCancel;\n@property (weak) IBOutlet NSButton *btnQuit;\n\n@property (weak) IBOutlet NSTextField *txtboxOutput;\n@property (weak) IBOutlet NSPopUpButton *popupBackup;\n@property (weak) IBOutlet NSPopUpButton *popupUsers;\n@property (weak) IBOutlet NSButton *btnToggleAll;\n@property (weak) IBOutlet NSScrollView *sclSessions;\n@property (weak) IBOutlet NSTableView *tblSessions;\n\n@property (weak) IBOutlet NSButton *btnBackup;\n@property (weak) IBOutlet NSButton *btnOutput;\n@property (weak) IBOutlet NSButton *btnShowLogs;\n@property (weak) IBOutlet NSButton *btnBackupDevice;\n\n@property (weak) IBOutlet NSProgressIndicator *progressBar;\n@property (weak) IBOutlet NSScrollView *sclViewLogs;\n@property (unsafe_unretained) IBOutlet NSTextView *txtViewLogs;\n\n- (void)writeLog:(NSString *)log;\n- (void)onStart;\n- (void)onComplete:(BOOL)cancelled;\n\n- (void)onSessionStart:(NSString *)usrName row:(NSInteger)row;\n- (void)onSessionProgress:(NSString *)sessionUsrName row:(NSInteger)row numberOfMessages:(NSUInteger)numberOfMessages numberOfTotalMessages:(NSUInteger)numberOfTotalMessages;\n- (void)onSessionComplete:(NSString *)usrName;\n\n@end\n\n"
  },
  {
    "path": "WechatExporter/ViewController.mm",
    "content": "//\r\n//  ViewController.mm\r\n//  WechatExporter\r\n//\r\n//  Created by Matthew on 2020/9/29.\r\n//  Copyright © 2020 Matthew. All rights reserved.\r\n//\r\n\r\n#import \"ViewController.h\"\r\n#import \"SessionDataSource.h\"\r\n#import \"HttpHelper.h\"\r\n#import \"AppConfiguration.h\"\r\n\r\n#include \"LoggerImpl.h\"\r\n#include \"ExportNotifierImpl.h\"\r\n#include \"PdfConverterImpl.h\"\r\n#include \"Utils.h\"\r\n#include \"Exporter.h\"\r\n#include \"IDeviceBackup.h\"\r\n#include \"WechatSource.h\"\r\n#include \"Updater.h\"\r\n\r\n@interface ViewController() <NSTableViewDelegate>\r\n{\r\n    LoggerImpl* m_logger;\r\n    ExportNotifierImpl *m_notifier;\r\n    PdfConverterImpl *m_pdfConverter;\r\n    \r\n    Exporter* m_exporter;\r\n    \r\n    // std::vector<BackupManifest> m_manifests;\r\n    std::vector<BackupItem> m_manifests;\r\n    std::vector<std::pair<Friend, std::vector<Session>>> m_usersAndSessions;\r\n    \r\n    SessionDataSource   *m_dataSource;\r\n    \r\n    NSIndexSet *m_columns;\r\n    NSColor *m_orgTextColor;\r\n}\r\n@end\r\n\r\n@implementation ViewController\r\n\r\n- (instancetype)init\r\n{\r\n    if (self = [super init])\r\n    {\r\n        m_dataSource = [[SessionDataSource alloc] init];\r\n        m_orgTextColor = [NSColor clearColor];\r\n    }\r\n    \r\n    return self;\r\n}\r\n\r\n- (instancetype)initWithCoder:(NSCoder *)coder\r\n{\r\n    if (self = [super initWithCoder:coder])\r\n    {\r\n        m_dataSource = [[SessionDataSource alloc] init];\r\n        m_orgTextColor = [NSColor clearColor];\r\n    }\r\n    \r\n    return self;\r\n}\r\n\r\n- (instancetype)initWithNibName:(NSNibName)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil\r\n{\r\n    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])\r\n    {\r\n        m_dataSource = [[SessionDataSource alloc] init];\r\n        m_orgTextColor = [NSColor clearColor];\r\n    }\r\n    \r\n    return self;\r\n}\r\n\r\n-(void)dealloc\r\n{\r\n    [self stopExporting];\r\n}\r\n\r\n- (void)stopExporting\r\n{\r\n    if (NULL != m_exporter)\r\n    {\r\n        m_exporter->cancel();\r\n        m_exporter->waitForComplition();\r\n        delete m_exporter;\r\n        m_exporter = NULL;\r\n    }\r\n    if (NULL != m_notifier)\r\n    {\r\n        delete m_notifier;\r\n        m_notifier = NULL;\r\n    }\r\n    if (NULL != m_logger)\r\n    {\r\n        delete m_logger;\r\n        m_logger = NULL;\r\n    }\r\n    if (NULL != m_pdfConverter)\r\n    {\r\n        delete m_pdfConverter;\r\n        m_pdfConverter = NULL;\r\n    }\r\n    \r\n#ifndef NDEBUG\r\n    [self.tblSessions setTarget:nil];\r\n    [self.tblSessions setDoubleAction:nil];\r\n#endif\r\n    [self.btnBackup setAction:nil];\r\n    [self.btnBackup setTarget:nil];\r\n    [self.btnOutput setAction:nil];\r\n    [self.btnOutput setTarget:nil];\r\n    [self.btnExport setAction:nil];\r\n    [self.btnExport setTarget:nil];\r\n    [self.btnCancel setAction:nil];\r\n    [self.btnCancel setTarget:nil];\r\n    [self.btnQuit setAction:nil];\r\n    [self.btnQuit setTarget:nil];\r\n    [self.popupBackup setAction:nil];\r\n    [self.popupBackup setTarget:nil];\r\n    [self.popupUsers setAction:nil];\r\n    [self.popupUsers setTarget:nil];\r\n    [self.btnShowLogs setTarget:nil];\r\n    [self.btnShowLogs setAction:nil];\r\n    [self.btnToggleAll setAction:nil];\r\n    [self.btnToggleAll setTarget:nil];\r\n    \r\n#ifndef NDEBUG\r\n    [self.btnBackupDevice setAction:nil];\r\n    [self.btnBackupDevice setTarget:nil];\r\n#endif\r\n}\r\n\r\n- (void)viewDidLoad\r\n{\r\n    [super viewDidLoad];\r\n    \r\n    m_columns = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.tblSessions.numberOfColumns)];\r\n    \r\n    self.popupBackup.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable;\r\n    self.btnBackup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;\r\n    \r\n    self.txtboxOutput.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable;\r\n    self.btnOutput.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;\r\n    \r\n    self.popupUsers.autoresizingMask = NSViewMinYMargin;\r\n    self.tblSessions.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;\r\n    self.sclSessions.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;\r\n    \r\n    self.btnShowLogs.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;\r\n    self.sclViewLogs.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;\r\n    \r\n    self.progressBar.autoresizingMask = NSViewMaxYMargin;\r\n    self.btnCancel.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin;\r\n    self.btnQuit.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin;\r\n    self.btnExport.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin;\r\n    \r\n#ifndef NDEBUG\r\n    self.btnBackupDevice.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin;\r\n    self.btnBackupDevice.hidden = NO;\r\n#endif\r\n\r\n    m_logger = new LoggerImpl(self);\r\n    m_notifier = new ExportNotifierImpl(self);\r\n    m_exporter = NULL;\r\n\r\n    [self.btnBackup setTarget:self];\r\n    [self.btnBackup setAction:@selector(btnBackupClicked:)];\r\n    [self.btnOutput setTarget:self];\r\n    [self.btnOutput setAction:@selector(btnOutputClicked:)];\r\n    [self.btnExport setTarget:self];\r\n    [self.btnExport setAction:@selector(btnExportClicked:)];\r\n    [self.btnCancel setTarget:self];\r\n    [self.btnCancel setAction:@selector(btnCancelClicked:)];\r\n    [self.btnQuit setTarget:self];\r\n    [self.btnQuit setAction:@selector(btnQuitClicked:)];\r\n    [self.btnShowLogs setTarget:self];\r\n    [self.btnShowLogs setAction:@selector(btnShowLogsClicked:)];\r\n    [self.popupBackup setTarget:self];\r\n    [self.popupBackup setAction:@selector(handlePopupButton:)];\r\n    [self.popupUsers setTarget:self];\r\n    [self.popupUsers setAction:@selector(handlePopupButton:)];\r\n    [self.btnToggleAll setTarget:self];\r\n    [self.btnToggleAll setAction:@selector(toggleAllSessions:)];\r\n#ifndef NDEBUG\r\n    [self.tblSessions setTarget:self];\r\n    [self.tblSessions setDoubleAction:@selector(tableViewDoubleClick:)];\r\n#endif\r\n    \r\n#ifndef NDEBUG\r\n    [self.btnBackupDevice setTarget:self];\r\n    [self.btnBackupDevice setAction:@selector(btnBackupDeviceClicked:)];\r\n#endif\r\n\r\n    NSRect frame = [self.tblSessions.headerView headerRectOfColumn:0];\r\n    NSRect btnFrame = self.btnToggleAll.frame;\r\n    btnFrame.size.width = btnFrame.size.height;\r\n    btnFrame.origin.x = (frame.size.width - btnFrame.size.width) / 2;\r\n    btnFrame.origin.y = (frame.size.height - btnFrame.size.height) / 2;\r\n    \r\n    self.btnToggleAll.frame = btnFrame;\r\n    \r\n    [self.tblSessions.headerView addSubview:self.btnToggleAll];\r\n    for (NSTableColumn *tableColumn in self.tblSessions.tableColumns)\r\n    {\r\n        NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:tableColumn.identifier ascending:YES selector:@selector(compare:)];\r\n        [tableColumn setSortDescriptorPrototype:sortDescriptor];\r\n    }\r\n    \r\n    self.tblSessions.dataSource = m_dataSource;\r\n    self.tblSessions.delegate = self;\r\n\r\n    self.txtboxOutput.stringValue = [AppConfiguration getLastOrDefaultOutputDir];\r\n\r\n#ifdef NDEBUG\r\n    NSString *previoudBackupDir = [AppConfiguration getLastBackupDir];\r\n    previoudBackupDir = nil;\r\n    if (nil != previoudBackupDir)\r\n    {\r\n#ifndef USING_DECODED_ITUNESBACKUP\r\n        std::unique_ptr<ManifestParser> parser(new ManifestParser([previoudBackupDir UTF8String], false));\r\n#else\r\n        std::unique_ptr<ManifestParser> parser(new DecodedManifestParser([previoudBackupDir UTF8String], false));\r\n#endif\r\n        std::vector<BackupItem> manifests;\r\n        if (parser->parse(manifests))\r\n        {\r\n            [self updateBackups:manifests withPreviousPath:previoudBackupDir];\r\n        }\r\n        else\r\n        {\r\n            m_logger->debug(parser->getLastError());\r\n        }\r\n    }\r\n#else\r\n    NSString *backupDir = [AppConfiguration getDefaultBackupDir:YES];\r\n    if (nil != backupDir)\r\n    {\r\n#ifndef USING_DECODED_ITUNESBACKUP\r\n        std::unique_ptr<ManifestParser> parser(new ManifestParser([backupDir UTF8String], false));\r\n#else\r\n        std::unique_ptr<ManifestParser> parser(new DecodedManifestParser([backupDir UTF8String], false));\r\n#endif\r\n        // std::unique_ptr<ManifestParser> parser(new ManifestParser([backupDir UTF8String], false));\r\n        \r\n        std::vector<BackupItem> manifests;\r\n        if (parser->parse(manifests))\r\n        {\r\n            NSString *previoudBackupDir = [AppConfiguration getLastBackupDir];\r\n            [self updateBackups:manifests withPreviousPath:previoudBackupDir];\r\n        }\r\n        else\r\n        {\r\n            m_logger->debug(parser->getLastError());\r\n        }\r\n    }\r\n#endif\r\n    \r\n    BOOL checkUpdateDisabled = [AppConfiguration isCheckingUpdateDisabled];\r\n    NSInteger lastChkUpdateTime = [AppConfiguration getLastCheckUpdateTime];\r\n#ifndef NDEBUG\r\n    lastChkUpdateTime = 0;\r\n#endif\r\n    if (!checkUpdateDisabled && ((getUnixTimeStamp() - (uint32_t)lastChkUpdateTime) > 86400))\r\n    {\r\n#ifndef NDEBUG\r\n        [self performSelector:@selector(checkUpdate) withObject:nil afterDelay:1];\r\n#else\r\n        [self performSelector:@selector(checkUpdate) withObject:nil afterDelay:5];\r\n#endif\r\n    }\r\n    \r\n#ifndef NDEBUG\r\n    [self performSelector:@selector(showGuide) withObject:nil afterDelay:1.0];\r\n#endif\r\n    \r\n    if (@available(macOS 10.14, *))\r\n    {\r\n        if (self.popupBackup.itemArray.count == 0)\r\n        {\r\n            NSFileManager * fm = [NSFileManager defaultManager];\r\n            NSString *backupDir = [AppConfiguration getDefaultBackupDir:NO];\r\n            BOOL readable = [fm isReadableFileAtPath:backupDir];\r\n            if (!readable)\r\n            {\r\n                typeof(self) __weak weakSelf = self;\r\n                dispatch_async(dispatch_get_main_queue(), ^{\r\n                    __strong typeof(weakSelf) strongSelf = weakSelf;  // strong by default\r\n                    if (nil == strongSelf)\r\n                    {\r\n                        return;\r\n                    }\r\n\r\n                    NSAlert *alert = [[NSAlert alloc] init];\r\n                    alert.messageText = NSLocalizedString(@\"grant-full-disk-access\", nil);\r\n                    alert.window.title = [NSRunningApplication currentApplication].localizedName;\r\n                    NSButton *btnGrant = [alert addButtonWithTitle:NSLocalizedString(@\"btn-grant-full-disk-access\", nil)];\r\n                    [btnGrant setTarget:strongSelf];\r\n                    [btnGrant setAction:@selector(btnGrantClicked:)];\r\n                    [alert addButtonWithTitle:NSLocalizedString(@\"btn-ok\", nil)];\r\n                    [alert runModal];\r\n                    [btnGrant setTarget:nil];\r\n                    [btnGrant setAction:nil];\r\n                });\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n- (void)updateBackups:(const std::vector<BackupItem>&) manifests withPreviousPath:(NSString *)previousPath\r\n{\r\n    if (manifests.empty())\r\n    {\r\n        return;\r\n    }\r\n    \r\n    size_t selectedIndex = (size_t)self.popupBackup.indexOfSelectedItem;\r\n    for (std::vector<BackupItem>::const_iterator it = manifests.cbegin(); it != manifests.cend(); ++it)\r\n    {\r\n        std::vector<BackupItem>::const_iterator it2 = std::find(m_manifests.cbegin(), m_manifests.cend(), *it);\r\n        if (it2 == m_manifests.cend())\r\n        {\r\n            m_manifests.push_back(*it);\r\n        }\r\n    }\r\n    \r\n    // update\r\n    [self.popupBackup removeAllItems];\r\n    for (std::vector<BackupItem>::const_iterator it = m_manifests.cbegin(); it != m_manifests.cend(); ++it)\r\n    {\r\n        std::string itemTitle = it->toString();\r\n        NSString* item = [NSString stringWithUTF8String:itemTitle.c_str()];\r\n        [self.popupBackup addItemWithTitle:item];\r\n        \r\n        if (selectedIndex == -1)\r\n        {\r\n            if (nil != previousPath && ![previousPath isEqualToString:@\"\"])\r\n            {\r\n                NSString* itemPath = [NSString stringWithUTF8String:it->getPath().c_str()];\r\n                if ([previousPath isEqualToString:itemPath])\r\n                {\r\n                    selectedIndex = std::distance(m_manifests.cbegin(), it);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    if (selectedIndex == -1 && self.popupBackup.numberOfItems > 0)\r\n    {\r\n        selectedIndex = 0;\r\n    }\r\n    if (selectedIndex != -1 && selectedIndex < [self.popupBackup numberOfItems])\r\n    {\r\n        [self setPopupButton:self.popupBackup selectedItemAt:selectedIndex];\r\n    }\r\n}\r\n\r\n- (IBAction)handlePopupButton:(NSPopUpButton *)popupButton\r\n{\r\n    if (popupButton == self.popupBackup)\r\n    {\r\n        m_usersAndSessions.clear();\r\n        [self.popupUsers removeAllItems];\r\n        self.txtViewLogs.string = @\"\";\r\n        // Clear Users and Sessions\r\n        [m_dataSource loadData:&m_usersAndSessions withAllUsers:YES indexOfSelectedUser:-1 includesSubscription:[AppConfiguration includeSubscriptions]];\r\n        [self.tblSessions reloadData];\r\n        \r\n        if (self.popupBackup.indexOfSelectedItem == -1 || self.popupBackup.indexOfSelectedItem >= m_manifests.size())\r\n        {\r\n            return;\r\n        }\r\n        \r\n        const BackupItem& manifest = m_manifests[self.popupBackup.indexOfSelectedItem];\r\n            \r\n        if (manifest.isEncrypted())\r\n        {\r\n            [self msgBox:NSLocalizedString(@\"err-encrypted-bkp-not-supported\", comment: \"\")];\r\n            return;\r\n        }\r\n        \r\n        NSString *backupPath = [NSString stringWithUTF8String:manifest.getPath().c_str()];\r\n#ifndef NDEBUG\r\n        [AppConfiguration setLastBackupDir:backupPath];\r\n#endif\r\n        [self performSelector:@selector(loadDataForBackup:) withObject:backupPath afterDelay:0.016];\r\n    }\r\n    else if (popupButton == self.popupUsers)\r\n    {\r\n        NSInteger indexOfSelectedItem = self.popupUsers.indexOfSelectedItem;\r\n        BOOL allUsers = (indexOfSelectedItem == 0);\r\n        if (indexOfSelectedItem != -1)\r\n        {\r\n            indexOfSelectedItem--;\r\n        }\r\n        [m_dataSource loadData:&m_usersAndSessions withAllUsers:allUsers indexOfSelectedUser:indexOfSelectedItem includesSubscription:[AppConfiguration includeSubscriptions]];\r\n        self.btnToggleAll.state = NSControlStateValueOn;\r\n        [self.tblSessions reloadData];\r\n    }\r\n}\r\n\r\n- (void)loadDataForBackup:(NSString *)backupPath\r\n{\r\n#ifndef NDEBUG\r\n    m_logger->write(\"Start loading users and sessions.\");\r\n#endif\r\n\r\n    __block NSString *backupDir = [NSString stringWithString:backupPath];\r\n    __block NSString *workDir = [[NSBundle mainBundle] resourcePath];\r\n    typeof(self) __weak weakSelf = self;\r\n    \r\n    [self setUIEnabled:NO withCancellable:NO];\r\n    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{\r\n        \r\n        __strong typeof(weakSelf) strongSelf = weakSelf;  // strong by default\r\n        if (nil != strongSelf)\r\n        {\r\n            Exporter exp([workDir UTF8String], [backupDir UTF8String], \"\", strongSelf->m_logger, NULL);\r\n            ExportOption options;\r\n            options.outputDebugLogs([AppConfiguration outputDebugLogs]);\r\n            exp.setOptions(options);\r\n            exp.setLanguageCode([[self getCurrentLanguageCode] UTF8String]);\r\n            // exp.setLanguageCode([[self getCurrentLanguageCode] UTF8String]);\r\n            exp.loadUsersAndSessions();\r\n            exp.swapUsersAndSessions(strongSelf->m_usersAndSessions);\r\n        }\r\n        \r\n        // update UI on the main thread\r\n        dispatch_async(dispatch_get_main_queue(), ^{\r\n            __strong typeof(weakSelf) strongSelf = weakSelf;  // strong by default\r\n            if (strongSelf) {\r\n    #ifndef NDEBUG\r\n                strongSelf->m_logger->write(\"Data Loaded.\");\r\n    #endif\r\n                [strongSelf loadUsers];\r\n                [strongSelf setUIEnabled:YES withCancellable:NO];\r\n            }\r\n        });\r\n    });\r\n    \r\n}\r\n\r\n- (void)filterSubscriptions\r\n{\r\n    if ([AppConfiguration includeSubscriptions])\r\n    {\r\n        \r\n    }\r\n}\r\n\r\n- (void)toggleAllSessions:(id)sender\r\n{\r\n    NSButton *btn = (NSButton *)sender;\r\n    if (btn.state == NSControlStateValueMixed)\r\n    {\r\n        [self.btnToggleAll setNextState];\r\n    }\r\n        \r\n    if (btn.state == NSControlStateValueOn)\r\n    {\r\n        [m_dataSource checkAllSessions:YES];\r\n    }\r\n    else if (btn.state == NSControlStateValueOff)\r\n    {\r\n        [m_dataSource checkAllSessions:NO];\r\n    }\r\n    \r\n    [self.tblSessions reloadData];\r\n}\r\n\r\n- (void)checkButtonTapped:(id)sender\r\n{\r\n    NSButton *btn = (NSButton *)sender;\r\n    NSControlStateValue state = [m_dataSource updateCheckStateAtRow:btn.tag];\r\n    self.btnToggleAll.state = state;\r\n}\r\n\r\n- (void)tableViewDoubleClick:(id)sender\r\n{\r\n    NSTableView *tableView = (NSTableView *)sender;\r\n    if (1 != tableView.clickedColumn && 3 != tableView.clickedColumn)\r\n    {\r\n        return;\r\n    }\r\n    NSView *view = [tableView viewAtColumn:tableView.clickedColumn row:tableView.clickedRow makeIfNecessary:NO];\r\n    if (nil == view)\r\n    {\r\n        return;\r\n    }\r\n    NSTextField *textField = (NSTextField *)view.subviews.firstObject;\r\n    if (nil != textField && nil != textField.stringValue)\r\n    {\r\n        [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];\r\n        [[NSPasteboard generalPasteboard] setString:textField.stringValue forType:NSStringPboardType];\r\n    }\r\n}\r\n\r\n- (void)btnGrantClicked:(id)sender\r\n{\r\n    [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@\"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles\"]];\r\n}\r\n\r\n- (void)btnShowLogsClicked:(id)sender\r\n{\r\n    BOOL logsHidden = self.sclViewLogs.hidden;\r\n    self.sclViewLogs.hidden = !logsHidden;\r\n    self.sclSessions.hidden = logsHidden;\r\n    self.btnShowLogs.title = NSLocalizedString((logsHidden ? @\"hide-logs\" : @\"show-logs\"), comment: \"\");\r\n}\r\n\r\n#ifndef NDEBUG\r\n- (void)btnBackupDeviceClicked:(id)sender\r\n{\r\n    NSButton* btn = (NSButton*)sender;\r\n    \r\n    std::vector<DeviceInfo> devices;\r\n    IDeviceBackup::queryDevices(devices);\r\n    [btn setEnabled:NO];\r\n    if (!devices.empty())\r\n    {\r\n        NSString *outputPath = self.txtboxOutput.stringValue;\r\n        outputPath = [outputPath stringByAppendingPathComponent:@\"Backup\"];\r\n        \r\n        IDeviceBackup deviceBackup(devices.front(), [outputPath UTF8String]);\r\n        deviceBackup.backup();\r\n    }\r\n    [btn setEnabled:YES];\r\n}\r\n#endif\r\n\r\n- (void)btnBackupClicked:(id)sender\r\n{\r\n    NSOpenPanel *panel = [NSOpenPanel openPanel];\r\n    panel.canChooseFiles = NO;\r\n    panel.canChooseDirectories = YES;\r\n    panel.allowsMultipleSelection = NO;\r\n    panel.canCreateDirectories = NO;\r\n    panel.showsHiddenFiles = YES;\r\n    \r\n    NSString *backupDir = [AppConfiguration getDefaultBackupDir:NO];\r\n    [panel setDirectoryURL:[NSURL fileURLWithPath:backupDir]]; // Set panel's default directory.\r\n    \r\n    [panel beginSheetModalForWindow:[self.view window] completionHandler: (^(NSInteger result) {\r\n        if (result == NSOKButton)\r\n        {\r\n            NSURL *backupUrl = panel.URL;\r\n            \r\n#ifndef USING_DECODED_ITUNESBACKUP\r\n            std::unique_ptr<ManifestParser> parser(new ManifestParser([backupUrl.path UTF8String], false));\r\n#else\r\n            std::unique_ptr<ManifestParser> parser(new DecodedManifestParser([backupUrl.path UTF8String], false));\r\n#endif\r\n            std::vector<BackupItem> manifests;\r\n            if (parser->parse(manifests) && !manifests.empty())\r\n            {\r\n                [self updateBackups:manifests withPreviousPath:nil];\r\n            }\r\n            else\r\n            {\r\n                [self msgBox:NSLocalizedString(@\"err-failed-to-parse-backup\", comment: \"\")];\r\n            }\r\n        }\r\n    })];\r\n}\r\n\r\n- (void)btnOutputClicked:(id)sender\r\n{\r\n    NSOpenPanel *panel = [NSOpenPanel openPanel];\r\n    panel.canChooseFiles = NO;\r\n    panel.canChooseDirectories = YES;\r\n    panel.allowsMultipleSelection = NO;\r\n    panel.canCreateDirectories = YES;\r\n    panel.showsHiddenFiles = NO;\r\n    \r\n    NSString *outputPath = self.txtboxOutput.stringValue;\r\n    if (nil == outputPath || [outputPath isEqualToString:@\"\"])\r\n    {\r\n        [panel setDirectoryURL:[NSURL URLWithString:NSHomeDirectory()]]; // Set panel's default directory.\r\n    }\r\n    else\r\n    {\r\n        [panel setDirectoryURL:[NSURL fileURLWithPath:outputPath]];\r\n    }\r\n    \r\n    [panel beginSheetModalForWindow:[self.view window] completionHandler: (^(NSInteger result){\r\n        if (result == NSOKButton)\r\n        {\r\n            NSURL *url = panel.directoryURL;\r\n            [AppConfiguration setLastOutputDir:url.path];\r\n            \r\n            self.txtboxOutput.stringValue = url.path;\r\n        }\r\n    })];\r\n}\r\n\r\n- (void)btnExportClicked:(id)sender\r\n{\r\n    if (NULL != m_exporter)\r\n    {\r\n        [self msgBox:NSLocalizedString(@\"err-exp-is-running\", nil)];\r\n        return;\r\n    }\r\n    \r\n    if (self.popupBackup.indexOfSelectedItem == -1 || self.popupBackup.indexOfSelectedItem >= m_manifests.size())\r\n    {\r\n        [self msgBox:NSLocalizedString(@\"err-no-backup-dir\", nil)];\r\n        return;\r\n    }\r\n    \r\n    const BackupItem& manifest = m_manifests[self.popupBackup.indexOfSelectedItem];\r\n    if (manifest.isEncrypted())\r\n    {\r\n        [self msgBox:NSLocalizedString(@\"err-encrypted-bkp-not-supported\", nil)];\r\n        return;\r\n    }\r\n    \r\n    std::string backup = manifest.getPath();\r\n    NSString *backupPath = [NSString stringWithUTF8String:backup.c_str()];\r\n    BOOL isDir = NO;\r\n    if (![[NSFileManager defaultManager] fileExistsAtPath:backupPath isDirectory:&isDir] || !isDir)\r\n    {\r\n        [self msgBox:NSLocalizedString(@\"err-backup-dir-doesnt-exist\", nil)];\r\n        return;\r\n    }\r\n    \r\n    NSString *outputPath = self.txtboxOutput.stringValue;\r\n    if (nil == outputPath || [outputPath isEqualToString:@\"\"])\r\n    {\r\n        [self msgBox:NSLocalizedString(@\"err-no-output-dir\", comment: \"\")];\r\n        return;\r\n    }\r\n    \r\n    if (![[NSFileManager defaultManager] fileExistsAtPath:outputPath isDirectory:&isDir] || !isDir)\r\n    {\r\n        [self msgBox:NSLocalizedString(@\"err-output-dir-doesnt-exist\", comment: \"\")];\r\n        // self.txtboxOutput focus\r\n        return;\r\n    }\r\n    \r\n    if ([AppConfiguration getIncrementalExporting])\r\n    {\r\n        uint64_t options = 0;\r\n        std::string exportTime;\r\n        std::string version;\r\n        if (Exporter::hasPreviousExporting([outputPath UTF8String], options, exportTime, version))\r\n        {\r\n            if (version.empty() && (m_usersAndSessions.size() > 1))\r\n            {\r\n                [self msgBox:NSLocalizedString(@\"invld-inc-exp-for-multi-users\", comment: \"\")];\r\n                return;\r\n            }\r\n            \r\n            NSString* prevExpFound = [NSString stringWithFormat:NSLocalizedString(@\"prev-exp-found\", comment: \"\"), [NSString stringWithUTF8String:exportTime.c_str()]];\r\n#ifdef NDEBUG\r\n            [self msgBox:prevExpFound];\r\n#endif\r\n        }\r\n        else\r\n        {\r\n#ifdef NDEBUG\r\n            [self msgBox:NSLocalizedString(@\"no-prev-exp-found\", comment: \"\")];\r\n#endif\r\n            // MessageBoxTimeout(m_hWnd, text, TEXT(\"\"), MB_OK, 0, 4000);\r\n        }\r\n    }\r\n    \r\n#ifndef NDEBUG\r\n    // ITunesDb iTunesDb(backup, \"\");\r\n    // std::vector<std::string> domains;\r\n    // domains.push_back(\"AppDomain-com.tencent.xin\");\r\n    // domains.push_back(\"AppDomainGroup-group.com.tencent.xin\");\r\n    // iTunesDb.copy([outputPath UTF8String], domains);\r\n#endif\r\n\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n    m_logger->setLogPath([outputPath UTF8String]);\r\n#endif\r\n    BOOL descOrder = [AppConfiguration getDescOrder];\r\n    BOOL textMode = [AppConfiguration isTextMode];\r\n    BOOL syncLoading = ![AppConfiguration getSyncLoading];\r\n    BOOL saveFilesInSessionFolder = [AppConfiguration getSavingInSession];\r\n    \r\n    NSInteger outputFormat = [AppConfiguration getOutputFormat];\r\n    if (outputFormat == OUTPUT_FORMAT_PDF)\r\n    {\r\n        syncLoading = YES;\r\n        // loadingDataOnScroll = false;\r\n        // supportingFilter = false;\r\n    }\r\n    \r\n    self.txtViewLogs.string = @\"\";\r\n    [self onStart];\r\n    NSDictionary *dict = @{@\"backup\": backupPath, @\"output\": outputPath, @\"descOrder\": @(descOrder), @\"textMode\": @(textMode), @\"syncLoading\": @(syncLoading), @\"saveFilesInSessionFolder\": @(saveFilesInSessionFolder)};\r\n    // [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:dict];\r\n    [self run:dict];\r\n    \r\n    [self performSelector:@selector(showGuide) withObject:nil afterDelay:1.0];\r\n}\r\n\r\n- (void)btnCancelClicked:(id)sender\r\n{\r\n    if (NULL == m_exporter)\r\n    {\r\n        // [self msgBox:@\"当前未执行导出。\"];\r\n        return;\r\n    }\r\n    \r\n    m_exporter->cancel();\r\n    [self.btnCancel setEnabled:NO];\r\n}\r\n\r\n- (void)btnQuitClicked:(id)sender\r\n{\r\n    [self.view.window.windowController close];\r\n}\r\n\r\n- (void)run:(NSDictionary *)dict\r\n{\r\n    NSString *backup = [dict objectForKey:@\"backup\"];\r\n    NSString *output = [dict objectForKey:@\"output\"];\r\n\r\n    if (backup == nil || output == nil)\r\n    {\r\n        [self msgBox:NSLocalizedString(@\"err-wrong-param\", comment: \"\")];\r\n        return;\r\n    }\r\n    \r\n    ExportOption options = [AppConfiguration buildOptions];\r\n    \r\n    // NSString *iTunesVersion = [dict objectForKey:@\"iTunesVersion\"];\r\n    NSNumber *textMode = [dict objectForKey:@\"textMode\"];\r\n    NSNumber *descOrder = [dict objectForKey:@\"descOrder\"];\r\n    NSNumber *syncLoading = [dict objectForKey:@\"syncLoading\"];\r\n    NSNumber *saveFilesInSessionFolder = [dict objectForKey:@\"saveFilesInSessionFolder\"];\r\n    \r\n    NSString *workDir = [[NSFileManager defaultManager] currentDirectoryPath];\r\n    \r\n    workDir = [[NSBundle mainBundle] resourcePath];\r\n    \r\n    std::map<std::string, std::map<std::string, void *>> usersAndSessions;\r\n    [m_dataSource getSelectedUserAndSessions:usersAndSessions];\r\n\r\n    if (NULL != m_pdfConverter)\r\n    {\r\n        delete m_pdfConverter;\r\n        m_pdfConverter = NULL;\r\n    }\r\n    if ([AppConfiguration isPdfMode])\r\n    {\r\n        m_pdfConverter = new PdfConverterImpl([output UTF8String]);\r\n        m_pdfConverter->setWorkDir(output);\r\n    }\r\n\r\n    m_exporter = new Exporter([workDir UTF8String], [backup UTF8String], [output UTF8String], m_logger, m_pdfConverter);\r\n    m_exporter->setLanguageCode([[self getCurrentLanguageCode] UTF8String]);\r\n\r\n    // m_exporter->setIncrementalExporting([AppConfiguration getIncrementalExporting]);\r\n    // m_exporter->supportsFilter([AppConfiguration getSupportingFilter]);\r\n    \r\n    // m_exporter->outputDebugLogs([AppConfiguration outputDebugLogs]);\r\n    // if ([AppConfiguration includeSubscriptions])\r\n    {\r\n        // m_exporter->includesSubscription();\r\n    }\r\n\r\n    if (options.isTextMode())\r\n    {\r\n        m_exporter->setExtName(\"txt\");\r\n        m_exporter->setTemplatesName(\"templates_txt\");\r\n    }\r\n\r\n    if (options.isPdfMode())\r\n    {\r\n        options.setSyncLoading();\r\n        options.supportsFilter(false);\r\n    }\r\n    \r\n    m_exporter->setOptions(options);\r\n    m_exporter->setNotifier(m_notifier);\r\n    \r\n    m_exporter->filterUsersAndSessions(usersAndSessions);\r\n    \r\n    m_exporter->run();\r\n}\r\n\r\n- (void)loadUsers\r\n{\r\n    [self.popupUsers removeAllItems];\r\n    if (m_usersAndSessions.empty())\r\n    {\r\n        return;\r\n    }\r\n    \r\n    [self.popupUsers addItemWithTitle:NSLocalizedString(@\"txt-all-wechat-users\", comment: \"\")];\r\n    // CComboBox cbmBox = GetDlgItem(IDC_USERS);\r\n    for (std::vector<std::pair<Friend, std::vector<Session>>>::const_iterator it = m_usersAndSessions.cbegin(); it != m_usersAndSessions.cend(); ++it)\r\n    {\r\n        NSString *displayName = [NSString stringWithUTF8String:it->first.getDisplayName().c_str()];\r\n        [self.popupUsers addItemWithTitle:displayName];\r\n    }\r\n    if ([self.popupUsers numberOfItems] > 0)\r\n    {\r\n        [self setPopupButton:self.popupUsers selectedItemAt:0];\r\n    }\r\n}\r\n\r\n- (void)setPopupButton:(NSPopUpButton *)popupButton selectedItemAt:(NSInteger)index\r\n{\r\n    [popupButton.menu performActionForItemAtIndex:index];\r\n}\r\n\r\n- (void)msgBox:(NSString *)msg\r\n{\r\n    __block NSString *localMsg = [NSString stringWithString:msg];\r\n    __block NSString *title = [NSRunningApplication currentApplication].localizedName;\r\n    dispatch_async(dispatch_get_main_queue(), ^{\r\n        NSAlert *alert = [[NSAlert alloc] init];\r\n        alert.messageText = localMsg;\r\n        alert.window.title = title;\r\n        [alert runModal];\r\n    });\r\n}\r\n\r\n- (void)setUIEnabled:(BOOL)enabled withCancellable:(BOOL)cancellable\r\n{\r\n    if (enabled)\r\n    {\r\n        self.view.window.styleMask |= NSClosableWindowMask;\r\n        [self.progressBar stopAnimation:nil];\r\n        \r\n        for (NSTableColumn *tableColumn in self.tblSessions.tableColumns)\r\n        {\r\n            NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:tableColumn.identifier ascending:YES selector:@selector(compare:)];\r\n            [tableColumn setSortDescriptorPrototype:sortDescriptor];\r\n        }\r\n    }\r\n    else\r\n    {\r\n        self.view.window.styleMask &= ~NSClosableWindowMask;\r\n        [self.progressBar startAnimation:nil];\r\n        \r\n        for (NSTableColumn *tableColumn in self.tblSessions.tableColumns)\r\n        {\r\n            // NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:tableColumn.identifier ascending:YES selector:@selector(compare:)];\r\n            [tableColumn setSortDescriptorPrototype:nil];\r\n        }\r\n    }\r\n    \r\n    [self.popupBackup setEnabled:enabled];\r\n    [self.btnOutput setEnabled:enabled];\r\n    [self.btnBackup setEnabled:enabled];\r\n    [self.btnExport setEnabled:enabled];\r\n    [self.btnCancel setEnabled:!enabled && cancellable];\r\n    [self.btnCancel setHidden:enabled];\r\n    [self.btnQuit setHidden:!enabled];\r\n    // [self.chkboxDesc setEnabled:enabled];\r\n    // [self.chkboxTextMode setEnabled:enabled];\r\n    // [self.chkboxSaveFilesInSessionFolder setEnabled:enabled];\r\n    \r\n    // self.tblSessions.allowsColumnReordering = enabled;\r\n}\r\n\r\n- (void)onStart\r\n{\r\n    [self setUIEnabled:NO withCancellable:YES];\r\n}\r\n\r\n- (void)onSessionStart:(NSString *)usrName row:(NSInteger)row\r\n{\r\n    [self updateRow:row];\r\n}\r\n\r\n- (void)onSessionProgress:(NSString *)sessionUsrName row:(NSInteger)row numberOfMessages:(NSUInteger)numberOfMessages numberOfTotalMessages:(NSUInteger)numberOfTotalMessages\r\n{\r\n    m_dataSource.numberOfMsgExported = numberOfMessages;\r\n    \r\n    NSTableCellView *cellView = [self.tblSessions viewAtColumn:2 row:row makeIfNecessary:NO];\r\n    if (nil != cellView)\r\n    {\r\n        cellView.textField.stringValue = [NSString stringWithFormat:@\"%ld / %ld\", (long)numberOfMessages, (long)numberOfTotalMessages];\r\n        // tableViewCell.textField.stringValue = [NSString stringWithFormat:@\"%ld\", (long)sessionItem.recordCount];\r\n    }\r\n}\r\n\r\n- (void)onSessionComplete:(NSString *)usrName\r\n{\r\n    [self updateRow:-1];\r\n}\r\n\r\n- (void)updateRow:(NSInteger)row\r\n{\r\n    if (row == m_dataSource.rowInProgress)\r\n    {\r\n        return;\r\n    }\r\n    \r\n    NSMutableIndexSet *rows = (row != -1) ? [NSMutableIndexSet indexSetWithIndex:row] : [NSMutableIndexSet indexSet];\r\n    if (m_dataSource.rowInProgress != -1)\r\n    {\r\n        [rows addIndex:m_dataSource.rowInProgress];\r\n    }\r\n    \r\n    m_dataSource.rowInProgress = row;\r\n    \r\n    BOOL rowVisible = NO;\r\n    NSRange visibleRange = [self.tblSessions rowsInRect:self.tblSessions.visibleRect];\r\n    if (row >= visibleRange.location && row < (visibleRange.location + visibleRange.length))\r\n    {\r\n        rowVisible = YES;\r\n    }\r\n    if (!rowVisible)\r\n    {\r\n        [self.tblSessions scrollRowToVisible:row];\r\n    }\r\n    \r\n    [self.tblSessions reloadDataForRowIndexes:rows columnIndexes:m_columns];\r\n}\r\n\r\n- (void)onComplete:(BOOL)cancelled\r\n{\r\n    [self updateRow:-1];\r\n    \r\n    if (NULL != m_pdfConverter)\r\n    {\r\n        if (!cancelled)\r\n        {\r\n            m_pdfConverter->executeCommand();\r\n        }\r\n\r\n        delete m_pdfConverter;\r\n        m_pdfConverter = NULL;\r\n    }\r\n    \r\n    [self setUIEnabled:YES withCancellable:YES];\r\n    \r\n    if (m_exporter)\r\n    {\r\n        m_exporter->waitForComplition();\r\n        delete m_exporter;\r\n        m_exporter = NULL;\r\n    }\r\n    \r\n#ifdef NDEBUG\r\n    // Don't open the folder on debug mode\r\n    if (!cancelled && [AppConfiguration getOpenningFolderAfterExp])\r\n    {\r\n        NSString *outputPath = self.txtboxOutput.stringValue;\r\n        NSURL *url = [NSURL fileURLWithPath:outputPath];\r\n        [[NSWorkspace sharedWorkspace] openURL: url];\r\n    }\r\n#endif\r\n}\r\n\r\n- (void)writeLog:(NSString *)log\r\n{\r\n    NSString *newLog = nil;\r\n   \r\n    if (nil == self.txtViewLogs.string || self.txtViewLogs.string.length == 0)\r\n    {\r\n        self.txtViewLogs.string = [log copy];\r\n    }\r\n    else\r\n    {\r\n        newLog = [NSString stringWithFormat:@\"%@\\n%@\", self.txtViewLogs.string, log];\r\n        self.txtViewLogs.string = newLog;\r\n    }\r\n\r\n    NSPoint newScrollOrigin;\r\n    // assume that the scrollview is an existing variable\r\n    if ([[self.sclViewLogs documentView] isFlipped])\r\n    {\r\n        newScrollOrigin = NSMakePoint(0.0, NSMaxY([[self.sclViewLogs documentView] frame])\r\n                                       -NSHeight([[self.sclViewLogs contentView] bounds]));\r\n    }\r\n    else\r\n    {\r\n        newScrollOrigin = NSMakePoint(0.0,0.0);\r\n    }\r\n\r\n    [[self.sclViewLogs documentView] scrollPoint:newScrollOrigin];\r\n}\r\n\r\n- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row\r\n{\r\n    NSTableCellView *cellView = [tableView makeViewWithIdentifier:[tableColumn identifier] owner:self];\r\n    if ([[tableColumn identifier] isEqualToString:@\"columnCheck\"])\r\n    {\r\n        NSButton *btn = (NSButton *)cellView.subviews.firstObject;\r\n        if (btn)\r\n        {\r\n            [btn setTarget:self];\r\n            [btn setAction:@selector(checkButtonTapped:)];\r\n            btn.tag = row;\r\n        }\r\n    }\r\n    \r\n    [m_dataSource bindCellView:cellView atRow:row andColumnId:[tableColumn identifier]];\r\n    \r\n    BOOL clearClr = (m_orgTextColor == [NSColor clearColor]);\r\n    if (m_dataSource.rowInProgress == row)\r\n    {\r\n        if (clearClr)\r\n        {\r\n            m_orgTextColor = cellView.textField.textColor;\r\n        }\r\n        cellView.textField.textColor = [NSColor redColor];\r\n        \r\n    }\r\n    else\r\n    {\r\n        if (!clearClr)\r\n        {\r\n            cellView.textField.textColor = m_orgTextColor;\r\n        }\r\n    }\r\n    \r\n    return cellView;\r\n}\r\n\r\n- (void)tableViewColumnDidResize:(NSNotification *)notification\r\n{\r\n    if ([notification.name isEqualToString:NSTableViewColumnDidResizeNotification])\r\n    {\r\n        NSTableView *tableView = (NSTableView *)notification.object;\r\n        NSTableColumn *column = [notification.userInfo objectForKey:@\"NSTableColumn\"];\r\n        \r\n        if (tableView == self.tblSessions && [column.identifier isEqualToString:@\"columnCheck\"])\r\n        {\r\n            NSRect frame = [self.tblSessions.headerView headerRectOfColumn:0];\r\n            NSRect btnFrame = self.btnToggleAll.frame;\r\n            btnFrame.origin.x = (frame.size.width - btnFrame.size.width) / 2;\r\n            btnFrame.origin.y = (frame.size.height - btnFrame.size.height) / 2;\r\n            \r\n            self.btnToggleAll.frame = btnFrame;\r\n        }\r\n        \r\n    }\r\n}\r\n\r\n- (void)showNewVersion:(NSString *)version withUrl:(NSString *)url\r\n{\r\n    NSAlert *alert = [[NSAlert alloc] init];\r\n    \r\n    NSString *message = [NSString stringWithFormat:NSLocalizedString(@\"prompt-new-version-found\", comment: \"\"), version];\r\n    NSString *title = [NSRunningApplication currentApplication].localizedName;\r\n\r\n    [alert addButtonWithTitle:NSLocalizedString(@\"btn-yes\", comment: \"\")];\r\n    [alert addButtonWithTitle:NSLocalizedString(@\"btn-no\", comment: \"\")];\r\n    alert.messageText = message;\r\n    alert.window.title = title;\r\n    [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse returnCode) {\r\n        if (returnCode == NSModalResponseOK)\r\n        {\r\n            [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];\r\n        }\r\n    }];\r\n    \r\n}\r\n\r\n- (void)showGuide\r\n{\r\n    if ([AppConfiguration getSkipGuide])\r\n    {\r\n        return;\r\n    }\r\n    \r\n    __block NSAlert *alert = [[NSAlert alloc] init];\r\n    [alert setAlertStyle:NSInformationalAlertStyle];\r\n    \r\n    alert.messageText = NSLocalizedString(@\"alert-update-options\", nil);\r\n    alert.window.title = [NSRunningApplication currentApplication].localizedName;\r\n    // NSButton *btnGrant = [alert addButtonWithTitle:NSLocalizedString(@\"btn-grant-full-disk-access\", nil)];\r\n    // [btnGrant setTarget:strongSelf];\r\n    // [btnGrant setAction:@selector(btnGrantClicked:)];\r\n    \r\n    BOOL isCN = [self isCurrentLanguageCN];\r\n    NSString *resName = isCN ? @\"MainMenuCN\" : @\"MainMenuEN\";\r\n    NSRect frame = isCN ? NSMakeRect(0, 0, 480, 183) : NSMakeRect(0, 0, 545, 183);\r\n    NSImageView *imageView = [[NSImageView alloc] initWithFrame:frame];\r\n\r\n    NSBundle *bundle = [NSBundle mainBundle];\r\n    imageView.image = [bundle imageForResource:resName];\r\n    \r\n    alert.accessoryView = imageView;\r\n    alert.showsSuppressionButton = YES;\r\n    [alert addButtonWithTitle:NSLocalizedString(@\"btn-ok\", nil)];\r\n    [alert layout];\r\n    NSRect frame1 = alert.accessoryView.superview.frame;\r\n    NSRect frame2 = alert.suppressionButton.frame;\r\n    if (frame2.origin.y > frame1.origin.y)\r\n    {\r\n        NSRect frame = frame1;\r\n        frame1.origin = NSMakePoint(frame1.origin.x, frame2.origin.y + frame2.size.height - frame1.size.height);\r\n        frame2.origin = NSMakePoint(frame2.origin.x, frame.origin.y);\r\n        \r\n        alert.accessoryView.superview.frame = frame1;\r\n        alert.suppressionButton.frame = frame2;\r\n    }\r\n    [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse returnCode) {\r\n        if (alert.suppressionButton.state == NSOnState)\r\n        {\r\n            [AppConfiguration setSkipGuide:YES];\r\n        }\r\n    }];\r\n    // [btnGrant setTarget:nil];\r\n    // [btnGrant setAction:nil];\r\n}\r\n    \r\n- (void)checkUpdate\r\n{\r\n    NSBundle *bundle = [NSBundle mainBundle];\r\n    NSDictionary *info = [bundle infoDictionary];\r\n    NSString *shortVersion = info[@\"CFBundleShortVersionString\"];\r\n    NSString *build = info[@\"CFBundleVersion\"];\r\n    \r\n    __block NSString *version = [NSString stringWithFormat:@\"%@.%@\", shortVersion, build];\r\n    __block typeof(self) __weak weakSelf = self;\r\n    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{\r\n        \r\n        NSString *userAgent = [HttpHelper standardUserAgent];\r\n        Updater updater([version UTF8String]);\r\n        updater.setUserAgent([userAgent UTF8String]);\r\n        bool hasNewVersion = updater.checkUpdate();\r\n        NSInteger lastChkUpdateTime = static_cast<NSInteger>(getUnixTimeStamp());\r\n        [AppConfiguration setLastCheckUpdateTime:lastChkUpdateTime];\r\n        \r\n        if (!hasNewVersion)\r\n        {\r\n            return;\r\n        }\r\n        __strong typeof(weakSelf) strongSelf = weakSelf;  // strong by default\r\n        if (nil == strongSelf)\r\n        {\r\n            return;\r\n        }\r\n\r\n        // update UI on the main thread\r\n        dispatch_async(dispatch_get_main_queue(), ^{\r\n            __strong typeof(weakSelf) strongSelf = weakSelf;  // strong by default\r\n            if (strongSelf) {\r\n                NSString *newVersion = [NSString stringWithUTF8String:updater.getNewVersion().c_str()];\r\n                NSString *url = [NSString stringWithUTF8String:updater.getUpdateUrl().c_str()];\r\n                \r\n                [strongSelf showNewVersion:newVersion withUrl:url];\r\n            }\r\n        });\r\n        \r\n    });\r\n}\r\n\r\n- (NSString *)getCurrentLanguageCode\r\n{\r\n    NSString *preferredLanguage = [[NSLocale preferredLanguages] objectAtIndex:0];\r\n    if ([preferredLanguage hasPrefix:@\"zh-Hans\"])\r\n    {\r\n        preferredLanguage = @\"zh-Hans\";\r\n    }\r\n    else\r\n    {\r\n        preferredLanguage = @\"en\";\r\n    }\r\n    \r\n#ifndef NDEBUG\r\n    preferredLanguage = @\"zh-Hans\";\r\n#endif\r\n    return preferredLanguage;\r\n}\r\n\r\n- (BOOL)isCurrentLanguageCN\r\n{\r\n    return [@\"zh-Hans\" isEqualToString:[self getCurrentLanguageCode]];\r\n}\r\n\r\n@end\r\n"
  },
  {
    "path": "WechatExporter/WechatExporter.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n<plist version=\"1.0\">\r\n<dict>\r\n\t<key>com.apple.security.app-sandbox</key>\r\n\t<false/>\r\n\t<key>com.apple.security.assets.pictures.read-only</key>\r\n\t<true/>\r\n\t<key>com.apple.security.cs.disable-library-validation</key>\r\n\t<true/>\r\n\t<key>com.apple.security.files.downloads.read-only</key>\r\n\t<true/>\r\n\t<key>com.apple.security.files.user-selected.read-write</key>\r\n\t<true/>\r\n</dict>\r\n</plist>\r\n"
  },
  {
    "path": "WechatExporter/core/AsyncExecutor.cpp",
    "content": "//\n//  AsyncExecutor.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2021/4/17.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#include \"AsyncExecutor.h\"\n#include \"Utils.h\"\n\nstd::atomic_uint32_t AsyncExecutor::m_nextTaskId(1u);\n\nuint32_t AsyncExecutor::genNextTaskId()\n{\n    return m_nextTaskId.fetch_add(1);\n}\n\nAsyncExecutor::Thread::Thread(AsyncExecutor *executor) : m_executor(executor),\n      m_thread(new std::thread(&AsyncExecutor::Thread::ThreadFunc, this))\n{\n}\n\nAsyncExecutor::Thread::~Thread()\n{\n    m_thread->join();\n    m_thread.reset();\n}\n\nvoid AsyncExecutor::Thread::ThreadFunc()\n{\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    std::string tname = m_executor->m_tag + std::to_string(++m_executor->m_tid);\n    setThreadName(tname.c_str());\n#endif\n    \n    m_executor->ThreadFunc();\n    std::unique_lock<std::mutex> lock(m_executor->m_mutex);\n    m_executor->m_nthreads--;\n    m_executor->m_dead_threads.push_back(this);\n    if ((m_executor->m_shutdown) && (m_executor->m_nthreads == 0))\n    {\n        m_executor->m_shutdown_cv.notify_one();\n    }\n}\n\nvoid AsyncExecutor::addTask(AsyncExecutor::Task* task)\n{\n    std::lock_guard<std::mutex> lock(m_mutex);\n\n    // Add works to the callbacks list\n    m_tasks.push(task);\n\n    // Increase pool size or notify as needed\n    if (m_threads_waiting == 0 && m_nthreads < m_max_threads)\n    {\n        // Kick off a new thread\n        m_nthreads++;\n        new Thread(this);\n    }\n    else\n    {\n        m_cv.notify_one();\n    }\n\n    // Also use this chance to harvest dead threads\n    if (!m_dead_threads.empty())\n    {\n        DestroyThreads(&m_dead_threads);\n    }\n}\n\nAsyncExecutor::AsyncExecutor(int reserve_threads, int max_threads, Callback *callback) :\n    m_callback(callback),\n    m_shutdown(false),\n    m_reserve_threads(reserve_threads),\n    m_max_threads(max_threads),\n    m_nthreads(0),\n    m_threads_waiting(0)\n{\n#ifndef NDEBUG\n    m_tid = 0;\n#endif\n    /*\n    for (int i = 0; i < m_reserve_threads; i++)\n    {\n        std::lock_guard<std::mutex> lock(m_mutex);\n        m_nthreads++;\n        new Thread(this);\n    }\n     */\n}\n\nAsyncExecutor::~AsyncExecutor()\n{\n    std::unique_lock<std::mutex> lock(m_mutex);\n    m_shutdown = true;\n    m_cv.notify_all();\n\n    while (m_nthreads != 0)\n    {\n        m_shutdown_cv.wait(lock);\n    }\n\n    DestroyThreads(&m_dead_threads);\n}\n\nvoid AsyncExecutor::DestroyThreads(std::list<Thread*> *threads)\n{\n    for (auto it = threads->begin(); it != threads->end(); it = threads->erase(it))\n    {\n        delete *it;\n    }\n}\n\nsize_t AsyncExecutor::getNumberOfQueue() const\n{\n    std::unique_lock<std::mutex> lock(m_mutex);\n    size_t size = m_tasks.size();\n    \n    return size;\n}\n\nvoid AsyncExecutor::shutdown()\n{\n    std::unique_lock<std::mutex> lock(m_mutex);\n    m_shutdown = true;\n    m_cv.notify_all();\n}\n\n// true: completed, false: timeout\nbool AsyncExecutor::waitForCompltion(unsigned int ms)\n{\n    std::unique_lock<std::mutex> lock(m_mutex);\n    if (m_nthreads != 0)\n    {\n        if (ms == 0)\n        {\n            m_shutdown_cv.wait(lock);\n            return false;\n        }\n        else\n        {\n            if (m_shutdown_cv.wait_for(lock, std::chrono::milliseconds(ms)) == std::cv_status::timeout)\n            {\n                return false;\n            }\n        }\n    }\n\n    return true;\n}\n\nvoid AsyncExecutor::cancel()\n{\n    std::queue<Task *> tasks;\n    {\n        std::unique_lock<std::mutex> lock(m_mutex);\n        tasks.swap(m_tasks);\n\t\tm_shutdown = true;\n\t\tm_cv.notify_all();\n    }\n    \n    while (!tasks.empty())\n    {\n        auto task = tasks.front();\n        tasks.pop();\n        delete task;\n    }\n}\n\nvoid AsyncExecutor::ThreadFunc()\n{\n    for (;;)\n    {\n        std::unique_lock<std::mutex> lock(m_mutex);\n\n        // Wait until work is available or we are shutting down.\n        if (!m_shutdown && m_tasks.empty())\n        {\n            // If there are too many threads waiting, then quit this thread\n            if (m_threads_waiting >= m_reserve_threads)\n            {\n                break;\n            }\n\n            m_threads_waiting++;\n            m_cv.wait(lock);\n            m_threads_waiting--;\n        }\n        \n        // Drain callbacks before considering shutdown to ensure all work gets completed.\n        if (!m_tasks.empty())\n        {\n            auto task = m_tasks.front();\n            m_tasks.pop();\n            lock.unlock();\n            if (NULL != m_callback)\n            {\n                m_callback->onTaskStart(this, task);\n            }\n            bool succeeded = task->run();\n            if (NULL != m_callback)\n            {\n                m_callback->onTaskComplete(this, task, succeeded);\n            }\n            delete task;\n        }\n        else if (m_shutdown)\n        {\n            break;\n        }\n    }\n}\n"
  },
  {
    "path": "WechatExporter/core/AsyncExecutor.h",
    "content": "//\n//  AsyncExecutor.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/4/17.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef AsyncExecutor_h\n#define AsyncExecutor_h\n\n#include <condition_variable>\n#include <mutex>\n#include <atomic>\n#include <thread>\n#include <queue>\n#include <list>\n\nclass AsyncExecutor\n{\npublic:\n    \n    class Task\n    {\n    public:\n        virtual bool run() = 0;\n        virtual int getType() const = 0;\n        virtual std::string getName() const\n        {\n            return \"\";\n        }\n        virtual bool hasError() const\n        {\n            return false;\n        }\n        virtual std::string getError() const\n        {\n            return \"\";\n        }\n\n        Task() : m_taskId(0u), m_userData(NULL)\n        {\n        }\n        virtual ~Task() {}\n        \n        uint32_t getTaskId() const\n        {\n            return m_taskId;\n        }\n        \n        void setTaskId(uint32_t taskId)\n        {\n            m_taskId = taskId;\n        }\n        \n        const void* getUserData() const\n        {\n            return m_userData;\n        }\n        \n        void setUserData(const void* userData)\n        {\n            m_userData = userData;\n        }\n        \n    private:\n        uint32_t        m_taskId;\n        const void*     m_userData;\n    };\n    \n    class Callback\n    {\n    public:\n        virtual void onTaskStart(const AsyncExecutor* executor, const Task *task) = 0;\n        virtual void onTaskComplete(const AsyncExecutor* executor, const Task *task, bool succeeded) = 0;\n        virtual ~Callback() {}\n    };\n    \nprotected:\n    class Thread\n    {\n    public:\n        Thread(AsyncExecutor* executor);\n        ~Thread();\n    private:\n        AsyncExecutor* m_executor;\n        std::unique_ptr<std::thread> m_thread;\n        void ThreadFunc();\n        \n    };\n\npublic:\n    explicit AsyncExecutor(int reserve_threads, int max_threads, Callback *callback);\n    ~AsyncExecutor();\n\n    static uint32_t genNextTaskId();\n    void addTask(Task *task);\n    \n    size_t getNumberOfQueue() const;\n    void shutdown();\n    void cancel();\n    // true: completed, false: timeout\n    bool waitForCompltion(unsigned int ms);\n    \n#if !defined(NDEBUG) || defined(DBG_PERF)\n    void setTag(const std::string& tag)\n    {\n        m_tag = tag;\n    }\n#endif\n\nprotected:\n    \n    Callback* m_callback;\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    std::string m_tag;\n    uint32_t m_tid;\n#endif\n    \n    mutable std::mutex m_mutex;\n    std::condition_variable m_cv;\n    std::condition_variable m_shutdown_cv;\n    bool m_shutdown;\n    std::queue<Task *> m_tasks;\n    int m_reserve_threads;\n    int m_max_threads;\n    int m_nthreads;\n\n    int m_threads_waiting;\n    std::list<Thread*> m_dead_threads;\n    \n    static std::atomic_uint32_t m_nextTaskId;\n\n    void ThreadFunc();\n    static void DestroyThreads(std::list<Thread*>* m_threads);\n    \n};\n\n#endif /* AsyncExecutor_h */\n"
  },
  {
    "path": "WechatExporter/core/AsyncTask.cpp",
    "content": "//\n//  AsyncTask.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2021/4/20.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#include \"AsyncTask.h\"\n#include <curl/curl.h>\n#include <iostream>\n#include <fstream>\n#ifdef _WIN32\n#include <atlstr.h>\n#ifndef NDEBUG\n#include <cassert>\n#endif\n#endif\n#include \"FileSystem.h\"\n#include \"Utils.h\"\n\n// #define FAKE_DOWNLOAD\nsize_t writeHttpDataToBuffer(void *buffer, size_t size, size_t nmemb, void *user_p)\n{\n    std::vector<unsigned char>* body = reinterpret_cast<std::vector<unsigned char> *>(user_p);\n    if (NULL != body)\n    {\n        size_t bytes = size * nmemb;\n\n        unsigned char *ptr = reinterpret_cast<unsigned char *>(buffer);\n        std::copy(ptr, ptr + bytes, back_inserter(*body));\n        return bytes;\n    }\n    \n    return 0;\n}\n\nvoid DownloadTask::initialize()\n{\n    curl_global_init(CURL_GLOBAL_ALL);\n}\n\nvoid DownloadTask::uninitialize()\n{\n    curl_global_cleanup();\n}\n\nbool DownloadTask::httpGet(const std::string& url, const std::vector<std::pair<std::string, std::string>>& headers, long& httpStatus, std::vector<unsigned char>& body)\n{\n    httpStatus = 0;\n    CURLcode res = CURLE_OK;\n    CURL *curl = NULL;\n    \n    body.clear();\n    \n#ifndef FAKE_DOWNLOAD\n    // User-Agent: WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0\n    curl = curl_easy_init();\n    \n#ifndef NDEBUG\n    struct curl_slist *host = NULL;\n#endif\n\n    struct curl_slist *chunk = NULL;\n    \n    for (std::vector<std::pair<std::string, std::string>>::const_iterator it = headers.cbegin(); it != headers.cend(); ++it)\n    {\n        if (it->first == \"User-Agent\")\n        {\n            curl_easy_setopt(curl, CURLOPT_USERAGENT, it->second.c_str());\n        }\n#ifndef NDEBUG\n        else if (it->first == \"RESOLVE\")\n        {\n            host = curl_slist_append(host, it->second.c_str());\n        }\n#endif\n        else\n        {\n            std::string header = it->first + \": \" + it->second;\n            chunk = curl_slist_append(chunk, header.c_str());\n        }\n    }\n    \n#ifndef NDEBUG\n    if (NULL != host)\n    {\n        curl_easy_setopt(curl, CURLOPT_RESOLVE, host);\n    }\n#endif\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    \n    if (NULL != chunk)\n    {\n        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);\n    }\n    // curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str());\n    curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L);\n    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60);\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &::writeHttpDataToBuffer);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void *>(&body));\n    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);\n    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);\n    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);\n\n    res = curl_easy_perform(curl);\n    if (res == CURLE_OK)\n    {\n        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus);\n        // m_error = curl_easy_strerror(res);\n    }\n    curl_easy_cleanup(curl);\n#ifndef NDEBUG\n    if (NULL != host)\n    {\n        curl_slist_free_all(host);\n    }\n#endif\n    if (NULL != chunk)\n    {\n        curl_slist_free_all(chunk);\n    }\n#endif // no FAKE_DOWNLOAD\n    \n    return res == CURLE_OK;\n}\n\nsize_t writeTaskHttpData(void *buffer, size_t size, size_t nmemb, void *user_p)\n{\n    DownloadTask *task = reinterpret_cast<DownloadTask *>(user_p);\n    if (NULL != task)\n    {\n        return task->writeData(buffer, size, nmemb);\n    }\n    \n    return 0;\n}\n\nDownloadTask::DownloadTask(const std::string &url, const std::string& output, const std::string& defaultFile, time_t mtime, const std::string& name/* = \"\"*/) : m_url(url), m_output(output), m_default(defaultFile), m_mtime(mtime), m_retries(0), m_name(name)\n{\n#ifndef NDEBUG\n    if (m_output.empty())\n    {\n        assert(false);\n    }\n#endif\n}\n\nunsigned int DownloadTask::getRetries() const\n{\n    return m_retries;\n}\n\nbool DownloadTask::run()\n{\n    std::string* urls[] = { &m_url, &m_urlBackup };\n    \n    for (int item = 0; item < sizeof(urls) / sizeof(std::string*); ++item)\n    {\n        if (urls[item]->empty())\n        {\n            continue;\n        }\n        for (int idx = 0; idx < DEFAULT_RETRIES; idx++)\n        {\n            if (downloadFile(*urls[item]))\n            {\n                return true;\n            }\n        }\n        \n        if (startsWith(*urls[item], \"http://\"))\n        {\n            std::string url = *urls[item];\n            url.replace(0, 7, \"https://\");\n            if (downloadFile(url))\n            {\n                return true;\n            }\n        }\n    }\n    \n    if (!m_default.empty())\n    {\n        if (copyFile(m_default, m_output))\n        {\n            return true;\n        }\n        else\n        {\n            m_error += \"\\r\\nFailed to copy default file: \" + m_default + \" => \" + m_output;\n        }\n    }\n    \n    return false;\n}\n\nbool DownloadTask::downloadFile(const std::string& url)\n{\n    ++m_retries;\n    \n    m_outputTmp = m_output + \".tmp\";\n    deleteFile(m_outputTmp);\n\n    CURLcode res = CURLE_OK;\n    CURL *curl = NULL;\n#ifndef NDEBUG\n    std::string logPath = m_output + \".http.log\";\n    \n#ifdef _WIN32\n    CA2W pszW(logPath.c_str(), CP_UTF8);\n    FILE* logFile = _wfopen((LPCWSTR)pszW, L\"wb\");\n#else\n    FILE* logFile = fopen(logPath.c_str(), \"wb\");\n#endif\n#endif\n    \n    std::string userAgent = m_userAgent.empty() ? \"WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0\" : m_userAgent;\n    \n    long httpStatus = 0;\n    \n#ifndef FAKE_DOWNLOAD\n    // User-Agent: WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0\n    curl = curl_easy_init();\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str());\n    curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L);\n    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60);\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &::writeTaskHttpData);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, this);\n    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);\n    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);\n    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);\n#ifndef NDEBUG\n    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);\n    curl_easy_setopt(curl, CURLOPT_STDERR, logFile);\n#endif\n\n    res = curl_easy_perform(curl);\n    if (res != CURLE_OK)\n    {\n        m_error = \"Failed \" + m_name + \"\\r\\n\";\n        m_error += curl_easy_strerror(res);\n        if (m_retries >= DEFAULT_RETRIES)\n        {\n            fprintf(stderr, \"%s: %s\\n\", m_error.c_str(), m_url.c_str());\n        }\n    }\n    else\n    {\n        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus);\n        \n#ifndef NDEBUG\n        char *lastUrl = NULL;\n        CURLcode res2 = curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &lastUrl);\n\n        if((CURLE_OK == res2) && lastUrl && m_url != lastUrl)\n        {\n            m_error += \" \\r\\nRedirect: \";\n            m_error += lastUrl;\n        }\n#endif\n    }\n    curl_easy_cleanup(curl);\n\n#ifndef NDEBUG\n    if (NULL != logFile)\n    {\n        fclose(logFile);\n    }\n#endif\n    \n    if (res == CURLE_OK && httpStatus == 200)\n    {\n        ::moveFile(m_outputTmp, m_output);\n        if (m_mtime > 0)\n        {\n            updateFileTime(m_output, m_mtime);\n        }\n#ifndef NDEBUG\n        ::deleteFile(logPath);\n#endif\n        return true;\n    }\n#else\n    return true;\n#endif // no FAKE_DOWNLOAD\n\n    if (m_error.empty())\n    {\n        m_error = \"HTTP Status:\" + std::to_string(httpStatus);\n    }\n    return false;\n}\n\nsize_t DownloadTask::writeData(void *buffer, size_t size, size_t nmemb)\n{\n    size_t bytesToWrite = size * nmemb;\n    if (appendFile(m_outputTmp, reinterpret_cast<const unsigned char *>(buffer), bytesToWrite))\n    {\n        return bytesToWrite;\n    }\n    return 0;\n}\n\nCopyTask::CopyTask(const std::string &src, const std::string& dest, const std::string& name) : m_src(src), m_dest(dest), m_name(name)\n{\n}\n\nbool CopyTask::run()\n{\n    if (::copyFile(m_src, m_dest))\n    {\n        return true;\n    }\n    \n    if (!existsFile(m_src))\n    {\n        m_error = \"Failed CP: \" + m_src + \"(not existed)  => \" + m_dest;\n    }\n    else\n    {\n        m_error = \"Failed CP: \" + m_src + \" => \" + m_dest;\n#ifdef _WIN32\n        DWORD lastError = ::GetLastError();\n        m_error += \"LastError:\" + std::to_string(lastError);\n#endif\n    }\n    \n    return false;\n}\n\nMp3Task::Mp3Task(const std::string &pcm, const std::string& mp3, unsigned int mtime) : m_pcm(pcm), m_mp3(mp3), m_mtime(mtime)\n{\n}\n\nvoid Mp3Task::swapBuffer(std::vector<unsigned char>& buffer)\n{\n    m_pcmData.swap(buffer);\n}\n\nbool Mp3Task::run()\n{\n    std::vector<unsigned char> pcmData;\n    bool isSilk = false;\n    bool res = silkToPcm(m_pcm, pcmData, isSilk, &m_error) && !pcmData.empty();\n    if (res)\n    {\n        res = pcmToMp3(pcmData, m_mp3);\n    }\n    else if (!isSilk)\n    {\n        res = amrToPcm(m_pcm, pcmData) && !pcmData.empty();\n        if (res)\n        {\n            res = amrPcmToMp3(pcmData, m_mp3);\n        }\n    }\n    if (res)\n    {\n        updateFileTime(m_mp3, m_mtime);\n        return true;\n    }\n    /*\n    if (res)\n    {\n        m_error = \"Failed pcmToMp3: \" + m_pcm + \" => \" + m_mp3;\n    }\n    else\n    {\n        m_error = \"Failed silkTpPcm: \" + m_pcm;\n    }\n     */\n    return false;\n}\n\nPdfTask::PdfTask(PdfConverter* pdfConveter, const std::string &src, const std::string& dest, const std::string& name) : m_pdfConverter(pdfConveter), m_src(src), m_dest(dest), m_name(name)\n{\n}\n\nbool PdfTask::run()\n{\n    return m_pdfConverter->convert(m_src, m_dest);\n}\n"
  },
  {
    "path": "WechatExporter/core/AsyncTask.h",
    "content": "//\n//  AsyncTask.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/4/20.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef AsyncTask_h\n#define AsyncTask_h\n\n#include <stdio.h>\n#include \"AsyncExecutor.h\"\n#include \"PdfConverter.h\"\n\n#define TASK_TYPE_DOWNLOAD  1\n#define TASK_TYPE_COPY      2\n#define TASK_TYPE_AUDIO     3\n#define TASK_TYPE_PDF       4\n\nclass DownloadTask : public AsyncExecutor::Task\n{\nprivate:\n    std::string m_url;\n    std::string m_urlBackup;\n    std::string m_output;\n    std::string m_default;\n    std::string m_outputTmp;\n    std::string m_error;\n    std::string m_userAgent;\n    time_t m_mtime;\n    unsigned int m_retries;\n    \n    std::string m_name;\n    \npublic:\n    static const unsigned int DEFAULT_RETRIES = 3;\n    \n    DownloadTask(const std::string &url, const std::string& output, const std::string& defaultFile, time_t mtime, const std::string& name = \"\");\n    virtual ~DownloadTask() {}\n    \n    virtual int getType() const\n    {\n        return TASK_TYPE_DOWNLOAD;\n    }\n    \n    virtual std::string getName() const\n    {\n        return m_name;\n    }\n    \n    void setUserAgent(const std::string& userAgent)\n    {\n        m_userAgent = userAgent;\n    }\n    \n    inline std::string getUrl() const\n    {\n        return m_url;\n    }\n    \n    inline std::string getOutput() const\n    {\n        return m_output;\n    }\n    \n    bool hasError() const\n    {\n        return !m_error.empty();\n    }\n    std::string getError() const\n    {\n        return m_error;\n    }\n    \n    static void initialize();\n    static void uninitialize();\n    static bool httpGet(const std::string& url, const std::vector<std::pair<std::string, std::string>>& headers, long& httpStatus, std::vector<unsigned char>& body);\n    \n    size_t writeData(void *buffer, size_t size, size_t nmemb);\n    \n    unsigned int getRetries() const;\n    \n    bool run();\n    \nprotected:\n    bool downloadFile(const std::string& url);\n};\n\nclass CopyTask : public AsyncExecutor::Task\n{\npublic:\n    CopyTask(const std::string &src, const std::string& dest, const std::string& name);\n    virtual ~CopyTask() {}\n    \n    virtual int getType() const\n    {\n        return TASK_TYPE_COPY;\n    }\n    virtual std::string getName() const\n    {\n        return m_name;\n    }\n    bool hasError() const\n    {\n        return !m_error.empty();\n    }\n    std::string getError() const\n    {\n        return m_error;\n    }\n    \n    bool run();\n    \nprivate:\n    std::string m_src;\n    std::string m_dest;\n    std::string m_name;\n    std::string m_error;\n};\n\nclass Mp3Task : public AsyncExecutor::Task\n{\npublic:\n    Mp3Task(const std::string &pcm, const std::string& mp3, unsigned int mtime);\n    virtual ~Mp3Task() {}\n    \n    virtual int getType() const\n    {\n        return TASK_TYPE_AUDIO;\n    }\n    virtual std::string getName() const\n    {\n        return \"Mp3: \" + m_mp3;\n    }\n    bool hasError() const\n    {\n        return !m_error.empty();\n    }\n    std::string getError() const\n    {\n        return m_error;\n    }\n    \n    void swapBuffer(std::vector<unsigned char>& buffer);\n\n    bool run();\n    \nprivate:\n    std::string m_pcm;\n    std::string m_mp3;\n    unsigned int m_mtime;\n    std::string m_error;\n    \n    std::vector<unsigned char> m_pcmData;\n};\n\nclass PdfTask : public AsyncExecutor::Task\n{\npublic:\n    PdfTask(PdfConverter* pdfConveter, const std::string &src, const std::string& dest, const std::string& name);\n    virtual ~PdfTask() {}\n    \n    virtual int getType() const\n    {\n        return TASK_TYPE_PDF;\n    }\n    virtual std::string getName() const\n    {\n        return \"PDF: \" + m_src + \" => \" + m_dest;\n    }\n    \n    bool run();\n    \nprivate:\n    PdfConverter   *m_pdfConverter;\n    std::string m_src;\n    std::string m_dest;\n    std::string m_name;\n};\n\n#endif /* AsyncTask_h */\n"
  },
  {
    "path": "WechatExporter/core/ByteArrayLocater.h",
    "content": "//\n//  ByteArrayLocater.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/10/19.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include <vector>\n\n#ifndef ByteArrayLocater_h\n#define ByteArrayLocater_h\n\nclass ByteArrayLocater\n{\npublic:\n    std::vector<int> locate(const unsigned char* data, int length, const unsigned char* candidate, int candidateLength)\n    {\n        std::vector<int> positions;\n        if (isEmptyLocate(data, length, candidate, candidateLength))\n            return positions;\n\n        for (int i = 0; i < length; i++)\n        {\n            if (!isMatch(data, length, i, candidate, candidateLength))\n                continue;\n\n            positions.push_back(i);\n        }\n\n        return positions;\n    }\n    \n    std::vector<std::pair<int, int>> locatePair(const unsigned char* data, int length, const unsigned char* startCandidate, int startCandidateLength, const unsigned char* endCandidate, int endCandidateLength)\n    {\n        std::vector<std::pair<int, int>> positions;\n        if (isEmptyLocate(data, length, startCandidate, startCandidateLength))\n            return positions;\n\n        for (int i = 0; i < length;)\n        {\n            if (!isMatch(data, length, i, startCandidate, startCandidateLength))\n            {\n                i++;\n                continue;\n            }\n            \n            int j = i + startCandidateLength;\n            \n            for (; j < length;)\n            {\n                if (!isMatch(data, length, j, endCandidate, endCandidateLength))\n                {\n                    j++;\n                    continue;\n                }\n                \n                positions.push_back(std::make_pair(i, j));\n                break;\n            }\n            \n            if (j == length)\n            {\n                // No endTag found\n                break;\n            }\n            \n            i = j + endCandidateLength;\n        }\n\n        return positions;\n    }\n\n    bool isMatch(const unsigned char* data, int length, int position, const unsigned char* candidate, int candidateLength)\n    {\n        if (candidateLength > (length - position))\n            return false;\n\n        for (int i = 0; i < candidateLength; i++)\n            if (data[position + i] != candidate[i])\n                return false;\n\n        return true;\n    }\n\n    bool isEmptyLocate(const unsigned char* data, int length, const unsigned char* candidate, int candidateLength)\n    {\n        return data == NULL\n            || candidate == NULL\n            || length == 0\n            || candidateLength == 0\n            || candidateLength > length;\n    }\n\n    \n};\n\n#endif /* ByteArrayLocater_h */\n"
  },
  {
    "path": "WechatExporter/core/DownloadPool.cpp",
    "content": "//\n//  DownloadPool.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include \"DownloadPool.h\"\n#include <curl/curl.h>\n#include <iostream>\n#include <fstream>\n#include <sys/types.h>\n#include <sys/stat.h>\n#include \"OSDef.h\"\n\nsize_t writeData(void *buffer, size_t size, size_t nmemb, void *user_p)\n{\n    Task *task = reinterpret_cast<Task *>(user_p);\n    if (NULL != task)\n    {\n        return task->writeData(buffer, size, nmemb);\n    }\n    \n    return 0;\n}\n\nvoid Task::run()\n{\n    std::ofstream output(m_output, std::fstream::in | std::fstream::out | std::fstream::trunc);\n    output.close();\n    \n    CURL *curl_handler = curl_easy_init();\n    curl_easy_setopt(curl_handler, CURLOPT_URL, m_url.c_str());\n    curl_easy_setopt(curl_handler, CURLOPT_TIMEOUT, 60);\n    curl_easy_setopt(curl_handler, CURLOPT_WRITEFUNCTION, &::writeData);\n    curl_easy_setopt(curl_handler, CURLOPT_WRITEDATA, this);\n\n    curl_easy_perform(curl_handler);\n    \n    curl_easy_cleanup(curl_handler);\n}\n\nsize_t Task::writeData(void *buffer, size_t size, size_t nmemb)\n{\n    std::ofstream file;\n    file.open (m_output, std::fstream::in | std::fstream::out | std::fstream::app | std::fstream::binary);\n    // file.write(buffer, size);\n    size_t bytesToWrite = size * nmemb;\n    file.write(reinterpret_cast<const char *>(buffer), bytesToWrite);\n    file.close();\n    \n    return bytesToWrite;\n}\n\nDownloadPool::DownloadPool()\n{\n    m_noMoreTask = false;\n    curl_global_init(CURL_GLOBAL_ALL);\n    \n    for (int idx = 0; idx < 4; idx++)\n    {\n        m_threads.push_back(std::thread(&DownloadPool::run, this));\n    }\n    // vecOfThreads.push_back(std::thread(func));\n    \n}\n\nDownloadPool::~DownloadPool()\n{\n    curl_global_cleanup();\n}\n\nvoid DownloadPool::addTask(const std::string &url, const std::string& output)\n{\n    std::string formatedPath = output;\n    std::replace(formatedPath.begin(), formatedPath.end(), DIR_SEP_R, DIR_SEP);\n#ifndef NDEBUG\n    struct stat buffer;\n    if (stat (formatedPath.c_str(), &buffer) == 0)\n    {\n        return;\n    }\n#endif\n\n    std::string uid = url + output;\n    bool existed = false;\n    Task task(url, formatedPath);\n    m_mtx.lock();\n    if (!(existed = (m_urls.find(uid) != m_urls.cend())))\n    {\n        m_urls.insert(uid);\n        m_queue.push(task);\n    }\n    m_mtx.unlock();\n    \n    if (existed)\n    {\n#ifndef NDEBUG\n        printf(\"URL Existed: %s\", url.c_str());\n#endif\n    }\n}\n\nvoid DownloadPool::setNoMoreTask()\n{\n    m_mtx.lock();\n    m_noMoreTask = true;\n    m_mtx.unlock();\n}\n\nvoid DownloadPool::run()\n{\n    while(1)\n    {\n        bool found = false;\n        bool noMoreTask = false;\n        \n        Task task;\n        m_mtx.lock();\n        \n        size_t queueSize = m_queue.size();\n        if (queueSize > 0)\n        {\n            task = m_queue.front();\n            m_queue.pop();\n            found = true;\n        }\n        \n        noMoreTask = m_noMoreTask;\n        \n        m_mtx.unlock();\n        \n        if (found)\n        {\n            // run task\n            task.run();\n            continue;\n        }\n        if (m_noMoreTask)\n        {\n            break;\n        }\n        else\n        {\n            std::this_thread::sleep_for(std::chrono::milliseconds(100));\n        }\n    }\n}\n\nvoid DownloadPool::finishAndWaitForExit()\n{\n    setNoMoreTask();\n    for (std::vector<std::thread>::iterator it = m_threads.begin(); it != m_threads.end(); ++it)\n    {\n        it->join();\n    }\n}\n"
  },
  {
    "path": "WechatExporter/core/DownloadPool.h",
    "content": "//\n//  DownloadPool.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#ifndef DownloadPool_h\n#define DownloadPool_h\n\n#include <stdio.h>\n#include <string>\n#include <queue>\n#include <set>\n#include <utility>\n#include <thread>\n#include <mutex>\n\nclass Task\n{\nprotected:\n    std::string m_url;\n    std::string m_output;\n    \npublic:\n    Task()\n    {\n    }\n    \n    Task(const std::string &url, const std::string& output)\n    {\n        m_url = url;\n        m_output = output;\n    }\n    \n    Task& operator=(const Task& task)\n    {\n        if (this != &task)\n        {\n            m_url = task.m_url;\n            m_output = task.m_output;\n        }\n        \n        return *this;\n    }\n    \n    size_t writeData(void *buffer, size_t size, size_t nmemb);\n    void run();\n};\n\nclass DownloadPool\n{\nprotected:\n    std::queue<Task> m_queue;\n    std::set<std::string> m_urls;\n    std::mutex m_mtx;\n    bool m_noMoreTask;\n    std::vector<std::thread> m_threads;\n    \npublic:\n    DownloadPool();\n    ~DownloadPool();\n    \n    void addTask(const std::string &url, const std::string& output);\n    void setNoMoreTask();\n    void run();\n    \n    void finishAndWaitForExit();\n};\n\n#endif /* DownloadPool_h */\n"
  },
  {
    "path": "WechatExporter/core/Downloader.cpp",
    "content": "//\n//  Downloader.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#ifdef USING_DOWNLOADER\n\n#include \"Downloader.h\"\n#include <curl/curl.h>\n#include <iostream>\n#include <fstream>\n#include <sys/types.h>\n#include <sys/stat.h>\n#include \"Utils.h\"\n#include \"FileSystem.h\"\n#ifdef _WIN32\n#include <atlstr.h>\n#ifndef NDEBUG\n#include <cassert>\n#endif\n#endif\n\n// #define FAKE_DOWNLOAD\nsize_t writeDataToBuffer(void *buffer, size_t size, size_t nmemb, void *user_p)\n{\n    std::vector<unsigned char>* body = reinterpret_cast<std::vector<unsigned char> *>(user_p);\n    if (NULL != body)\n    {\n        size_t bytes = size * nmemb;\n\n        unsigned char *ptr = reinterpret_cast<unsigned char *>(buffer);\n        std::copy(ptr, ptr + bytes, back_inserter(*body));\n        return bytes;\n    }\n    \n    return 0;\n}\n\nbool Downloader::httpGet(const std::string& url, const std::vector<std::pair<std::string, std::string>>& headers, long& httpStatus, std::vector<unsigned char>& body)\n{\n    httpStatus = 0;\n    CURLcode res = CURLE_OK;\n    CURL *curl = NULL;\n    \n    body.clear();\n    \n#ifndef FAKE_DOWNLOAD\n    // User-Agent: WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0\n    curl = curl_easy_init();\n    \n#ifndef NDEBUG\n    struct curl_slist *host = NULL;\n#endif\n\n    struct curl_slist *chunk = NULL;\n    \n    for (std::vector<std::pair<std::string, std::string>>::const_iterator it = headers.cbegin(); it != headers.cend(); ++it)\n    {\n        if (it->first == \"User-Agent\")\n        {\n            curl_easy_setopt(curl, CURLOPT_USERAGENT, it->second.c_str());\n        }\n#ifndef NDEBUG\n        else if (it->first == \"RESOLVE\")\n        {\n            host = curl_slist_append(host, it->second.c_str());\n        }\n#endif\n        else\n        {\n            std::string header = it->first + \": \" + it->second;\n            chunk = curl_slist_append(chunk, header.c_str());\n        }\n    }\n    \n#ifndef NDEBUG\n    if (NULL != host)\n    {\n        curl_easy_setopt(curl, CURLOPT_RESOLVE, host);\n    }\n#endif\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    \n    if (NULL != chunk)\n    {\n        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);\n    }\n    // curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str());\n    curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L);\n    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60);\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &::writeDataToBuffer);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void *>(&body));\n    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);\n    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);\n    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);\n\n    res = curl_easy_perform(curl);\n    if (res == CURLE_OK)\n    {\n        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus);\n        // m_error = curl_easy_strerror(res);\n    }\n    curl_easy_cleanup(curl);\n#ifndef NDEBUG\n    if (NULL != host)\n    {\n        curl_slist_free_all(host);\n    }\n#endif\n    if (NULL != chunk)\n    {\n        curl_slist_free_all(chunk);\n    }\n#endif // no FAKE_DOWNLOAD\n    \n    return res == CURLE_OK;\n}\n\nsize_t writeTaskData(void *buffer, size_t size, size_t nmemb, void *user_p)\n{\n    Task *task = reinterpret_cast<Task *>(user_p);\n    if (NULL != task)\n    {\n        return task->writeData(buffer, size, nmemb);\n    }\n    \n    return 0;\n}\n\nTask::Task(uint32_t taskId, const std::string &url, const std::string& output, time_t mtime, bool localCopy/* = false*/) : m_taskId(taskId), m_url(url), m_output(output), m_mtime(mtime), m_localCopy(localCopy), m_retries(0)\n{\n#ifndef NDEBUG\n    if (m_output.empty())\n    {\n        assert(false);\n    }\n#endif\n}\n\nunsigned int Task::getRetries() const\n{\n    return m_retries;\n}\n\nbool Task::run()\n{\n    return m_localCopy ? copyFile() : downloadFile();\n}\n\nbool Task::downloadFile()\n{\n    ++m_retries;\n    \n    m_outputTmp = m_output + \".tmp\";\n    deleteFile(m_outputTmp);\n\n\tCURLcode res = CURLE_OK;\n    CURL *curl = NULL;\n#ifndef NDEBUG\n    std::string logPath = m_output + \".log\";\n    \n#ifdef _WIN32\n    CA2W pszW(logPath.c_str(), CP_UTF8);\n    FILE* logFile = _wfopen((LPCWSTR)pszW, L\"wb\");\n#else\n    FILE* logFile = fopen(logPath.c_str(), \"wb\");\n#endif\n#endif\n    \n    std::string userAgent = m_userAgent.empty() ? \"WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0\" : m_userAgent;\n    \n#ifndef FAKE_DOWNLOAD\n    // User-Agent: WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0\n    curl = curl_easy_init();\n    curl_easy_setopt(curl, CURLOPT_URL, m_url.c_str());\n    curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str());\n    curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L);\n    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60);\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &::writeTaskData);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, this);\n    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);\n\tcurl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);\n\tcurl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);\n#ifndef NDEBUG\n    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);\n    curl_easy_setopt(curl, CURLOPT_STDERR, logFile);\n#endif\n\n\tlong httpStatus = 0;\n    res = curl_easy_perform(curl);\n\tif (res != CURLE_OK)\n\t{\n        m_error = curl_easy_strerror(res);\n        if (m_retries >= MAX_RETRIES)\n        {\n            fprintf(stderr, \"%s: %s\\n\", m_error.c_str(), m_url.c_str());\n        }\n\t}\n\telse\n\t{\n\t\tcurl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus);\n\t}\n    curl_easy_cleanup(curl);\n#endif // no FAKE_DOWNLOAD\n\n#ifndef NDEBUG\n    if (NULL != logFile)\n    {\n        fclose(logFile);\n    }\n#endif\n    \n    if (res == CURLE_OK && httpStatus == 200)\n    {\n        ::moveFile(m_outputTmp, m_output);\n        if (m_mtime > 0)\n        {\n            updateFileTime(m_output, m_mtime);\n        }\n#ifndef NDEBUG\n        ::deleteFile(logPath);\n#endif\n    }\n\n    return res == CURLE_OK;\n}\n\nbool Task::copyFile()\n{\n\treturn ::copyFile(m_url, m_output);\n}\n\nsize_t Task::writeData(void *buffer, size_t size, size_t nmemb)\n{\n\tsize_t bytesToWrite = size * nmemb;\n\tif (appendFile(m_outputTmp, reinterpret_cast<const unsigned char *>(buffer), bytesToWrite))\n\t{\n\t\treturn bytesToWrite;\n\t}\n\treturn 0;\n}\n\nstd::atomic_uint32_t Downloader::m_nextTaskId(1u);\n\nDownloader::Downloader(Logger* logger) : m_logger(logger)\n{\n    m_noMoreTask = false;\n    m_downloadTaskSize = 0;\n}\n\nDownloader::~Downloader()\n{\n}\n\nvoid Downloader::initialize()\n{\n    curl_global_init(CURL_GLOBAL_ALL);\n}\n\nvoid Downloader::uninitialize()\n{\n    curl_global_cleanup();\n}\n\nvoid Downloader::setUserAgent(const std::string& userAgent)\n{\n    m_userAgent = userAgent;\n}\n\nuint32_t Downloader::addTask(const std::string &url, const std::string& output, time_t mtime, std::string type/* = \"\"*/)\n{\n    uint32_t taskId = m_nextTaskId.fetch_add(1);\n    \n#ifndef NDEBUG\n    if (url == \"/0\" || url.empty() || output.empty())\n    {\n        int aa = 0;\n    }\n    \n    if (!startsWith(url, \"http://\") && !startsWith(url, \"https://\") && !startsWith(url, \"file://\"))\n    {\n        assert(false);\n    }\n#endif\n    m_mtx.lock();\n    if (m_threads.empty())\n    {\n        for (int idx = 0; idx < 4; idx++)\n        {\n            m_threads.push_back(std::thread(&Downloader::run, this, idx));\n        }\n    }\n    m_mtx.unlock();\n    \n    std::string formatedPath = output;\n    std::replace(formatedPath.begin(), formatedPath.end(), DIR_SEP_R, DIR_SEP);\n    std::string uid = url + output;\n    bool existed = false;\n    \n    m_mtx.lock();\n    if (startsWith(url, \"file://\"))\n    {\n        Task task(taskId, url.substr(7), formatedPath, mtime, true);\n        m_copyQueue.push(task);\n    }\n    else\n    {\n        std::map<std::string, std::string>::const_iterator it = m_urls.find(url);\n        existed = (it != m_urls.cend());\n        \n        if (!existed)\n        {\n            m_urls[url] = output;\n            Task task(taskId, url, formatedPath, mtime);\n            task.setUserAgent(m_userAgent);\n            m_queue.push(task);\n            m_downloadTaskSize++;\n#ifndef NDEBUG\n            std::string key = type.empty() ? \"unkownd\" : type;\n            std::map<std::string, uint32_t>::iterator it = m_statsType.find(key);\n            if (it == m_statsType.end())\n            {\n                m_statsType.insert(std::pair<std::string, uint32_t>(key, 1));\n            }\n            else\n            {\n                ++(it->second);\n            }\n#endif\n        }\n        else if (output != it->second)\n        {\n            Task task(taskId, it->second, formatedPath, mtime, true);\n            m_copyQueue.push(task);\n        }\n    }\n\n    m_mtx.unlock();\n    \n    if (existed)\n    {\n#ifndef NDEBUG\n        // printf(\"URL Existed: %s\\r\\n\", url.c_str());\n#endif\n    }\n    \n    return taskId;\n}\n\n#ifndef NDEBUG\nstd::string Downloader::getStats() const\n{\n    std::string stats;\n    \n    for (std::map<std::string, uint32_t>::const_iterator it = m_statsType.cbegin(); it != m_statsType.cend(); ++it)\n    {\n        stats.append(it->first + \":\" + std::to_string(it->second) + \" \");\n    }\n    \n    return stats;\n}\n#endif\n\nvoid Downloader::setNoMoreTask()\n{\n    m_mtx.lock();\n    m_noMoreTask = true;\n    m_mtx.unlock();\n}\n\nvoid Downloader::run(int idx)\n{\n    while(1)\n    {\n        bool found = false;\n        bool noMoreTask = false;\n        \n        Task task;\n        m_mtx.lock();\n        \n        noMoreTask = m_noMoreTask && m_downloadTaskSize == 0;\n        \n        size_t queueSize = m_queue.size();\n        if (queueSize > 0)\n        {\n            task = m_queue.front();\n            m_queue.pop();\n            found = true;\n        }\n        else if (noMoreTask)\n        {\n            if (!m_copyQueue.empty())\n            {\n                task = m_copyQueue.front();\n                m_copyQueue.pop();\n                found = true;\n            }\n        }\n\n        m_mtx.unlock();\n        \n        if (found)\n        {\n            // run task\n#ifndef NDEBUG\n            if (!task.isLocalCopy())\n            {\n                std::string log = \"Start downloading: \" + task.getUrl() + \" (\" + std::to_string(task.getRetries() + 1) + \")\";\n                // m_logger->debug(log);\n            }\n#endif\n            bool succeeded = task.run();\n            if (!task.isLocalCopy())\n            {\n                m_mtx.lock();\n                // unsigned int dlTaskSizeChanging = 0;\n                if (!succeeded && task.getRetries() < Task::MAX_RETRIES)\n                {\n                    // Retry to download it\n                    m_queue.push(task);\n                }\n                if (succeeded || task.getRetries() >= Task::MAX_RETRIES)\n                {\n                    m_downloadTaskSize--;\n                }\n                m_mtx.unlock();\n#ifndef NDEBUG\n                std::string log = \"Downloading \";\n                log += succeeded ? \"Succeeded\" : \"Failed\";\n                log += \":\" + task.getUrl() + \" => \" + task.getOutput() + \" (\" + std::to_string(task.getRetries() + 1) + \")\";\n                // m_logger->debug(log);\n#endif\n                \n                if (!succeeded/* && task.getRetries() >= Task::MAX_RETRIES*/)\n                {\n                    std::string log = \"Failed Download(\" + std::to_string(task.getRetries()) + \"): \" + task.getUrl() + \" => \" + task.getOutput() + \" ERR:\" + task.getError();\n                    m_logger->write(log);\n                }\n                else if (task.getRetries() > 1) // succeeded but ever failed\n                {\n                    std::string log = \"Succeeded Download(\" + std::to_string(task.getRetries()) + \"): \" + task.getUrl() + \" => \" + task.getOutput();\n                    m_logger->write(log);\n                }\n            }\n            \n            continue;\n        }\n        if (noMoreTask)\n        {\n            break;\n        }\n        else\n        {\n            std::this_thread::sleep_for(std::chrono::milliseconds(100));\n        }\n    }\n}\n\nvoid Downloader::cancel()\n{\n    std::queue<Task> empty;\n\tstd::queue<Task> empty2;\n    m_mtx.lock();\n    m_downloadTaskSize -= m_queue.size();\n    m_queue.swap(empty);\n\tm_copyQueue.swap(empty2);\n    m_mtx.unlock();\n}\n\nvoid Downloader::shutdown()\n{\n    setNoMoreTask();\n    for (std::vector<std::thread>::iterator it = m_threads.begin(); it != m_threads.end(); ++it)\n    {\n        it->join();\n    }\n}\n\nint Downloader::getCount() const\n{\n    return static_cast<int>(m_urls.size());\n}\n\nint Downloader::getRunningCount() const\n{\n    size_t count  = 0;\n    m_mtx.lock();\n    count = m_queue.size();\n    m_mtx.unlock();\n    \n    return static_cast<int>(count);\n}\n\n#endif // USING_DOWNLOADER\n"
  },
  {
    "path": "WechatExporter/core/Downloader.h",
    "content": "//\n//  Downloader.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#ifndef Downloader_h\n#define Downloader_h\n\n#include <stdio.h>\n#include <string>\n#include <queue>\n#include <map>\n#include <utility>\n#include <thread>\n#include <mutex>\n#include <atomic>\n#include \"Logger.h\"\n\n#ifdef USING_DOWNLOADER\n\nclass Task\n{\nprotected:\n    uint32_t m_taskId;\n    std::string m_url;\n    std::string m_output;\n    std::string m_outputTmp;\n    std::string m_error;\n    std::string m_userAgent;\n    time_t m_mtime;\n    bool m_localCopy;\n    unsigned int m_retries;\npublic:\n    static const unsigned int MAX_RETRIES = 3;\npublic:\n    Task() : m_taskId(0), m_localCopy(false)\n    {\n    }\n    \n    Task(uint32_t taskId, const std::string &url, const std::string& output, time_t mtime, bool localCopy = false);\n    \n    void setUserAgent(const std::string& userAgent)\n    {\n        m_userAgent = userAgent;\n    }\n    \n    inline std::string getUrl() const\n    {\n        return m_url;\n    }\n    \n    inline std::string getOutput() const\n    {\n        return m_output;\n    }\n    \n    inline std::string getError() const\n    {\n        return m_error;\n    }\n    \n    bool isLocalCopy() const\n    {\n        return m_localCopy;\n    }\n    \n    Task& operator=(const Task& task)\n    {\n        if (this != &task)\n        {\n            m_taskId = task.m_taskId;\n            m_url = task.m_url;\n            m_output = task.m_output;\n            m_mtime = task.m_mtime;\n            m_localCopy = task.m_localCopy;\n            m_retries = task.m_retries;\n        }\n        \n        return *this;\n    }\n    \n    size_t writeData(void *buffer, size_t size, size_t nmemb);\n    bool run();\n    unsigned int getRetries() const;\n    \nprotected:\n    bool downloadFile();\n    bool copyFile();\n};\n\nclass Downloader\n{\nprotected:\n    std::queue<Task> m_queue;\n    std::queue<Task> m_copyQueue;\n    std::map<std::string, std::string> m_urls;  // url => local file path for first download\n    mutable std::mutex m_mtx;\n    bool m_noMoreTask;\n    size_t m_downloadTaskSize;    // +1 when task is added, -1 when download is completed\n    std::vector<std::thread> m_threads;\n    std::string m_userAgent;\n    static std::atomic_uint32_t m_nextTaskId;\n    \n    Logger* m_logger;\n    \npublic:\n    Downloader(Logger* logger);\n    ~Downloader();\n    \n    void setUserAgent(const std::string& userAgent);\n    \n    // return taskId\n    uint32_t addTask(const std::string &url, const std::string& output, time_t mtime, std::string type = \"\");\n    void setNoMoreTask();\n    void run(int idx);\n    \n    void cancel();\n    void shutdown();\n    int getCount() const;\n    int getRunningCount() const;\n\n    static void initialize();\n    static void uninitialize();\n    \n    static bool httpGet(const std::string& url, const std::vector<std::pair<std::string, std::string>>& headers, long& httpStatus, std::vector<unsigned char>& body);\n    \n#ifndef NDEBUG\n    std::string getStats() const;\n#endif\n    \nprotected:\n    const Task& dequeue();\n    \n    \n#ifndef NDEBUG\n    std::map<std::string, uint32_t> m_statsType;\n#endif\n};\n\n#endif // USING_DOWNLOADER\n\n#endif /* Downloader_h */\n"
  },
  {
    "path": "WechatExporter/core/ExportContext.h",
    "content": "//\n//  ExportContext.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/7/15.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef ExportContext_h\n#define ExportContext_h\n\n#define EXPORT_CONTEXT_VERSION_2_0  \"2.0\"\n#define EXPORT_CONTEXT_VERSION      \"3.0\"\n\n#include <string>\n#include <vector>\n#include <list>\n#include <map>\n#include <set>\n\n#include <sqlite3.h>\n\n#define RETURN_FALSE_IF_FAILED(rc) if (SQLITE_OK != rc) { return false; }\n\n\n#define WXEXP_DATA_FOLDER   \".wxexp\"\n#ifndef NDEBUG\n#define WXEXP_DATA_FILE   \"wxexp.txt\"\n#else\n#define WXEXP_DATA_FILE   \"wxexp.dat\"\n#endif\n\nclass ExportContext\n{\nprivate:\n    std::string m_version;\n    uint64_t m_options;\n    std::time_t m_exportTime;\n    \n    std::string m_usrName;\n    std::vector<std::pair<std::string, std::list<std::string>>> m_accountAndSessions;\n    \n    std::map<std::string, int64_t> m_maxIdForSessions;  //\n    \n    sqlite3*        m_db;\n    sqlite3_stmt*   m_stmt;\n    \n    uint64_t        m_maxIdForSession;\n    \nprivate:\n    void closeDb()\n    {\n        if (NULL != m_stmt)\n        {\n            sqlite3_finalize(m_stmt);\n            m_stmt = NULL;\n        }\n        if (NULL != m_db)\n        {\n            sqlite3_close(m_db);\n            m_db = NULL;\n        }\n    }\n    \npublic:\n    ExportContext() : m_db(NULL), m_stmt(NULL), m_maxIdForSession(0)\n    {\n    }\n    \n    ~ExportContext()\n    {\n        closeDb();\n    }\n    \n    uint64_t getOptions() const\n    {\n        return m_options;\n    }\n    \n    void setOptions(uint64_t options)\n    {\n        m_options = options;\n    }\n    \n    void refreshExportTime()\n    {\n        std::time(&m_exportTime);\n    }\n    \n    std::time_t getExportTime() const\n    {\n        return m_exportTime;\n    }\n    \n    size_t getNumberOfSessions() const\n    {\n        return m_maxIdForSessions.size();\n    }\n    \n    size_t getNumberOfAccounts() const\n    {\n        return m_accountAndSessions.size();\n    }\n    \n    std::string getVersion() const\n    {\n        return m_version;\n    }\n    \n    size_t getNumberOfSessions(const std::string& account) const\n    {\n        size_t numberOfSessions = 0;\n        for (auto it = m_accountAndSessions.cbegin(); it != m_accountAndSessions.cend(); ++it)\n        {\n            if (it->first == account)\n            {\n                numberOfSessions = it->second.size();\n                break;\n            }\n        }\n        return numberOfSessions;\n    }\n\n    bool getMaxId(const std::string& accountUsrName, const std::string& sessionUsrName, int64_t& maxId) const\n    {\n        std::string key = accountUsrName + \"\\t\" + sessionUsrName;\n        std::map<std::string, int64_t>::const_iterator it = m_maxIdForSessions.find(key);\n        if (it != m_maxIdForSessions.cend())\n        {\n            maxId = it->second;\n            return true;\n        }\n        return false;\n    }\n\n    void setMaxId(const std::string& accountUsrName, const std::string& sessionUsrName, int64_t maxId)\n    {\n        auto it = m_accountAndSessions.begin();\n        for (; it != m_accountAndSessions.end(); ++it)\n        {\n            if (it->first == accountUsrName)\n            {\n                break;\n            }\n        }\n        // m_accountAndSessions\n        if (it == m_accountAndSessions.end())\n        {\n            it = m_accountAndSessions.insert(it, std::pair<std::string, std::list<std::string>>(accountUsrName, std::list<std::string>()));\n        }\n        it->second.push_back(sessionUsrName);\n\n        std::string key = accountUsrName + \"\\t\" + sessionUsrName;\n        std::map<std::string, int64_t>::iterator it2 = m_maxIdForSessions.find(key);\n        if (it2 != m_maxIdForSessions.end())\n        {\n            maxId = it2->second;\n        }\n        else\n        {\n            m_maxIdForSessions.insert(it2, std::pair<std::string, int64_t>(key, maxId));\n        }\n    }\n    \n    bool prepareUserDatabase(const Friend& user, const std::string& outputDir)\n    {\n        closeDb();\n        std::string dbPath = combinePath(outputDir, WXEXP_DATA_FOLDER, user.getHash() + \".db\");\n        int rc = openSqlite3Database(dbPath, &m_db, false);\n        \n        if (SQLITE_OK == rc)\n        {\n            sqlite3_exec(m_db, \"PRAGMA mmap_size=268435456;PRAGMA synchronous=OFF;\", NULL, NULL, NULL); // 256M:268435456  2M 2097152\n        }\n\n        return (rc == SQLITE_OK);\n    }\n    \n    bool prepareSessionTable(const Session& session)\n    {\n        if (NULL == m_db)\n        {\n            return false;\n        }\n        \n        if (NULL != m_stmt)\n        {\n            sqlite3_finalize(m_stmt);\n            m_stmt = NULL;\n        }\n        \n        std::string sql = \"CREATE TABLE IF NOT EXISTS Chat_\" + session.getHash() + \"(CreateTime INTEGER DEFAULT 0, Des INTEGER, MesLocalID INTEGER PRIMARY KEY, Message TEXT, MesSvrID INTEGER DEFAULT 0, Status INTEGER DEFAULT 0, TableVer INTEGER DEFAULT 1, Type INTEGER);\";\n        sql += \"CREATE INDEX IF NOT EXISTS Chat_\" + session.getHash() + \"_Index ON Chat_\" + session.getHash() + \"(MesSvrID);\";\n        sql += \"CREATE INDEX IF NOT EXISTS Chat_\" + session.getHash() + \"_Index2 ON Chat_\" + session.getHash() + \"(CreateTime)\";\n        \n        int rc = sqlite3_exec(m_db, sql.c_str(), NULL, NULL, NULL);\n        RETURN_FALSE_IF_FAILED(rc);\n        \n        m_maxIdForSession = 0;\n        sql = \"SELECT MAX(MesLocalID) from Chat_\" + session.getHash();\n        \n        rc = sqlite3_prepare_v2(m_db, sql.c_str(), (int)(sql.size()), &m_stmt, NULL);\n        if (rc == SQLITE_OK)\n        {\n            if ((rc = sqlite3_step(m_stmt)) == SQLITE_ROW)\n            {\n                m_maxIdForSession = sqlite3_column_int64(m_stmt, 0);\n            }\n            sqlite3_finalize(m_stmt);\n            m_stmt = NULL;\n        }\n\n        sql = \"INSERT INTO Chat_\" + session.getHash() + \"(CreateTime,Des,MesLocalID,Message,MesSvrID,Status,TableVer,Type) VALUES(?,?,?,?,?,?,?,?)\";\n        rc = sqlite3_prepare_v2(m_db, sql.c_str(), (int)(sql.size()), &m_stmt, NULL);\n        \n        return (rc == SQLITE_OK);\n    }\n    \n    bool insertMessage(const Session& session, const WXMSG& msg)\n    {\n        if (msg.msgIdValue <= m_maxIdForSession)\n        {\n            return true;\n        }\n        \n        if (NULL == m_db || NULL == m_stmt)\n        {\n            return false;\n        }\n\n        sqlite3_reset(m_stmt);\n        sqlite3_clear_bindings(m_stmt);\n        \n        int rc = sqlite3_bind_int(m_stmt, 1, (int)msg.createTime);\n        rc = sqlite3_bind_int(m_stmt, 2, msg.des);\n        RETURN_FALSE_IF_FAILED(rc);\n        rc = sqlite3_bind_int64(m_stmt, 3, msg.msgIdValue);\n        RETURN_FALSE_IF_FAILED(rc);\n        rc = sqlite3_bind_text(m_stmt, 4, msg.content.c_str(), (int)(msg.content.size()), NULL);\n        RETURN_FALSE_IF_FAILED(rc);\n        rc = sqlite3_bind_int64(m_stmt, 5, (sqlite_int64)msg.msgSvrId);\n        RETURN_FALSE_IF_FAILED(rc);\n        rc = sqlite3_bind_int(m_stmt, 6, msg.status);\n        RETURN_FALSE_IF_FAILED(rc);\n        rc = sqlite3_bind_int(m_stmt, 7, msg.tableVersion);\n        RETURN_FALSE_IF_FAILED(rc);\n        rc = sqlite3_bind_int(m_stmt, 8, msg.type);\n        RETURN_FALSE_IF_FAILED(rc);\n        \n        rc = sqlite3_step(m_stmt);\n        if (SQLITE_DONE != rc && SQLITE_ROW != rc)\n        {\n            const char* err = sqlite3_errmsg(m_db);\n            int aa = 0;\n        }\n        \n        return (rc == SQLITE_DONE || rc == SQLITE_ROW);\n    }\n    \n    void saveMessage(const WXMSG& msg)\n    {\n        // CREATE TABLE IF NOT EXISTS Chat_%%CHAT_ID%%(CreateTime INTEGER DEFAULT 0, Des INTEGER, ImgStatus INTEGER DEFAULT 0, MesLocalID INTEGER, Message TEXT, MesSvrID INTEGER DEFAULT 0  PRIMARY KEY, Status INTEGER DEFAULT 0, Type INTEGER)\n\n        // CREATE INDEX IF NOT EXISTS Chat_%%CHAT_ID%%_ct ON Chat_%%CHAT_ID%% (CreateTime)\n    }\n\n    std::string serialize() const\n    {\n        Json::Value contextObj(Json::objectValue);\n        contextObj[\"version\"] = Json::Value(EXPORT_CONTEXT_VERSION);\n        contextObj[\"options\"] = Json::Value(m_options);\n        contextObj[\"exportTime\"] = Json::Value(static_cast<int64_t>(m_exportTime));\n        contextObj[\"accounts\"] = Json::Value(Json::arrayValue);\n        Json::Value& accounts = contextObj[\"accounts\"];\n        \n        int64_t maxId = 0;\n        auto it3 = m_maxIdForSessions.cend();\n        \n        for (auto it = m_accountAndSessions.cbegin(); it != m_accountAndSessions.cend(); ++it)\n        {\n            Json::Value& accountObj = accounts.append(Json::Value(Json::objectValue));\n            \n            accountObj[\"usrName\"] = Json::Value(it->first);\n            accountObj[\"sessions\"] = Json::Value(Json::arrayValue);\n            \n            Json::Value& sessionsObj = accountObj[\"sessions\"];\n            const std::list<std::string>& sessions = it->second;\n            \n            std::set<std::string> uniqueSessions;\n            \n            for (auto it2 = sessions.cbegin(); it2 != sessions.cend(); ++it2)\n            {\n                Json::Value& itemObj = sessionsObj.append(Json::Value(Json::objectValue));\n                \n                std::string key = it->first + \"\\t\" + *it2;\n                \n                itemObj[\"usrName\"] = Json::Value(*it2);\n                maxId = 0;\n                it3 = m_maxIdForSessions.find(key);\n                if (it3 != m_maxIdForSessions.cend())\n                {\n                    maxId = it3->second;\n                }\n                itemObj[\"maxId\"] = Json::Value(maxId);\n            }\n        }\n\n        Json::StreamWriterBuilder builder;\n#ifndef NDEBUG\n        builder[\"indentation\"] = \"\\t\";\n        builder[\"emitUTF8\"] = true;\n#else\n        builder[\"indentation\"] = \"\";\n#endif\n        \n        return Json::writeString(builder, contextObj);\n    }\n    \n    bool upgrade(const std::string& data)\n    {\n        Json::CharReaderBuilder builder;\n        std::unique_ptr<Json::CharReader> reader(builder.newCharReader());\n\n        Json::Value contextObj;\n        Json::String error;\n        if (!reader->parse(data.c_str(), data.c_str() + data.size(), &contextObj, &error))\n        {\n            return false;\n        }\n        \n        if (contextObj.isMember(\"version\"))\n        {\n            m_version = contextObj[\"version\"].asString();\n        }\n        else\n        {\n            m_version.clear();\n        }\n        \n        if (m_version == EXPORT_CONTEXT_VERSION)\n        {\n            return true;\n        }\n\n        // First version\n        if (!contextObj.isObject() || !contextObj.isMember(\"options\") || !contextObj.isMember(\"exportTime\") || !contextObj.isMember(\"sessions\"))\n        {\n            return false;\n        }\n        \n        const Json::Value& maxIdForSessions = contextObj[\"sessions\"];\n        if (!maxIdForSessions.isArray())\n        {\n            return false;\n        }\n\n        if (m_version != EXPORT_CONTEXT_VERSION)\n        {\n            int options = contextObj[\"options\"].asInt();\n            ExportOption eo;\n            eo.fromSessionParsingOptions(options);\n            m_options = (uint64_t)eo;\n        }\n        else\n        {\n            m_options = contextObj[\"options\"].asUInt64();\n        }\n        \n        // m_options = contextObj[\"options\"].asInt();\n        m_exportTime = static_cast<std::time_t>(contextObj[\"exportTime\"].asInt64());\n        \n        m_maxIdForSessions.clear();\n    \n        for (Json::ArrayIndex idx = 0; idx < maxIdForSessions.size(); idx++)\n        {\n            const Json::Value& itemObj = maxIdForSessions[idx];\n            \n            if (!itemObj.isObject() || !itemObj.isMember(\"usrName\") || !itemObj.isMember(\"maxId\"))\n            {\n                return false;\n            }\n            \n            m_maxIdForSessions.insert(std::pair<std::string, int64_t>(m_usrName + \"\\t\" + itemObj[\"usrName\"].asString(), itemObj[\"maxId\"].asInt64()));\n        }\n        \n        return true;\n    }\n\n    bool unserialize(const std::string& data)\n    {\n        Json::CharReaderBuilder builder;\n        std::unique_ptr<Json::CharReader> reader(builder.newCharReader());\n\n        Json::Value contextObj;\n        Json::String error;\n        if (!reader->parse(data.c_str(), data.c_str() + data.size(), &contextObj, &error))\n        {\n            return false;\n        }\n        \n        if (contextObj.isMember(\"version\"))\n        {\n            m_version = contextObj[\"version\"].asString();\n        }\n        else\n        {\n            m_version.clear();\n        }\n        \n        if (m_version.empty())\n        {\n            // First version\n            if (!contextObj.isObject() || !contextObj.isMember(\"options\") || !contextObj.isMember(\"exportTime\") || !contextObj.isMember(\"sessions\"))\n            {\n                return false;\n            }\n            \n            const Json::Value& maxIdForSessions = contextObj[\"sessions\"];\n            if (!maxIdForSessions.isArray())\n            {\n                return false;\n            }\n\n            m_options = contextObj[\"options\"].asUInt64();\n            m_exportTime = static_cast<std::time_t>(contextObj[\"exportTime\"].asInt64());\n            \n            m_accountAndSessions.clear();\n            m_maxIdForSessions.clear();\n\n            for (Json::ArrayIndex idx = 0; idx < maxIdForSessions.size(); idx++)\n            {\n                const Json::Value& itemObj = maxIdForSessions[idx];\n                \n                if (!itemObj.isObject() || !itemObj.isMember(\"usrName\") || !itemObj.isMember(\"maxId\"))\n                {\n                    return false;\n                }\n                \n                m_maxIdForSessions.insert(std::pair<std::string, int64_t>(m_usrName + \"\\t\" + itemObj[\"usrName\"].asString(), itemObj[\"maxId\"].asInt64()));\n            }\n        }\n        else\n        {\n            if (!contextObj.isObject() || !contextObj.isMember(\"options\") || !contextObj.isMember(\"exportTime\") || !contextObj.isMember(\"accounts\"))\n            {\n                return false;\n            }\n            \n            const Json::Value& accounts = contextObj[\"accounts\"];\n            if (!accounts.isArray())\n            {\n                return false;\n            }\n\n            if (m_version == EXPORT_CONTEXT_VERSION_2_0)\n            {\n                int options = contextObj[\"options\"].asInt();\n                ExportOption eo;\n                eo.fromSessionParsingOptions(options);\n                m_options = (uint64_t)eo;\n            }\n            else\n            {\n                m_options = contextObj[\"options\"].asUInt64();\n            }\n\t\t\tm_exportTime = static_cast<std::time_t>(contextObj[\"exportTime\"].asInt64());\n            \n            m_accountAndSessions.clear();\n            m_maxIdForSessions.clear();\n            \n            if (!accounts.empty())\n            {\n                m_accountAndSessions.reserve(accounts.size());\n            }\n\n            for (Json::ArrayIndex idx = 0; idx < accounts.size(); idx++)\n            {\n                const Json::Value& accountObj = accounts[idx];\n                \n                if (!accountObj.isObject() || !accountObj.isMember(\"usrName\") || !accountObj.isMember(\"sessions\"))\n                {\n                    return false;\n                }\n                const Json::Value& sessionsObj = accountObj[\"sessions\"];\n                if (!sessionsObj.isArray())\n                {\n                    return false;\n                }\n                \n                std::string account = accountObj[\"usrName\"].asString();\n                auto it = m_accountAndSessions.insert(m_accountAndSessions.end(), std::pair<std::string, std::list<std::string>>(account, std::list<std::string>()));\n\n                for (Json::ArrayIndex idx = 0; idx < sessionsObj.size(); idx++)\n                {\n                    const Json::Value& itemObj = sessionsObj[idx];\n                    \n                    if (!itemObj.isObject() || !itemObj.isMember(\"usrName\") || !itemObj.isMember(\"maxId\"))\n                    {\n                        return false;\n                    }\n                    \n                    auto it2 = it->second.insert(it->second.begin(), itemObj[\"usrName\"].asString());\n                    int64_t maxId = itemObj[\"maxId\"].asInt64();\n                    if (maxId != 0)\n                    {\n                        m_maxIdForSessions.insert(std::pair<std::string, int64_t>(account + \"\\t\" + (*it2), itemObj[\"maxId\"].asInt64()));\n                    }\n                }\n                \n            }\n        }\n\n        return true;\n    }\n};\n\nclass PageInfo\n{\nprivate:\n    uint32_t m_page;\n    uint32_t m_count;\n    uint16_t m_year;\n    uint16_t m_month;\n    std::string m_text;\n    std::string m_fileName;\n\npublic:\n    \n    PageInfo(uint32_t page, uint32_t count, uint16_t year, uint16_t month, const std::string& fileName) : m_page(page), m_count(count), m_year(year), m_month(month), m_text(std::to_string(page + 1)), m_fileName(fileName)\n    {\n    }\n    \n    PageInfo(uint32_t page, uint32_t count, uint16_t year, uint16_t month, const std::string& fileName, const std::string& text) : m_page(page), m_count(count), m_year(year), m_month(month), m_text(text), m_fileName(fileName)\n    {\n    }\n\n    uint32_t getPage() const\n    {\n        return m_page;\n    }\n    \n    uint32_t getCount() const\n    {\n        return m_count;\n    }\n    \n    void setCount(uint32_t count)\n    {\n        m_count = count;\n    }\n    \n    uint16_t getYear() const\n    {\n        return m_year;\n    }\n    \n    uint16_t getMonth() const\n    {\n        return m_month;\n    }\n    \n    std::string getText() const\n    {\n        return m_text;\n    }\n    \n    std::string getFileName() const\n    {\n        return m_fileName;\n    }\n    \n};\n\nclass WXMSG;\n\nclass Pager\n{\npublic:\n    \n    Pager() : m_last(m_pages.end()), m_totalNumberOfPreviousPages(0)\n    {\n    }\n    \n    virtual ~Pager()\n    {\n    }\n    \n    virtual bool buildNewPage(const WXMSG *msg, const std::vector<std::string>& messages)\n    {\n        return false;\n    }\n    \n    const std::vector<PageInfo>& getPages() const\n    {\n        return m_pages;\n    }\n    \n    bool hasPages() const\n    {\n        return !m_pages.empty();\n    }\n    \nprotected:\n    std::vector<PageInfo> m_pages;\n    std::vector<PageInfo>::iterator m_last;\n    uint64_t m_totalNumberOfPreviousPages;\n};\n\nclass NumberPager : public Pager\n{\npublic:\n    \n    NumberPager(uint32_t pageSize) : Pager(), m_pageSize(pageSize), m_numberOfRawMsgs(0)\n    {\n    }\n    \n    virtual ~NumberPager()\n    {\n    }\n    \n    virtual bool buildNewPage(const WXMSG *msg, const std::vector<std::string>& messages)\n    {\n        m_numberOfRawMsgs++;\n        bool newPage = ((m_numberOfRawMsgs % m_pageSize) == 1);\n        if (newPage)\n        {\n            m_last = m_pages.emplace(m_pages.end(), (uint32_t)m_pages.size(), (uint32_t)(messages.size() - m_totalNumberOfPreviousPages), 0, 0, std::to_string(m_pages.size() + 1));\n            m_totalNumberOfPreviousPages = messages.size();\n            return false;\n        }\n        else\n        {\n            m_last->setCount(m_last->getCount() + (uint32_t)(messages.size() - m_totalNumberOfPreviousPages));\n            m_totalNumberOfPreviousPages = messages.size();\n        }\n        \n        return true;\n        \n\n        return newPage;\n    }\n    \nprivate:\n    uint32_t m_pageSize;\n    uint64_t m_numberOfRawMsgs;\n};\n\nclass YearPager : public Pager\n{\npublic:\n    \n    YearPager() : Pager(), m_previousYear(0)\n    {\n    }\n    \n    bool buildNewPage(const WXMSG *msg, const std::vector<std::string>& messages)\n    {\n        std::time_t ts = msg->createTime;\n        std::tm* t1 = std::localtime(&ts);\n        uint16_t year = t1->tm_year + 1900;\n        \n        if (m_previousYear != year)\n        {\n            m_last = m_pages.emplace(m_pages.end(), (uint32_t)m_pages.size(), (uint32_t)(messages.size() - m_totalNumberOfPreviousPages), year, 0, std::to_string(m_pages.size() + 1));\n            \n            m_totalNumberOfPreviousPages = messages.size();\n            m_previousYear = year;\n            return false;\n        }\n        else\n        {\n            m_last->setCount(m_last->getCount() + (uint32_t)(messages.size() - m_totalNumberOfPreviousPages));\n            m_totalNumberOfPreviousPages = messages.size();\n        }\n        \n        return true;\n    }\n    \nprotected:\n    uint16_t m_previousYear;\n    \n};\n\nclass YearMonthPager : public YearPager\n{\npublic:\n    \n    YearMonthPager() : YearPager(), m_previousMonth(0)\n    {\n    }\n    \n    bool buildNewPage(const WXMSG *msg, const std::vector<std::string>& messages)\n    {\n        std::time_t ts = msg->createTime;\n        std::tm* t1 = std::localtime(&ts);\n        uint16_t year = t1->tm_year + 1900;\n        uint16_t month = t1->tm_mon + 1;\n        \n        if ((m_previousYear != year) || (m_previousMonth != month))\n        {\n            m_last = m_pages.emplace(m_pages.end(), (uint32_t)m_pages.size(), (uint32_t)(messages.size() - m_totalNumberOfPreviousPages), year, month, std::to_string(m_pages.size() + 1));\n            \n            m_totalNumberOfPreviousPages = messages.size();\n            m_previousYear = year;\n            m_previousMonth = month;\n            return false;\n        }\n        else\n        {\n            m_last->setCount(m_last->getCount() + (uint32_t)(messages.size() - m_totalNumberOfPreviousPages));\n            m_totalNumberOfPreviousPages = messages.size();\n        }\n        \n        return true;\n    }\n    \nprotected:\n    uint16_t m_previousMonth;\n    \n};\n\n\n#endif /* ExportContext_h */\n"
  },
  {
    "path": "WechatExporter/core/ExportNotifier.h",
    "content": "#ifndef ExportNotifier_h\n#define ExportNotifier_h\n\nclass ExportNotifier\n{\npublic:\n\n\tvirtual ~ExportNotifier() {}\n\n    virtual void onStart() const = 0;\n\tvirtual void onProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const = 0;\n\tvirtual void onComplete(bool cancelled) const = 0;\n    \n    virtual void onUserSessionStart(const std::string& usrName, uint32_t numberOfSessions) const = 0;\n    virtual void onUserSessionComplete(const std::string& usrName) const = 0;\n    \n    virtual void onSessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages) const = 0;\n    virtual void onSessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const = 0;\n    virtual void onSessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled) const = 0;\n    \n    virtual void onTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks) const = 0;\n    virtual void onTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalMessages) const = 0;\n    virtual void onTasksComplete(const std::string& usrName, bool cancelled) const = 0;\n\n};\n\n#endif /* ExportNotifier_h */\n"
  },
  {
    "path": "WechatExporter/core/ExportOption.h",
    "content": "//\n//  ExportOption.h\n//  WechatExporter\n//\n//  Created by Matthew on 2022/7/10.\n//  Copyright © 2022 Matthew. All rights reserved.\n//\n\n#ifndef ExportOption_h\n#define ExportOption_h\n\nenum SessionParsingOption\n{\n    SPO_IGNORE_AVATAR = 1 << 0,\n    SPO_IGNORE_AUDIO = 1 << 1,\n    SPO_IGNORE_IMAGE = 1 << 2,\n    SPO_IGNORE_VIDEO = 1 << 3,\n    SPO_IGNORE_EMOJI = 1 << 4,\n    SPO_IGNORE_FILE = 1 << 5,\n    SPO_IGNORE_CARD = 1 << 6,\n    SPO_IGNORE_SHARING = 1 << 7,\n    SPO_IGNORE_HTML_ENC = 1 << 8,\n    SPO_TEXT_MODE = 0xFFFF,\n    SPO_PDF_MODE = 1 << 16,\n    SPO_DOC_MODE = 1 << 17,\n    \n    SPO_USING_REMOTE_EMOJI = 1 << 19,\n    SPO_DESC = 1 << 20,\n    SPO_ICON_IN_SESSION = 1 << 21,     // Put Head Icon and Emoji files in the folder of session\n    SPO_SYNC_LOADING = 1 << 22,\n    SPO_SUPPORT_FILTER = 1 << 23,\n    SPO_INCLUDING_SUBSCRIPTION = 1 << 24,\n    \n    SPO_OVERWRITE_EXISTING_FIlE = 1 << 26,\n    SPO_EXP_GROUP_MEMBERS = 1 << 27,\n    SPO_EXP_CONTACTS = 1 << 28,\n    SPO_OUTPUT_DBG_LOGS = 1 << 29,\n    SPO_INCREMENTAL_EXP = 1 << 30,\n\n    SPO_END\n};\n\nenum EXPORT_OPTION : uint64_t\n{\n    EO_INCREMENTAL_EXP = 1ull << 0,\n    EO_OUTPUT_DBG_LOGS = 1ull << 1,\n    EO_OVERWRT_EXISTING_FIlE = 1ull << 2,\n    EO_FILTER_BY_NAME = 1ull << 3,\n    EO_EXP_GROUP_MEMBERS = 1ull << 4,\n    EO_EXP_CONTACTS = 1ull << 5,\n\n    EO_TEXT_MODE = 1ull << 12,\n    EO_PDF_MODE = 1ull << 13,\n    EO_DOC_MODE = 1ull << 14,\n    \n    EO_TIME_DESC = 1ull << 20,\n    EO_USING_REMOTE_EMOJI = 1ull << 21,\n\n    // Sync\n    // Async\n    //  onscroll\n    //  normal pager\n    //\n    \n    EO_LOADING_ONSCRLL = 1ull << 28,\n\n    EO_PAGER_NORMAL = 1ull << 29,\n    EO_PAGER_YEAR = 1ull << 30,\n    EO_PAGER_MONTH = 1ull << 31,\n    \n    EO_ASYNC_MASK = 0xFull << 28,  // bits\n    EO_PAGER_MASK = 0x7ull << 29,  // bits\n    \n    EO_SUPPORT_FILTER = 1ull << 40,\n    EO_INCLUDING_SUBSCRIPTION = 1ull << 41,\n\n    EO_END,\n    // EO_LARGR_VALUE =\n};\n\nclass ExportOption\n{\npublic:\n    ExportOption() : m_options(0)\n    {\n    }\n    \n    ExportOption(uint64_t options) : m_options(options)\n    {\n    }\n    \n    ExportOption& operator=(uint64_t options)\n    {\n        m_options = options;\n        return *this;\n    }\n    \n    void setTextMode(bool textMode = true)\n    {\n        if (textMode)\n            m_options |= EO_TEXT_MODE;\n        else\n            m_options &= ~EO_TEXT_MODE;\n    }\n    \n    bool isHtmlMode() const\n    {\n        return (m_options & EO_TEXT_MODE) == 0 && (m_options & EO_PDF_MODE) == 0;\n    }\n    \n    bool isTextMode() const\n    {\n        return (m_options & EO_TEXT_MODE) == EO_TEXT_MODE;\n    }\n\n    void setPdfMode(bool pdfMode = true)\n    {\n        setTextMode(!pdfMode);  // html mode\n        if (pdfMode)\n            m_options |= EO_PDF_MODE;\n        else\n            m_options &= ~EO_PDF_MODE;\n    }\n    \n    bool isPdfMode() const\n    {\n        return (m_options & EO_PDF_MODE) == EO_PDF_MODE;\n    }\n\n    void setOrder(bool asc = true)\n    {\n        if (asc)\n            m_options &= ~EO_TIME_DESC;\n        else\n            m_options |= EO_TIME_DESC;\n    }\n    \n    bool isDesc() const\n    {\n        return (m_options & EO_TIME_DESC) == EO_TIME_DESC;\n    }\n\n    void saveFilesInSessionFolder(bool flag = true)\n    {\n    }\n    \n    void filterByName()\n    {\n        m_options |= EO_FILTER_BY_NAME;\n    }\n    \n    bool isFilteredByName() const\n    {\n        return (m_options & EO_FILTER_BY_NAME) == EO_FILTER_BY_NAME;\n    }\n\n    void setIncrementalExporting(bool incrementalExporting)\n    {\n        if (incrementalExporting)\n            m_options |= EO_INCREMENTAL_EXP;\n        else\n            m_options &= ~EO_INCREMENTAL_EXP;\n    }\n    \n    bool isIncrementalExporting() const\n    {\n        return (m_options & EO_INCREMENTAL_EXP) == EO_INCREMENTAL_EXP;\n    }\n\n    void supportsFilter(bool supportsFilter = true)\n    {\n        if (supportsFilter)\n            m_options |= EO_SUPPORT_FILTER;\n        else\n            m_options &= ~EO_SUPPORT_FILTER;\n    }\n    \n    bool isSupportingFilter() const\n    {\n        return (m_options & EO_SUPPORT_FILTER) == EO_SUPPORT_FILTER;\n    }\n\n    void outputDebugLogs(bool outputDebugLogs)\n    {\n        if (outputDebugLogs)\n            m_options |= EO_OUTPUT_DBG_LOGS;\n        else\n            m_options &= ~EO_OUTPUT_DBG_LOGS;\n    }\n    \n    bool isOutputtingDebugLogs() const\n    {\n        return (m_options & EO_OUTPUT_DBG_LOGS) == EO_OUTPUT_DBG_LOGS;\n    }\n\n    void useRemoteEmoji(bool useEmojiUrl)\n    {\n        if (useEmojiUrl)\n            m_options |= EO_USING_REMOTE_EMOJI;\n        else\n            m_options &= ~EO_USING_REMOTE_EMOJI;\n    }\n    \n    bool isUsingRemoteEmoji() const\n    {\n        return (m_options & EO_USING_REMOTE_EMOJI) == EO_USING_REMOTE_EMOJI;\n    }\n    \n    void includesSubscription()\n    {\n        m_options |= EO_INCLUDING_SUBSCRIPTION;\n    }\n    \n    bool isIncludingSubscription() const\n    {\n        return (m_options & EO_INCLUDING_SUBSCRIPTION) == EO_INCLUDING_SUBSCRIPTION;\n    }\n    \n    bool isSyncLoading() const\n    {\n        return (m_options & EO_ASYNC_MASK) == 0;\n    }\n    \n    bool isAsyncLoading() const\n    {\n        return (m_options | EO_ASYNC_MASK) != 0;\n    }\n    \n    void setSyncLoading()\n    {\n        m_options &= ~EO_ASYNC_MASK;\n    }\n\n    void setLoadingDataOnScroll(bool loadingDataOnScroll = true)\n    {\n        if (loadingDataOnScroll)\n            m_options |= EO_LOADING_ONSCRLL;\n        else\n            m_options &= ~EO_LOADING_ONSCRLL;\n    }\n    \n    bool getLoadingDataOnScroll() const\n    {\n        return (m_options & EO_LOADING_ONSCRLL) == EO_LOADING_ONSCRLL;\n    }\n    \n    bool hasPager() const\n    {\n        return (m_options & EO_PAGER_MASK) != 0;\n    }\n    \n    void setPager()\n    {\n        m_options |= EO_PAGER_NORMAL;\n        m_options &= ~EO_PAGER_YEAR;\n        m_options &= ~EO_PAGER_MONTH;\n    }\n\n    \n    void setPagerByYear()\n    {\n        m_options &= ~EO_PAGER_NORMAL;\n        m_options |= EO_PAGER_YEAR;\n        m_options &= ~EO_PAGER_MONTH;\n    }\n\n    bool isPagerByYear() const\n    {\n        return (m_options & EO_PAGER_YEAR) == EO_PAGER_YEAR;\n    }\n    \n    void setPagerByMonth()\n    {\n        m_options &= ~EO_PAGER_NORMAL;\n        m_options &= ~EO_PAGER_YEAR;\n        m_options |= EO_PAGER_MONTH;\n    }\n\n    bool isPagerByMonth() const\n    {\n        return (m_options & EO_PAGER_MONTH) == EO_PAGER_MONTH;\n    }\n    \n    \n    operator uint64_t() const\n    {\n        return m_options;\n    }\n    \n    void fromSessionParsingOptions(int options)\n    {\n        m_options = 0;\n        \n        if ((options & SPO_TEXT_MODE) == SPO_TEXT_MODE)\n        {\n            m_options |= EO_TEXT_MODE;\n        }\n        if ((options & SPO_PDF_MODE) == SPO_PDF_MODE)\n        {\n            m_options |= EO_PDF_MODE;\n        }\n        if ((options & SPO_DOC_MODE) == SPO_DOC_MODE)\n        {\n            m_options |= EO_DOC_MODE;\n        }\n        \n        if ((options & SPO_USING_REMOTE_EMOJI) == SPO_USING_REMOTE_EMOJI)\n        {\n            m_options |= EO_USING_REMOTE_EMOJI;\n        }\n        if ((options & SPO_DESC) == SPO_DESC)\n        {\n            m_options |= EO_TIME_DESC;\n        }\n        if ((options & SPO_SYNC_LOADING) == SPO_SYNC_LOADING)\n        {\n            m_options &= ~EO_ASYNC_MASK;\n        }\n        if ((options & SPO_SUPPORT_FILTER) == SPO_SUPPORT_FILTER)\n        {\n            m_options |= EO_SUPPORT_FILTER;\n        }\n        \n        if ((options & SPO_INCLUDING_SUBSCRIPTION) == SPO_INCLUDING_SUBSCRIPTION)\n        {\n            m_options |= EO_INCLUDING_SUBSCRIPTION;\n        }\n        if ((options & SPO_OVERWRITE_EXISTING_FIlE) == SPO_OVERWRITE_EXISTING_FIlE)\n        {\n            m_options |= EO_OVERWRT_EXISTING_FIlE;\n        }\n        if ((options & SPO_EXP_GROUP_MEMBERS) == SPO_EXP_GROUP_MEMBERS)\n        {\n            m_options |= EO_EXP_GROUP_MEMBERS;\n        }\n        if ((options & SPO_EXP_CONTACTS) == SPO_EXP_CONTACTS)\n        {\n            m_options |= EO_EXP_CONTACTS;\n        }\n        if ((options & SPO_OUTPUT_DBG_LOGS) == SPO_OUTPUT_DBG_LOGS)\n        {\n            m_options |= EO_OUTPUT_DBG_LOGS;\n        }\n        if ((options & SPO_INCREMENTAL_EXP) == SPO_INCREMENTAL_EXP)\n        {\n            m_options |= EO_INCREMENTAL_EXP;\n        }\n    }\n    \nprivate:\n    uint64_t m_options;\n};\n\n\n#endif /* ExportOption_h */\n"
  },
  {
    "path": "WechatExporter/core/Exporter.cpp",
    "content": "//\n//  Exporter.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include \"Exporter.h\"\n#include <json/json.h>\n#ifdef USING_DOWNLOADER\n#include \"Downloader.h\"\n#else\n#include \"AsyncTask.h\"\n#endif\n#include \"TaskManager.h\"\n#include \"WechatParser.h\"\n#include \"ExportContext.h\"\n#ifdef _WIN32\n#include <winsock.h>\n#endif\n\n\n#define USING_NEW_TEMPLATE 1\n\n#ifndef NDEBUG\nstatic const size_t PAGE_SIZE = 100;\n#else\nstatic const size_t PAGE_SIZE = 1000;\n#endif\n\nExporter::Exporter(const std::string& workDir, const std::string& backup, const std::string& output, Logger* logger, PdfConverter* pdfConverter)\n{\n    m_running = false;\n    m_iTunesDb = NULL;\n    m_iTunesDbShare = NULL;\n    m_workDir = workDir;\n    m_backup = backup;\n    m_output = output;\n    m_logger = logger;\n    m_pdfConverter = pdfConverter;\n    m_notifier = NULL;\n    m_cancelled = false;\n    m_options = 0;\n    // m_filterByName = false;\n    m_extName = \"html\";\n    m_templatesName = \"templates\";\n    m_exportContext = NULL;\n}\n\nExporter::~Exporter()\n{\n    if (NULL != m_exportContext)\n    {\n        delete m_exportContext;\n        m_exportContext = NULL;\n    }\n    releaseITunes();\n    m_logger = NULL;\n    m_notifier = NULL;\n}\n\nvoid Exporter::initializeExporter()\n{\n    // Disable Memory Stats in sqlite\n    sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0);\n#ifdef USING_DOWNLOADER\n    Downloader::initialize();\n#else\n    DownloadTask::initialize();\n#endif\n}\n\nvoid Exporter::uninitializeExporter()\n{\n#ifdef USING_DOWNLOADER\n    Downloader::uninitialize();\n#else\n    DownloadTask::uninitialize();\n#endif\n}\n\nvoid Exporter::setOptions(const ExportOption& options)\n{\n    m_options = options;\n}\n\n/*\nvoid Exporter::includesSubscription()\n{\n    m_options.includesSubscription();\n}\n*/\n\nbool Exporter::hasDebugLogs() const\n{\n    return m_options.isOutputtingDebugLogs();\n}\n\nbool Exporter::isSubscriptionIncluded() const\n{\n    return m_options.isIncludingSubscription();\n}\n\nbool Exporter::hasPreviousExporting(const std::string& outputDir, uint64_t& options, std::string& exportTime, std::string& version)\n{\n    std::string fileName = combinePath(outputDir, WXEXP_DATA_FOLDER, WXEXP_DATA_FILE);\n    if (!existsFile(fileName))\n    {\n        return false;\n    }\n    \n    ExportContext context;\n    if (!loadExportContext(fileName, &context))\n    {\n        return false;\n    }\n    \n    options = context.getOptions();\n\tversion = context.getVersion();\n    std::time_t ts = context.getExportTime();\n    std::tm * ptm = std::localtime(&ts);\n    char buffer[32];\n\tstd::strftime(buffer, 32, \"%Y-%m-%d %H:%M\", ptm);\n    \n    exportTime = buffer;\n    \n    return true;\n}\n\nbool Exporter::loadExportContext(const std::string& contextFile, ExportContext *context)\n{\n    std::string contents = readFile(contextFile);\n    if (contents.empty())\n    {\n        return false;\n    }\n\n    if (!context->unserialize(contents) || context->getNumberOfSessions() == 0)\n    {\n        return false;\n    }\n    \n    return true;\n}\n\nvoid Exporter::setNotifier(ExportNotifier *notifier)\n{\n    m_notifier = notifier;\n}\n\nbool Exporter::isRunning() const\n{\n    return m_running;\n}\n\nvoid Exporter::cancel()\n{\n    m_cancelled = true;\n}\n\nvoid Exporter::waitForComplition()\n{\n    if (!isRunning())\n    {\n        return;\n    }\n\n    m_thread.join();\n}\n\nvoid Exporter::setExtName(const std::string& extName)\n{\n    m_extName = extName;\n}\n\nvoid Exporter::setTemplatesName(const std::string& templatesName)\n{\n    m_templatesName = templatesName;\n}\n\nvoid Exporter::setLanguageCode(const std::string& languageCode)\n{\n    m_languageCode = languageCode;\n}\n\nvoid Exporter::filterUsersAndSessions(const std::map<std::string, std::map<std::string, void *>>& usersAndSessions)\n{\n    m_usersAndSessionsFilter = usersAndSessions;\n}\n\nbool Exporter::run()\n{\n    if (isRunning() || m_thread.joinable())\n    {\n        m_logger->write(m_resManager.getLocaleString(\"Previous task has not completed.\"));\n        \n        return false;\n    }\n\n    if (!existsDirectory(m_output))\n    {\n        m_logger->write(formatString(m_resManager.getLocaleString(\"Can't access output directory: %s\"), m_output.c_str()));\n        return false;\n    }\n    \n    m_running = true;\n\n    std::thread th(&Exporter::runImpl, this);\n    m_thread.swap(th);\n\n    return true;\n}\n\nbool Exporter::loadUsersAndSessions()\n{\n    m_usersAndSessions.clear();\n    \n    m_resManager.initLocaleResource(m_workDir, m_languageCode);\n\n    if (!loadITunes(false))\n    {\n        m_logger->write(formatString(m_resManager.getLocaleString(\"Failed to parse the backup data of iTunes in the directory: %s\"), m_backup.c_str()));\n        notifyComplete();\n        return false;\n    }\n    m_logger->debug(\"ITunes Database loaded.\");\n    \n    WechatInfoParser wechatInfoParser(m_iTunesDb);\n    if (wechatInfoParser.parse(m_wechatInfo))\n    {\n        m_logger->write(formatString(m_resManager.getLocaleString(\"iTunes Version: %s, iOS Version: %s, WeChat Version: %s\"), m_iTunesDb->getVersion().c_str(), m_iTunesDb->getIOSVersion().c_str(), m_wechatInfo.getShortVersion().c_str()));\n    }\n    \n    std::vector<Friend> users;\n    \n    LoginInfo2Parser loginInfo2Parser(m_iTunesDb, hasDebugLogs() ? m_logger : NULL);\n    if (!loginInfo2Parser.parse(users))\n    {\n        if (hasDebugLogs())\n        {\n            m_logger->debug(loginInfo2Parser.getError());\n        }\n        return false;\n    }\n\n    m_logger->debug(\"WeChat Users loaded.\");\n    m_usersAndSessions.reserve(users.size()); // Avoid re-allocation and causing the pointer changed\n    for (std::vector<Friend>::const_iterator it = users.cbegin(); it != users.cend(); ++it)\n    {\n        std::vector<std::pair<Friend, std::vector<Session>>>::iterator it2 = m_usersAndSessions.emplace(m_usersAndSessions.cend(), std::pair<Friend, std::vector<Session>>(*it, std::vector<Session>()));\n        Friends friends;\n        loadUserFriendsAndSessions(it2->first, friends, it2->second, false);\n    }\n\n    return true;\n}\n\nvoid Exporter::swapUsersAndSessions(std::vector<std::pair<Friend, std::vector<Session>>>& usersAndSessions)\n{\n    usersAndSessions.swap(m_usersAndSessions);\n}\n\nbool Exporter::runImpl()\n{\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    setThreadName(\"exp\");\n#endif\n    time_t startTime;\n    std::time(&startTime);\n    notifyStart();\n    \n#if !defined(NDEBUG) || defined(DBG_PERF)\n    makeDirectory(combinePath(m_output, \"dbg\"));\n#endif\n    \n    if (!m_resManager.initResources(m_workDir, m_languageCode, m_templatesName))\n    {\n        m_logger->write(formatString(\"Failed to load resources in %s.\", m_workDir.c_str()));\n\n        if (hasDebugLogs())\n        {\n            std::string emptyTemplates = m_resManager.checkEmptyTemplates();\n            if (!emptyTemplates.empty())\n            {\n                m_logger->write(\"Empty template: \" + emptyTemplates);\n            }\n        }\n    }\n\n    m_logger->write(formatString(m_resManager.getLocaleString(\"iTunes Backup: %s\"), m_backup.c_str()));\n\n    if (!loadITunes())\n    {\n        m_logger->write(formatString(m_resManager.getLocaleString(\"Failed to parse the backup data of iTunes in the directory: %s\"), m_backup.c_str()));\n        notifyComplete();\n        return false;\n    }\n    m_logger->debug(\"ITunes Database loaded.\");\n    \n    WechatInfoParser wechatInfoParser(m_iTunesDb);\n    if (wechatInfoParser.parse(m_wechatInfo))\n    {\n        m_logger->write(formatString(m_resManager.getLocaleString(\"iTunes Version: %s, WeChat Version: %s\"), m_iTunesDb->getVersion().c_str(), m_wechatInfo.getShortVersion().c_str()));\n    }\n\n    m_logger->write(m_resManager.getLocaleString(\"Finding WeChat accounts...\"));\n\n    std::vector<Friend> users;\n\n    LoginInfo2Parser loginInfo2Parser(m_iTunesDb, hasDebugLogs() ? m_logger : NULL);\n    if (!loginInfo2Parser.parse(users))\n    {\n        m_logger->write(m_resManager.getLocaleString(\"Failed to find WeChat account.\"));\n        if (hasDebugLogs())\n        {\n            m_logger->debug(loginInfo2Parser.getError());\n        }\n        notifyComplete();\n        return false;\n    }\n\n    m_logger->write(formatString(m_resManager.getLocaleString(\"%d WeChat account(s) found.\"), (int)(users.size())));\n\n    // if (m_options.isIncrementalExporting())\n    {\n        std::string path = combinePath(m_output, WXEXP_DATA_FOLDER);\n        makeDirectory(path);\n    }\n    if (NULL == m_exportContext)\n    {\n        m_exportContext = new ExportContext();\n    }\n    uint64_t orgOptions = m_options;\n    std::string contextFileName = combinePath(m_output, WXEXP_DATA_FOLDER, WXEXP_DATA_FILE);\n    if ((m_options.isIncrementalExporting()) && loadExportContext(contextFileName, m_exportContext))\n    {\n        // Use the previous options\n        m_options = m_exportContext->getOptions();\n        m_options.setIncrementalExporting(true);\n        m_logger->write(m_resManager.getLocaleString(\"Incremental Exporting\"));\n    }\n    else\n    {\n        // If there is no export context, save current options\n        m_exportContext->setOptions(m_options);\n    }\n    \n    std::string htmlBody;\n\n    std::set<std::string> userFileNames;\n    for (std::vector<Friend>::iterator it = users.begin(); it != users.end(); ++it)\n    {\n        if (m_cancelled)\n        {\n            break;\n        }\n        \n        if (!m_usersAndSessionsFilter.empty())\n        {\n            const std::string& nameForFilter = m_options.isFilteredByName() ? it->getDisplayName() : it->getUsrName();\n            if (m_usersAndSessionsFilter.find(nameForFilter) == m_usersAndSessionsFilter.cend())\n            {\n                continue;\n            }\n        }\n        \n        if (!buildFileNameForUser(*it, userFileNames))\n        {\n            m_logger->write(formatString(m_resManager.getLocaleString(\"Can't build directory name for user: %s. Skip it.\"), it->getUsrName().c_str()));\n            continue;\n        }\n\n        std::string userOutputPath;\n        exportUser(*it, userOutputPath);\n        \n        std::string userItem = m_resManager.getTemplate(\"listitem\");\n        replaceAll(userItem, \"%%ITEMPICPATH%%\", userOutputPath + \"/Portrait/\" + it->getLocalPortrait());\n        if (!m_options.isTextMode())\n        {\n            replaceAll(userItem, \"%%ITEMLINK%%\", encodeUrl(it->getOutputFileName()) + \"/index.\" + m_extName);\n            replaceAll(userItem, \"%%ITEMTEXT%%\", safeHTML(it->getDisplayName()));\n        }\n        else\n        {\n            replaceAll(userItem, \"%%ITEMLINK%%\", it->getOutputFileName() + \"/index.\" + m_extName);\n            replaceAll(userItem, \"%%ITEMTEXT%%\", it->getDisplayName());\n        }\n        \n        htmlBody += userItem;\n    }\n    \n    std::string fileName = combinePath(m_output, \"index.\" + m_extName);\n\n    std::string html = m_resManager.getTemplate(\"listframe\");\n    \n    replaceAll(html, \"%%USERNAME%%\", \"\");\n    replaceAll(html, \"%%TBODY%%\", htmlBody);\n    \n    writeFile(fileName, html);\n    \n    m_options = orgOptions;\n    if (m_exportContext->getNumberOfSessions() > 0)\n    {\n        m_exportContext->refreshExportTime();\n        fileName = combinePath(m_output, WXEXP_DATA_FOLDER, WXEXP_DATA_FILE);\n        writeFile(fileName, m_exportContext->serialize());\n    }\n    \n    delete m_exportContext;\n    m_exportContext = NULL;\n    \n    time_t endTime = 0;\n    std::time(&endTime);\n    int seconds = static_cast<int>(difftime(endTime, startTime));\n    std::ostringstream stream;\n    \n    int minutes = seconds / 60;\n    int hours = minutes / 60;\n    stream << std::setfill('0') << std::setw(2) << hours << ':'\n        << std::setfill('0') << std::setw(2) << (minutes % 60) << ':'\n        << std::setfill('0') << std::setw(2) << (seconds % 60);\n    \n    m_logger->write(formatString(m_resManager.getLocaleString((m_cancelled ? \"Cancelled in %s.\" : \"Completed in %s.\")), stream.str().c_str()));\n    \n    notifyComplete(m_cancelled);\n    \n    return true;\n}\n\nbool Exporter::exportUser(Friend& user, std::string& userOutputPath)\n{\n    std::string uidMd5 = user.getHash();\n    \n    std::string userBase = combinePath(\"Documents\", uidMd5);\n    // Use display name first, it it can't be created, use uid hash\n    userOutputPath = user.getOutputFileName();\n    std::string outputBase = combinePath(m_output, userOutputPath);\n    if (!existsDirectory(outputBase))\n    {\n        if (!makeDirectory(outputBase))\n        {\n            userOutputPath = user.getHash();\n            outputBase = combinePath(m_output, userOutputPath);\n            if (!existsDirectory(outputBase))\n            {\n                if (!makeDirectory(outputBase))\n                {\n                    return false;\n                }\n            }\n        }\n    }\n    \n    if (!m_options.isTextMode())\n    {\n        std::string portraitPath = combinePath(outputBase, \"Portrait\");\n        makeDirectory(portraitPath);\n        std::string defaultPortrait = combinePath(portraitPath, \"DefaultAvatar.png\");\n        copyFile(combinePath(m_workDir, \"res\", \"DefaultAvatar.png\"), defaultPortrait, true);\n    }\n    /*\n    if (false && (m_options & SPO_IGNORE_EMOJI) == 0)\n    {\n        std::string emojiPath = combinePath(outputBase, \"Emoji\");\n        makeDirectory(emojiPath);\n    }\n     */\n    \n    // if (m_options.isIncrementalExporting())\n    {\n        std::string path = combinePath(m_output, WXEXP_DATA_FOLDER, user.getUsrName());\n        makeDirectory(path);\n        \n        m_exportContext->prepareUserDatabase(user, m_output);\n    }\n    \n    m_logger->write(formatString(m_resManager.getLocaleString(\"Handling account: %s, WeChat Id: %s\"), user.getDisplayName().c_str(), user.getUsrName().c_str()));\n    \n    m_logger->write(m_resManager.getLocaleString(\"Reading account info.\"));\n    m_logger->write(m_resManager.getLocaleString(\"Reading chat info\"));\n    \n    Friends friends;\n    std::vector<Session> sessions;\n    loadUserFriendsAndSessions(user, friends, sessions);\n    \n    m_logger->write(formatString(m_resManager.getLocaleString(\"%d chats found.\"), (int)(sessions.size())));\n    \n    Friend* myself = friends.getFriend(user.getHash());\n    if (NULL == myself)\n    {\n        Friend& newUser = friends.addFriend(user.getHash());\n        newUser = user;\n        myself = &user;\n    }\n    \n    std::string userBody;\n    \n    std::map<std::string, std::map<std::string, void *>>::const_iterator itUser = m_usersAndSessionsFilter.cend();\n    if (!m_usersAndSessionsFilter.empty())\n    {\n        std::string nameForFilter = m_options.isFilteredByName() ? user.getDisplayName() : user.getUsrName();\n        itUser = m_usersAndSessionsFilter.find(nameForFilter);\n    }\n    \n    bool pdfOutput = (m_options.isPdfMode() && NULL != m_pdfConverter);\n    if (pdfOutput)\n    {\n        m_pdfConverter->makeUserDirectory(userOutputPath);\n    }\n    \n#ifdef USING_DOWNLOADER\n    Downloader downloader(m_logger);\n#else\n    TaskManager taskManager(m_logger);\n#endif\n#ifndef NDEBUG\n    m_logger->debug(\"UA: \" + m_wechatInfo.buildUserAgent());\n#endif\n    \n#ifdef USING_DOWNLOADER\n    downloader.setUserAgent(m_wechatInfo.buildUserAgent());\n#else\n    taskManager.setUserAgent(m_wechatInfo.buildUserAgent());\n#endif\n\n    MessageParser msgParser(*m_iTunesDb, *m_iTunesDbShare, taskManager, friends, *myself, m_options, m_workDir, outputBase, m_resManager);\n    \n    if (!m_options.isTextMode())\n    {\n#ifndef NDEBUG\n        m_logger->debug(\"Download avatar: *\" + user.getPortrait() + \"* => \" + combinePath(outputBase, \"Portrait\", user.getLocalPortrait()));\n#endif\n        msgParser.copyPortraitIcon(NULL, user, combinePath(outputBase, \"Portrait\"));\n        // downloader.addTask(user.getPortrait(), combinePath(outputBase, \"Portrait\", user.getLocalPortrait()), 0);\n    }\n    \n    // Export Contacts\n    // std::map<std::string, Friend>\n    // WeChat Id/Display Name/Portrait URL\n    // csv\n    std::string csvContents;\n    std::vector<const Friend *>friendList;\n    friends.toArraySortedByDisplayName(friendList);\n    std::string oneQuato = \"\\\"\";\n    std::string twoQuatos = \"\\\"\\\"\";\n    std::string twoQuatos2 = \"\\\",\\\"\";\n    for (auto it = friendList.cbegin(); it != friendList.cend(); ++it)\n    {\n#ifdef NDEBUG   // Release\n        if ((*it)->isSubscription() || (*it)->isChatroom())\n        {\n            continue;\n        }\n\n        std::string wechatId = (*it)->getWxName();\n        std::string displayName = (*it)->getDisplayName();\n        replaceAll(displayName, oneQuato, twoQuatos);\n        csvContents += oneQuato + wechatId + twoQuatos2 + displayName + twoQuatos2 + (*it)->getPortrait() + twoQuatos2 + (*it)->buildTagDesc(m_tags) + \"\\\"\\r\\n\";\n#else\n        std::string wechatId = (*it)->getWxName();\n        if (wechatId == (*it)->getUsrName())\n        {\n            wechatId.clear();\n        }\n        std::string displayName = (*it)->getDisplayName();\n        replaceAll(displayName, oneQuato, twoQuatos);\n        csvContents += oneQuato + (*it)->getUsrName() + twoQuatos2 + wechatId + twoQuatos2 + displayName + twoQuatos2 + (*it)->buildTagDesc(m_tags) + twoQuatos2 + (*it)->getPortrait() + \"\\\"\\r\\n\";\n#endif\n    }\n    std::string contactPath = combinePath(m_output, userOutputPath + \"_Contacts.csv\");\n    writeFile(contactPath, csvContents);\n    \n    std::string weChatIdFormat = m_resManager.getLocaleString(\" (WeChat ID: %s)\");\n\n    std::set<std::string> sessionFileNames;\n    for (std::vector<Session>::iterator it = sessions.begin(); it != sessions.end(); ++it)\n    {\n        if (m_cancelled)\n        {\n            break;\n        }\n        \n        if (!m_usersAndSessionsFilter.empty() && itUser != m_usersAndSessionsFilter.cend() && !(itUser->second.empty()))\n        {\n            std::map<std::string, void *>::const_iterator itSession = itUser->second.cend();\n            const std::string& nameForFilter = m_options.isFilteredByName() ? it->getDisplayName() : it->getUsrName();\n            if ((itSession = itUser->second.find(nameForFilter)) == itUser->second.cend())\n            {\n                continue;\n            }\n            \n            it->setData(itSession->second);\n        }\n        \n        int recordCount = it->getRecordCount();\n        if (m_options.isIncrementalExporting())\n        {\n            int64_t maxMsgId = 0;\n            m_exportContext->getMaxId(user.getUsrName(), it->getUsrName(), maxMsgId);\n            if (maxMsgId > 0)\n            {\n                recordCount = SessionParser::calcNumberOfMessages(*it, maxMsgId);\n            }\n        }\n\n\t\tnotifySessionStart(it->getUsrName(), it->getData(), recordCount);\n        \n        if (!buildFileNameForUser(*it, sessionFileNames))\n        {\n            m_logger->write(formatString(m_resManager.getLocaleString(\"Can't build directory name for chat: %s. Skip it.\"), it->getDisplayName().c_str()));\n\t\t\tnotifySessionComplete(it->getUsrName(), it->getData(), m_cancelled);\n            continue;\n        }\n        \n        std::string sessionDisplayName = it->getDisplayName();\n#ifndef NDEBUG\n        m_logger->write(formatString(m_resManager.getLocaleString(\"%d/%d: Handling the chat with %s\"), (std::distance(sessions.begin(), it) + 1), sessions.size(), sessionDisplayName.c_str()) + \" uid:\" + it->getUsrName());\n#else\n        m_logger->write(formatString(m_resManager.getLocaleString(\"%d/%d: Handling the chat with %s\"), (std::distance(sessions.begin(), it) + 1), sessions.size(), sessionDisplayName.c_str()));\n#endif\n        if (!isSubscriptionIncluded() && it->isSubscription())\n        {\n            m_logger->write(formatString(m_resManager.getLocaleString(\"Skip subscription: %s\"), sessionDisplayName.c_str()));\n\t\t\tnotifySessionComplete(it->getUsrName(), it->getData(), m_cancelled);\n            continue;\n        }\n        if (!m_options.isTextMode())\n        {\n            // Download avatar for session\n            msgParser.copyPortraitIcon(&(*it), *it, combinePath(outputBase, \"Portrait\"));\n        }\n        int count = exportSession(*myself, msgParser, *it, userBase, outputBase);\n        \n        m_logger->write(formatString(m_resManager.getLocaleString(\"Succeeded handling %d messages.\"), count));\n\n        if (count > 0)\n        {\n            std::string userItem = m_resManager.getTemplate(\"listitem\");\n            \n            std::string userItemText = sessionDisplayName;\n            if (!it->isChatroom())\n            {\n                std::string wxName = it->isWxNameEmpty() ? \"\" : it->getWxName();\n                if (!wxName.empty() && userItemText != wxName)\n                {\n                    userItemText += formatString(weChatIdFormat, wxName.c_str());\n                }\n            }\n            \n            replaceAll(userItem, \"%%ITEMPICPATH%%\", \"Portrait/\" + it->getLocalPortrait());\n            if (!m_options.isTextMode())\n            {\n                replaceAll(userItem, \"%%ITEMLINK%%\", encodeUrl(it->getOutputFileName()) + \"/index.\" + m_extName);\n                replaceAll(userItem, \"%%ITEMTEXT%%\", safeHTML(userItemText));\n            }\n            else\n            {\n                replaceAll(userItem, \"%%ITEMLINK%%\", it->getOutputFileName() + \".\" + m_extName);\n                replaceAll(userItem, \"%%ITEMTEXT%%\", userItemText);\n            }\n            \n            userBody += userItem;\n        }\n\n\t\tnotifySessionComplete(it->getUsrName(), it->getData(), m_cancelled);\n        \n        if (pdfOutput)\n        {\n            // std::string \n            std::string htmlFileName = combinePath(outputBase, it->getOutputFileName(), \"index.\" + m_extName);\n            if (existsFile(htmlFileName))\n            {\n                std::string pdfFileName = combinePath(m_output, \"pdf\", userOutputPath, it->getOutputFileName() + \".pdf\");\n                // taskManager.convertPdf(&(*it), htmlFileName, pdfFileName, m_pdfConverter);\n                m_pdfConverter->convert(htmlFileName, pdfFileName);\n            }\n        }\n    }\n\n    std::string html = m_resManager.getTemplate(\"listframe\");\n    replaceAll(html, \"%%USERNAME%%\", \" - \" + user.getDisplayName());\n    replaceAll(html, \"%%TBODY%%\", userBody);\n    \n    std::string fileName = combinePath(outputBase, \"index.\" + m_extName);\n    writeFile(fileName, html);\n\n    size_t dlCount = 0;\n    size_t prevDlCount = 0;\n    if (m_cancelled)\n    {\n#ifdef USING_DOWNLOADER\n        downloader.cancel();\n#else\n        taskManager.cancel();\n#endif\n    }\n    else\n    {\n#ifdef USING_DOWNLOADER\n        dlCount = downloader.getRunningCount();\n        prevDlCount = dlCount;\n        if (dlCount > 0)\n        {\n            m_logger->write(\"Waiting for tasks: \" + std::to_string(dlCount));\n        }\n#else\n        std::string queueDesc;\n        dlCount = taskManager.getNumberOfQueue(queueDesc);\n        prevDlCount = dlCount;\n        if (dlCount > 0)\n        {\n            m_logger->write(\"Waiting for tasks: \" + queueDesc);\n        }\n        taskManager.shutdown();\n#endif\n    }\n\n    notifyTasksStart(user.getUsrName(), static_cast<uint32_t>(dlCount));\n    \n#ifdef USING_DOWNLOADER\n    downloader.shutdown();\n#else\n    unsigned int timeout = m_cancelled ? 0 : 512;\n    for (int idx = 1; ; ++idx)\n    {\n        if (taskManager.waitForCompltion(timeout))\n        {\n            break;\n        }\n        \n        if (m_cancelled)\n        {\n            taskManager.cancel();\n            timeout = 0;\n        }\n        else if ((idx % 2) == 0)\n        {\n            std::string queueDesc;\n            size_t curDlCount = taskManager.getNumberOfQueue(queueDesc);\n            if (curDlCount != prevDlCount)\n            {\n                notifyTasksProgress(user.getUsrName(), static_cast<uint32_t>(prevDlCount - curDlCount), static_cast<uint32_t>(dlCount));\n                prevDlCount = curDlCount;\n            }\n        }\n    }\n#endif\n\n    if (dlCount != prevDlCount)\n    {\n        notifyTasksProgress(user.getUsrName(), static_cast<uint32_t>(dlCount - prevDlCount), static_cast<uint32_t>(dlCount));\n    }\n    notifyTasksComplete(user.getUsrName(), m_cancelled);\n    \n#ifndef NDEBUG\n    // m_logger->debug(formatString(\"Total Downloads: %d\", downloader.getCount()));\n    // m_logger->debug(\"Download Stats: \" + downloader.getStats());\n#endif\n\n    return true;\n}\n\nbool Exporter::loadUserFriendsAndSessions(const Friend& user, Friends& friends, std::vector<Session>& sessions, bool detailedInfo/* = true*/)\n{\n    std::string uidMd5 = user.getHash();\n    std::string userBase = combinePath(\"Documents\", uidMd5);\n\n    std::string wcdbPath = m_iTunesDb->findRealPath(combinePath(userBase, \"DB\", \"WCDB_Contact.sqlite\"));\n    FriendsParser friendsParser(detailedInfo);\n#ifndef NDEBUG\n    friendsParser.setOutputPath(m_output);\n#endif\n    friendsParser.parseWcdb(wcdbPath, friends);\n\n    m_logger->debug(\"WeChat Friends(\" + std::to_string(friends.friends.size()) + \") for: \" + user.getDisplayName() + \" loaded.\");\n    \n    m_tags.clear();\n    if (detailedInfo)\n    {\n        friendsParser.parseFriendTags(m_iTunesDb, user.getHash(), m_tags);\n    }\n\n    SessionsParser sessionsParser(m_iTunesDb, m_iTunesDbShare, friends, m_wechatInfo.getCellDataVersion(), m_logger, detailedInfo);\n    \n    sessionsParser.parse(user, sessions);\n \n    std::sort(sessions.begin(), sessions.end(), SessionLastMsgTimeCompare());\n    \n#ifndef NDEBUG\n    int idx = 0;\n    for (auto it = sessions.cbegin(); it != sessions.cend(); ++it)\n    {\n        if (it->isSubscription())\n        {\n            continue;\n        }\n        \n        printf(\"%u : %s\\t%s\\r\\n\",it->getLastMessageTime(), it->getDisplayName().c_str(), it->getUsrName().c_str());\n        idx ++;\n        if (idx > 20)\n        {\n            // break;\n        }\n    }\n    \n    idx = 0;\n#endif\n    \n    // m_logger->debug(\"WeChat Sessions for: \" + user.getDisplayName() + \" loaded.\");\n    return true;\n}\n\nbool Exporter::buildScriptFile(const std::string& fileName, std::vector<std::string>::const_iterator b, std::vector<std::string>::const_iterator e, const PageInfo& page) const\n{\n    std::string scripts = m_resManager.getTemplate(\"scripts\");\n    Json::Value jsonMsgs(Json::arrayValue);\n    for (auto it = b; it != e; ++it)\n    {\n        jsonMsgs.append(*it);\n    }\n    Json::StreamWriterBuilder builder;\n#ifndef NDEBUG\n    builder[\"indentation\"] = \"\\t\";  // assume default for comments is None\n    builder[\"emitUTF8\"] = true;\n#else\n    builder[\"indentation\"] = \"\";\n#endif\n    std::string moreMsgs = Json::writeString(builder, jsonMsgs);\n\n    replaceAll(scripts, \"%%JSON_DATA%%\", moreMsgs);\n    \n    // std::string dataFileName = combinePath(dataPath, \"msg-\" + page.getFileName() + \".js\");\n    writeFile(fileName, scripts);\n    \n    return true;\n}\n\nint Exporter::exportSession(const Friend& user, const MessageParser& msgParser, const Session& session, const std::string& userBase, const std::string& outputBase)\n{\n    if (session.isDbFileEmpty())\n    {\n        return 0;\n    }\n    \n    std::string sessionBasePath = combinePath(outputBase, session.getOutputFileName(), \"Files\");\n    if (!m_options.isTextMode())\n    {\n        std::string portraitPath = combinePath(sessionBasePath, \"Portrait\");\n        makeDirectory(portraitPath);\n        // std::string defaultPortrait = combinePath(portraitPath, \"DefaultAvatar.png\");\n        // copyFile(combinePath(m_workDir, \"res\", \"DefaultAvatar.png\"), defaultPortrait, true);\n        \n        makeDirectory(combinePath(sessionBasePath, \"Emoji\"));\n    }\n\n    std::vector<std::string> messages;\n    std::vector<std::string>::size_type numberOfExportedMsgs = 0;\n    if (session.getRecordCount() > 0)\n    {\n        messages.reserve(session.getRecordCount());\n    }\n    \n    int64_t maxMsgId = 0;\n    m_exportContext->getMaxId(user.getUsrName(), session.getUsrName(), maxMsgId);\n    if (m_options.isIncrementalExporting())\n    {\n        m_exportContext->prepareSessionTable(session);\n    }\n\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    m_logger->debug(\"DB: \" + session.getDbFile() + \" Table: Chat_\" + session.getHash());\n#endif\n    \n    int numberOfMsgs = 0;\n    SessionParser sessionParser(m_options);\n    std::unique_ptr<SessionParser::MessageEnumerator> enumerator(sessionParser.buildMsgEnumerator(session, maxMsgId));\n    std::vector<TemplateValues> tvs;\n    std::unique_ptr<Pager> pager;\n    uint16_t year = 0;\n    uint16_t month = 0;\n    size_t msgPosOfPage = 0;\n    \n    std::string dataPath = combinePath(outputBase, session.getOutputFileName(), \"Files\", \"Data\");\n    makeDirectory(dataPath);\n    \n    if (m_options.isAsyncLoading())\n    {\n        if (m_options.isPagerByYear())\n        {\n            pager.reset((Pager *)(new YearPager()));\n        }\n        else if (m_options.isPagerByMonth())\n        {\n            pager.reset((Pager *)(new YearMonthPager()));\n        }\n        else\n        {\n            pager.reset((Pager *)(new NumberPager(PAGE_SIZE)));\n        }\n    }\n    else\n    {\n        pager.reset(new Pager());\n    }\n    \n    WXMSG msg;\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    m_logger->debug(\"Start exporting session\");\n#endif\n    while (enumerator->nextMessage(msg))\n    {\n#if !defined(NDEBUG) || defined(DBG_PERF)\n        // m_logger->debug(\"Export msg: \" + msg.msgId);\n#endif\n        if (m_options.isIncrementalExporting())\n        {\n            m_exportContext->insertMessage(session, msg);\n        }\n        \n        if (msg.msgIdValue > maxMsgId)\n        {\n            maxMsgId = msg.msgIdValue;\n        }\n        \n        tvs.clear();\n        if (!msgParser.parse(msg, session, tvs))\n        {\n            if (hasDebugLogs() && msgParser.hasError())\n            {\n                m_logger->debug(msgParser.getError());\n            }\n        }\n\n        exportMessage(session, tvs, messages);\n        ++numberOfMsgs;\n\n        pager->buildNewPage(&msg, messages);\n        \n        notifySessionProgress(session.getUsrName(), session.getData(), numberOfMsgs, session.getRecordCount());\n#if !defined(NDEBUG) || defined(DBG_PERF)\n        // m_logger->debug(\"Finish exporting msg: \" + msg.msgId);\n#endif\n        if (m_cancelled)\n        {\n            break;\n        }\n    }\n\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    m_logger->debug(\"Finish exporting session\");\n#endif\n\n    if (maxMsgId > 0)\n    {\n        m_exportContext->setMaxId(user.getUsrName(), session.getUsrName(), maxMsgId);\n    }\n    \n    if (numberOfMsgs == 0)  // no messages or no new messages for incremental exporting\n    {\n        return (maxMsgId > 0) ? 1 : numberOfMsgs;\n    }\n    \n    std::string contentOfMembers;\n    if (session.isChatroom())\n    {\n        // Export memebers\n    }\n    \n    std::string rawMsgFileName = combinePath(m_output, WXEXP_DATA_FOLDER, session.getOwner()->getUsrName(), session.getUsrName() + \".dat\");\n    if (m_options.isIncrementalExporting())\n    {\n        m_logger->debug(\"Merge incremental messages.\");\n        mergeMessages(rawMsgFileName, messages);\n    }\n    \n    m_logger->debug(\"Save messages for incremental exporting.\");\n    serializeMessages(rawMsgFileName, messages);\n    \n    // m_logger->debug(\"After serializeMessages.\");\n\n    if (numberOfMsgs > 0)\n    {\n        Json::Value jsonPages(Json::arrayValue);\n        \n        if (pager->hasPages())\n        {\n            numberOfExportedMsgs = 0;\n            auto pages = pager->getPages();\n            for (auto it = pages.cbegin(); it != pages.cend(); ++it)\n            {\n                auto b = messages.cbegin() + numberOfExportedMsgs;\n                auto e = b + it->getCount();\n                buildScriptFile(combinePath(dataPath, \"msg-\" + it->getFileName() + \".js\"), b, e, *it);\n                numberOfExportedMsgs += it->getCount();\n                \n                Json::Value jsonPage(Json::objectValue);\n                jsonPage[\"numnberOfMsgs\"] = it->getCount();\n                jsonPage[\"text\"] = it->getText();\n                jsonPage[\"page\"] = it->getPage() + 1;\n                if (it->getYear() > 0)\n                {\n                    jsonPage[\"year\"] = it->getYear();\n                }\n                if (it->getMonth() > 0)\n                {\n                    jsonPage[\"month\"] = it->getMonth();\n                }\n                jsonPages.append(jsonPage);\n            }\n        }\n        \n        Json::StreamWriterBuilder builder;\n#ifndef NDEBUG\n        builder[\"indentation\"] = \"\\t\";  // assume default for comments is None\n        builder[\"emitUTF8\"] = true;\n#else\n        builder[\"indentation\"] = \"\";\n#endif\n        std::string pageData = Json::writeString(builder, jsonPages);\n\n        auto b = messages.cbegin();\n        // No page for text mode\n        auto e = (m_options.isTextMode() || m_options.isSyncLoading() || (messages.size() <= PAGE_SIZE)) ? messages.cend() : (b + PAGE_SIZE);\n        \n        // const size_t numberOfMessages = std::distance(e, messages.cend());\n        const size_t numberOfMessages = numberOfMsgs;\n        const size_t numberOfPages = (messages.size() + PAGE_SIZE - 1) / PAGE_SIZE;\n        \n#if USING_NEW_TEMPLATE\n        std::map<std::string, std::string> values;\n        \n#ifndef NDEBUG\n        values[\"%%USRNAME%%\"] = user.getUsrName() + \" - \" + user.getHash();\n        values[\"%%SESSION_USRNAME%%\"] = session.getUsrName() + \" - \" + session.getHash();\n#else\n        values[\"%%USRNAME%%\"] = \"\";\n        values[\"%%SESSION_USRNAME%%\"] = \"\";\n#endif\n        values[\"%%DISPLAYNAME%%\"] = session.getDisplayName();\n        values[\"%%WX_CHAT_HISTORY%%\"] = m_resManager.getLocaleString(\"WeChat Chat History\");\n        values[\"%%ASYNC_LOADING_TYPE%%\"] = m_options.getLoadingDataOnScroll() ? \"onscroll\" : \"initial\";\n        \n        if (m_options.isAsyncLoading())\n        {\n            if (m_options.hasPager())\n            {\n                values[\"%%ASYNC_LOADING_TYPE%%\"] = \"pager\";\n                if (m_options.isPagerByYear())\n                {\n                    values[\"%%PAGER_TYPE%%\"] = \"2\";\n                }\n                else if (m_options.isPagerByMonth())\n                {\n                    values[\"%%PAGER_TYPE%%\"] = \"3\";\n                }\n                else\n                {\n                    values[\"%%PAGER_TYPE%%\"] = \"1\";\n                }\n            }\n            else\n            {\n                values[\"%%ASYNC_LOADING_TYPE%%\"] = \"onscroll\";\n            }\n        }\n        else\n        {\n            values[\"%%ASYNC_LOADING_TYPE%%\"] = \"\";\n        }\n\n        values[\"%%SIZE_OF_PAGE%%\"] = std::to_string(PAGE_SIZE);\n        values[\"%%NUMBER_OF_MSGS%%\"] = std::to_string(numberOfMessages);\n        values[\"%%NUMBER_OF_PAGES%%\"] = std::to_string(numberOfPages);\n\n        values[\"%%DATA_PATH%%\"] = \"Files/Data\";\n        values[\"%%PAGE_DATA%%\"] = pageData;\n        \n        // m_logger->debug(\"Before join\");\n        if (!m_options.isHtmlMode() || m_options.isSyncLoading())\n        {\n            values[\"%%BODY%%\"] = join(messages.cbegin(), messages.cend(), \"\");\n        }\n        // m_logger->debug(\"After join\");\n        values[\"%%HEADER_FILTER%%\"] = m_options.isSupportingFilter() ? m_resManager.getTemplate(\"filter\") : \"\";\n\n        const std::string& html = m_resManager.buildFromTemplate(\"frame\", values);\n        m_logger->debug(\"After build html from template\");\n        \n#else   // NOT USING_NEW_TEMPLATE\n        \n        std::string html = m_resManager.getTemplate(\"frame\");\n#if !defined(NDEBUG) || defined(DBG_PERF)\n        if (html.empty())\n        {\n            m_logger->write(\"Template file is empty: frame\");\n        }\n#endif\n        \n#ifndef NDEBUG\n        replaceAll(html, \"%%USRNAME%%\", user.getUsrName() + \" - \" + user.getHash());\n        replaceAll(html, \"%%SESSION_USRNAME%%\", session.getUsrName() + \" - \" + session.getHash());\n#else\n        replaceAll(html, \"%%USRNAME%%\", \"\");\n        replaceAll(html, \"%%SESSION_USRNAME%%\", \"\");\n#endif\n        replaceAll(html, \"%%DISPLAYNAME%%\", session.getDisplayName());\n        replaceAll(html, \"%%WX_CHAT_HISTORY%%\", m_resManager.getLocaleString(\"WeChat Chat History\"));\n        replaceAll(html, \"%%ASYNC_LOADING_TYPE%%\", m_options.getLoadingDataOnScroll() ? \"onscroll\" : \"initial\");\n        \n        replaceAll(html, \"%%SIZE_OF_PAGE%%\", std::to_string(PAGE_SIZE));\n        replaceAll(html, \"%%NUMBER_OF_MSGS%%\", std::to_string(numberOfMessages));\n        replaceAll(html, \"%%NUMBER_OF_PAGES%%\", std::to_string(numberOfPages));\n        \n        replaceAll(html, \"%%DATA_PATH%%\", \"Files/Data\");\n        \n        replaceAll(html, \"%%PAGE_DATA%%\", pageData);\n        \n        replaceAll(html, \"%%BODY%%\", join(b, e, \"\"));\n        replaceAll(html, \"%%HEADER_FILTER%%\", m_options.isSupportingFilter() ? m_resManager.getTemplate(\"filter\") : \"\");\n        \n#endif\n        \n        std::string fileName = combinePath(outputBase, session.getOutputFileName(), \"index.\" + m_extName);\n        if (!writeFile(fileName, html))\n        {\n            m_logger->write(\"Failed to write chat history file: \" + fileName);\n        }\n        // m_logger->debug(\"Finish writing html file\");\n    }\n    \n    // m_logger->debug(\"Session exporting ends.\");\n    \n    return numberOfMsgs;\n}\n\nbool Exporter::exportMessage(const Session& session, const std::vector<TemplateValues>& tvs, std::vector<std::string>& messages)\n{\n    std::string content;\n    for (std::vector<TemplateValues>::const_iterator it = tvs.cbegin(); it != tvs.cend(); ++it)\n    {\n#if USING_NEW_TEMPLATE\n        content.append(m_resManager.buildFromTemplate(it->getName(), it->getValues()));\n#else\n        content.append(buildContentFromTemplateValues(*it));\n#endif\n    }\n    \n    messages.push_back(content);\n    return m_cancelled;\n}\n\nbool Exporter::exportPageToFile(const Friend& user, const Session& session, const std::vector<TemplateValues>& tvs, std::vector<std::string>& messages, const PageInfo& pageInfo, const std::string& outputBase)\n{\n    if (messages.empty())\n    {\n        return true;\n    }\n    \n    std::string dataPath = combinePath(outputBase, session.getOutputFileName(), \"Files\", \"Data\");\n    makeDirectory(dataPath);\n    \n    std::string rawMsgFileName = combinePath(m_output, WXEXP_DATA_FOLDER, session.getOwner()->getUsrName(), session.getUsrName() + \".dat\");\n    if (m_options.isIncrementalExporting())\n    {\n        m_logger->debug(\"Merge incremental messages.\");\n        mergeMessages(rawMsgFileName, messages);\n    }\n    \n    m_logger->debug(\"Save messages for incremental exporting.\");\n    serializeMessages(rawMsgFileName, messages);\n    \n    // m_logger->debug(\"After serializeMessages.\");\n\n    // auto b = messages.cbegin();\n    // No page for text mode\n    // auto e = (((m_options & (SPO_TEXT_MODE | SPO_SYNC_LOADING)) != 0) || (messages.size() <= PAGE_SIZE)) ? messages.cend() : (b + PAGE_SIZE);\n    \n    // const size_t numberOfMessages = std::distance(e, messages.cend());\n    // const size_t numberOfPages = (numberOfMessages + PAGE_SIZE - 1) / PAGE_SIZE;\n    \n#if USING_NEW_TEMPLATE\n    std::map<std::string, std::string> values;\n    \n#ifndef NDEBUG\n    values[\"%%USRNAME%%\"] = user.getUsrName() + \" - \" + user.getHash();\n    values[\"%%SESSION_USRNAME%%\"] = session.getUsrName() + \" - \" + session.getHash();\n#else\n    values[\"%%USRNAME%%\"] = \"\";\n    values[\"%%SESSION_USRNAME%%\"] = \"\";\n#endif\n    values[\"%%DISPLAYNAME%%\"] = session.getDisplayName();\n    values[\"%%WX_CHAT_HISTORY%%\"] = m_resManager.getLocaleString(\"WeChat Chat History\");\n    values[\"%%ASYNC_LOADING_TYPE%%\"] = m_options.getLoadingDataOnScroll() ? \"onscroll\" : \"initial\";\n    \n    values[\"%%SIZE_OF_PAGE%%\"] = std::to_string(PAGE_SIZE);\n    // values[\"%%NUMBER_OF_MSGS%%\"] = std::to_string(numberOfMessages);\n    // values[\"%%NUMBER_OF_PAGES%%\"] = std::to_string(numberOfPages);\n\n    values[\"%%DATA_PATH%%\"] = \"Files/Data\";\n\n    // m_logger->debug(\"Before join\");\n    values[\"%%BODY%%\"] = join(messages.cbegin(), messages.cend(), \"\");\n    // m_logger->debug(\"After join\");\n    values[\"%%HEADER_FILTER%%\"] = m_options.isSupportingFilter() ? m_resManager.getTemplate(\"filter\") : \"\";\n\n    const std::string& html = m_resManager.buildFromTemplate(\"frame\", values);\n    m_logger->debug(\"After build html from template\");\n#else   // NOT USING_NEW_TEMPLATE\n    std::string html = m_resManager.getTemplate(\"frame\");\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    if (html.empty())\n    {\n        m_logger->write(\"Template file is empty: frame\");\n    }\n#endif\n#ifndef NDEBUG\n    replaceAll(html, \"%%USRNAME%%\", user.getUsrName() + \" - \" + user.getHash());\n    replaceAll(html, \"%%SESSION_USRNAME%%\", session.getUsrName() + \" - \" + session.getHash());\n#else\n    replaceAll(html, \"%%USRNAME%%\", \"\");\n    replaceAll(html, \"%%SESSION_USRNAME%%\", \"\");\n#endif\n    replaceAll(html, \"%%DISPLAYNAME%%\", session.getDisplayName());\n    replaceAll(html, \"%%WX_CHAT_HISTORY%%\", m_resManager.getLocaleString(\"WeChat Chat History\"));\n    replaceAll(html, \"%%ASYNC_LOADING_TYPE%%\", m_options.getLoadingDataOnScroll() ? \"onscroll\" : \"initial\");\n    \n    replaceAll(html, \"%%SIZE_OF_PAGE%%\", std::to_string(PAGE_SIZE));\n    // replaceAll(html, \"%%NUMBER_OF_MSGS%%\", std::to_string(numberOfMessages));\n    // replaceAll(html, \"%%NUMBER_OF_PAGES%%\", std::to_string(numberOfPages));\n    \n    replaceAll(html, \"%%DATA_PATH%%\", \"Files/Data\");\n    \n    replaceAll(html, \"%%BODY%%\", join(messages.cbegin(), messages.cend(), \"\"));\n    replaceAll(html, \"%%HEADER_FILTER%%\", m_options.isSupportingFilter() ? m_resManager.getTemplate(\"filter\") : \"\");\n    \n#endif\n\n    std::string outputFileName = \"index\";\n    if (pageInfo.getPage() > 0)\n    {\n        outputFileName += \"-\" + std::to_string(pageInfo.getPage());\n    }\n    outputFileName += m_extName;\n    \n    std::string fullFileName = combinePath(outputBase, session. getOutputFileName(), outputFileName);\n    if (!writeFile(fullFileName, html))\n    {\n        m_logger->write(\"Failed to write chat history file: \" + fullFileName);\n    }\n    // m_logger->debug(\"Finish writing html file\");\n    \n    // if ((m_options & SPO_SYNC_LOADING) == 0 && numberOfPages > 0)\n    {\n        // makeDirectory(dataPath);\n\n        // for (size_t page = 0; page < numberOfPages; ++page)\n        {\n            // b = e;\n            std::string scripts = m_resManager.getTemplate(\"scripts\");\n            // e = (page == (numberOfPages - 1)) ? messages.cend() : (b + PAGE_SIZE);\n            Json::Value jsonMsgs(Json::arrayValue);\n            for (auto it = messages.cbegin(); it != messages.cend(); ++it)\n            {\n                jsonMsgs.append(*it);\n            }\n            Json::StreamWriterBuilder builder;\n#ifndef NDEBUG\n            builder[\"indentation\"] = \"\\t\";  // assume default for comments is None\n            builder[\"emitUTF8\"] = true;\n#else\n            builder[\"indentation\"] = \"\";\n#endif\n            std::string moreMsgs = Json::writeString(builder, jsonMsgs);\n\n            replaceAll(scripts, \"%%JSON_DATA%%\", moreMsgs);\n            \n            fullFileName = combinePath(dataPath, \"msg-\" + std::to_string(pageInfo.getPage() + 1) + \".js\");\n            writeFile(fullFileName, scripts);\n        }\n    }\n    \n    return true;\n}\n\nvoid Exporter::serializeMessages(const std::string& fileName, const std::vector<std::string>& messages)\n{\n    File file;\n    size_t byteWriten = 0;\n    if (file.open(fileName, false))\n    {\n        uint32_t size = htonl(static_cast<uint32_t>(messages.size()));\n        file.write(reinterpret_cast<const unsigned char *>(&size), sizeof(size), byteWriten);\n        for (std::vector<std::string>::const_iterator it = messages.cbegin(); it != messages.cend(); ++it)\n        {\n            size = htonl(static_cast<uint32_t>(it->size()));\n            file.write(reinterpret_cast<const unsigned char *>(&size), sizeof(size), byteWriten);\n            // appendFile(fileName, reinterpret_cast<const unsigned char *>(&size), sizeof(size));\n            file.write(reinterpret_cast<const unsigned char *>(it->c_str()), it->size(), byteWriten);\n        }\n        file.close();\n    }\n}\n\nvoid Exporter::unserializeMessages(const std::string& fileName, std::vector<std::string>& messages)\n{\n    std::vector<unsigned char> data;\n    if (!readFile(fileName, data))\n    {\n        messages.clear();\n        return;\n    }\n    \n    size_t dataSize = data.size();\n    if (dataSize < sizeof(uint32_t))\n    {\n        return;\n    }\n    \n    size_t offset = 0;\n    uint32_t itemSize = 0;\n    memcpy(&itemSize, &data[offset], sizeof(uint32_t));\n    offset += sizeof(uint32_t);\n    itemSize = ntohl(itemSize);\n    \n    messages.clear();\n    messages.reserve(itemSize);\n    \n    uint32_t sizeOfString = 0;\n    for (uint32_t idx = 0; idx < itemSize; ++idx)\n    {\n        if (offset + sizeof(uint32_t) > dataSize)\n        {\n            break;\n        }\n        memcpy(&sizeOfString, &data[offset], sizeof(uint32_t));\n        offset += sizeof(uint32_t);\n        sizeOfString = ntohl(sizeOfString);\n        \n        if (offset + sizeOfString > dataSize)\n        {\n            break;\n        }\n        \n        messages.emplace_back(reinterpret_cast<const char *>(&data[offset]), sizeOfString);\n        offset += sizeOfString;\n    }\n}\n\nvoid Exporter::mergeMessages(const std::string& fileName, std::vector<std::string>& messages)\n{\n    std::vector<std::string> orgMessages;\n    unserializeMessages(fileName, orgMessages);\n    \n    // std::string contents = readFile(fileName);\n    if (!m_options.isDesc())\n    {\n        // ASC\n        messages.swap(orgMessages);\n    }\n    \n    messages.reserve(messages.size() + orgMessages.size());\n    messages.insert(messages.cend(), orgMessages.cbegin(), orgMessages.cend());\n}\n\nbool Exporter::buildFileNameForUser(Friend& user, std::set<std::string>& existingFileNames)\n{\n    std::string names[] = {user.getDisplayName(), user.getUsrName(), user.getHash()};\n    \n    bool succeeded = false;\n    for (int idx = 0; idx < 3; ++idx)\n    {\n        std::string outputFileName = removeInvalidCharsForFileName(names[idx]);\n        if (isValidFileName(outputFileName))\n        {\n            if ( existingFileNames.find(outputFileName) != existingFileNames.cend())\n            {\n                int idx = 1;\n                while (idx++)\n                {\n                    if (existingFileNames.find(outputFileName + \"_\" + std::to_string(idx)) == existingFileNames.cend())\n                    {\n                        outputFileName += \"_\" + std::to_string(idx);\n                        break;\n                    }\n                }\n            }\n            user.setOutputFileName(outputFileName);\n            existingFileNames.insert(outputFileName);\n            succeeded = true;\n            break;\n        }\n    }\n    \n    return succeeded;\n}\n\nvoid Exporter::releaseITunes()\n{\n    if (NULL != m_iTunesDb)\n    {\n        delete m_iTunesDb;\n        m_iTunesDb = NULL;\n    }\n    if (NULL != m_iTunesDbShare)\n    {\n        delete m_iTunesDbShare;\n        m_iTunesDbShare = NULL;\n    }\n}\n\nbool Exporter::loadITunes(bool detailedInfo/* = true*/)\n{\n    releaseITunes();\n    \n#ifndef USING_DECODED_ITUNESBACKUP\n    m_iTunesDb = new ITunesDb(m_backup, \"Manifest.db\");\n#else\n    m_iTunesDb = new DecodedWechatITunesDb(m_backup, \"Manifest.db\");\n#endif\n    if (!detailedInfo)\n    {\n        std::function<bool(const char*, int)> fn = std::bind(&Exporter::filterITunesFile, this, std::placeholders::_1, std::placeholders::_2);\n        m_iTunesDb->setLoadingFilter(fn);\n    }\n    if (!m_iTunesDb->load(\"AppDomain-com.tencent.xin\", !detailedInfo))\n    {\n        return false;\n    }\n#ifndef USING_DECODED_ITUNESBACKUP\n    m_iTunesDbShare = new ITunesDb(m_backup, \"Manifest.db\");\n#else\n    m_iTunesDbShare = new DecodedWechatITunesDb(m_backup, \"Manifest.db\");\n#endif\n    \n    if (!m_iTunesDbShare->load(\"AppDomainGroup-group.com.tencent.xin\"))\n    {\n        // Optional\n        // return false;\n    }\n    \n    return true;\n}\n\nstd::string Exporter::getITunesVersion() const\n{\n    return NULL != m_iTunesDb ? m_iTunesDb->getVersion() : \"\";\n}\n\nstd::string Exporter::getIOSVersion() const\n{\n    return NULL != m_iTunesDb ? m_iTunesDb->getIOSVersion() : \"\";\n}\n\nstd::string Exporter::getWechatVersion() const\n{\n    return m_wechatInfo.getVersion();\n}\n\nstd::string Exporter::buildContentFromTemplateValues(const TemplateValues& tv) const\n{\n#if !defined(NDEBUG) && defined(SAMPLING_TMPL)\n    std::string alignment = \"\";\n#endif\n    std::string content = m_resManager.getTemplate(tv.getName());\n    for (TemplateValues::const_iterator it = tv.cbegin(); it != tv.cend(); ++it)\n    {\n        if (startsWith(it->first, \"%\"))\n        {\n            replaceAll(content, it->first, it->second);\n        }\n#if !defined(NDEBUG) && defined(SAMPLING_TMPL)\n        if (it->first == \"%%ALIGNMENT%%\")\n        {\n            alignment = it->second;\n        }\n#endif\n    }\n    \n    std::string::size_type pos = 0;\n\n    while ((pos = content.find(\"%%\", pos)) != std::string::npos)\n    {\n        std::string::size_type posEnd = content.find(\"%%\", pos + 2);\n        if (posEnd == std::string::npos)\n        {\n            break;\n        }\n        \n        content.erase(pos, posEnd + 2 - pos);\n    }\n    \n#if !defined(NDEBUG) && defined(SAMPLING_TMPL)\n    std::string fileName = \"sample_\" + tv.getName() + alignment + \".html\";\n    writeFile(combinePath(m_output, \"dbg\", fileName), content);\n#endif\n    \n    return content;\n}\n\nvoid Exporter::notifyStart()\n{\n    if (m_notifier)\n    {\n        m_notifier->onStart();\n    }\n}\n\nvoid Exporter::notifyComplete(bool cancelled/* = false*/)\n{\n    if (m_notifier)\n    {\n        m_notifier->onComplete(cancelled);\n    }\n}\n\nvoid Exporter::notifyProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages)\n{\n    if (m_notifier)\n    {\n        m_notifier->onProgress(numberOfMessages, numberOfTotalMessages);\n    }\n}\n\nvoid Exporter::notifySessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages)\n{\n    if (m_notifier)\n    {\n        m_notifier->onSessionStart(sessionUsrName, sessionData, numberOfTotalMessages);\n    }\n}\n\nvoid Exporter::notifySessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled/* = false*/)\n{\n    if (m_notifier)\n    {\n        m_notifier->onSessionComplete(sessionUsrName, sessionData, cancelled);\n    }\n}\n\nvoid Exporter::notifySessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages)\n{\n    if (m_notifier)\n    {\n        m_notifier->onSessionProgress(sessionUsrName, sessionData, numberOfMessages, numberOfTotalMessages);\n    }\n}\n\nvoid Exporter::notifyTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks)\n{\n    if (m_notifier)\n    {\n        m_notifier->onTasksStart(usrName, numberOfTotalTasks);\n    }\n}\n\nvoid Exporter::notifyTasksComplete(const std::string& usrName, bool cancelled/* = false*/)\n{\n    if (m_notifier)\n    {\n        m_notifier->onTasksComplete(usrName, cancelled);\n    }\n}\n\nvoid Exporter::notifyTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalTasks)\n{\n    if (m_notifier)\n    {\n        m_notifier->onTasksProgress(usrName, numberOfCompletedTasks, numberOfTotalTasks);\n    }\n}\n\nbool Exporter::filterITunesFile(const char *file, int flags) const\n{\n    if (startsWith(file, \"Documents/MMappedKV/\"))\n    {\n        return startsWith(file, \"mmsetting\", 20);\n    }\n    \n    if (std::strncmp(file, \"Documents/MapDocument/\", 22) == 0 ||\n        std::strncmp(file, \"Library/WebKit/\", 15) == 0)\n    {\n        return false;\n    }\n    \n    const char *str = std::strchr(file, '/');\n    if (str != NULL)\n    {\n        str = std::strchr(str + 1, '/');\n        if (str != NULL)\n        {\n            if (std::strncmp(str, \"/Audio/\", 7) == 0 ||\n                std::strncmp(str, \"/Img/\", 5) == 0 ||\n                std::strncmp(str, \"/OpenData/\", 10) == 0 ||\n                std::strncmp(str, \"/Video/\", 7) == 0 ||\n                std::strncmp(str, \"/appicon/\", 9) == 0 ||\n                std::strncmp(str, \"/translate/\", 11) == 0 ||\n                std::strncmp(str, \"/Brand/\", 7) == 0 ||\n                std::strncmp(str, \"/Pattern_v3/\", 12) == 0 ||\n                std::strncmp(str, \"/WCPay/\", 7) == 0)\n            {\n                return false;\n            }\n        }\n    }\n    \n    return true;\n}\n"
  },
  {
    "path": "WechatExporter/core/Exporter.h",
    "content": "//\n//  Exporter.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include <set>\n#include <string>\n#include <thread>\n#include <atomic>\n\n#include \"Logger.h\"\n#include \"PdfConverter.h\"\n#include \"WechatObjects.h\"\n#include \"ExportOption.h\"\n#include \"ITunesParser.h\"\n#include \"ExportNotifier.h\"\n#include \"ResManager.h\"\n\n// #define USING_ASYNC_TASK_FOR_MP3\n\n#ifndef Exporter_h\n#define Exporter_h\n\nclass MessageParser;\nclass TemplateValues;\nclass ExportContext;\nclass PageInfo;\n\nclass Exporter\n{\nprotected:\n    std::atomic_bool m_running;\n    std::thread m_thread;\n\n    // semaphore& m_signal;\n    std::string m_workDir;\n    \n    WechatInfo m_wechatInfo;\n    std::string m_backup;\n    std::string m_output;\n    Logger* m_logger;\n    PdfConverter* m_pdfConverter;\n    \n    ITunesDb *m_iTunesDb;\n    ITunesDb *m_iTunesDbShare;\n    ResManager m_resManager;\n    \n    std::map<std::string, std::string> m_templates;\n    std::map<std::string, std::string> m_localeStrings;\n\n    ExportNotifier* m_notifier;\n    \n    std::atomic<bool> m_cancelled;\n    ExportOption m_options;\n    // bool m_filterByName;\n    std::string m_extName;\n    std::string m_templatesName;\n    \n    std::map<std::string, std::map<std::string, void *>> m_usersAndSessionsFilter;\n    \n    std::vector<std::pair<Friend, std::vector<Session>>> m_usersAndSessions;\n    \n    ExportContext*  m_exportContext;\n    \n    std::string m_languageCode;\n    \n    std::map<uint64_t, std::string> m_tags;\n\npublic:\n    Exporter(const std::string& workDir, const std::string& backup, const std::string& output, Logger* logger, PdfConverter* pdfConverter);\n    ~Exporter();\n\n    void setNotifier(ExportNotifier *notifier);\n    \n    bool loadUsersAndSessions();\n    void swapUsersAndSessions(std::vector<std::pair<Friend, std::vector<Session>>>& usersAndSessions);\n\n    bool run();\n    bool isRunning() const;\n    void cancel();\n    void waitForComplition();\n    \n    void setOptions(const ExportOption& options);\n    \n    void filterUsersAndSessions(const std::map<std::string, std::map<std::string, void *>>& usersAndSessions);\n    \n    void setExtName(const std::string& extName);\n    void setTemplatesName(const std::string& templatesName);\n    \n    void setFilterByName();\n    \n    void setLanguageCode(const std::string& languageCode);\n    \n    std::string getITunesVersion() const;\n    std::string getIOSVersion() const;\n    std::string getWechatVersion() const;\n    \n    static void initializeExporter();\n    static void uninitializeExporter();\n    \n    static bool hasPreviousExporting(const std::string& outputDir, uint64_t& options, std::string& exportTime, std::string& version);\n\nprotected:\n    bool runImpl();\n    bool exportUser(Friend& user, std::string& userOutputPath);\n    // bool loadUserSessions(Friend& user, std::vector<Session>& sessions) const;\n    bool loadUserFriendsAndSessions(const Friend& user, Friends& friends, std::vector<Session>& sessions, bool detailedInfo = true);\n    int exportSession(const Friend& user, const MessageParser& msgParser, const Session& session, const std::string& userBase, const std::string& outputBase);\n    \n    bool exportMessage(const Session& session, const std::vector<TemplateValues>& tvs, std::vector<std::string>& messages);\n\n    bool buildScriptFile(const std::string& fileName, std::vector<std::string>::const_iterator b, std::vector<std::string>::const_iterator e, const PageInfo& page) const;\n    \n    \n    void releaseITunes();\n    bool loadITunes(bool detailedInfo = true);\n    \n    bool hasDebugLogs() const;\n    bool isSubscriptionIncluded() const;\n\n    void notifyStart();\n    void notifyComplete(bool cancelled = false);\n    void notifyProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages);\n    void notifySessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages);\n    void notifySessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled = false);\n    void notifySessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages);\n    void notifyTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks);\n    void notifyTasksComplete(const std::string& usrName, bool cancelled = false);\n    void notifyTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalTasks);\n    bool buildFileNameForUser(Friend& user, std::set<std::string>& existingFileNames);\n    std::string buildContentFromTemplateValues(const TemplateValues& values) const;\n    \n    bool filterITunesFile(const char * file, int flags) const;\n    \n    bool exportPageToFile(const Friend& user, const Session& session, const std::vector<TemplateValues>& tvs, std::vector<std::string>& messages, const PageInfo& pagenfo, const std::string& outputBase);\n    void serializeMessages(const std::string& fileName, const std::vector<std::string>& messages);\n    void unserializeMessages(const std::string& fileName, std::vector<std::string>& messages);\n    void mergeMessages(const std::string& fileName, std::vector<std::string>& messages);\n    \n    static bool loadExportContext(const std::string& contextFile, ExportContext *context);\n    \n    \n};\n\n#endif /* Exporter_h */\n"
  },
  {
    "path": "WechatExporter/core/FileSystem.cpp",
    "content": "//\r\n//  FileSystem_Win.cpp\r\n//  WechatExporter\r\n//\r\n//  Created by Matthew on 2021/1/21.\r\n//  Copyright © 2021 Matthew. All rights reserved.\r\n//\r\n\r\n#include \"FileSystem.h\"\r\n#include \"Utils.h\"\r\n#ifndef NDEBUG\r\n#include <cassert>\r\n#endif\r\n// #include <iomanip>\r\n#include <fstream>\r\n#include <queue>\r\n\r\n#ifdef _WIN32\r\n#include <algorithm>\r\n#include <atlstr.h>\r\n#include <sys/utime.h>\r\n#include <Shlwapi.h>\r\n#include <shlobj_core.h>\r\n#else //  _WIN32\r\n\r\n#ifdef __APPLE__\r\n// https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/copyfile.3.html\r\n#include <copyfile.h>\r\n#endif\r\n#include <sys/stat.h>\r\n#include <sys/statvfs.h>\r\n#include <sys/types.h>\r\n#include <dirent.h>\r\n#include <errno.h>\r\n#include <fts.h>\r\n#endif //  _WIN32\r\n\r\n#ifdef _WIN32\r\ninline size_t getFileSizeImpl(LPCTSTR path)\r\n#else\r\ninline size_t getFileSizeImpl(const std::string& path)\r\n#endif\r\n{\r\n#ifdef _WIN32\r\n    HANDLE hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);\r\n    if (hFile == INVALID_HANDLE_VALUE)\r\n        return -1; // error condition, could call GetLastError to find out more\r\n\r\n    LARGE_INTEGER size;\r\n    if (!GetFileSizeEx(hFile, &size))\r\n    {\r\n        CloseHandle(hFile);\r\n        return -1; // error condition, could call GetLastError to find out more\r\n    }\r\n\r\n    CloseHandle(hFile);\r\n    return (size_t)size.QuadPart;\r\n#else\r\n    struct stat sb;\r\n    int rc = stat(path.c_str(), &sb);\r\n    return rc == 0 ? sb.st_size : -1;\r\n#endif\r\n}\r\n\r\nsize_t getFileSize(const std::string& path)\r\n{\r\n#ifdef _WIN32\r\n    CW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n    return getFileSizeImpl((LPCTSTR)pszT);\r\n#else\r\n    return getFileSizeImpl(path);\r\n#endif\r\n}\r\n\r\n#ifdef _WIN32\r\ninline bool existsDirectoryImpl(LPCTSTR lpszPath)\r\n{\r\n    DWORD dwAttrib = ::GetFileAttributes(lpszPath);\r\n    return (dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY);\r\n}\r\n\r\ninline bool isDirectoryImpl(LPCTSTR lpszPath)\r\n{\r\n    DWORD dwAttrib = ::GetFileAttributes(lpszPath);\r\n    return (dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY);\r\n}\r\n\r\ntime_t FileTimeToTime(FILETIME ft)\r\n{\r\n    // takes the last modified date\r\n    LARGE_INTEGER date, adjust;\r\n    date.HighPart = ft.dwHighDateTime;\r\n    date.LowPart = ft.dwLowDateTime;\r\n    \r\n    const uint64_t EpochShift = 116444736000000000;\r\n\r\n    if (date.QuadPart < EpochShift)\r\n        return -1;\r\n    // 100-nanoseconds = milliseconds * 10000\r\n    \r\n    // removes the diff between 1970 and 1601\r\n    date.QuadPart -= EpochShift;\r\n\r\n    // converts back from 100-nanoseconds to seconds\r\n    return date.QuadPart / 10000000;\r\n}\r\n\r\n#else\r\n\r\ninline bool isDirectoryImpl(const std::string& path)\r\n{\r\n    struct stat sb;\r\n    return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));\r\n}\r\n\r\n#endif\r\n\r\n\r\nbool existsDirectory(const std::string& path)\r\n{\r\n#ifdef _WIN32\r\n\tCW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n\treturn existsDirectoryImpl(pszT);\r\n#else\r\n    struct stat sb;\r\n    return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));\r\n#endif\r\n}\r\n\r\n#ifndef _WIN32\r\nint makePathImpl(const std::string::value_type *path, mode_t mode)\r\n{\r\n    struct stat st;\r\n    int status = 0;\r\n\r\n    if (stat(path, &st) != 0)\r\n    {\r\n        // Directory does not exist. EEXIST for race condition\r\n        if (mkdir(path, mode) != 0 && errno != EEXIST)\r\n            status = -1;\r\n    }\r\n    else if (!S_ISDIR(st.st_mode))\r\n    {\r\n        // errno = ENOTDIR;\r\n        status = -1;\r\n    }\r\n\r\n    return status;\r\n}\r\n#endif\r\n\r\nbool makeDirectory(const std::string& path)\r\n{\r\n    if (path == \"/Users/matthew/Documents/WxExp/WechatHistory.Test/Matthew/朱磊, 石绮莹, Matthew 施立波/Portrait\" || path == \"/Users/matthew/Documents/WxExp/WechatHistory.Test/Matthew/朱磊, 石绮莹, Matthew 施立波/Emoji\" || path == \"/Users/matthew/Documents/WxExp/WechatHistory.Test/Matthew/朱磊, 石绮莹, Matthew 施立波/Portrait/\" || path == \"/Users/matthew/Documents/WxExp/WechatHistory.Test/Matthew/朱磊, 石绮莹, Matthew 施立波/Emoji/\")\r\n    {\r\n        int aa = 0;\r\n    }\r\n#ifndef NDEBUG\r\n    // The path must be full/absolute path\r\n#ifdef _WIN32\r\n    assert(path.find(\":\\\\\") != std::string::npos);\r\n#else\r\n    assert(startsWith(path, \"/\"));\r\n#endif\r\n#endif\r\n    \r\n#ifdef _WIN32\r\n\tCW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n\r\n\treturn ::SHCreateDirectoryEx(NULL, (LPCTSTR)pszT, NULL) == ERROR_SUCCESS;\r\n#else\r\n    std::vector<std::string::value_type> copypath;\r\n    copypath.reserve(path.size() + 1);\r\n    std::copy(path.begin(), path.end(), std::back_inserter(copypath));\r\n    copypath.push_back('\\0');\r\n    std::replace(copypath.begin(), copypath.end(), '\\\\', '/');\r\n    \r\n    std::vector<std::string::value_type>::iterator itStart = copypath.begin();\r\n    std::vector<std::string::value_type>::iterator it;\r\n    \r\n    int status = 0;\r\n    mode_t mode = 0755;\r\n    while (status == 0 && (it = std::find(itStart, copypath.end(), '/')) != copypath.end())\r\n    {\r\n        if (it != copypath.begin())\r\n        {\r\n            // Neither root nor double slash in path\r\n            *it = '\\0';\r\n            status = makePathImpl(&copypath[0], mode);\r\n            *it = '/';\r\n        }\r\n        itStart = it + 1;\r\n    }\r\n    if (status == 0)\r\n    {\r\n        status = makePathImpl(&copypath[0], mode);\r\n    }\r\n    \r\n    return status == 0;\r\n#endif\r\n}\r\n\r\nbool deleteFile(const std::string& path)\r\n{\r\n#ifdef _WIN32\r\n\tCW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n\treturn ::DeleteFile((LPCTSTR)pszT) == TRUE;\r\n#else\r\n    return 0 == std::remove(path.c_str());\r\n#endif\r\n}\r\n\r\nbool deleteDirectory(const std::string& path)\r\n{\r\n#ifdef _WIN32\r\n\tCW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n\r\n\tsize_t len = _tcslen(pszT);\r\n\tTCHAR* pszT2 = new TCHAR[len + 2];\r\n\t_tcscpy(pszT2, pszT);\r\n\tpszT2[len + 1] = 0;\r\n\tpszT2[len] = 0;\r\n\r\n\tSHFILEOPSTRUCT file_op = {\r\n\t\tNULL,\r\n\t\tFO_DELETE,\r\n\t\tpszT2,\r\n\t\tNULL,\r\n\t\tFOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT,\r\n\t\tFALSE,\r\n\t\tNULL,\r\n\t\tNULL };\r\n\r\n\tbool result = (SHFileOperation(&file_op) == 0);\r\n\tdelete[] pszT2;\r\n\treturn result;\r\n#else\r\n    int ret = 0;\r\n    FTS *ftsp = NULL;\r\n    FTSENT *curr;\r\n\r\n    // Cast needed (in C) because fts_open() takes a \"char * const *\", instead\r\n    // of a \"const char * const *\", which is only allowed in C++. fts_open()\r\n    // does not modify the argument.\r\n    char *files[] = { (char *) path.c_str(), NULL };\r\n\r\n    // FTS_NOCHDIR  - Avoid changing cwd, which could cause unexpected behavior\r\n    //                in multithreaded programs\r\n    // FTS_PHYSICAL - Don't follow symlinks. Prevents deletion of files outside\r\n    //                of the specified directory\r\n    // FTS_XDEV     - Don't cross filesystem boundaries\r\n    ftsp = fts_open(files, FTS_NOCHDIR | FTS_PHYSICAL | FTS_XDEV, NULL);\r\n    if (!ftsp) {\r\n        // fprintf(stderr, \"%s: fts_open failed: %s\\n\", dir, strerror(errno));\r\n        ret = -1;\r\n        goto finish;\r\n    }\r\n\r\n    while ((curr = fts_read(ftsp)))\r\n    {\r\n        switch (curr->fts_info) {\r\n        case FTS_NS:\r\n        case FTS_DNR:\r\n        case FTS_ERR:\r\n            // fprintf(stderr, \"%s: fts_read error: %s\\n\", curr->fts_accpath, strerror(curr->fts_errno));\r\n            break;\r\n\r\n        case FTS_DC:\r\n        case FTS_DOT:\r\n        case FTS_NSOK:\r\n            // Not reached unless FTS_LOGICAL, FTS_SEEDOT, or FTS_NOSTAT were\r\n            // passed to fts_open()\r\n            break;\r\n\r\n        case FTS_D:\r\n            // Do nothing. Need depth-first search, so directories are deleted\r\n            // in FTS_DP\r\n            break;\r\n\r\n        case FTS_DP:\r\n        case FTS_F:\r\n        case FTS_SL:\r\n        case FTS_SLNONE:\r\n        case FTS_DEFAULT:\r\n            if (remove(curr->fts_accpath) < 0) {\r\n                // fprintf(stderr, \"%s: Failed to remove: %s\\n\", curr->fts_path, strerror(curr->fts_errno));\r\n                ret = -1;\r\n            }\r\n            break;\r\n        }\r\n    }\r\n\r\nfinish:\r\n    if (ftsp)\r\n    {\r\n        fts_close(ftsp);\r\n    }\r\n\r\n    return ret == 0;\r\n#endif\r\n}\r\n\r\n#ifdef _WIN32\r\ninline bool existsFileImpl(LPCTSTR path)\r\n#else\r\ninline bool existsFileImpl(const std::string& path)\r\n#endif\r\n{\r\n#ifdef _WIN32\r\n    DWORD dwAttrib = ::GetFileAttributes(path);\r\n    return (dwAttrib != INVALID_FILE_ATTRIBUTES &&\r\n        (dwAttrib & FILE_ATTRIBUTE_DIRECTORY) == 0);\r\n#else\r\n    struct stat sb;\r\n    return (stat(path.c_str(), &sb) == 0);\r\n#endif\r\n}\r\n\r\nbool existsFile(const std::string& path)\r\n{\r\n#ifdef _WIN32\r\n\tCW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n\treturn existsFileImpl((LPCTSTR)pszT);\r\n#else\r\n    return existsFileImpl(path);\r\n#endif\r\n}\r\n\r\nbool listSubDirectories(const std::string& path, std::vector<std::string>& subDirectories)\r\n{\r\n#ifdef _WIN32\r\n\tWIN32_FIND_DATA FindFileData;\r\n\tHANDLE hFind = INVALID_HANDLE_VALUE;\r\n\r\n\tstd::string formatedPath = combinePath(path, \"*.*\");\r\n\tstd::replace(formatedPath.begin(), formatedPath.end(), ALT_DIR_SEP, DIR_SEP);\r\n\r\n\tCW2T localPath(CA2W(formatedPath.c_str(), CP_UTF8));\r\n\r\n\thFind = ::FindFirstFile((LPTSTR)localPath, &FindFileData);\r\n\tif (hFind == INVALID_HANDLE_VALUE)\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\tdo\r\n\t{\r\n\t\tif (_tcscmp(FindFileData.cFileName, TEXT(\".\")) == 0 || _tcscmp(FindFileData.cFileName, TEXT(\"..\")) == 0)\r\n\t\t{\r\n\t\t\tcontinue;\r\n\t\t}\r\n\t\tif (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)\r\n\t\t{\r\n\t\t\tCW2A pszU8(CT2W(FindFileData.cFileName), CP_UTF8);\r\n\t\t\tsubDirectories.push_back((LPCSTR)pszU8);\r\n\t\t}\r\n\t} while (::FindNextFile(hFind, &FindFileData));\r\n\t::FindClose(hFind);\r\n#else\r\n    struct dirent *entry;\r\n    DIR *dir = opendir(path.c_str());\r\n    if (dir == NULL)\r\n    {\r\n        return false;\r\n    }\r\n\r\n    while ((entry = readdir(dir)) != NULL)\r\n    {\r\n        if (strcmp(entry->d_name, \".\") == 0 || strcmp(entry->d_name, \"..\") == 0)\r\n        {\r\n            continue;\r\n        }\r\n        std::string subDir = entry->d_name;\r\n        // TODO: Check directory or not\r\n        if (isDirectoryImpl(combinePath(path, subDir)))\r\n        {\r\n            subDirectories.push_back(subDir);\r\n        }\r\n    }\r\n    closedir(dir);\r\n#endif\r\n    return true;\r\n}\r\n\r\nbool listDirectory(const std::string& path, std::vector<std::string>& subDirectories, std::vector<std::string>& subFiles)\r\n{\r\n#ifdef _WIN32\r\n    WIN32_FIND_DATA FindFileData;\r\n    HANDLE hFind = INVALID_HANDLE_VALUE;\r\n\r\n    std::string formatedPath = combinePath(path, \"*.*\");\r\n    std::replace(formatedPath.begin(), formatedPath.end(), ALT_DIR_SEP, DIR_SEP);\r\n\r\n    CW2T localPath(CA2W(formatedPath.c_str(), CP_UTF8));\r\n\r\n    hFind = ::FindFirstFile((LPTSTR)localPath, &FindFileData);\r\n    if (hFind == INVALID_HANDLE_VALUE)\r\n    {\r\n        return false;\r\n    }\r\n    do\r\n    {\r\n        if (_tcscmp(FindFileData.cFileName, TEXT(\".\")) == 0 || _tcscmp(FindFileData.cFileName, TEXT(\"..\")) == 0)\r\n        {\r\n            continue;\r\n        }\r\n        \r\n        CW2A pszU8(CT2W(FindFileData.cFileName), CP_UTF8);\r\n        if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)\r\n        {\r\n            subDirectories.push_back((LPCSTR)pszU8);\r\n        }\r\n        else\r\n        {\r\n            subFiles.push_back((LPCSTR)pszU8);\r\n        }\r\n    } while (::FindNextFile(hFind, &FindFileData));\r\n    ::FindClose(hFind);\r\n#else\r\n    struct dirent *entry;\r\n    DIR *dir = opendir(path.c_str());\r\n    if (dir == NULL)\r\n    {\r\n        return false;\r\n    }\r\n\r\n    while ((entry = readdir(dir)) != NULL)\r\n    {\r\n        if (strcmp(entry->d_name, \".\") == 0 || strcmp(entry->d_name, \"..\") == 0)\r\n        {\r\n            continue;\r\n        }\r\n        std::string subDir = entry->d_name;\r\n        if (isDirectoryImpl(combinePath(path, subDir)))\r\n        {\r\n            subDirectories.push_back(subDir);\r\n        }\r\n        else\r\n        {\r\n            subFiles.push_back(subDir);\r\n        }\r\n    }\r\n    closedir(dir);\r\n#endif\r\n    return true;\r\n}\r\n\r\n#ifdef _WIN32\r\ninline bool copyFileImpl(LPCTSTR src, LPCTSTR dest)\r\n#else\r\ninline bool copyFileImpl(const std::string& src, const std::string& dest)\r\n#endif\r\n{\r\n#ifdef _WIN32\r\n    BOOL bRet = ::CopyFile(src, dest, TRUE);\r\n#ifndef NDEBUG\r\n    DWORD err = ::GetLastError();\r\n    TCHAR buffer[256] = { 0 };\r\n    _itot((int)err, buffer, 10);\r\n    _tcscat(buffer, TEXT(\" \"));\r\n    _tcscat(buffer, dest);\r\n    assert(buffer);\r\n#endif\r\n    return (bRet == TRUE);\r\n#elif defined(__APPLE__)\r\n    /* Initialize a state variable */\r\n    copyfile_state_t s;\r\n    s = copyfile_state_alloc();\r\n    /* Copy the data and extended attributes of one file to another */\r\n    int ret = copyfile(src.c_str(), dest.c_str(), s, COPYFILE_ALL);\r\n    /* Release the state variable */\r\n    copyfile_state_free(s);\r\n\r\n    return (ret == 0);\r\n#else\r\n    std::ifstream ss(src, std::ios::in | std::ios::binary);\r\n    if (!ss.is_open())\r\n    {\r\n#ifndef NDEBUG\r\n        // assert(false);\r\n#endif\r\n        return false;\r\n    }\r\n    std::ofstream ds(dest, std::ios::out | std::ios::binary | std::ios::trunc);\r\n    if (!ds.is_open())\r\n    {\r\n#ifndef NDEBUG\r\n        assert(false);\r\n#endif\r\n        ss.close();\r\n        return false;\r\n    }\r\n    ds << ss.rdbuf();\r\n\r\n    ss.close();\r\n    ds.close();\r\n    \r\n    return true;\r\n#endif\r\n}\r\n\r\nbool copyFile(const std::string& src, const std::string& dest, bool overwrite)\r\n{\r\n#ifdef _WIN32\r\n\tCW2T pszDest(CA2W(dest.c_str(), CP_UTF8));\r\n\tif (::PathFileExists((LPCTSTR)pszDest) && !overwrite)\r\n\t{\r\n        return true;\r\n    }\r\n    \r\n    CW2T pszSrc(CA2W(src.c_str(), CP_UTF8));\r\n\treturn copyFileImpl(pszSrc, pszDest);\r\n#else\r\n    if (existsFile(dest) && !overwrite)\r\n    {\r\n        return false;\r\n    }\r\n    \r\n    return copyFileImpl(src, dest);\r\n#endif\r\n}\r\n\r\n\r\n#ifdef _WIN32\r\ninline bool checkFileNewer(LPCTSTR src, LPCTSTR dest)\r\n#else\r\ninline bool checkFileNewer(const std::string& src, const std::string& dest)\r\n#endif\r\n{\r\n#ifdef _WIN32\r\n    if (!::PathFileExists(dest))\r\n    {\r\n        return true;\r\n    }\r\n    // Check file size\r\n    if (getFileSizeImpl(src) != getFileSizeImpl(dest))\r\n    {\r\n        return true;\r\n    }\r\n    \r\n    // Check last modified time\r\n    HANDLE hFileSrc = CreateFile(src, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);\r\n    if (hFileSrc == INVALID_HANDLE_VALUE)\r\n        return true;\r\n    \r\n    HANDLE hFileDest = CreateFile(dest, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);\r\n    if (hFileDest == INVALID_HANDLE_VALUE)\r\n    {\r\n        CloseHandle(hFileSrc);\r\n        return true;\r\n    }\r\n\r\n    LARGE_INTEGER sizeSrc;\r\n    LARGE_INTEGER sizeDest;\r\n    if (!GetFileSizeEx(hFileSrc, &sizeSrc) || !GetFileSizeEx(hFileDest, &sizeDest) || sizeSrc.QuadPart != sizeDest.QuadPart)\r\n    {\r\n        CloseHandle(hFileSrc);\r\n        CloseHandle(hFileDest);\r\n        return true;\r\n    }\r\n    \r\n    FILETIME ftSrc, ftDest;\r\n    if (!GetFileTime(hFileSrc, NULL, NULL, &ftSrc) || !GetFileTime(hFileDest, NULL, NULL, &ftDest))\r\n    {\r\n        CloseHandle(hFileSrc);\r\n        CloseHandle(hFileDest);\r\n        return true;\r\n    }\r\n    \r\n    CloseHandle(hFileSrc);\r\n    CloseHandle(hFileDest);\r\n\r\n    return CompareFileTime(&ftSrc, &ftDest) != 0;\r\n#else\r\n    if (!existsFile(dest))\r\n    {\r\n        return true;\r\n    }\r\n    \r\n    struct stat sbSrc, sbDest;\r\n    int rc = stat(src.c_str(), &sbSrc);\r\n    if (rc != 0)\r\n    {\r\n        return true;\r\n    }\r\n    rc = stat(dest.c_str(), &sbDest);\r\n    if (rc != 0)\r\n    {\r\n        return true;\r\n    }\r\n\r\n    return (sbSrc.st_size != sbDest.st_size) || (sbSrc.st_mtime != sbDest.st_mtime);\r\n#endif\r\n    return true;\r\n}\r\n\r\nbool copyFileIfNewer(const std::string& src, const std::string& dest)\r\n{\r\n#ifdef _WIN32\r\n    CW2T pszSrc(CA2W(src.c_str(), CP_UTF8));\r\n    CW2T pszDest(CA2W(dest.c_str(), CP_UTF8));\r\n    if (!checkFileNewer((LPCTSTR)pszSrc, (LPCTSTR)pszDest))\r\n    {\r\n        return true;\r\n    }\r\n    return copyFileImpl(pszSrc, pszDest);\r\n    \r\n#else\r\n    if (!checkFileNewer(src, dest))\r\n    {\r\n        return true;\r\n    }\r\n    return copyFileImpl(src, dest);\r\n#endif\r\n}\r\n\r\nbool copyDirectory(const std::string& src, const std::string& dest)\r\n{\r\n#ifdef _WIN32\r\n    TCHAR pszSrc[MAX_PATH] = { 0 };\r\n    _tcscpy(pszSrc, (LPCTSTR)CW2T(CA2W(src.c_str(), CP_UTF8)));\r\n    if (!existsDirectoryImpl((LPCTSTR)pszSrc))\r\n    {\r\n        return false;\r\n    }\r\n    \r\n    PathAppend(pszSrc, TEXT(\"*\"));\r\n    TCHAR pszDest[MAX_PATH] = { 0 };\r\n    _tcscpy(pszDest, (LPCTSTR)CW2T(CA2W(dest.c_str(), CP_UTF8)));\r\n    \r\n    pszSrc[_tcslen(pszSrc) + 1] = 0;\r\n    pszDest[_tcslen(pszDest) + 1] = 0;\r\n\r\n    SHFILEOPSTRUCTW s = { 0 };\r\n    s.wFunc = FO_COPY;\r\n    s.pTo = pszDest;\r\n    s.pFrom = pszSrc;\r\n    s.fFlags = FOF_SILENT | FOF_NOCONFIRMMKDIR | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NO_UI;\r\n    int res = SHFileOperation( &s );\r\n\r\n    return res == 0;\r\n// #elif defined(__APPLE__)\r\n    \r\n#else\r\n    if (src.empty() || dest.empty())\r\n    {\r\n        return false;\r\n    }\r\n\r\n    struct stat st;\r\n\r\n    /* if src does not exist */\r\n    if ((stat(src.c_str(), &st) < 0) || !S_ISDIR(st.st_mode))\r\n    {\r\n        // Source directory does not exist;\r\n        return false;\r\n    }\r\n\r\n    /* if dst directory does not exist */\r\n    if ((stat(dest.c_str(), &st) < 0) || !S_ISDIR(st.st_mode))\r\n    {\r\n        /* create it */\r\n        if (!makeDirectory(dest/*, 0755*/))\r\n        {\r\n            // Unable to create destination directory;\r\n            return false;\r\n        }\r\n    }\r\n    \r\n    std::queue<std::pair<std::string, std::string>> pairs;\r\n    pairs.push(std::pair<std::string, std::string>(src, dest));\r\n    std::vector<std::pair<std::string, std::string>>::reference ref = pairs.front();\r\n    if (ref.first[ref.first.size() - 1] != DIR_SEP)\r\n    {\r\n        ref.first += DIR_SEP;\r\n    }\r\n    if (ref.second[ref.second.size() - 1] != DIR_SEP)\r\n    {\r\n        ref.second += DIR_SEP;\r\n    }\r\n    \r\n    bool res = true;\r\n    while (!pairs.empty())\r\n    {\r\n        ref = pairs.front();\r\n        \r\n        /* loop over src directory contents */\r\n        DIR *cur_dir = opendir(ref.first.c_str());\r\n        if (cur_dir)\r\n        {\r\n            struct dirent* ep;\r\n            while ((ep = readdir(cur_dir)))\r\n            {\r\n                if ((strcmp(ep->d_name, \".\") == 0) || (strcmp(ep->d_name, \"..\") == 0))\r\n                {\r\n                    continue;\r\n                }\r\n                std::string srcpath = ref.first + ep->d_name;\r\n                std::string dstpath = ref.second + ep->d_name;\r\n                if (isDirectoryImpl(srcpath))\r\n                {\r\n                    pairs.push(std::pair<std::string, std::string>(srcpath + DIR_SEP, dstpath + DIR_SEP));\r\n                }\r\n                else\r\n                {\r\n                    if (!copyFile(srcpath, dstpath))\r\n                    {\r\n                        res = false;\r\n                        break;\r\n                    }\r\n                }\r\n            }\r\n            closedir(cur_dir);\r\n        }\r\n        \r\n        pairs.pop();\r\n    }\r\n\r\n    return res;\r\n#endif\r\n}\r\n\r\nbool moveFile(const std::string& src, const std::string& dest, bool overwrite/* = true*/)\r\n{\r\n#ifndef NDEBUG\r\n\tif (src.empty())\r\n\t{\r\n\t\tassert(!\"src is empty\");\r\n\t}\r\n\tif (dest.empty())\r\n\t{\r\n\t\tassert(!\"dest is empty\");\r\n\t}\r\n#endif\r\n#ifdef _WIN32\r\n\tCW2T pszSrc(CA2W(src.c_str(), CP_UTF8));\r\n\tCW2T pszDest(CA2W(dest.c_str(), CP_UTF8));\r\n\tif (overwrite && ::PathFileExists((LPCTSTR)pszDest))\r\n\t{\r\n\t\t::DeleteFile((LPCTSTR)pszDest);\r\n\t}\r\n\tbool bRet = false;\r\n\tif (::PathFileExists((LPCTSTR)pszSrc))\r\n\t{\r\n\t\tbRet = ::MoveFile((LPCTSTR)pszSrc, (LPCTSTR)pszDest) == TRUE;\r\n#ifndef NDEBUG\r\n\t\tif (!bRet)\r\n\t\t{\r\n\t\t\t// MessageBox(NULL, (LPCTSTR)pszSrc, (LPCTSTR)pszDest, MB_OK);\r\n\t\t\tassert(!((LPCTSTR)pszDest));\r\n\t\t}\r\n#endif\r\n\t}\r\n    return bRet;\r\n#else\r\n    if (overwrite)\r\n    {\r\n        remove(dest.c_str());\r\n    }\r\n    else if (existsFile(dest))\r\n    {\r\n        return false;\r\n    }\r\n    if (!existsFile(src))\r\n    {\r\n        return false;\r\n    }\r\n    int ret = rename(src.c_str(), dest.c_str());;\r\n#ifndef NDEBUG\r\n    assert(ret == 0);\r\n#endif\r\n    return ret == 0;\r\n#endif\r\n}\r\n\r\n#ifdef _WIN32\r\nCString removeInvalidCharsForFileName(const CString& fileName)\r\n{\r\n\tLPCTSTR invalidChars = TEXT(\"[\\\\/:*?\\\"<>|]\");\r\n\tCString validFileName = fileName;\r\n\tfor (LPCTSTR ptr = invalidChars; *ptr != '\\0'; ptr++)\r\n\t{\r\n\t\tvalidFileName.Remove(*ptr);\r\n\t}\r\n\tvalidFileName.TrimLeft(TEXT(\".\"));\r\n\tvalidFileName.TrimRight(TEXT(\" .\"));\r\n\r\n\tif (validFileName.GetLength() > 255)\r\n\t{\r\n\t\tvalidFileName = validFileName.Left(255);\r\n\t}\r\n\treturn validFileName;\r\n    \r\n}\r\n#endif\r\n\r\nstd::string removeInvalidCharsForFileName(const std::string& fileName)\r\n{\r\n    /*\r\n    Windows NT:\r\n    Do not use any of the following characters when naming your files or folders:\r\n        / ? < > \\ : * | ” and any character you can type with the Ctrl key\r\n    File and folder names may be up to 256 characters long\r\n    The maximum length of a full path is 260 characters\r\n    Placing a space at the end of the name\r\n    Placing a period at the end of the name\r\n\r\n     MAC:\r\n     The only illegal character for file and folder names in Mac OS X is the colon “:”\r\n     File and folder names are not permitted to begin with a dot “.”\r\n     File and folder names may be up to 255 characters in length\r\n     */\r\n\r\n#ifdef _WIN32\r\n\tCW2T pszT(CA2W(fileName.c_str(), CP_UTF8));\r\n\r\n\tCString validFileName = removeInvalidCharsForFileName(CString(pszT));\r\n\r\n\tCW2A pszU8(CT2W(validFileName), CP_UTF8);\r\n\treturn std::string(pszU8);\r\n#else\r\n    std::string validFileName = fileName;\r\n    \r\n    static std::string invalidChars = \"[\\\\/:*?\\\"<>|]\";\r\n\r\n    for (unsigned int i = 0; i < invalidChars.size(); ++i)\r\n    {\r\n        validFileName.erase(remove(validFileName.begin(), validFileName.end(), invalidChars[i]), validFileName.end());\r\n    }\r\n\r\n    size_t pos = validFileName.find_first_not_of(\".\");\r\n    if (pos == std::string::npos)\r\n    {\r\n        return \"\";\r\n    }\r\n    else if (pos != 0)\r\n    {\r\n        validFileName.erase(0, pos);\r\n    }\r\n\r\n    pos = validFileName.find_last_not_of(\" .\");\r\n    if (pos == std::string::npos)\r\n    {\r\n        return \"\";\r\n    }\r\n    else\r\n    {\r\n        validFileName.erase(pos + 1);\r\n    }\r\n\r\n    if (validFileName.size() > 255)\r\n    {\r\n        validFileName = validFileName.substr(0, 255);\r\n    }\r\n    return validFileName;\r\n#endif\r\n}\r\n\r\nbool isValidFileName(const std::string& fileName)\r\n{\r\n#ifdef _WIN32\r\n    TCHAR tmpPath[MAX_PATH] = { 0 };\r\n    if (GetTempPath(MAX_PATH, tmpPath))\r\n    {\r\n        // CT2A t2a(charPath);\r\n        // tempDir = (LPCSTR)t2a;\r\n    }\r\n\r\n    TCHAR path[MAX_PATH] = { 0 };\r\n    CW2T pszT(CA2W(fileName.c_str(), CP_UTF8));\r\n    ::PathCombine(path, tmpPath, (LPCTSTR)pszT);\r\n\r\n    bool valid = false;\r\n    if (::CreateDirectory(path, NULL))\r\n    {\r\n        valid = true;\r\n        RemoveDirectory(path);\r\n    }\r\n    else\r\n    {\r\n        valid = (::GetLastError() == ERROR_ALREADY_EXISTS);\r\n    }\r\n    return valid;\r\n#else\r\n    char const *tmpdir = getenv(\"TMPDIR\");\r\n    \r\n    std::string tempDir;\r\n\r\n    if (tmpdir == NULL)\r\n    {\r\n        tempDir = \"/tmp\";\r\n    }\r\n    else\r\n    {\r\n        tempDir = tmpdir;\r\n    }\r\n    \r\n    std::string path = combinePath(tempDir, fileName);\r\n    int status = mkdir(path.c_str(), 0);\r\n    int lastErrorNo = errno;\r\n    if (status == 0)\r\n    {\r\n        remove(path.c_str());\r\n    }\r\n    \r\n    return status == 0 || lastErrorNo == EEXIST;\r\n#endif\r\n}\r\n\r\nstd::string readFile(const std::string& path)\r\n{\r\n    std::string contents;\r\n    std::vector<unsigned char> data;\r\n    if (readFile(path, data) && !data.empty())\r\n    {\r\n        contents.append(reinterpret_cast<const char*>(&data[0]), data.size());\r\n    }\r\n    \r\n    return contents;\r\n}\r\n\r\nbool readFile(const std::string& path, std::vector<unsigned char>& data)\r\n{\r\n#ifdef _WIN32\r\n    CA2W pszW(path.c_str(), CP_UTF8);\r\n    std::ifstream ifs(pszW, std::ios::in | std::ios::binary | std::ios::ate);\r\n#else\r\n    std::ifstream ifs(path, std::ios::in | std::ios::binary | std::ios::ate);\r\n#endif\r\n    \r\n    if (ifs.is_open())\r\n    {\r\n        std::streampos size = ifs.tellg();\r\n        if ((long long)size > 0)\r\n        {\r\n            std::vector<std::string::value_type> buffer;\r\n            data.resize(size);\r\n            \r\n            ifs.seekg (0, std::ios::beg);\r\n            ifs.read((char *)(&data[0]), size);\r\n        }\r\n        else\r\n        {\r\n\t\t\tdata.clear();\r\n        }\r\n\t\tifs.close();\r\n        \r\n        return true;\r\n    }\r\n    \r\n    return false;\r\n}\r\n\r\nbool writeFile(const std::string& path, const std::vector<unsigned char>& data)\r\n{\r\n    return writeFile(path, &(data[0]), data.size());\r\n}\r\n\r\nbool writeFile(const std::string& path, const std::string& data)\r\n{\r\n    return writeFile(path, reinterpret_cast<const unsigned char*>(data.c_str()), data.size());\r\n}\r\n\r\nbool writeFile(const std::string& path, const unsigned char* data, size_t dataLength)\r\n{\r\n#ifdef _WIN32\r\n    CW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n\r\n    HANDLE hFile = CreateFile(pszT, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);\r\n    if (hFile == INVALID_HANDLE_VALUE)\r\n    {\r\n        return false;\r\n    }\r\n\r\n    DWORD dwBytesToWrite = static_cast<DWORD>(dataLength);\r\n    DWORD dwBytesWritten = 0;\r\n    BOOL bErrorFlag = WriteFile(hFile, data, dwBytesToWrite, &dwBytesWritten, NULL);\r\n\r\n    ::CloseHandle(hFile);\r\n    return (TRUE == bErrorFlag);\r\n#else\r\n\r\n    std::ofstream ofs;\r\n    ofs.open(path, std::ios::out | std::ios::binary | std::ios::trunc);\r\n    if (ofs.is_open())\r\n    {\r\n        ofs.write(reinterpret_cast<const char *>(data), dataLength);\r\n        ofs.close();\r\n        return true;\r\n    }\r\n\r\n    return false;\r\n#endif\r\n}\r\n\r\nbool appendFile(const std::string& path, const std::string& data)\r\n{\r\n    return appendFile(path, reinterpret_cast<const unsigned char*>(data.c_str()), data.size());\r\n}\r\n\r\nbool appendFile(const std::string& path, const unsigned char* data, size_t dataLength)\r\n{\r\n#ifdef _WIN32\r\n    CW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n\r\n    HANDLE hFile = ::CreateFile(pszT, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);\r\n    if (hFile == INVALID_HANDLE_VALUE)\r\n    {\r\n        return false;\r\n    }\r\n\r\n    ::SetFilePointer(hFile, 0, 0, FILE_END);\r\n    DWORD dwBytesToWrite = static_cast<DWORD>(dataLength);\r\n    DWORD dwBytesWritten = 0;\r\n    BOOL bErrorFlag = WriteFile(hFile, data, dwBytesToWrite, &dwBytesWritten, NULL);\r\n    ::CloseHandle(hFile);\r\n    return (TRUE == bErrorFlag);\r\n#else\r\n    std::ofstream ofs;\r\n    ofs.open(path, std::fstream::in | std::fstream::out | std::fstream::app | std::fstream::binary);\r\n    if (ofs.is_open())\r\n    {\r\n        ofs.write(reinterpret_cast<const char *>(data), dataLength);\r\n        ofs.close();\r\n        return true;\r\n    }\r\n\r\n    return false;\r\n#endif\r\n}\r\n\r\nstd::string combinePath(const std::string& p1, const std::string& p2)\r\n{\r\n    if (p1.empty() && p2.empty())\r\n    {\r\n        return \"\";\r\n    }\r\n\r\n    std::string path;\r\n#ifdef _WIN32\r\n    TCHAR buffer[MAX_PATH] = { 0 };\r\n    CW2T pszT1(CA2W(p1.c_str(), CP_UTF8));\r\n    CW2T pszT2(CA2W(p2.c_str(), CP_UTF8));\r\n\r\n    LPTSTR psz = ::PathCombine(buffer, pszT1, pszT2);\r\n#ifndef NDEBUG\r\n    assert(psz != NULL);\r\n#endif\r\n    CW2A pszU8(CT2W(buffer), CP_UTF8);\r\n    path = pszU8;\r\n#else\r\n    \r\n    if (!p1.empty())\r\n    {\r\n        path = p1;\r\n        if (path[path.size() - 1] != DIR_SEP && path[path.size() - 1] != ALT_DIR_SEP)\r\n        {\r\n            path += DIR_SEP;\r\n        }\r\n        \r\n        path += p2;\r\n    }\r\n    else\r\n    {\r\n        path = p2;\r\n    }\r\n#endif\r\n#ifndef NDEBUG\r\n    if (path.empty())\r\n    {\r\n        assert(false);\r\n    }\r\n#endif\r\n    return path;\r\n}\r\n\r\nstd::string combinePath(const std::string& p1, const std::string& p2, const std::string& p3)\r\n{\r\n    return combinePath(combinePath(p1, p2), p3);\r\n}\r\n\r\nstd::string combinePath(const std::string& p1, const std::string& p2, const std::string& p3, const std::string& p4)\r\n{\r\n    return combinePath(combinePath(p1, p2, p3), p4);\r\n}\r\n\r\nstd::string normalizePath(const std::string& path)\r\n{\r\n    std::string p = path;\r\n    normalizePath(p);\r\n    return p;\r\n}\r\n\r\nvoid normalizePath(std::string& path)\r\n{\r\n    std::replace(path.begin(), path.end(), ALT_DIR_SEP, DIR_SEP);\r\n}\r\n\r\nint calcFreeSpace(const std::string& path, uint64_t& freeSpace)\r\n{\r\n    int res = -1;\r\n#ifdef _WIN32\r\n    ULARGE_INTEGER value;\r\n    value.QuadPart = 0;\r\n    if (GetDiskFreeSpaceExA(path.c_str(), &value, NULL, NULL))\r\n    {\r\n        res = 0;\r\n        freeSpace = value.QuadPart;\r\n    }\r\n#else\r\n    struct statvfs fs;\r\n    memset(&fs, '\\0', sizeof(fs));\r\n    res = statvfs(path.c_str(), &fs);\r\n    if (res == 0)\r\n    {\r\n        freeSpace = (uint64_t)fs.f_bavail * (uint64_t)fs.f_bsize;\r\n    }\r\n#endif\r\n    return res;\r\n}\r\n\r\nFileEnumerator::File::File() : m_isDir(false), m_isNormalFile(false), m_fileSize(0)\r\n{\r\n}\r\n\r\nconst std::string& FileEnumerator::File::getFileName() const\r\n{\r\n    return m_fileName;\r\n}\r\n\r\nconst std::string& FileEnumerator::File::getFullPath() const\r\n{\r\n    return m_fullPath;\r\n}\r\n\r\nbool FileEnumerator::File::isDirectory() const\r\n{\r\n    return m_isDir;\r\n}\r\n\r\nbool FileEnumerator::File::isNormalFile() const\r\n{\r\n    return m_isNormalFile;\r\n}\r\n\r\ntime_t FileEnumerator::File::getModifiedTime() const\r\n{\r\n    return m_modifiedTime;\r\n}\r\n\r\nuint64_t FileEnumerator::File::getFileSize() const\r\n{\r\n    return m_fileSize;\r\n}\r\n\r\nFileEnumerator::FileEnumerator(const std::string& path) : m_path(path), m_handle(NULL), m_first(true)\r\n{\r\n#ifdef _WIN32\r\n    HANDLE hFind = INVALID_HANDLE_VALUE;\r\n    m_handle = (void *)hFind;\r\n#else\r\n    \r\n#endif\r\n}\r\n\r\nFileEnumerator::~FileEnumerator()\r\n{\r\n#ifdef _WIN32\r\n    HANDLE hFind = (HANDLE)m_handle;\r\n    if (hFind != INVALID_HANDLE_VALUE)\r\n    {\r\n        ::FindClose(hFind);\r\n    }\r\n#else\r\n    DIR *dir = (DIR *)m_handle;\r\n    if (dir != NULL)\r\n    {\r\n        closedir(dir);\r\n    }\r\n#endif\r\n}\r\n\r\nbool FileEnumerator::nextFile(File& file)\r\n{\r\n#ifdef _WIN32\r\n    bool res = false;\r\n    HANDLE hFind = (HANDLE)m_handle;\r\n    WIN32_FIND_DATA FindFileData;\r\n    \r\n    if (m_first)\r\n    {\r\n        m_first = false;\r\n        std::string formatedPath = combinePath(m_path, \"*.*\");\r\n        std::replace(formatedPath.begin(), formatedPath.end(), ALT_DIR_SEP, DIR_SEP);\r\n\r\n        CW2T pszPath(CA2W(formatedPath.c_str(), CP_UTF8));\r\n        \r\n        hFind = ::FindFirstFile((LPTSTR)pszPath, &FindFileData);\r\n        m_handle = (void *)hFind;\r\n        if (hFind == INVALID_HANDLE_VALUE)\r\n        {\r\n            return res;\r\n        }\r\n        do\r\n        {\r\n            if (_tcscmp(FindFileData.cFileName, TEXT(\".\")) == 0 || _tcscmp(FindFileData.cFileName, TEXT(\"..\")) == 0)\r\n            {\r\n                continue;\r\n            }\r\n            \r\n            res = true;\r\n            break;\r\n        } while (::FindNextFile(hFind, &FindFileData));\r\n    }\r\n    else\r\n    {\r\n        if (hFind == INVALID_HANDLE_VALUE)\r\n        {\r\n            return res;\r\n        }\r\n\r\n        while (::FindNextFile(hFind, &FindFileData))\r\n        {\r\n            if (_tcscmp(FindFileData.cFileName, TEXT(\".\")) == 0 || _tcscmp(FindFileData.cFileName, TEXT(\"..\")) == 0)\r\n            {\r\n                continue;\r\n            }\r\n\r\n            res = true;\r\n            break;\r\n        }\r\n    }\r\n    \r\n    if (res)\r\n    {\r\n        file.m_fileName = (LPCSTR)CW2A(CT2W(FindFileData.cFileName), CP_UTF8);\r\n        file.m_fullPath = combinePath(m_path, file.m_fileName);\r\n        file.m_isDir = ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY);\r\n        file.m_isNormalFile = !file.m_isDir && ((FindFileData.dwFileAttributes & (FILE_ATTRIBUTE_NORMAL | FILE_ATTRIBUTE_ARCHIVE)) != 0);\r\n\t\tif (file.m_isDir)\r\n\t\t{\r\n\t\t\tfile.m_fileSize = (FindFileData.nFileSizeHigh * (MAXDWORD + 1)) + FindFileData.nFileSizeLow;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tstruct stat st;\r\n\t\t\tstat(file.m_fullPath.c_str(), &st);\r\n\t\t}\r\n\t\t\r\n        file.m_modifiedTime = FileTimeToTime(FindFileData.ftLastWriteTime);\r\n        \r\n#ifndef NDEBUG\r\n        struct __stat64 st;\r\n        _tstat64(CW2T(CA2W(file.m_fullPath.c_str(), CP_UTF8)), &st);\r\n        assert(file.m_isNormalFile == S_ISREG(st.st_mode));\r\n\t\tif (!S_ISDIR(st.st_mode))\r\n\t\t{\r\n\t\t\tassert(file.m_fileSize == st.st_size);\r\n\t\t}\r\n#endif\r\n    }\r\n    \r\n    return res;\r\n#else\r\n    DIR *dir = (DIR *)m_handle;\r\n    bool res = false;\r\n    if (m_first)\r\n    {\r\n        m_first = false;\r\n        dir = opendir(m_path.c_str());\r\n        m_handle = (void *)dir;\r\n        if (dir == NULL)\r\n        {\r\n            return res;\r\n        }\r\n    }\r\n    \r\n    struct dirent *entry;\r\n    while ((entry = readdir(dir)) != NULL)\r\n    {\r\n        if (strcmp(entry->d_name, \".\") == 0 || strcmp(entry->d_name, \"..\") == 0)\r\n        {\r\n            continue;\r\n        }\r\n        \r\n        file.m_fileName = entry->d_name;\r\n        file.m_fullPath = combinePath(m_path, file.m_fileName);\r\n        \r\n        struct stat st;\r\n        stat(file.m_fullPath.c_str(), &st);\r\n\r\n        file.m_fileSize = st.st_size;\r\n        file.m_isDir = S_ISDIR(st.st_mode);\r\n        file.m_isNormalFile = S_ISREG(st.st_mode);\r\n        file.m_modifiedTime = st.st_mtime;\r\n            \r\n        res = true;\r\n        break;\r\n    }\r\n    \r\n    return res;\r\n#endif\r\n}\r\n\r\nFile::File() :\r\n#ifdef _WIN32\r\nm_file(INVALID_HANDLE_VALUE)\r\n#else\r\n    m_file(NULL)\r\n#endif\r\n{\r\n}\r\n\r\nFile::~File()\r\n{\r\n    close();\r\n}\r\n\r\nbool File::open(const std::string& path, bool readOnly/* = true*/)\r\n{\r\n#ifdef _WIN32\r\n    CW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n    if (readOnly)\r\n    {\r\n        m_file = CreateFile((LPCTSTR)pszT, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);\r\n    }\r\n    else\r\n    {\r\n        m_file = CreateFile((LPCTSTR)pszT, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);\r\n    }\r\n    return (INVALID_HANDLE_VALUE != m_file);\r\n#else\r\n    m_file = fopen(path.c_str(), readOnly ? \"rb\" : \"wb\");\r\n    return NULL != m_file;\r\n#endif\r\n}\r\n\r\nbool File::read(unsigned char* buffer, size_t bytesToRead, size_t& bytesRead)\r\n{\r\n#ifdef _WIN32\r\n    DWORD numberOfBytesRead = 0;\r\n    BOOL res = ReadFile(m_file, buffer, (DWORD)bytesToRead, &numberOfBytesRead, NULL);\r\n    if (res)\r\n    {\r\n        bytesRead = numberOfBytesRead;\r\n    }\r\n    return res;\r\n#else\r\n    if (m_file != NULL && bytesToRead == 0)\r\n    {\r\n        bytesRead = 0;\r\n        return true;\r\n    }\r\n    size_t res = fread(buffer, 1, bytesToRead, m_file);\r\n    if (res < bytesToRead)\r\n    {\r\n        bytesRead = res;\r\n        if (feof(m_file))\r\n        {\r\n            return true;\r\n        }\r\n        \r\n        return ferror(m_file) != 0;\r\n    }\r\n    return true;\r\n#endif\r\n    \r\n}\r\n\r\nbool File::write(const unsigned char* buffer, size_t bytesToWrite, size_t& bytesWritten)\r\n{\r\n#ifdef _WIN32\r\n    DWORD numberOfBytesWritten = 0;\r\n    BOOL res = WriteFile(m_file, buffer, static_cast<DWORD>(bytesToWrite), &numberOfBytesWritten, NULL);\r\n    bytesWritten = res ? numberOfBytesWritten : 0;\r\n    return (TRUE == res);\r\n#else\r\n    if (NULL != m_file && bytesToWrite == 0)\r\n    {\r\n        bytesWritten = 0;\r\n        return true;\r\n    }\r\n    \r\n    bytesWritten = fwrite(buffer, 1, bytesToWrite, m_file);\r\n    return (bytesWritten == bytesToWrite);\r\n#endif\r\n}\r\n\r\nvoid File::close()\r\n{\r\n#ifdef _WIN32\r\n    if (INVALID_HANDLE_VALUE != m_file)\r\n    {\r\n        CloseHandle(m_file);\r\n        m_file = INVALID_HANDLE_VALUE;\r\n    }\r\n#else\r\n    if (NULL != m_file)\r\n    {\r\n        fclose(m_file);\r\n        m_file = NULL;\r\n    }\r\n#endif\r\n}\r\n"
  },
  {
    "path": "WechatExporter/core/FileSystem.h",
    "content": "//\n//  FileSystem.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/1/21.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef FileSystem_h\n#define FileSystem_h\n\n#include <string>\n#include <vector>\n\n#ifdef _WIN32\n#include <windows.h>\n#if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG)\n  #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)\n#endif\n#if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR)\n  #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)\n#endif\n#endif\n\n#ifdef _WIN32\n#define DIR_SEP '\\\\'\n#define DIR_SEP_STR \"\\\\\"\n#define ALT_DIR_SEP '/'\n#else\n#define DIR_SEP '/'\n#define DIR_SEP_STR \"/\"\n#define ALT_DIR_SEP '\\\\'\n#endif\n\nsize_t getFileSize(const std::string& path);\nbool existsDirectory(const std::string& path);\nbool makeDirectory(const std::string& path);\nbool deleteFile(const std::string& path);\nbool deleteDirectory(const std::string& path);\nbool existsFile(const std::string& path);\nbool listSubDirectories(const std::string& path, std::vector<std::string>& subDirectories);\nbool listDirectory(const std::string& path, std::vector<std::string>& subDirectories, std::vector<std::string>& subFiles);\nbool copyFile(const std::string& src, const std::string& dest, bool overwrite = true);\nbool copyFileIfNewer(const std::string& src, const std::string& dest);\nbool copyDirectory(const std::string& src, const std::string& dest);\nbool moveFile(const std::string& src, const std::string& dest, bool overwrite = true);\n// ref: https://blackbeltreview.wordpress.com/2015/01/27/illegal-filename-characters-on-windows-vs-mac-os/\nbool isValidFileName(const std::string& fileName);\nstd::string removeInvalidCharsForFileName(const std::string& fileName);\n\nstd::string readFile(const std::string& path);\nbool readFile(const std::string& path, std::vector<unsigned char>& data);\nbool writeFile(const std::string& path, const std::vector<unsigned char>& data);\nbool writeFile(const std::string& path, const std::string& data);\nbool writeFile(const std::string& path, const unsigned char* data, size_t dataLength);\nbool appendFile(const std::string& path, const std::string& data);\nbool appendFile(const std::string& path, const unsigned char* data, size_t dataLength);\n\n\n\nstd::string combinePath(const std::string& p1, const std::string& p2);\nstd::string combinePath(const std::string& p1, const std::string& p2, const std::string& p3);\nstd::string combinePath(const std::string& p1, const std::string& p2, const std::string& p3, const std::string& p4);\n\nstd::string normalizePath(const std::string& path);\nvoid normalizePath(std::string& path);\n\n// return zero for succeeding and non-zero for failure\nint calcFreeSpace(const std::string& path, uint64_t& freeSpace);\n\n#ifdef _WIN32\ntime_t FileTimeToTime(FILETIME ft);\n#endif\n\nclass FileEnumerator\n{\npublic:\n    class File\n    {\n    public:\n        \n        friend FileEnumerator;\n        \n        File();\n        \n        const std::string& getFileName() const;\n        const std::string& getFullPath() const;\n        bool isDirectory() const;\n        bool isNormalFile() const;\n        time_t getModifiedTime() const;\n        uint64_t getFileSize() const;\n        \n    private:\n        std::string m_fileName;\n        std::string m_fullPath;\n        time_t m_modifiedTime;\n        bool m_isDir;\n        bool m_isNormalFile;\n        uint64_t m_fileSize;\n    };\n    \n    FileEnumerator(const std::string& path);\n    ~FileEnumerator();\n    \n    bool isValid() const;\n    \n    bool nextFile(File& file);\n    \nprivate:\n    std::string m_path;\n    void* m_handle;\n    bool m_first;\n};\n\nclass File\n{\npublic:\n    File();\n    // Will close the file\n    ~File();\n    \n    bool open(const std::string& path, bool readOnly = true);\n    bool read(unsigned char* buffer, size_t bytesToRead, size_t& bytesRead);\n    bool write(const unsigned char* buffer, size_t bytesToWrite, size_t& bytesWritten);\n    \n    void close();\n    \nprivate:\n#ifdef _WIN32\n    HANDLE m_file;\n#else\n    FILE* m_file;\n#endif\n};\n\n\n#endif /* FileSystem_h */\n"
  },
  {
    "path": "WechatExporter/core/IDeviceBackup.cpp",
    "content": "//\n//  IDevice.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2021/12/7.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#include \"IDeviceBackup.h\"\n#include <algorithm>\n#include <stdlib.h>\n#include <errno.h>\n#include <time.h>\n#include <assert.h>\n#include <inttypes.h>\n#include <limits.h>\n#include <sys/stat.h>\n#include <dirent.h>\n#if defined(_WIN32)\n#include <windows.h>\n#include <atlstr.h>\n#include <shlobj_core.h>\n#else\n#include <libgen.h>\n#include <unistd.h>\n#endif\n\n\n#include <plist/plist.h>\n#include <libimobiledevice/libimobiledevice.h>\n#include <libimobiledevice/lockdown.h>\n#include <libimobiledevice/installation_proxy.h>\n#include <libimobiledevice/notification_proxy.h>\n#include <libimobiledevice/afc.h>\n#include <libimobiledevice/mobilebackup2.h>\n#include <libimobiledevice/sbservices.h>\n#include <libimobiledevice/diagnostics_relay.h>\n\n\n#include \"endianness.h\"\n\n#include \"Utils.h\"\n#include \"ITunesParser.h\"\n#include \"FileSystem.h\"\n\n#ifdef _WIN32\n\n\nint __cdecl printlog(const char *format, ...)\n{\n    char str[1024];\n\n    va_list argptr;\n    va_start(argptr, format);\n    int ret = vsnprintf(str, sizeof(str), format, argptr);\n    va_end(argptr);\n\n\n    OutputDebugString((LPCTSTR)CW2T(CA2W(str, CP_UTF8)));\n\n    return ret;\n}\n\n#else\n\n#ifndef NDEBUG\n#define printlog printf\n#else\n#define printlog(format, ...) (void)0\n#endif\n#endif\n\n#define DEVICE_VERSION(maj, min, patch) (((maj & 0xFF) << 16) | ((min & 0xFF) << 8) | (patch & 0xFF))\n#define PRINT_VERBOSE(min_level, ...) { printlog(__VA_ARGS__); }\n\n#define CODE_SUCCESS 0x00\n#define CODE_ERROR_LOCAL 0x06\n#define CODE_ERROR_REMOTE 0x0b\n#define CODE_FILE_DATA 0x0c\n\n#define MAC_EPOCH 978307200\n\n\n#ifdef _WIN32\nvoid usleep(__int64 usec)\n{\n    HANDLE timer;\n    LARGE_INTEGER ft;\n\n    ft.QuadPart = -(10 * usec); // Convert to 100 nanosecond interval, negative value indicates relative time\n\n    timer = CreateWaitableTimer(NULL, TRUE, NULL);\n    SetWaitableTimer(timer, &ft, 0, NULL, NULL, 0);\n    WaitForSingleObject(timer, INFINITE);\n    CloseHandle(timer);\n}\n#endif\n\nenum plist_format_t\n{\n    PLIST_FORMAT_XML,\n    PLIST_FORMAT_BINARY\n};\n\nstd::string formatDiskSize(uint64_t size)\n{\n    char buf[80];\n    double sz;\n    if (size >= 1000000000000LL) {\n        sz = ((double)size / 1000000000000.0f);\n        sprintf(buf, \"%0.1f TB\", sz);\n    } else if (size >= 1000000000LL) {\n        sz = ((double)size / 1000000000.0f);\n        sprintf(buf, \"%0.1f GB\", sz);\n    } else if (size >= 1000000LL) {\n        sz = ((double)size / 1000000.0f);\n        sprintf(buf, \"%0.1f MB\", sz);\n    } else if (size >= 1000LL) {\n        sz = ((double)size / 1000.0f);\n        sprintf(buf, \"%0.1f KB\", sz);\n    } else {\n        sprintf(buf, \"%d Bytes\", (int)size);\n    }\n    return std::string(buf);\n}\n\nint writePlistFile(plist_t plist, const std::string& filename, enum plist_format_t format)\n{\n    char *buffer = NULL;\n    uint32_t length;\n\n    if (!plist || filename.empty())\n        return 0;\n\n    if (format == PLIST_FORMAT_XML)\n        plist_to_xml(plist, &buffer, &length);\n    else if (format == PLIST_FORMAT_BINARY)\n        plist_to_bin(plist, &buffer, &length);\n    else\n        return 0;\n\n    bool res = writeFile(filename, (const unsigned char*)buffer, length);\n\n\tplist_mem_free(buffer);\n\n    return res ? 1 : 0;\n}\n\nint readPlistFile(plist_t *plist, const std::string& filename)\n{\n    if (filename.empty())\n        return 0;\n\n    std::vector<unsigned char> buffer;\n    if (!readFile(filename, buffer)) {\n        return 0;\n    }\n\n    plist_from_memory((const char *)&buffer[0], (uint32_t)buffer.size(), plist);\n\n    return 1;\n}\n\nnamespace fs\n{\n#ifdef _WIN32\nstatic int win32err_to_errno(int err_value)\n{\n    switch (err_value) {\n        case ERROR_FILE_NOT_FOUND:\n        case ERROR_PATH_NOT_FOUND:\n        case ERROR_INVALID_DRIVE:\n            return ENOENT;\n        case ERROR_ALREADY_EXISTS:\n        case ERROR_FILE_EXISTS:\n            return EEXIST;\n        case ERROR_HANDLE_DISK_FULL:\n        case ERROR_DISK_FULL:\n            return ENOSPC;\n        default:\n            return EFAULT;\n    }\n}\n\nstatic int win32err_to_device_error(DWORD errValue)\n{\n    switch (errValue)\n    {\n    case ERROR_FILE_NOT_FOUND:\n    case ERROR_PATH_NOT_FOUND:\n    case ERROR_INVALID_DRIVE:\n        return -6;\n    case ERROR_ALREADY_EXISTS:\n    case ERROR_FILE_EXISTS:\n        return -7;\n    case ERROR_HANDLE_DISK_FULL:\n    case ERROR_DISK_FULL:\n        return -15;\n    default:\n        return -1;\n    }\n}\n#endif\n\nstatic int convertErrnoToDeviceError(int errnoValue)\n{\n    switch (errnoValue)\n    {\n    case ENOENT:    /* No such file or directory */\n        return -6;\n    case EEXIST:    /* File exists */\n        return -7;\n    case ENOTDIR:   /* Not a directory */\n        return -8;\n    case EISDIR:    /* Is a directory */\n        return -9;\n    case ELOOP:     /* Too many levels of symbolic links */\n        return -10;\n    case EIO:       /* Input/output error */\n        return -11;\n    case ENOSPC:    /* No space left on device */\n        return -15;\n    default:\n        return -1;\n    }\n}\n\n// return errno\nstatic int deleteFile(const std::string& path)\n{\n    int e = 0;\n#ifdef _WIN32\n    CW2T szPath(CA2W(path.c_str(), CP_UTF8));\n    if (!DeleteFile((LPCTSTR)szPath))\n    {\n        e = win32err_to_errno(GetLastError());\n    }\n#else\n    if (unlink(path.c_str()) < 0)\n    {\n        e = errno;\n    }\n#endif\n    return e;\n}\n\n// return errno\n#ifdef _WIN32\nstatic int deleteDirectoryRecursively(LPCTSTR szPath)\n{\n#ifndef NDEBUG\n\tassert((_tcslen(szPath) + 4) < MAX_PATH);\n#endif\n    TCHAR szPath1[MAX_PATH] = { 0 };\n    _tcscpy(szPath1, szPath);\n\tPathAddBackslash(szPath1);\n\tPathAppend(szPath1, TEXT(\"*.*\"));\n\n    \n    WIN32_FIND_DATA FindFileData;\n    HANDLE hFind = INVALID_HANDLE_VALUE;\n\n    hFind = ::FindFirstFile((LPCTSTR)szPath1, &FindFileData);\n    if (hFind == INVALID_HANDLE_VALUE)\n    {\n        return win32err_to_errno(GetLastError());\n    }\n    \n    int res = 0;\n    do\n    {\n        if (_tcscmp(FindFileData.cFileName, TEXT(\".\")) == 0 || _tcscmp(FindFileData.cFileName, TEXT(\"..\")) == 0)\n        {\n            continue;\n        }\n        \n#ifndef NDEBUG\n\t\tassert((_tcslen(szPath) + 1 + _tcslen(FindFileData.cFileName)) < MAX_PATH);\n#endif\n        _tcscpy(szPath1, szPath);\n\t\tPathAddBackslash(szPath1);\n\t\tPathAppend(szPath1, FindFileData.cFileName);\n        \n        if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)\n        {\n            res = deleteDirectoryRecursively(szPath1);\n            if (res != 0)\n            {\n                break;\n            }\n        }\n        else\n        {\n            if (!DeleteFile(szPath1))\n            {\n                res = win32err_to_errno(GetLastError());\n                break;\n            }\n        }\n    } while (::FindNextFile(hFind, &FindFileData));\n    ::FindClose(hFind);\n    \n    if (res == 0)\n    {\n        if (!RemoveDirectory(szPath))\n        {\n            res = win32err_to_errno(GetLastError());\n        }\n    }\n    \n    return res;\n}\n#endif\nstatic int deleteDirectoryRecursively(const std::string& path)\n{\n#ifdef _WIN32\n    std::string p = path;\n    std::replace(p.begin(), p.end(), ALT_DIR_SEP, DIR_SEP);\n    CW2T pszPath(CA2W(p.c_str(), CP_UTF8));\n    return deleteDirectoryRecursively(pszPath);\n#else\n    int res = 0;\n    \n    DIR *d = opendir(path.c_str());\n    if (NULL == d)\n    {\n        res = errno;\n        return res;\n    }\n    \n    size_t path_len = path.size();\n    \n    struct dirent *p;\n\n    res = 0;\n    while (!res && (p = readdir(d)))\n    {\n        char *buf = NULL;\n        size_t len = 0;\n\n        /* Skip the names \".\" and \"..\" as we don't want to recurse on them. */\n        if (!strcmp(p->d_name, \".\") || !strcmp(p->d_name, \"..\"))\n            continue;\n\n        len = path_len + strlen(p->d_name) + 2;\n        buf = (char *)malloc(len);\n\n        if (NULL == buf)\n        {\n            res = errno;\n            break;\n        }\n        struct stat statbuf;\n\n        strcpy(buf, path.c_str());\n        strcat(buf, \"/\");\n        strcat(buf, p->d_name);\n        if (stat(buf, &statbuf) != 0)\n        {\n            res = errno;\n            free(buf);\n            break;\n        }\n        \n        if (S_ISDIR(statbuf.st_mode))\n            res = deleteDirectoryRecursively(buf);\n        else\n        {\n            if (unlink(buf) != 0)\n            {\n                res = errno;\n            }\n        }\n        free(buf);\n        \n        if (res != 0)\n        {\n            break;\n        }\n    }\n    closedir(d);\n\n    if (!res)\n    {\n        if (rmdir(path.c_str()) != 0)\n        {\n            res = errno;\n        }\n    }\n\n    return res;\n#endif\n}\n\n// return errno\nstatic int moveFile(const std::string& src, const std::string& dest)\n{\n    int e = 0;\n#ifdef _WIN32\n    CW2T szSrc(CA2W(src.c_str(), CP_UTF8));\n\tCW2T szDest(CA2W(dest.c_str(), CP_UTF8));\n    if (!MoveFile((LPCTSTR)szSrc, (LPCTSTR)szDest))\n    {\n        e = win32err_to_errno(GetLastError());\n    }\n#else\n    if (rename(src.c_str(), dest.c_str()) < 0)\n    {\n        e = errno;\n    }\n#endif\n    return e;\n}\n\nstatic int makeDirectory(const std::string& path, mode_t mode)\n{\n#ifdef _WIN32\n    CW2T pszPath(CA2W(path.c_str(), CP_UTF8));\n\n    if (PathFileExists(pszPath))\n    {\n        return 0;\n    }\n    \n    DWORD err = ::SHCreateDirectoryEx(NULL, (LPCTSTR)pszPath, NULL);\n    return (err == ERROR_SUCCESS) ? 0 : win32err_to_errno(err);\n#else\n    if (path.empty()) return -1;\n    \n    if (mkdir(path.c_str(), mode) == 0 || errno == EEXIST)\n    {\n        return 0;\n    }\n    \n    std::vector<std::string::value_type> copypath;\n    copypath.reserve(path.size() + 1);\n    std::copy(path.begin(), path.end(), std::back_inserter(copypath));\n    copypath.push_back('\\0');\n    \n    std::vector<std::string::value_type>::iterator itStart = copypath.begin();\n    std::vector<std::string::value_type>::iterator it;\n    \n    int status = 0;\n    if (*itStart == '/') ++itStart;  // Skip root path\n    while (status == 0 && (it = std::find(itStart, copypath.end(), '/')) != copypath.end())\n    {\n        // Neither root nor double slash in path\n        *it = '\\0';\n        \n        if (mkdir(&copypath[0], mode) != 0)\n        {\n            status = (errno == EEXIST) ? 0 : errno;\n        }\n        *it = '/';\n        itStart = it + 1;\n    }\n    if (status == 0)\n    {\n        if (mkdir(&copypath[0], mode) != 0)\n        {\n            status = (errno == EEXIST) ? 0 : errno;\n        }\n    }\n    \n    return status;\n#endif\n}\n\nclass FileEnumerator\n{\npublic:\n\tclass FileInfo\n\t{\n\tpublic:\n\n\t\tfriend FileEnumerator;\n\n\t\tFileInfo() : m_isDir(false), m_isNormalFile(false), m_fileSize(0)\n\t\t{\n\n\t\t}\n\n\t\tconst std::string& getFileName() const\n\t\t{\n\t\t\treturn m_fileName;\n\t\t}\n\n\t\tbool isDirectory() const\n\t\t{\n\t\t\treturn m_isDir;\n\t\t}\n\n\t\tbool isNormalFile() const\n\t\t{\n\t\t\treturn m_isNormalFile;\n\t\t}\n\n\t\ttime_t getModifiedTime() const\n\t\t{\n\t\t\treturn m_modifiedTime;\n\t\t}\n\n\t\tuint64_t getFileSize() const\n\t\t{\n\t\t\treturn m_fileSize;\n\t\t}\n\n\tprivate:\n\t\tstd::string m_fileName;\n\t\ttime_t m_modifiedTime;\n\t\tbool m_isDir;\n\t\tbool m_isNormalFile;\n\t\tuint64_t m_fileSize;\n\t};\n\n\tFileEnumerator(const std::string& path) : m_path(path),\n#ifdef _WIN32\n\t\tm_handle(INVALID_HANDLE_VALUE), \n#else\n\t\tm_handle(NULL), \n#endif\n\t\tm_first(true)\n\t{\n#ifdef _WIN32\n\t\t_tcscpy(m_szPath, CW2T(CA2W(path.c_str(), CP_UTF8)));\n#endif\n\t}\n\t~FileEnumerator()\n\t{\n#ifdef _WIN32\n\t\tif (m_handle != INVALID_HANDLE_VALUE)\n\t\t{\n\t\t\t::FindClose(m_handle);\n\t\t}\n#else\n\t\tif (NULL != m_handle)\n\t\t{\n\t\t\tclosedir(m_handle);\n\t\t}\n#endif\n\t}\n\n\tbool isValid() const\n\t{\n#ifdef _WIN32\n\t\treturn INVALID_HANDLE_VALUE != m_handle;\n#else\n\t\treturn NULL != m_handle;\n#endif\n\t}\n\n\tbool nextFile(FileInfo& file)\n\t{\n#ifdef _WIN32\n\t\tbool res = false;\n\t\tWIN32_FIND_DATA FindFileData;\n\n\t\tif (m_first)\n\t\t{\n\t\t\tTCHAR findPath[MAX_PATH] = { 0 };\n\t\t\t_tcscpy(findPath, m_szPath);\n\t\t\tPathAppend(findPath, TEXT(\"*.*\"));\n\t\t\tm_first = false;\n\n\t\t\tm_handle = ::FindFirstFile(findPath, &FindFileData);\n\t\t\tif (m_handle == INVALID_HANDLE_VALUE)\n\t\t\t{\n\t\t\t\treturn res;\n\t\t\t}\n\t\t\tdo\n\t\t\t{\n\t\t\t\tif (_tcscmp(FindFileData.cFileName, TEXT(\".\")) == 0 || _tcscmp(FindFileData.cFileName, TEXT(\"..\")) == 0)\n\t\t\t\t{\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tres = true;\n\t\t\t\tbreak;\n\t\t\t} while (::FindNextFile(m_handle, &FindFileData));\n\t\t}\n\t\telse\n\t\t{\n\t\t\tif (INVALID_HANDLE_VALUE == m_handle)\n\t\t\t{\n\t\t\t\treturn res;\n\t\t\t}\n\n\t\t\twhile (::FindNextFile(m_handle, &FindFileData))\n\t\t\t{\n\t\t\t\tif (_tcscmp(FindFileData.cFileName, TEXT(\".\")) == 0 || _tcscmp(FindFileData.cFileName, TEXT(\"..\")) == 0)\n\t\t\t\t{\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tres = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (res)\n\t\t{\n\t\t\tfile.m_fileName = (LPCSTR)CW2A(CT2W(FindFileData.cFileName), CP_UTF8);\n\t\t\tfile.m_isDir = ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY);\n\t\t\tfile.m_isNormalFile = !file.m_isDir && ((FindFileData.dwFileAttributes & (FILE_ATTRIBUTE_NORMAL | FILE_ATTRIBUTE_ARCHIVE)) != 0);\n\t\t\tif (file.m_isDir)\n\t\t\t{\n\t\t\t\tTCHAR szPath[MAX_PATH] = { 0 };\n\t\t\t\t_tcscpy(szPath, m_szPath);\n\t\t\t\tPathAppend(szPath, FindFileData.cFileName);\n\t\t\t\tstruct _stati64 st;\n\t\t\t\t_tstat64(szPath, &st);\n\t\t\t\tfile.m_fileSize = st.st_size;\n\t\t\t\tfile.m_modifiedTime = st.st_mtime;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfile.m_fileSize = (FindFileData.nFileSizeHigh * (MAXDWORD + 1)) + FindFileData.nFileSizeLow;\n\t\t\t\tfile.m_modifiedTime = ::FileTimeToTime(FindFileData.ftLastWriteTime);\n\t\t\t}\n\t\t}\n\n\t\treturn res;\n#else\n\t\tbool res = false;\n\t\tif (m_first)\n\t\t{\n\t\t\tm_first = false;\n            m_handle = opendir(m_path.c_str());\n\t\t\tif (NULL == m_handle)\n\t\t\t{\n\t\t\t\treturn res;\n\t\t\t}\n\t\t}\n\n\t\tstruct dirent *entry;\n\t\twhile ((entry = readdir(m_handle)) != NULL)\n\t\t{\n\t\t\tif (strcmp(entry->d_name, \".\") == 0 || strcmp(entry->d_name, \"..\") == 0)\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfile.m_fileName = entry->d_name;\n\t\t\tstd::string fullPath = combinePath(m_path, file.m_fileName);\n\n\t\t\tstruct stat st;\n\t\t\tstat(fullPath.c_str(), &st);\n\n\t\t\tfile.m_fileSize = st.st_size;\n\t\t\tfile.m_isDir = S_ISDIR(st.st_mode);\n\t\t\tfile.m_isNormalFile = S_ISREG(st.st_mode);\n\t\t\tfile.m_modifiedTime = st.st_mtime;\n\n\t\t\tres = true;\n\t\t\tbreak;\n\t\t}\n\n\t\treturn res;\n#endif\n\t}\n\nprivate:\n\tstd::string m_path;\n#ifdef _WIN32\n\tTCHAR m_szPath[MAX_PATH];\n\tHANDLE m_handle;\n#else\n\tDIR* m_handle;\n#endif\n\tbool m_first;\n};\n\n} // namespace fs\n\nclass IDeviceBackupClient\n{\npublic:\n    \n    struct File\n    {\n        std::string fileId;\n        size_t size;\n        \n        File() : size(0)\n        {\n        }\n        \n        File(const std::string& fid, size_t sz) : fileId(fid), size(sz)\n        {\n        }\n    };\n    \n\t/*\n    struct __string_less\n    {\n        // _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX11\n        bool operator()(const std::string& __x, const std::string& __y) const {return __x < __y;}\n        bool operator()(const std::pair<std::string, std::string>& __x, const std::string& __y) const {return __x.first < __y;}\n        bool operator()(const File& __x, const std::string& __y) const {return __x.fileId < __y;}\n        bool operator()(const File& __x, const File& __y) const {return __x.fileId < __y.fileId;}\n    };\n\t*/\n\n\tIDeviceBackupClient(const std::string udid) : m_device(NULL), m_afc(NULL), m_lockfile(0), m_np(NULL), m_client(NULL), m_mobilebackup2(NULL), m_udid(udid)\n    {\n    }\n    \n    ~IDeviceBackupClient()\n    {\n        unlockAfc();\n        \n        if (NULL != m_mobilebackup2)\n        {\n            mobilebackup2_client_free(m_mobilebackup2);\n        }\n        \n        if (NULL != m_afc)\n        {\n            afc_client_free(m_afc);\n        }\n        if (m_np)\n        {\n            np_client_free(m_np);\n        }\n        if (NULL != m_client)\n        {\n            lockdownd_client_free(m_client);\n        }\n        if (NULL != m_device)\n        {\n            idevice_free(m_device);\n        }\n    }\n    \n    bool init()\n    {\n        lockdownd_error_t err = LOCKDOWN_E_UNKNOWN_ERROR;\n        idevice_error_t devErr = idevice_new_with_options(&m_device, m_udid.c_str(), (idevice_options)(IDEVICE_LOOKUP_USBMUX | IDEVICE_LOOKUP_NETWORK));\n        if (devErr == IDEVICE_E_SUCCESS && NULL != m_device)\n        {\n            err = lockdownd_client_new(m_device, &m_client, CLIENT_ID);\n        }\n        \n        return LOCKDOWN_E_SUCCESS == err;\n    }\n    \n    lockdownd_error_t initWithHandShake()\n    {\n        lockdownd_error_t err = LOCKDOWN_E_UNKNOWN_ERROR;\n        idevice_error_t devErr = idevice_new_with_options(&m_device, m_udid.c_str(), (idevice_options)(IDEVICE_LOOKUP_USBMUX | IDEVICE_LOOKUP_NETWORK));\n        if (devErr == IDEVICE_E_SUCCESS && NULL != m_device)\n        {\n\t\t\terr = lockdownd_client_new_with_handshake(m_device, &m_client, CLIENT_ID);\n        }\n        \n        return err;\n    }\n    \n    void updateDeviceInfo(DeviceInfo& device, lockdownd_error_t err)\n    {\n        if (LOCKDOWN_E_SUCCESS == err)\n        {\n            device.setLocked(false);\n            device.setTrustPending(false);\n            device.setTrusted(true);\n        }\n        else\n        {\n            if (LOCKDOWN_E_PASSWORD_PROTECTED == err)\n            {\n                device.setLocked(true);\n            }\n            if (LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING == err)\n            {\n                device.setTrustPending(true);\n            }\n            if (LOCKDOWN_E_PAIRING_FAILED == err || LOCKDOWN_E_PAIRING_PROHIBITED_OVER_THIS_CONNECTION)\n            {\n                device.setTrusted(false);\n            }\n        }\n    }\n\n    void freeClient()\n    {\n        if (NULL != m_client)\n        {\n            lockdownd_client_free(m_client);\n            m_client = NULL;\n        }\n    }\n    \n    inline bool needUnlock(lockdownd_error_t err) const\n    {\n        return (LOCKDOWN_E_PASSWORD_PROTECTED == err);\n    }\n    \n    inline bool needTrust(lockdownd_error_t err) const\n    {\n        return (LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING == err);\n    }\n    \n    bool loadFiles(const std::string& backupPath, const std::string& udid)\n    {\n        \n        m_files.clear();\n        \n        ITunesDb iTunesDb(combinePath(backupPath, udid), \"\");\n        \n        std::unique_ptr<ITunesDb::ITunesFileEnumerator> enumerator(iTunesDb.buildEnumerator(std::vector<std::string>(), true));\n        if (enumerator->isInvalid())\n        {\n            return false;\n        }\n\n#ifndef NDEBUG\n        uint32_t zeroFile = 0;\n        uint32_t nonzeroFile = 0;\n#endif\n        \n        ITunesFile file;\n        // m_files.reserve(2048);\n        while (enumerator->nextFile(file))\n        {\n            if (file.domain == \"AppDomain-com.tencent.xin\" || file.domain == \"AppDomainGroup-group.com.tencent.xin\")\n            {\n                continue;\n            }\n            if (!file.blobParsed)\n            {\n                ITunesDb::parseFileInfo(&file);\n            }\n#ifndef NDEBUG\n            if (file.size > 0)\n            {\n                nonzeroFile ++;\n            }\n            else\n            {\n                zeroFile ++;\n            }\n#endif\n            if (iTunesDb.isMbdb())\n            {\n                m_files.insert(m_files.end(), std::pair<std::string, size_t>(file.fileId, file.size));\n            }\n            else\n            {\n                m_files.insert(m_files.end(), std::pair<std::string, size_t>(file.fileId.substr(0, 2) + \"/\" + file.fileId, file.size));\n            }\n        }\n        \n#ifndef NDEBUG\n        printlog(\"Manifest.db: zero: %u, nonzero: %u\\r\\n\", zeroFile, nonzeroFile);\n#endif\n\n        return true;\n    }\n    \n    bool queryFileSize(const std::string& fileId, size_t& fileSize) const\n    {\n        std::map<std::string, size_t>::const_iterator it = m_files.find(fileId);\n        if (it == m_files.cend())\n        {\n            return false;\n        }\n        fileSize = it->second;\n        return true;\n    }\n    \n    bool queryDeviceName(std::string& name) const\n    {\n        if (NULL != m_client)\n        {\n            char *deviceName = NULL;\n            if ((LOCKDOWN_E_SUCCESS == lockdownd_get_device_name(m_client, &deviceName)) && NULL != deviceName)\n            {\n                name = deviceName;\n                plist_mem_free(deviceName);\n                return true;\n            }\n        }\n        \n        return false;\n    }\n    \n    bool queryAppContainer(const std::string& bundleId, std::string& container)\n    {\n        if (NULL == m_device || NULL == m_client)\n        {\n            return false;\n        }\n    \n        lockdownd_service_descriptor_t service = NULL;\n        lockdownd_error_t lderr = LOCKDOWN_E_SUCCESS;\n        if (((lderr = lockdownd_start_service(m_client, \"com.apple.mobile.installation_proxy\", &service)) != LOCKDOWN_E_SUCCESS) || NULL == service)\n        {\n            return false;\n        }\n        \n        instproxy_client_t ipc = NULL;\n        instproxy_error_t err = instproxy_client_new(m_device, service, &ipc);\n\n        if (service)\n        {\n            lockdownd_service_descriptor_free(service);\n        }\n        service = NULL;\n\n        bool res = false;\n        if (err == INSTPROXY_E_SUCCESS)\n        {\n            char *value = NULL;\n            err = getContainderForBundleIdentifier(ipc, bundleId.c_str(), &value);\n            if (err == INSTPROXY_E_SUCCESS && NULL != value)\n            {\n                container = value;\n                res = true;\n            }\n            if (NULL != value)\n            {\n                free(value);\n            }\n            \n            instproxy_client_free(ipc);\n        }\n        \n        return res;;\n    }\n    \n    void setOverallProgressFromMessage(IDeviceBackup *pThis, plist_t message, char* identifier)\n    {\n        plist_t node = NULL;\n        double progress = 0.0;\n\n        if (!strcmp(identifier, \"DLMessageDownloadFiles\")) {\n            node = plist_array_get_item(message, 3);\n        } else if (!strcmp(identifier, \"DLMessageUploadFiles\")) {\n            node = plist_array_get_item(message, 2);\n        } else if (!strcmp(identifier, \"DLMessageMoveFiles\") || !strcmp(identifier, \"DLMessageMoveItems\")) {\n            node = plist_array_get_item(message, 3);\n        } else if (!strcmp(identifier, \"DLMessageRemoveFiles\") || !strcmp(identifier, \"DLMessageRemoveItems\")) {\n            node = plist_array_get_item(message, 3);\n        }\n\n        if (node != NULL) {\n            plist_get_real_val(node, &progress);\n            pThis->setOverallProgress(progress);\n        }\n    }\n    \n    void doPostNotification(idevice_t device, const char *notification)\n    {\n        lockdownd_service_descriptor_t service = NULL;\n        np_client_t np;\n\n        lockdownd_client_t lockdown = NULL;\n\n        if (lockdownd_client_new_with_handshake(device, &lockdown, CLIENT_ID) != LOCKDOWN_E_SUCCESS) {\n            return;\n        }\n\n        lockdownd_error_t ldret = lockdownd_start_service(lockdown, NP_SERVICE_NAME, &service);\n        if (service && service->port) {\n            np_client_new(device, service, &np);\n            if (np) {\n                np_post_notification(np, notification);\n                np_client_free(np);\n            }\n        } else {\n            printlog(\"ERROR: Could not start service %s: %s\\n\", NP_SERVICE_NAME, lockdownd_strerror(ldret));\n        }\n\n        if (service) {\n            lockdownd_service_descriptor_free(service);\n            service = NULL;\n        }\n        lockdownd_client_free(lockdown);\n    }\n\n    static void notifyCallback(const char *notification, void *userdata)\n    {\n        if (strlen(notification) == 0) {\n            return;\n        }\n        if (!strcmp(notification, NP_SYNC_CANCEL_REQUEST)) {\n            PRINT_VERBOSE(1, \"User has cancelled the backup process on the device.\\n\");\n            IDeviceBackup *pThis = reinterpret_cast<IDeviceBackup *>(userdata);\n            pThis->cancel();\n        } else if (!strcmp(notification, NP_BACKUP_DOMAIN_CHANGED)) {\n            /// backup_domain_changed = 1;\n        } else {\n            PRINT_VERBOSE(1, \"Unhandled notification '%s' (TODO: implement)\\n\", notification);\n        }\n    }\n    \n    static int checkSnapshotState(const std::string& path, const std::string& udid, const char *matches)\n    {\n        int ret = 0;\n        plist_t status_plist = NULL;\n        std::string file_path = combinePath(path, udid, \"Status.plist\");\n\n        readPlistFile(&status_plist, file_path);\n        \n        if (!status_plist) {\n            printlog(\"Could not read Status.plist!\\n\");\n            return ret;\n        }\n        plist_t node = plist_dict_get_item(status_plist, \"SnapshotState\");\n        if (node && PLIST_IS_STRING(node)) {\n            const char* sval = plist_get_string_ptr(node, NULL);\n            if (sval) {\n                ret = (strcmp(sval, matches) == 0) ? 1 : 0;\n            }\n        } else {\n            printlog(\"%s: ERROR could not get SnapshotState key from Status.plist!\\n\", __func__);\n        }\n        plist_free(status_plist);\n        return ret;\n    }\n\n    instproxy_error_t getContainderForBundleIdentifier(instproxy_client_t client, const char* appid, char** container)\n    {\n        if (!client || !appid)\n            return INSTPROXY_E_INVALID_ARG;\n\n        plist_t apps = NULL;\n\n        // create client options for any application types\n        plist_t client_opts = instproxy_client_options_new();\n        instproxy_client_options_add(client_opts, \"ApplicationType\", \"Any\", NULL);\n\n        // only return attributes we need\n        instproxy_client_options_set_return_attributes(client_opts, \"CFBundleIdentifier\", \"CFBundleExecutable\", \"Path\", \"Container\", \"EnvironmentVariables\", NULL);\n\n        // only query for specific appid\n        const char* appids[] = {appid, NULL};\n\n        // query device for list of apps\n        instproxy_error_t ierr = instproxy_lookup(client, appids, client_opts, &apps);\n\n        instproxy_client_options_free(client_opts);\n\n        if (ierr != INSTPROXY_E_SUCCESS) {\n            return ierr;\n        }\n        \n        plist_t app_found = plist_access_path(apps, 1, appid);\n        if (!app_found) {\n            if (apps)\n                plist_free(apps);\n            *container = NULL;\n            return INSTPROXY_E_OP_FAILED;\n        }\n\n        const char* container_str = NULL;\n        uint64_t strLength = 0;\n        plist_t container_p = plist_dict_get_item(app_found, \"Container\");\n        if (container_p) {\n            container_str = plist_get_string_ptr(container_p, &strLength);\n        }\n\n        if (!container_str) {\n            plist_free(apps);\n            return INSTPROXY_E_OP_FAILED;\n        }\n\n        char* ret = (char*)malloc(strLength + 1);\n        strcpy(ret, container_str);\n        \n        *container = ret;\n\n        plist_free(apps);\n        return INSTPROXY_E_SUCCESS;\n    }\n    \n    void handleListDirectory(plist_t message, const char *backupDir)\n    {\n        if (!message || (!PLIST_IS_ARRAY(message)) || plist_array_get_size(message) < 2 || !backupDir) return;\n\n        plist_t node = plist_array_get_item(message, 1);\n        char *str = NULL;\n        if (plist_get_node_type(node) == PLIST_STRING)\n        {\n            plist_get_string_val(node, &str);\n        }\n        if (!str)\n        {\n            printlog(\"ERROR: Malformed DLContentsOfDirectory message\\n\");\n            // TODO error handling\n            return;\n        }\n\n        std::string relativePath = str;\n        if (relativePath.size() > (m_udid.size() + 1) && startsWith(relativePath, m_udid))\n        {\n            relativePath = relativePath.substr(m_udid.size() + 1);\n        }\n        else\n        {\n            relativePath.clear();\n        }\n        std::string path = combinePath(backupDir, str);\n        plist_mem_free(str);\n\n        plist_t dirlist = plist_new_dict();\n\n        fs::FileEnumerator fileEnumerator(path);\n        fs::FileEnumerator::FileInfo fi;\n        while (fileEnumerator.nextFile(fi))\n        {\n            plist_t fdict = plist_new_dict();\n\n            const char *ftype = \"DLFileTypeUnknown\";\n            if (fi.isDirectory()) {\n                ftype = \"DLFileTypeDirectory\";\n            } else if (fi.isNormalFile()) {\n                ftype = \"DLFileTypeRegular\";\n            }\n            \n            size_t fileSize = fi.getFileSize();\n            if (!fi.isDirectory() && (fileSize == 0) && fi.isNormalFile())\n            {\n                bool found = queryFileSize(relativePath + \"/\" + fi.getFileName(), fileSize);\n#ifndef NDEBUG\n                if (found && fileSize > 0)\n                {\n                    static int zeroCnt = 0;\n                    zeroCnt ++;\n\n                    // printlog(\"DBG::Found zero file: %u\\r\\n\", zeroCnt);\n                }\n#endif\n            }\n            plist_dict_set_item(fdict, \"DLFileType\", plist_new_string(ftype));\n            plist_dict_set_item(fdict, \"DLFileSize\", plist_new_uint(fileSize));\n            plist_dict_set_item(fdict, \"DLFileModificationDate\",\n                        plist_new_date((uint32_t)((uint64_t)fi.getModifiedTime() - MAC_EPOCH), 0));\n\n            plist_dict_set_item(dirlist, fi.getFileName().c_str(), fdict);\n        }\n\n        /* TODO error handling */\n        mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, 0, NULL, dirlist);\n        plist_free(dirlist);\n        if (err != MOBILEBACKUP2_E_SUCCESS) {\n            printlog(\"Could not send status response, error %d\\n\", err);\n        }\n    }\n    \n    void handleMakeDirectory(plist_t message, const char *backup_dir)\n    {\n        if (!message || (!PLIST_IS_ARRAY(message)) || plist_array_get_size(message) < 2 || !backup_dir) return;\n\n        plist_t dir = plist_array_get_item(message, 1);\n        char *str = NULL;\n        int errcode = 0;\n        char *errdesc = NULL;\n        plist_get_string_val(dir, &str);\n\n        std::string newpath = combinePath(backup_dir, str);\n        plist_mem_free(str);\n\n        if ((errcode = fs::makeDirectory(newpath, 0755)) != 0) {\n            errdesc = strerror(errcode);\n            if (errno != EEXIST) {\n                printlog(\"mkdir: %s (%d)\\n\", errdesc, errcode);\n            }\n            errcode = fs::convertErrnoToDeviceError(errcode);\n        }\n        mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, NULL);\n        if (err != MOBILEBACKUP2_E_SUCCESS) {\n            printlog(\"Could not send status response, error %d\\n\", err);\n        }\n    }\n\n    int receiveFilename(IDeviceBackup *pThis, char** filename)\n    {\n        uint32_t nlen = 0;\n        uint32_t rlen = 0;\n\n        do {\n            nlen = 0;\n            rlen = 0;\n            mobilebackup2_receive_raw(m_mobilebackup2, (char*)&nlen, 4, &rlen);\n            nlen = be32toh(nlen);\n\n            if ((nlen == 0) && (rlen == 4)) {\n                // a zero length means no more files to receive\n                return 0;\n            } else if(rlen == 0) {\n                // device needs more time, waiting...\n                continue;\n            } else if (nlen > 4096) {\n                // filename length is too large\n                printlog(\"ERROR: %s: too large filename length (%d)!\\n\", __func__, nlen);\n                return 0;\n            }\n\n            if (*filename != NULL) {\n                free(*filename);\n                *filename = NULL;\n            }\n\n            *filename = (char*)malloc(nlen+1);\n\n            rlen = 0;\n            mobilebackup2_receive_raw(m_mobilebackup2, *filename, nlen, &rlen);\n            if (rlen != nlen) {\n                printlog(\"ERROR: %s: could not read filename\\n\", __func__);\n                return 0;\n            }\n\n            char* p = *filename;\n            p[rlen] = 0;\n\n            break;\n        } while(1 && !pThis->isCanclled());\n\n        return nlen;\n    }\n\n    int handleSendFile(const char *backup_dir, const char *path, plist_t *errplist)\n    {\n        uint32_t nlen = 0;\n        uint32_t pathlen = (uint32_t)strlen(path);\n        uint32_t bytes = 0;\n#ifdef _WIN32\n        std::string localfile = combinePath(backup_dir, normalizePath(path));\n        CA2W szLocalFile(localfile.c_str(), CP_UTF8);\n        struct _stati64 fst;\n#else\n        std::string localfile = combinePath(backup_dir, path);\n        struct stat fst;\n#endif\n\n        char buf[32768];\n\n        FILE *f = NULL;\n        uint32_t slen = 0;\n        int errcode = -1;\n        int result = -1;\n        uint32_t length;\n#ifdef _WIN32\n        uint64_t total;\n        uint64_t sent;\n#else\n        off_t total;\n        off_t sent;\n#endif\n\n        mobilebackup2_error_t err;\n\n        /* send path length */\n        nlen = htobe32(pathlen);\n        err = mobilebackup2_send_raw(m_mobilebackup2, (const char*)&nlen, sizeof(nlen), &bytes);\n        if (err != MOBILEBACKUP2_E_SUCCESS) {\n            goto leave_proto_err;\n        }\n        if (bytes != (uint32_t)sizeof(nlen)) {\n            err = MOBILEBACKUP2_E_MUX_ERROR;\n            goto leave_proto_err;\n        }\n\n        /* send path */\n        err = mobilebackup2_send_raw(m_mobilebackup2, path, pathlen, &bytes);\n        if (err != MOBILEBACKUP2_E_SUCCESS) {\n            goto leave_proto_err;\n        }\n        if (bytes != pathlen) {\n            err = MOBILEBACKUP2_E_MUX_ERROR;\n            goto leave_proto_err;\n        }\n\n#ifdef _WIN32\n        if (_wstati64((LPCWSTR)szLocalFile, &fst) < 0)\n#else\n        if (stat(localfile.c_str(), &fst) < 0)\n#endif\n        {\n            if (errno != ENOENT)\n                printlog(\"%s: stat failed on '%s': %d\\n\", __func__, localfile.c_str(), errno);\n            errcode = errno;\n            goto leave;\n        }\n\n        total = fst.st_size;\n\n        PRINT_VERBOSE(1, \"Sending '%s' (%s)\\n\", path, formatDiskSize(total).c_str());\n        \n        if (total == 0) {\n            errcode = 0;\n            goto leave;\n        }\n\n#ifdef _WIN32\n        f = _wfopen((LPCWSTR)szLocalFile, L\"rb\");\n#else\n        f = fopen(localfile.c_str(), \"rb\");\n#endif\n        if (!f) {\n            printlog(\"%s: Error opening local file '%s': %d\\n\", __func__, localfile.c_str(), errno);\n            errcode = errno;\n            goto leave;\n        }\n\n        sent = 0;\n        do {\n            length = ((total-sent) < (long long)sizeof(buf)) ? (uint32_t)total-sent : (uint32_t)sizeof(buf);\n            /* send data size (file size + 1) */\n            nlen = htobe32(length+1);\n            memcpy(buf, &nlen, sizeof(nlen));\n            buf[4] = CODE_FILE_DATA;\n            err = mobilebackup2_send_raw(m_mobilebackup2, (const char*)buf, 5, &bytes);\n            if (err != MOBILEBACKUP2_E_SUCCESS) {\n                goto leave_proto_err;\n            }\n            if (bytes != 5) {\n                goto leave_proto_err;\n            }\n\n            /* send file contents */\n            size_t r = fread(buf, 1, sizeof(buf), f);\n            if (r <= 0) {\n                printlog(\"%s: read error\\n\", __func__);\n                errcode = errno;\n                goto leave;\n            }\n            err = mobilebackup2_send_raw(m_mobilebackup2, buf, r, &bytes);\n            if (err != MOBILEBACKUP2_E_SUCCESS) {\n                goto leave_proto_err;\n            }\n            if (bytes != (uint32_t)r) {\n                printlog(\"Error: sent only %d of %d bytes\\n\", bytes, (int)r);\n                goto leave_proto_err;\n            }\n            sent += r;\n        } while (sent < total);\n        fclose(f);\n        f = NULL;\n        errcode = 0;\n\n    leave:\n        if (errcode == 0) {\n            result = 0;\n            nlen = 1;\n            nlen = htobe32(nlen);\n            memcpy(buf, &nlen, 4);\n            buf[4] = CODE_SUCCESS;\n            mobilebackup2_send_raw(m_mobilebackup2, buf, 5, &bytes);\n        } else {\n            if (!*errplist) {\n                *errplist = plist_new_dict();\n            }\n            char *errdesc = strerror(errcode);\n            mb2_multi_status_add_file_error(*errplist, path, fs::convertErrnoToDeviceError(errcode), errdesc);\n\n            length = strlen(errdesc);\n            nlen = htobe32(length+1);\n            memcpy(buf, &nlen, 4);\n            buf[4] = CODE_ERROR_LOCAL;\n            slen = 5;\n            memcpy(buf+slen, errdesc, length);\n            slen += length;\n            err = mobilebackup2_send_raw(m_mobilebackup2, (const char*)buf, slen, &bytes);\n            if (err != MOBILEBACKUP2_E_SUCCESS) {\n                printlog(\"could not send message\\n\");\n            }\n            if (bytes != slen) {\n                printlog(\"could only send %d from %d\\n\", bytes, slen);\n            }\n        }\n\n    leave_proto_err:\n        if (f)\n            fclose(f);\n        return result;\n    }\n\n    void handleSendFiles(plist_t message, const char *backup_dir)\n    {\n        uint32_t cnt;\n        uint32_t i = 0;\n        uint32_t sent;\n        plist_t errplist = NULL;\n\n        if (!message || (plist_get_node_type(message) != PLIST_ARRAY) || (plist_array_get_size(message) < 2) || !backup_dir) return;\n\n        plist_t files = plist_array_get_item(message, 1);\n        cnt = plist_array_get_size(files);\n\n        for (i = 0; i < cnt; i++) {\n            plist_t val = plist_array_get_item(files, i);\n            if (plist_get_node_type(val) != PLIST_STRING) {\n                continue;\n            }\n            char *str = NULL;\n            plist_get_string_val(val, &str);\n            if (!str)\n                continue;\n\n            if (handleSendFile(backup_dir, str, &errplist) < 0) {\n                plist_mem_free(str);\n                printlog(\"Error when sending file '%s' to device\\n\", str);\n                // TODO: perhaps we can continue, we've got a multi status response?!\n                break;\n            }\n            plist_mem_free(str);\n        }\n\n        /* send terminating 0 dword */\n        uint32_t zero = 0;\n        mobilebackup2_send_raw(m_mobilebackup2, (char*)&zero, 4, &sent);\n\n        if (!errplist) {\n            plist_t emptydict = plist_new_dict();\n            mobilebackup2_send_status_response(m_mobilebackup2, 0, NULL, emptydict);\n            plist_free(emptydict);\n        } else {\n            mobilebackup2_send_status_response(m_mobilebackup2, -13, \"Multi status\", errplist);\n            plist_free(errplist);\n        }\n    }\n\n    int handleReceiveFiles(IDeviceBackup *pThis, plist_t message, const std::string& backupDir)\n    {\n        uint64_t backup_real_size = 0;\n        uint64_t backup_total_size = 0;\n        uint32_t blocksize = 0;\n        uint32_t bdone;\n        uint32_t rlen;\n        uint32_t nlen = 0;\n        uint32_t r;\n        char buf[32768];\n        char *fname = NULL;\n        char *dname = NULL;\n        std::string bname;\n        char code = 0;\n        char last_code = 0;\n        plist_t node = NULL;\n        FILE *f = NULL;\n        unsigned int file_count = 0;\n        int errcode = 0;\n        char *errdesc = NULL;\n        \n        if (!message || (!PLIST_IS_ARRAY(message)) || plist_array_get_size(message) < 4 || backupDir.empty()) return 0;\n\n        node = plist_array_get_item(message, 3);\n        if (plist_get_node_type(node) == PLIST_UINT) {\n            plist_get_uint_val(node, &backup_total_size);\n        }\n        if (backup_total_size > 0) {\n            PRINT_VERBOSE(1, \"Receiving files\\n\");\n        }\n\n        do {\n            if (pThis->isCanclled())\n                break;\n\n            nlen = receiveFilename(pThis, &dname);\n            if (nlen == 0) {\n                break;\n            }\n\n            nlen = receiveFilename(pThis, &fname);\n            if (!nlen) {\n                break;\n            }\n            std::string destFileName = (fname != NULL) ? fname : \"\";\n\n            bname = combinePath(backupDir, fname);\n\n#ifndef NDEBUG\n            static uint32_t filtered_cnt = 0;\n#endif\n            \n            bool filtered = false;\n            if (backup_total_size > 0)\n            {\n                filtered = pThis->filter(dname, fname);\n#ifndef NDEBUG\n                if (filtered) filtered_cnt++;\n#endif\n            }\n            \n            if (fname != NULL) {\n                free(fname);\n                fname = NULL;\n            }\n#ifndef NDEBUG\n            if (!filtered)\n            {\n                // printlog(\"DBG::dname=%s\\r\\n\", dname);\n                // printlog(\"DBG::Write File: %s\\r\\n\", destFileName.c_str());\n            }\n            // printlog(\"DBG::filtered_cnt = %u\\r\\n\", filtered_cnt);\n#endif\n            \n            r = 0;\n            nlen = 0;\n            mobilebackup2_receive_raw(m_mobilebackup2, (char*)&nlen, 4, &r);\n            if (r != 4) {\n                printlog(\"ERROR: %s: could not receive code length!\\n\", __func__);\n                break;\n            }\n            nlen = be32toh(nlen);\n\n            last_code = code;\n            code = 0;\n\n            mobilebackup2_receive_raw(m_mobilebackup2, &code, 1, &r);\n            if (r != 1) {\n                printlog(\"ERROR: %s: could not receive code!\\n\", __func__);\n                break;\n            }\n\n            /* TODO remove this */\n            if ((code != CODE_SUCCESS) && (code != CODE_FILE_DATA) && (code != CODE_ERROR_REMOTE)) {\n                PRINT_VERBOSE(1, \"Found new flag %02x\\n\", code);\n            }\n\n            fs::deleteFile(bname);\n            \n#ifdef _WIN32\n            f = _wfopen((LPCWSTR)CA2W(bname.c_str(), CP_UTF8), L\"wb\");\n#else\n            f = fopen(bname.c_str(), \"wb\");\n#endif\n            while (f && (code == CODE_FILE_DATA)) {\n                blocksize = nlen-1;\n                bdone = 0;\n                rlen = 0;\n                while (bdone < blocksize) {\n                    if ((blocksize - bdone) < sizeof(buf)) {\n                        rlen = blocksize - bdone;\n                    } else {\n                        rlen = sizeof(buf);\n                    }\n                    mobilebackup2_receive_raw(m_mobilebackup2, buf, rlen, &r);\n                    if ((int)r <= 0) {\n                        break;\n                    }\n                    \n                    if (!filtered)\n                    {\n                        fwrite(buf, 1, r, f);\n                    }\n\n                    bdone += r;\n                }\n                if (bdone == blocksize) {\n                    backup_real_size += blocksize;\n                }\n                /*\n                if (backup_total_size > 0) {\n                    print_progress(backup_real_size, backup_total_size);\n                }\n                 */\n                if (pThis->isCanclled())\n                    break;\n                nlen = 0;\n                mobilebackup2_receive_raw(m_mobilebackup2, (char*)&nlen, 4, &r);\n                nlen = be32toh(nlen);\n                if (nlen > 0) {\n                    last_code = code;\n                    mobilebackup2_receive_raw(m_mobilebackup2, &code, 1, &r);\n                } else {\n                    break;\n                }\n            }\n            if (f) {\n                fclose(f);\n                file_count++;\n                if (filtered)\n                {\n                    std::map<std::string, size_t>::iterator it = m_files.find(destFileName);\n                    if (it == m_files.end())\n                    {\n                        m_files.insert(it, std::pair<std::string, size_t>(destFileName, blocksize));\n                    }\n                    else\n                    {\n                        it->second = blocksize;\n                    }\n                }\n            } else {\n                errcode = fs::convertErrnoToDeviceError(errno);\n                errdesc = strerror(errno);\n                printlog(\"Error opening '%s' for writing: %s\\n\", bname.c_str(), errdesc);\n                break;\n            }\n            if (nlen == 0) {\n                break;\n            }\n\n            /* check if an error message was received */\n            if (code == CODE_ERROR_REMOTE) {\n                /* error message */\n                char *msg = (char*)malloc(nlen);\n                mobilebackup2_receive_raw(m_mobilebackup2, msg, nlen-1, &r);\n                msg[r] = 0;\n                /* If sent using CODE_FILE_DATA, end marker will be CODE_ERROR_REMOTE which is not an error! */\n                if (last_code != CODE_FILE_DATA) {\n                    printlog(\"\\nReceived an error message from device: %s\\n\", msg);\n                }\n                free(msg);\n            }\n        } while (1);\n\n        if (fname != NULL)\n            free(fname);\n\n        /* if there are leftovers to read, finish up cleanly */\n        if ((int)nlen-1 > 0) {\n            PRINT_VERBOSE(1, \"\\nDiscarding current data hunk.\\n\");\n            fname = (char*)malloc(nlen-1);\n            mobilebackup2_receive_raw(m_mobilebackup2, fname, nlen-1, &r);\n            free(fname);\n            fs::deleteFile(bname);\n        }\n\n        /* clean up */\n        if (dname != NULL)\n            free(dname);\n\n        plist_t empty_plist = plist_new_dict();\n        mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, empty_plist);\n        plist_free(empty_plist);\n\n        return file_count;\n    }\n    \n    void handleMoveFiles(IDeviceBackup *pThis, plist_t message, char *dlmsg, const std::string& outputPath)\n    {\n        /* perform a series of rename operations */\n        setOverallProgressFromMessage(pThis, message, dlmsg);\n        plist_t moves = plist_array_get_item(message, 1);\n        uint32_t cnt = plist_dict_get_size(moves);\n        PRINT_VERBOSE(1, \"Moving %d file%s\\n\", cnt, (cnt == 1) ? \"\" : \"s\");\n        plist_dict_iter iter = NULL;\n        plist_dict_new_iter(moves, &iter);\n        int errcode = 0;\n        const char *errdesc = NULL;\n        if (iter) {\n            char *key = NULL;\n            plist_t val = NULL;\n            do {\n                plist_dict_next_item(moves, iter, &key, &val);\n                if (key && PLIST_IS_STRING(val)) {\n                    const char *str = plist_get_string_ptr(val, NULL);\n                    if (str) {\n                        std::map<std::string, size_t>::iterator it = m_files.find(key);\n                        if (it != m_files.end())\n                        {\n                            size_t fileSize = it->second;\n                            m_files.erase(it);\n                            m_files[str] = fileSize;\n                        }\n                        \n                        std::string newpath = combinePath(outputPath, str);\n                        std::string oldpath = combinePath(outputPath, key);\n\n                        if (existsDirectory(newpath))\n                            fs::deleteDirectoryRecursively(newpath);\n                        else\n                            fs::deleteFile(newpath);\n                        if ((errcode = fs::moveFile(oldpath, newpath)) != 0)\n                        {\n                            printlog(\"Renameing '%s' to '%s' failed: %s (%d)\\n\", oldpath.c_str(), newpath.c_str(), strerror(errno), errno);\n                            errdesc = strerror(errcode);\n                            errcode = fs::convertErrnoToDeviceError(errcode);\n                            break;\n                        }\n                    }\n                    plist_mem_free(key);\n                    key = NULL;\n                }\n            } while (val);\n            plist_mem_free(iter);\n        } else {\n            errcode = -1;\n            errdesc = \"Could not create dict iterator\";\n            printlog(\"Could not create dict iterator\\n\");\n        }\n        plist_t empty_dict = plist_new_dict();\n        mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, empty_dict);\n        plist_free(empty_dict);\n        if (err != MOBILEBACKUP2_E_SUCCESS) {\n            printlog(\"Could not send status response, error %d\\n\", err);\n        }\n    }\n    \n    void handleRemoveFiles(IDeviceBackup *pThis, plist_t message, char *dlmsg, const std::string& outputPath)\n    {\n        setOverallProgressFromMessage(pThis, message, dlmsg);\n        plist_t removes = plist_array_get_item(message, 1);\n        uint32_t cnt = plist_array_get_size(removes);\n        PRINT_VERBOSE(1, \"Removing %d file%s\\n\", cnt, (cnt == 1) ? \"\" : \"s\");\n        uint32_t ii = 0;\n        int errcode = 0;\n        const char *errdesc = NULL;\n        for (ii = 0; ii < cnt; ii++) {\n            plist_t val = plist_array_get_item(removes, ii);\n            if (plist_get_node_type(val) == PLIST_STRING) {\n                char *str = NULL;\n                plist_get_string_val(val, &str);\n                if (str) {\n                    const char *checkfile = strchr(str, '/');\n                    int suppress_warning = 0;\n                    if (checkfile) {\n                        if (strcmp(checkfile+1, \"Manifest.mbdx\") == 0) {\n                            suppress_warning = 1;\n                        }\n                    }\n                    std::string newpath = combinePath(outputPath, str);\n                    plist_mem_free(str);\n                    int res = 0;\n                    if (existsDirectory(newpath)) {\n                        res = fs::deleteDirectoryRecursively(newpath);\n                    } else {\n                        res = fs::deleteFile(newpath);\n                    }\n                    if (res != 0 && res != ENOENT)\n                    {\n                        if (!suppress_warning)\n                            printlog(\"Could not remove '%s'\\n\", newpath.c_str());\n                        errcode = fs::convertErrnoToDeviceError(res);\n                        errdesc = strerror(res);\n                    }\n                    else\n                    {\n                        printlog(\"Removed: %s\\n\", newpath.c_str());\n                    }\n                }\n            }\n        }\n        plist_t empty_dict = plist_new_dict();\n        mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, empty_dict);\n        plist_free(empty_dict);\n        if (err != MOBILEBACKUP2_E_SUCCESS) {\n            printlog(\"Could not send status response, error %d\\n\", err);\n        }\n    }\n    \n    void handleCopyItem(IDeviceBackup *pThis, plist_t message, char *dlmsg, const std::string& outputPath)\n    {\n        plist_t srcpath = plist_array_get_item(message, 1);\n        plist_t dstpath = plist_array_get_item(message, 2);\n        int errcode = 0;\n        const char *errdesc = NULL;\n        if ((plist_get_node_type(srcpath) == PLIST_STRING) && (plist_get_node_type(dstpath) == PLIST_STRING)) {\n            char *src = NULL;\n            char *dst = NULL;\n            plist_get_string_val(srcpath, &src);\n            plist_get_string_val(dstpath, &dst);\n            if (src && dst) {\n                std::string oldpath = combinePath(outputPath, src);\n                std::string newpath = combinePath(outputPath, dst);\n\n                PRINT_VERBOSE(1, \"Copying '%s' to '%s'\\n\", src, dst);\n\n                /* check that src exists */\n                if (existsDirectory(oldpath)) {\n                    copyDirectory(oldpath, newpath);\n                } else if (existsFile(oldpath)) {\n                    copyFile(oldpath, newpath);\n                }\n            }\n            plist_mem_free(src);\n            plist_mem_free(dst);\n        }\n        plist_t empty_dict = plist_new_dict();\n        mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, empty_dict);\n        plist_free(empty_dict);\n        if (err != MOBILEBACKUP2_E_SUCCESS) {\n            printlog(\"Could not send status response, error %d\\n\", err);\n        }\n    }\n    \n    void handleProcessMessage(plist_t message, int& operation_ok, int& result_code, const std::string outputDir)\n    {\n#ifndef NDEBUG\n        writePlistFile(message, combinePath(outputDir, \"dbg\", \"error.plist\"), PLIST_FORMAT_XML);\n#endif\n        plist_t node_tmp = plist_array_get_item(message, 1);\n        if (!PLIST_IS_DICT(node_tmp)) {\n            printlog(\"Unknown message received!\\n\");\n        }\n        plist_t nn;\n        int error_code = -1;\n        nn = plist_dict_get_item(node_tmp, \"ErrorCode\");\n        if (nn && PLIST_IS_UINT(nn)) {\n            uint64_t ec = 0;\n            plist_get_uint_val(nn, &ec);\n            error_code = (uint32_t)ec;\n            if (error_code == 0) {\n                operation_ok = 1;\n                result_code = 0;\n            } else {\n                result_code = -error_code;\n            }\n        }\n        nn = plist_dict_get_item(node_tmp, \"ErrorDescription\");\n        const char *str = NULL;\n        if (nn && PLIST_IS_STRING(nn)) {\n            str = plist_get_string_ptr(nn, NULL);\n        }\n        if (error_code != 0) {\n            if (str) {\n                printlog(\"ErrorCode %d: %s\\n\", error_code, str);\n            } else {\n                printlog(\"ErrorCode %d: (Unknown)\\n\", error_code);\n            }\n        }\n        nn = plist_dict_get_item(node_tmp, \"Content\");\n        if (nn && PLIST_IS_STRING(nn)) {\n            str = plist_get_string_ptr(nn, NULL);\n            PRINT_VERBOSE(1, \"Content:\\n\");\n            printlog(\"%s\", str);\n        }\n    }\n    \n    void mb2_multi_status_add_file_error(plist_t status_dict, const char *path, int error_code, const char *error_message)\n    {\n        if (!status_dict) return;\n        plist_t filedict = plist_new_dict();\n        plist_dict_set_item(filedict, \"DLFileErrorString\", plist_new_string(error_message));\n        plist_dict_set_item(filedict, \"DLFileErrorCode\", plist_new_uint(error_code));\n        plist_dict_set_item(status_dict, path, filedict);\n    }\n\n    void mobilebackup_afc_get_file_contents(afc_client_t afc, const char *filename, char **data, uint64_t *size)\n    {\n        if (!afc || !data || !size) {\n            return;\n        }\n\n        char **fileinfo = NULL;\n        uint32_t fsize = 0;\n\n        afc_get_file_info(afc, filename, &fileinfo);\n        if (!fileinfo) {\n            return;\n        }\n        int i;\n        for (i = 0; fileinfo[i]; i+=2) {\n            if (!strcmp(fileinfo[i], \"st_size\")) {\n                fsize = atol(fileinfo[i+1]);\n                break;\n            }\n        }\n        afc_dictionary_free(fileinfo);\n\n        if (fsize == 0) {\n            return;\n        }\n\n        uint64_t f = 0;\n        afc_file_open(afc, filename, AFC_FOPEN_RDONLY, &f);\n        if (!f) {\n            return;\n        }\n        char *buf = (char*)malloc((uint32_t)fsize);\n        uint32_t done = 0;\n        while (done < fsize) {\n            uint32_t bread = 0;\n            afc_file_read(afc, f, buf+done, 65536, &bread);\n            if (bread > 0) {\n                done += bread;\n            } else {\n                break;\n            }\n        }\n        if (done == fsize) {\n            *size = fsize;\n            *data = buf;\n        } else {\n            free(buf);\n        }\n        afc_file_close(afc, f);\n    }\n    \n    plist_t newInfoPlist(const char* udid, idevice_t device, afc_client_t afc)\n    {\n        /* gather data from lockdown */\n        plist_t value_node = NULL;\n        plist_t root_node = NULL;\n        plist_t itunes_settings = NULL;\n        plist_t min_itunes_version = NULL;\n        std::string udid_uppercase;\n\n        lockdownd_client_t lockdown = NULL;\n        if (lockdownd_client_new_with_handshake(device, &lockdown, CLIENT_ID) != LOCKDOWN_E_SUCCESS) {\n            return NULL;\n        }\n\n        plist_t ret = plist_new_dict();\n\n        /* get basic device information in one go */\n        lockdownd_get_value(lockdown, NULL, NULL, &root_node);\n\n        /* get iTunes settings */\n        lockdownd_get_value(lockdown, \"com.apple.iTunes\", NULL, &itunes_settings);\n\n        /* get minimum iTunes version */\n        lockdownd_get_value(lockdown, \"com.apple.mobile.iTunes\", \"MinITunesVersion\", &min_itunes_version);\n\n        lockdownd_client_free(lockdown);\n\n        /* get a list of installed user applications */\n        plist_t app_dict = plist_new_dict();\n        plist_t installed_apps = plist_new_array();\n        instproxy_client_t ip = NULL;\n        if (instproxy_client_start_service(device, &ip, CLIENT_ID) == INSTPROXY_E_SUCCESS) {\n            plist_t client_opts = instproxy_client_options_new();\n            instproxy_client_options_add(client_opts, \"ApplicationType\", \"User\", NULL);\n            instproxy_client_options_set_return_attributes(client_opts, \"CFBundleIdentifier\", \"ApplicationSINF\", \"iTunesMetadata\", NULL);\n\n            plist_t apps = NULL;\n            instproxy_browse(ip, client_opts, &apps);\n\n            sbservices_client_t sbs = NULL;\n            if (sbservices_client_start_service(device, &sbs, CLIENT_ID) != SBSERVICES_E_SUCCESS) {\n                printlog(\"Couldn't establish sbservices connection. Continuing anyway.\\n\");\n            }\n\n            if (apps && PLIST_IS_ARRAY(apps)) {\n                uint32_t app_count = plist_array_get_size(apps);\n                uint32_t i;\n                for (i = 0; i < app_count; i++) {\n                    plist_t app_entry = plist_array_get_item(apps, i);\n                    plist_t bundle_id = plist_dict_get_item(app_entry, \"CFBundleIdentifier\");\n                    if (bundle_id) {\n                        char *bundle_id_str = NULL;\n                        plist_array_append_item(installed_apps, plist_copy(bundle_id));\n\n                        plist_get_string_val(bundle_id, &bundle_id_str);\n                        plist_t sinf = plist_dict_get_item(app_entry, \"ApplicationSINF\");\n                        plist_t meta = plist_dict_get_item(app_entry, \"iTunesMetadata\");\n                        if (sinf && meta) {\n                            plist_t adict = plist_new_dict();\n                            plist_dict_set_item(adict, \"ApplicationSINF\", plist_copy(sinf));\n                            if (sbs) {\n                                char *pngdata = NULL;\n                                uint64_t pngsize = 0;\n                                sbservices_get_icon_pngdata(sbs, bundle_id_str, &pngdata, &pngsize);\n                                if (pngdata) {\n                                    plist_dict_set_item(adict, \"PlaceholderIcon\", plist_new_data(pngdata, pngsize));\n                                    plist_mem_free(pngdata);\n                                }\n                            }\n                            plist_dict_set_item(adict, \"iTunesMetadata\", plist_copy(meta));\n                            plist_dict_set_item(app_dict, bundle_id_str, adict);\n                        }\n                        plist_mem_free(bundle_id_str);\n                    }\n                }\n            }\n            plist_free(apps);\n\n            if (sbs) {\n                sbservices_client_free(sbs);\n            }\n\n            instproxy_client_options_free(client_opts);\n\n            instproxy_client_free(ip);\n        }\n\n        /* Applications */\n        plist_dict_set_item(ret, \"Applications\", app_dict);\n\n        /* set fields we understand */\n        value_node = plist_dict_get_item(root_node, \"BuildVersion\");\n        plist_dict_set_item(ret, \"Build Version\", plist_copy(value_node));\n\n        value_node = plist_dict_get_item(root_node, \"DeviceName\");\n        plist_dict_set_item(ret, \"Device Name\", plist_copy(value_node));\n        plist_dict_set_item(ret, \"Display Name\", plist_copy(value_node));\n\n        std::string uuid = makeUuid();\n        plist_dict_set_item(ret, \"GUID\", plist_new_string(uuid.c_str()));\n\n        value_node = plist_dict_get_item(root_node, \"IntegratedCircuitCardIdentity\");\n        if (value_node)\n            plist_dict_set_item(ret, \"ICCID\", plist_copy(value_node));\n\n        value_node = plist_dict_get_item(root_node, \"InternationalMobileEquipmentIdentity\");\n        if (value_node)\n            plist_dict_set_item(ret, \"IMEI\", plist_copy(value_node));\n\n        /* Installed Applications */\n        plist_dict_set_item(ret, \"Installed Applications\", installed_apps);\n\n        plist_dict_set_item(ret, \"Last Backup Date\", plist_new_date(time(NULL) - MAC_EPOCH, 0));\n\n        value_node = plist_dict_get_item(root_node, \"MobileEquipmentIdentifier\");\n        if (value_node)\n            plist_dict_set_item(ret, \"MEID\", plist_copy(value_node));\n\n        value_node = plist_dict_get_item(root_node, \"PhoneNumber\");\n        if (value_node && (plist_get_node_type(value_node) == PLIST_STRING)) {\n            plist_dict_set_item(ret, \"Phone Number\", plist_copy(value_node));\n        }\n\n        /* FIXME Product Name */\n\n        value_node = plist_dict_get_item(root_node, \"ProductType\");\n        plist_dict_set_item(ret, \"Product Type\", plist_copy(value_node));\n\n        value_node = plist_dict_get_item(root_node, \"ProductVersion\");\n        plist_dict_set_item(ret, \"Product Version\", plist_copy(value_node));\n\n        value_node = plist_dict_get_item(root_node, \"SerialNumber\");\n        plist_dict_set_item(ret, \"Serial Number\", plist_copy(value_node));\n\n        /* FIXME Sync Settings? */\n\n        value_node = plist_dict_get_item(root_node, \"UniqueDeviceID\");\n        plist_dict_set_item(ret, \"Target Identifier\", plist_new_string(udid));\n\n        plist_dict_set_item(ret, \"Target Type\", plist_new_string(\"Device\"));\n\n        /* uppercase */\n        udid_uppercase = toUpper((char*)udid);\n        plist_dict_set_item(ret, \"Unique Identifier\", plist_new_string(udid_uppercase.c_str()));\n\n        char *data_buf = NULL;\n        uint64_t data_size = 0;\n        mobilebackup_afc_get_file_contents(afc, \"/Books/iBooksData2.plist\", &data_buf, &data_size);\n        if (data_buf) {\n            plist_dict_set_item(ret, \"iBooks Data 2\", plist_new_data(data_buf, data_size));\n            free(data_buf);\n        }\n\n        plist_t files = plist_new_dict();\n        const char *itunesfiles[] = {\n            \"ApertureAlbumPrefs\",\n            \"IC-Info.sidb\",\n            \"IC-Info.sidv\",\n            \"PhotosFolderAlbums\",\n            \"PhotosFolderName\",\n            \"PhotosFolderPrefs\",\n            \"VoiceMemos.plist\",\n            \"iPhotoAlbumPrefs\",\n            \"iTunesApplicationIDs\",\n            \"iTunesPrefs\",\n            \"iTunesPrefs.plist\",\n            NULL\n        };\n        int i = 0;\n        for (i = 0; itunesfiles[i]; i++) {\n            data_buf = NULL;\n            data_size = 0;\n            char *fname = (char*)malloc(strlen(\"/iTunes_Control/iTunes/\") + strlen(itunesfiles[i]) + 1);\n            strcpy(fname, \"/iTunes_Control/iTunes/\");\n            strcat(fname, itunesfiles[i]);\n            mobilebackup_afc_get_file_contents(afc, fname, &data_buf, &data_size);\n            free(fname);\n            if (data_buf) {\n                plist_dict_set_item(files, itunesfiles[i], plist_new_data(data_buf, data_size));\n                free(data_buf);\n            }\n        }\n        plist_dict_set_item(ret, \"iTunes Files\", files);\n\n        plist_dict_set_item(ret, \"iTunes Settings\", itunes_settings ? plist_copy(itunes_settings) : plist_new_dict());\n\n        /* since we usually don't have iTunes, let's get the minimum required iTunes version from the device */\n        if (min_itunes_version) {\n            plist_dict_set_item(ret, \"iTunes Version\", plist_copy(min_itunes_version));\n        } else {\n            plist_dict_set_item(ret, \"iTunes Version\", plist_new_string(\"10.0.1\"));\n        }\n\n        plist_free(itunes_settings);\n        plist_free(min_itunes_version);\n        plist_free(root_node);\n\n        return ret;\n    }\n    \n    bool willEncrypt()\n    {\n        uint8_t willEncrypt = 0;\n        plist_t node = NULL;\n        lockdownd_get_value(m_client, \"com.apple.mobile.backup\", \"WillEncrypt\", &node);\n        if (node)\n        {\n            if (PLIST_IS_BOOLEAN(node))\n            {\n                plist_get_bool_val(node, &willEncrypt);\n            }\n            plist_free(node);\n            node = NULL;\n        }\n\n        return willEncrypt != 0;\n    }\n\n    int getProductVersion()\n    {\n        int deviceVersion = 0;\n        plist_t node = NULL;\n        lockdownd_get_value(m_client, NULL, \"ProductVersion\", &node);\n        if (node)\n        {\n            const char *productVersion = NULL;\n            if (PLIST_IS_STRING(node))\n            {\n                productVersion = plist_get_string_ptr(node, NULL);\n            }\n            \n            if (productVersion)\n            {\n                int vers[3] = { 0, 0, 0 };\n                if (sscanf(productVersion, \"%d.%d.%d\", &vers[0], &vers[1], &vers[2]) >= 2) {\n                    deviceVersion = DEVICE_VERSION(vers[0], vers[1], vers[2]);\n                }\n            }\n            \n            plist_free(node);\n            node = NULL;\n        }\n\n        return deviceVersion;\n    }\n    \n    inline mobilebackup2_error_t sendHelloMessage(double& remote_version)\n    {\n        /* send Hello message */\n        double local_versions[2] = {2.0, 2.1};\n        return mobilebackup2_version_exchange(m_mobilebackup2, local_versions, 2, &remote_version);\n    }\n    \n    bool backup(IDeviceBackup *pThis, const std::string& outputPath)\n    {\n        /* start notification_proxy */\n        \n        char* sourceUdid = NULL;\n        mobilebackup2_error_t err = MOBILEBACKUP2_E_SUCCESS;\n        int isFullBackup = 0;\n        \n        // uint64_t lockfile = 0;\n        int result_code = -1;\n\n        lockdownd_service_descriptor_t service = NULL;\n        lockdownd_error_t ldret = lockdownd_start_service(m_client, NP_SERVICE_NAME, &service);\n        if ((ldret == LOCKDOWN_E_SUCCESS) && service && service->port)\n        {\n            np_client_new(m_device, service, &m_np);\n            np_set_notify_callback(m_np, notifyCallback, (void *)pThis);\n            const char *noties[5] = {\n                NP_SYNC_CANCEL_REQUEST,\n                NP_SYNC_SUSPEND_REQUEST,\n                NP_SYNC_RESUME_REQUEST,\n                NP_BACKUP_DOMAIN_CHANGED,\n                NULL\n            };\n            np_observe_notifications(m_np, noties);\n        }\n        else\n        {\n            printlog(\"ERROR: Could not start service %s: %s\\n\", NP_SERVICE_NAME, lockdownd_strerror(ldret));\n            return false;\n        }\n        \n        freeService(service);\n        \n        /* start AFC, we need this for the lock file */\n        ldret = lockdownd_start_service(m_client, AFC_SERVICE_NAME, &service);\n        if ((ldret != LOCKDOWN_E_SUCCESS) || NULL == service || 0 == service->port)\n        {\n            freeService(service);\n            printlog(\"ERROR: Could not start service %s: %s\\n\", AFC_SERVICE_NAME, lockdownd_strerror(ldret));\n            return false;\n        }\n\n        afc_client_new(m_device, service, &m_afc);\n        freeService(service);\n\n        /* start mobilebackup service and retrieve port */\n        ldret = lockdownd_start_service_with_escrow_bag(m_client, MOBILEBACKUP2_SERVICE_NAME, &service);\n        freeClient();\n        if ((ldret != LOCKDOWN_E_SUCCESS) || NULL == service || 0 == service->port)\n        {\n            freeService(service);\n            return false;\n        }\n        \n        PRINT_VERBOSE(1, \"Started \\\"%s\\\" service on port %d.\\n\", MOBILEBACKUP2_SERVICE_NAME, service->port);\n        mobilebackup2_client_new(m_device, service, &m_mobilebackup2);\n\n        freeService(service);\n\n        double remote_version = 0.0;\n        err = sendHelloMessage(remote_version);\n        if (err != MOBILEBACKUP2_E_SUCCESS)\n        {\n            printlog(\"Could not perform backup protocol version exchange, error code %d\\n\", err);\n            return false;\n        }\n\n        PRINT_VERBOSE(1, \"Negotiated Protocol Version %.1f\\n\", remote_version);\n\n        /* check abort conditions */\n        \n        /*\n        if (pThis->isCanclled())\n        {\n            PRINT_VERBOSE(1, \"Aborting as requested by user...\\n\");\n            skipBackup = true;\n            goto checkpoint;\n        }\n         */\n\n        /* verify existing Info.plist */\n        std::string info_path;\n        /* backup directory must contain an Info.plist */\n        info_path = combinePath(outputPath, m_udid, \"Info.plist\");\n        plist_t infoPlist = NULL;\n        if (!info_path.empty() && existsFile(info_path))\n        {\n            /// PRINT_VERBOSE(1, \"Reading Info.plist from backup.\\n\");\n            readPlistFile(&infoPlist, info_path);\n\n            if (!infoPlist)\n            {\n                printlog(\"Could not read Info.plist\\n\");\n                isFullBackup = 1;\n            }\n        } else {\n            isFullBackup = 1;\n        }\n\n        doPostNotification(m_device, NP_SYNC_WILL_START);\n        afc_file_open(m_afc, \"/com.apple.itunes.lock_sync\", AFC_FOPEN_RW, &m_lockfile);\n        \n        if (m_lockfile)\n        {\n            afc_error_t aerr;\n            doPostNotification(m_device, NP_SYNC_LOCK_REQUEST);\n            int i = 0;\n            for (; i < LOCK_ATTEMPTS; i++)\n            {\n                aerr = afc_file_lock(m_afc, m_lockfile, AFC_LOCK_EX);\n                if (aerr == AFC_E_SUCCESS)\n                {\n                    doPostNotification(m_device, NP_SYNC_DID_START);\n                    break;\n                }\n                else if (aerr == AFC_E_OP_WOULD_BLOCK)\n                {\n                    usleep(LOCK_WAIT);\n                    continue;\n                }\n                else\n                {\n                    printlog(\"ERROR: could not lock file! error code: %d\\n\", aerr);\n                    afc_file_close(m_afc, m_lockfile);\n                    m_lockfile = 0;\n                    /// cmd = CMD_LEAVE;\n                }\n            }\n            if (i == LOCK_ATTEMPTS) {\n                printlog(\"ERROR: timeout while locking for sync\\n\");\n                afc_file_close(m_afc, m_lockfile);\n                m_lockfile = 0;\n                // skipBackup = true;\n                return false;\n            }\n        }\n\n        PRINT_VERBOSE(1, \"Starting backup...\\n\");\n\n        /* make sure backup device sub-directory exists */\n        std::string devBackupDir = combinePath(outputPath, m_udid);\n        fs::makeDirectory(devBackupDir, 0755);\n        \n        /* TODO: check domain com.apple.mobile.backup key RequiresEncrypt and WillEncrypt with lockdown */\n        /* TODO: verify battery on AC enough battery remaining */\n\n        /* re-create Info.plist (Device infos, IC-Info.sidb, photos, app_ids, iTunesPrefs) */\n        if (infoPlist)\n        {\n            plist_free(infoPlist);\n            infoPlist = NULL;\n        }\n        infoPlist = newInfoPlist(m_udid.c_str(), m_device, m_afc);\n        if (!infoPlist)\n        {\n            printlog(\"Failed to generate Info.plist - aborting\\n\");\n            // skipBackup = true;\n            return false;\n        }\n        \n        fs::deleteFile(info_path);\n        writePlistFile(infoPlist, info_path.c_str(), PLIST_FORMAT_XML);\n        \n        plist_free(infoPlist);\n        infoPlist = NULL;\n\n        plist_t opts = NULL;\n        if (pThis->forceFullBackup())\n        {\n            /// PRINT_VERBOSE(1, \"Enforcing full backup from device.\\n\");\n            opts = plist_new_dict();\n            plist_dict_set_item(opts, \"ForceFullBackup\", plist_new_bool(1));\n        }\n        /* request backup from device with manifest from last backup */\n        /*\n        if (willEncrypt) {\n            /// PRINT_VERBOSE(1, \"Backup will be encrypted.\\n\");\n        } else {\n            /// PRINT_VERBOSE(1, \"Backup will be unencrypted.\\n\");\n        }\n         */\n        PRINT_VERBOSE(1, \"Requesting backup from device...\\n\");\n        err = mobilebackup2_send_request(m_mobilebackup2, \"Backup\", m_udid.c_str(), sourceUdid, opts);\n        if (opts)\n            plist_free(opts);\n        if (err == MOBILEBACKUP2_E_SUCCESS)\n        {\n            if (isFullBackup)\n            {\n                PRINT_VERBOSE(1, \"Full backup mode.\\n\");\n            }\n            else\n            {\n                PRINT_VERBOSE(1, \"Incremental backup mode.\\n\");\n            }\n        }\n        else\n        {\n            if (err == MOBILEBACKUP2_E_BAD_VERSION) {\n                printlog(\"ERROR: Could not start backup process: backup protocol version mismatch!\\n\");\n            } else if (err == MOBILEBACKUP2_E_REPLY_NOT_OK) {\n                printlog(\"ERROR: Could not start backup process: device refused to start the backup process.\\n\");\n            } else {\n                printlog(\"ERROR: Could not start backup process: unspecified error occurred\\n\");\n            }\n            // skipBackup = true;\n            return false;\n        }\n        \n        /* reset operation success status */\n        \n        plist_t message = NULL;\n        char *dlmsg = NULL;\n        \n        mobilebackup2_error_t mberr = MOBILEBACKUP2_E_SUCCESS;\n        int operation_ok = 0;\n        int file_count = 0;\n        int progress_finished = 0;\n\n        /* process series of DLMessage* operations */\n        do {\n            mberr = mobilebackup2_receive_message(m_mobilebackup2, &message, &dlmsg);\n            if (mberr == MOBILEBACKUP2_E_RECEIVE_TIMEOUT) {\n                PRINT_VERBOSE(2, \"Device is not ready yet, retrying...\\n\");\n                // goto files_out;\n            } else if (mberr != MOBILEBACKUP2_E_SUCCESS) {\n                PRINT_VERBOSE(0, \"ERROR: Could not receive from mobilebackup2 (%d)\\n\", mberr);\n                pThis->cancel();\n                // goto files_out;\n            }\n            else\n            {\n#ifndef NDEBUG\n                static unsigned int fileIndex = 0;\n                fileIndex++;\n                char fileName[32] = { 0 };\n                sprintf(fileName, \"message_%04u.plist\", fileIndex);\n#ifdef _WIN32\n                std::string filePath = combinePath(outputPath, \"dbg\", \"new\", fileName);\n#else\n                std::string filePath = combinePath(outputPath, \"dbg\", \"org\", fileName);\n#endif\n                writePlistFile(message, filePath, PLIST_FORMAT_XML);\n#endif\n                \n                if (!strcmp(dlmsg, \"DLMessageDownloadFiles\")) {\n                    /* device wants to download files from the computer */\n                    setOverallProgressFromMessage(pThis, message, dlmsg);\n                    handleSendFiles(message, outputPath.c_str());\n                } else if (!strcmp(dlmsg, \"DLMessageUploadFiles\")) {\n                    /* device wants to send files to the computer */\n                    setOverallProgressFromMessage(pThis, message, dlmsg);\n                    file_count += handleReceiveFiles(pThis, message, outputPath.c_str());\n                } else if (!strcmp(dlmsg, \"DLMessageGetFreeDiskSpace\")) {\n                    /* device wants to know how much disk space is available on the computer */\n                    uint64_t freespace = 0;\n                    int res = calcFreeSpace(outputPath, freespace);\n                    plist_t freespace_item = plist_new_uint(freespace);\n                    mobilebackup2_send_status_response(m_mobilebackup2, res, NULL, freespace_item);\n                    plist_free(freespace_item);\n                } else if (!strcmp(dlmsg, \"DLMessagePurgeDiskSpace\")) {\n                    /* device wants to purge disk space on the host - not supported */\n                    plist_t empty_dict = plist_new_dict();\n                    err = mobilebackup2_send_status_response(m_mobilebackup2, -1, \"Operation not supported\", empty_dict);\n                    plist_free(empty_dict);\n                } else if (!strcmp(dlmsg, \"DLContentsOfDirectory\")) {\n                    /* list directory contents */\n                    handleListDirectory(message, outputPath.c_str());\n                } else if (!strcmp(dlmsg, \"DLMessageCreateDirectory\")) {\n                    /* make a directory */\n                    handleMakeDirectory(message, outputPath.c_str());\n                } else if (!strcmp(dlmsg, \"DLMessageMoveFiles\") || !strcmp(dlmsg, \"DLMessageMoveItems\")) {\n                    /* perform a series of rename operations */\n                    handleMoveFiles(pThis, message, dlmsg, outputPath);\n                } else if (!strcmp(dlmsg, \"DLMessageRemoveFiles\") || !strcmp(dlmsg, \"DLMessageRemoveItems\")) {\n                    handleRemoveFiles(pThis, message, dlmsg, outputPath);\n                } else if (!strcmp(dlmsg, \"DLMessageCopyItem\")) {\n                    handleCopyItem(pThis, message, dlmsg, outputPath);\n                } else if (!strcmp(dlmsg, \"DLMessageDisconnect\")) {\n                    break;\n                } else if (!strcmp(dlmsg, \"DLMessageProcessMessage\")) {\n                    handleProcessMessage(message, operation_ok, result_code, outputPath);\n                    break;\n                }\n\n                /* print status */\n                \n                if ((pThis->getOverallProgress() > 0) && !progress_finished) {\n                    if (pThis->getOverallProgress() >= 100.0f) {\n                        progress_finished = 1;\n                    }\n                    // print_progress_real(pThis->getOverallProgress(), 0);\n                    PRINT_VERBOSE(1, \" Finished\\n\");\n                }\n            }\n\n            plist_free(message);\n            message = NULL;\n            plist_mem_free(dlmsg);\n            dlmsg = NULL;\n\n            if (pThis->isCanclled())\n            {\n                break;\n            }\n        } while (1);\n        \n        plist_free(message);\n        plist_mem_free(dlmsg);\n\n        /* report operation status to user */\n        bool res = false;\n        PRINT_VERBOSE(1, \"Received %d files from device.\\n\", file_count);\n        if (operation_ok && checkSnapshotState(outputPath, m_udid, \"finished\")) {\n            PRINT_VERBOSE(1, \"Backup Successful.\\n\");\n            res = true;\n        } else {\n            if (pThis->isCanclled()) {\n                PRINT_VERBOSE(1, \"Backup Aborted.\\n\");\n            } else {\n                PRINT_VERBOSE(1, \"Backup Failed (Error Code %d).\\n\", -result_code);\n            }\n        }\n\n        return res;\n    }\n    \n    inline void freeService(lockdownd_service_descriptor_t& service)\n    {\n        if (service != NULL)\n        {\n            lockdownd_service_descriptor_free(service);\n            service = NULL;\n        }\n    }\n    \n    void unlockAfc()\n    {\n        if (m_lockfile)\n        {\n            afc_file_lock(m_afc, m_lockfile, AFC_LOCK_UN);\n            afc_file_close(m_afc, m_lockfile);\n            m_lockfile = 0;\n            \n            doPostNotification(m_device, NP_SYNC_DID_FINISH);\n        }\n    }\n\n    idevice_t getDevice() const\n    {\n        return m_device;\n    }\n    \n    lockdownd_client_t getClient() const\n    {\n        return m_client;\n    }\n    \n    \nprivate:\n    idevice_t m_device;\n    afc_client_t m_afc;\n    uint64_t m_lockfile;\n    np_client_t m_np;\n    lockdownd_client_t m_client;\n    mobilebackup2_client_t m_mobilebackup2;\n    std::string m_udid;\n    std::map<std::string, size_t> m_files;\n};\n\nIDeviceBackup::IDeviceBackup(const DeviceInfo& deviceInfo, const std::string& outputPath) : m_deviceInfo(deviceInfo), m_outputPath(outputPath), m_quitFlag(0), m_overallProgress(0)\n{\n}\n\nbool IDeviceBackup::queryDevices(std::vector<DeviceInfo> &devices)\n{\n    idevice_info_t *devList = NULL;\n    int numberOfDevices = 0;\n    \n    if (idevice_get_device_list_extended(&devList, &numberOfDevices) < 0)\n    {\n        printlog(\"ERROR: Unable to retrieve device list!\\n\");\n        return false;\n    }\n    devices.clear();\n    for (int idx = 0; devList[idx] != NULL; idx++)\n    {\n        std::vector<DeviceInfo>::iterator it = devices.emplace(devices.end());\n        it->setUdid(devList[idx]->udid);\n        it->setUsb(devList[idx]->conn_type == CONNECTION_USBMUXD);\n        \n        std::string value;\n        if (queryDeviceName(it->getUdid(), value))\n        {\n            it->setName(value);\n        }\n        \n        IDeviceBackupClient dbc(it->getUdid());\n        \n        lockdownd_error_t err = dbc.initWithHandShake();\n        dbc.updateDeviceInfo(*it, err);\n        if (LOCKDOWN_E_SUCCESS == err)\n        {\n            if (dbc.queryAppContainer(BUNDLEID_WECHAT, value))\n            {\n                it->setWechatPath(value);\n            }\n        }\n    }\n    idevice_device_list_extended_free(devList);\n    \n    return true;\n}\n\nbool IDeviceBackup::queryDeviceName(const std::string& udid, std::string& name)\n{\n    if (udid.empty())\n    {\n        return false;\n    }\n\n    IDeviceBackupClient dbc(udid);\n    if (!dbc.init())\n    {\n        return false;\n    }\n    \n    return dbc.queryDeviceName(name);\n}\n\nbool IDeviceBackup::queryWechatPath(const std::string& udid, std::string& wechatPath)\n{\n    if (udid.empty())\n    {\n        return false;\n    }\n    \n    IDeviceBackupClient dbc(udid);\n    if (dbc.initWithHandShake() != LOCKDOWN_E_SUCCESS)\n    {\n        return false;\n    }\n    \n    bool res = dbc.queryAppContainer(BUNDLEID_WECHAT, wechatPath);\n    return res;\n}\n\nbool IDeviceBackup::forceFullBackup() const\n{\n    return false;\n}\n\nvoid IDeviceBackup::cancel()\n{\n    ++m_quitFlag;\n}\n\nbool IDeviceBackup::isCanclled() const\n{\n    return m_quitFlag > 0u;\n}\n\nint IDeviceBackup::getErrCode() const\n{\n    return m_errCode;\n}\n\nconst std::string IDeviceBackup::getErrMsg() const\n{\n    return m_errMsg;\n}\n\nvoid IDeviceBackup::setError(int errCode, const std::string& errMsg)\n{\n    m_errCode = errCode;\n    m_errMsg = errMsg;\n}\n\nbool IDeviceBackup::filter(const std::string& srcPath, const std::string& destPath) const\n{\n    return ((srcPath.find(m_deviceInfo.getWechatUuid()) == std::string::npos) && (srcPath.find(\"/Containers/Shared/AppGroup/\") == std::string::npos));\n}\nbool IDeviceBackup::backup()\n{\n    // Verify if backup directory exists\n    if (!existsDirectory(m_outputPath))\n    {\n        printlog(\"ERROR: Backup directory does not exist: %s\\n\", m_outputPath.c_str());\n        return false;\n    }\n    \n    IDeviceBackupClient dbc(m_deviceInfo.getUdid());\n    \n    dbc.loadFiles(m_outputPath, m_deviceInfo.getUdid());\n    \n    if (dbc.initWithHandShake() != LOCKDOWN_E_SUCCESS)\n    {\n        return false;\n    }\n\n    return dbc.backup(this, m_outputPath);\n}\n\ndouble IDeviceBackup::getOverallProgress() const\n{\n    return (double)m_overallProgress;\n}\n\nvoid IDeviceBackup::setOverallProgress(double progress)\n{\n    if (progress > 0.0)\n        m_overallProgress = progress;\n}\n"
  },
  {
    "path": "WechatExporter/core/IDeviceBackup.h",
    "content": "//\n//  IDevice.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/12/7.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef IDevice_h\n#define IDevice_h\n\n#include <string>\n#include <vector>\n#include <atomic>\n\nclass DeviceInfo\n{\nprivate:\n    std::string m_udid;\n    std::string m_name;\n    bool m_usb;\n\tbool m_locked;\n    bool m_trustPending;\n\tbool m_trusted;\n    std::string m_wechatPath;\n    std::string m_wechatUuid;\n\npublic:\n    DeviceInfo()\n    {\n    }\n    \n    ~DeviceInfo()\n    {\n        \n    }\n    \n    std::string getUdid() const\n    {\n        return m_udid;\n    }\n    \n    void setUdid(const std::string& udid)\n    {\n        m_udid = udid;\n    }\n    \n    std::string getName() const\n    {\n        return m_name;\n    }\n    \n    void setName(const std::string& name)\n    {\n        m_name = name;\n    }\n    \n    bool isUsb() const\n    {\n        return m_usb;\n    }\n    \n    void setUsb(bool usb)\n    {\n        m_usb = usb;\n    }\n    \n    void setLocked(bool locked)\n    {\n        m_locked = locked;\n    }\n\n\tbool needUnlock() const\n\t{\n\t\treturn m_locked;\n\t}\n    \n    \n    void setTrustPending(bool trustPending)\n    {\n        m_trustPending = trustPending;\n    }\n    \n    bool isTrustPending() const\n    {\n        return m_trustPending;\n    }\n    \n    void setTrusted(bool trusted)\n    {\n        m_trusted = trusted;\n    }\n\n\tbool needTrust() const\n\t{\n\t\treturn !m_trusted;\n\t}\n    \n    std::string getWechatPath() const\n    {\n        return m_wechatPath;\n    }\n    \n    void setWechatPath(const std::string& wechatPath)\n    {\n        m_wechatPath = wechatPath;\n        parseWechatUuid();\n    }\n    \n    std::string getWechatUuid() const\n    {\n        return m_wechatUuid;\n    }\n    \nprivate:\n    \n    void parseWechatUuid()\n    {\n        m_wechatUuid.clear();\n        \n        // /private/var/mobile/Containers/Data/Application/5A875B93-B816-459E-B6BA-C4A3D4162B6F\n        const std::string startTag = \"/Containers/Data/Application/\";\n        std::string::size_type pos = m_wechatPath.find(startTag);\n        if (pos != std::string::npos)\n        {\n            std::string::size_type pos2 = m_wechatPath.find(\"/\", pos + startTag.size());\n            if (pos2 != std::string::npos)\n            {\n                m_wechatUuid = m_wechatPath.substr(pos + startTag.size(), pos2 - (pos + startTag.size()));\n            }\n            else\n            {\n                m_wechatUuid = m_wechatPath.substr(pos + startTag.size());\n            }\n        }\n    }\n    \n};\n\n#define BUNDLEID_WECHAT \"com.tencent.xin\"\n#define CLIENT_ID \"wxexp\"\n\n#define LOCK_ATTEMPTS 50\n#define LOCK_WAIT 200000\n\n// IDeviceBackupClient will refer libmobiledevice\nclass IDeviceBackupClient;\n\nclass IDeviceBackup\n{\npublic:\n\n    static bool queryDevices(std::vector<DeviceInfo>& devices);\n    \n    static bool queryDeviceName(const std::string& udid, std::string& name);\n    static bool queryWechatPath(const std::string& udid, std::string& wechatPath);\n    \npublic:\n    IDeviceBackup(const DeviceInfo& deviceInfo, const std::string& outputPath);\n    bool backup();\n    // bool backup1();\n    \n    \n    bool forceFullBackup() const;\n    void cancel();\n    bool isCanclled() const;\n    \n    bool filter(const std::string& srcPath, const std::string& destPath) const;\n    \n    double getOverallProgress() const;\n    void setOverallProgress(double progress);\n    \n    int getErrCode() const;\n    const std::string getErrMsg() const;\n    void setError(int errCode, const std::string& errMsg);\nprivate:\n    DeviceInfo  m_deviceInfo;\n    std::string m_outputPath;\n    \n    unsigned int m_quitFlag;\n    \n    int m_errCode;\n    std::string m_errMsg;\n    \n    std::atomic<double> m_overallProgress;\n};\n\n#endif /* IDevice_h */\n"
  },
  {
    "path": "WechatExporter/core/ITunesParser.cpp",
    "content": "//\n//  ITunesParser.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/29.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include \"ITunesParser.h\"\n#include <stdio.h>\n#include <map>\n#include <queue>\n#include <set>\n#include <sys/types.h>\n#include <sqlite3.h>\n#include <algorithm>\n#include <plist/plist.h>\n#include <libxml/tree.h>\n#include <libxml/parser.h>\n\n#ifndef NDEBUG\n#include <assert.h>\n#endif\n#include <sys/stat.h>\n#if defined(_WIN32)\n// #define S_IFMT          0170000         /* [XSI] type of file mask */\n// #define S_IFDIR         0040000         /* [XSI] directory */\n\n#include <shlwapi.h>\n#include <atlstr.h>\n\n#else\n#include <unistd.h>\n#include <dirent.h>\n\n#endif\n\n#include \"MbdbReader.h\"\n#include \"Utils.h\"\n#include \"FileSystem.h\"\n\ninline std::string getPlistStringValue(plist_t node)\n{\n    std::string value;\n    \n    if (NULL != node)\n    {\n        uint64_t length = 0;\n        const char* ptr = plist_get_string_ptr(node, &length);\n        if (length > 0)\n        {\n            value.assign(ptr, length);\n        }\n    }\n    \n    return value;\n}\n\ninline std::string getPlistStringValue(plist_t node, const char* key)\n{\n    std::string value;\n    \n    if (NULL != node)\n    {\n        plist_t subNode = plist_dict_get_item(node, key);\n        if (NULL != subNode)\n        {\n            uint64_t length = 0;\n            const char* ptr = plist_get_string_ptr(subNode, &length);\n            if (length > 0)\n            {\n                value.assign(ptr, length);\n            }\n        }\n    }\n    \n    return value;\n}\n\nstruct __string_less\n{\n    // _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX11\n    bool operator()(const std::string& __x, const std::string& __y) const {return __x < __y;}\n    bool operator()(const std::pair<std::string, std::string>& __x, const std::string& __y) const {return __x.first < __y;}\n    bool operator()(const ITunesFile* __x, const std::string& __y) const {return __x->relativePath < __y;}\n    bool operator()(const ITunesFile* __x, const ITunesFile* __y) const {return __x->relativePath < __y->relativePath;}\n};\n\nclass SqliteITunesFileEnumerator : public ITunesDb::ITunesFileEnumerator\n{\npublic:\n    SqliteITunesFileEnumerator(const std::string& dbPath, const std::vector<std::string>& domains, bool onlyFile) : m_db(NULL), m_stmt(NULL), m_onlyFile(onlyFile)\n    {\n#ifndef NDEBUG\n        if (!existsFile(dbPath))\n        {\n            return;\n        }\n#endif\n        int rc = openSqlite3Database(dbPath, &m_db);\n        if (rc != SQLITE_OK)\n        {\n            // printf(\"Open database failed!\");\n            closeDb();\n            return;\n        }\n\n        sqlite3_exec(m_db, \"PRAGMA mmap_size=268435456;\", NULL, NULL, NULL); // 256M:268435456  2M 2097152\n        sqlite3_exec(m_db, \"PRAGMA synchronous=OFF;\", NULL, NULL, NULL);\n        \n        std::string sql = \"SELECT fileID,domain,relativePath,flags,file FROM Files\";\n        if (domains.size() > 0)\n        {\n            sql += \" WHERE \";\n            // domain=?\";\n            std::vector<std::string> conditions(domains.size(), \"domain=?\");\n                \n            sql += join(conditions, \" OR \");\n        }\n\n        rc = sqlite3_prepare_v2(m_db, sql.c_str(), (int)(sql.size()), &m_stmt, NULL);\n        if (rc != SQLITE_OK)\n        {\n            std::string error = sqlite3_errmsg(m_db);\n            closeDb();\n            return;\n        }\n        \n        int idx = 1;\n        for (std::vector<std::string>::const_iterator it = domains.cbegin(); it != domains.cend(); ++it, ++idx)\n        {\n            rc = sqlite3_bind_text(m_stmt, idx, (*it).c_str(), (int)((*it).size()), NULL);\n            if (rc != SQLITE_OK)\n            {\n                finalizeStmt();\n                closeDb();\n                return;\n            }\n        }\n    }\n    \n    virtual bool isInvalid() const\n    {\n        return NULL == m_db || NULL == m_stmt;\n    }\n    \n    virtual bool nextFile(ITunesFile& file)\n    {\n        while (sqlite3_step(m_stmt) == SQLITE_ROW)\n        {\n            int flags = sqlite3_column_int(m_stmt, 3);\n            if (m_onlyFile && flags != 1)\n            {\n                // Putting flags=1 into sql causes sqlite3 to use index of flags instead of domain and don't know why...\n                // So filter the directory with the code\n                continue;\n            }\n            \n            const char *relativePath = reinterpret_cast<const char*>(sqlite3_column_text(m_stmt, 2));\n            if (NULL != relativePath)\n            {\n                file.relativePath = relativePath;\n            }\n            else\n            {\n                file.relativePath.clear();\n            }\n            const char *domain = reinterpret_cast<const char*>(sqlite3_column_text(m_stmt, 1));\n            if (NULL != domain)\n            {\n                file.domain = domain;\n            }\n            else\n            {\n                file.domain.clear();\n            }\n            const char *fileId = reinterpret_cast<const char*>(sqlite3_column_text(m_stmt, 0));\n            if (NULL != fileId)\n            {\n                file.fileId = fileId;\n            }\n            else\n            {\n                file.fileId.clear();\n            }\n\n            file.flags = static_cast<unsigned int>(flags);\n            // Files\n            const unsigned char *blob = reinterpret_cast<const unsigned char*>(sqlite3_column_blob(m_stmt, 4));\n            int blobBytes = sqlite3_column_bytes(m_stmt, 4);\n            file.blob.clear();\n            file.size = 0;\n            file.modifiedTime = 0;\n            if (blobBytes > 0 && NULL != blob)\n            {\n                std::vector<unsigned char> blobVector(blob, blob + blobBytes);\n                file.blob.insert(file.blob.end(), blob, blob + blobBytes);\n            }\n            file.blobParsed = false;\n\n\t\t\treturn true;\n            // break;\n        }\n\n        return false;\n    }\n    \n    virtual ~SqliteITunesFileEnumerator()\n    {\n        finalizeStmt();\n        closeDb();\n    }\n    \nprivate:\n    void closeDb()\n    {\n        if (NULL != m_db)\n        {\n            sqlite3_close(m_db);\n            m_db = NULL;\n        }\n    }\n    \n    void finalizeStmt()\n    {\n        if (NULL != m_stmt)\n        {\n            sqlite3_finalize(m_stmt);\n            m_stmt = NULL;\n        }\n    }\nprivate:\n    sqlite3*        m_db;\n    sqlite3_stmt*   m_stmt;\n    \n    bool m_onlyFile;\n};\n\nclass MbdbITunesFileEnumerator : public ITunesDb::ITunesFileEnumerator\n{\npublic:\n    MbdbITunesFileEnumerator(const std::string& dbPath, const std::vector<std::string>& domains, bool onlyFile) : m_valid(false), m_domains(domains), m_onlyFile(onlyFile)\n    {\n        std::memset(m_fixedData, 0, 40);\n        if (!m_reader.open(dbPath))\n        {\n            return;\n        }\n        \n        m_valid = true;\n    }\n    \n    virtual bool isInvalid() const\n    {\n        return !m_valid;\n    }\n    \n    virtual bool nextFile(ITunesFile& file)\n    {\n        std::string domainInFile;\n        std::string path;\n        std::string linkTarget;\n        std::string dataHash;\n        std::string alwaysNull;\n        unsigned short fileMode = 0;\n        bool isDir = false;\n        bool skipped = false;\n        \n        // bool hasFilter = (bool)m_loadingFilter;\n\n        while (m_reader.hasMoreData())\n        {\n            if (!m_reader.read(domainInFile))\n            {\n                break;\n            }\n            \n            skipped = false;\n            if (!existsDomain(domainInFile))\n            {\n                skipped = true;\n            }\n            \n            if (skipped)\n            {\n                // will skip it\n                m_reader.skipString();    // path\n                m_reader.skipString();    // linkTarget\n                m_reader.skipString();    // dataHash\n                m_reader.skipString();    // alwaysNull;\n                \n                m_reader.read(m_fixedData, 40);\n                int propertyCount = m_fixedData[39];\n\n                for (int j = 0; j < propertyCount; ++j)\n                {\n                    m_reader.skipString();\n                    m_reader.skipString();\n                }\n            }\n            else\n            {\n                m_reader.read(path);\n                m_reader.read(linkTarget);\n                m_reader.readD(dataHash);\n                m_reader.readD(alwaysNull);\n                \n                m_reader.read(m_fixedData, 40);\n\n                fileMode = (m_fixedData[0] << 8) | m_fixedData[1];\n                \n                isDir = S_ISDIR(fileMode);\n                \n                // unsigned char flags = fixedData[38];\n                \n                if (m_onlyFile && isDir)\n                {\n                    skipped = true;\n                }\n                \n                unsigned int aTime = bigEndianToNative(*((uint32_t *)(m_fixedData + 18)));\n                unsigned int bTime = bigEndianToNative(*((uint32_t *)(m_fixedData + 22)));\n                // unsigned int cTime = bigEndianToNative(*((uint32_t *)(m_fixedData + 26)));\n                \n                file.size = bigEndianToNative(*((int64_t *)(m_fixedData + 30)));\n                \n                int propertyCount = m_fixedData[39];\n                \n                for (int j = 0; j < propertyCount; ++j)\n                {\n                    if (skipped)\n                    {\n                        m_reader.skipString(); // name\n                        m_reader.skipString(); // value\n                    }\n                    else\n                    {\n                        std::string name;\n                        std::string value;\n                        \n                        m_reader.read(name);\n                        m_reader.read(value);\n                    }\n                }\n                \n                if (!skipped)\n                {\n                    file.relativePath = path;\n                    file.domain = domainInFile;\n                    file.fileId = sha1(domainInFile + \"-\" + path);\n                    file.flags = isDir ? 2 : 1;\n                    file.modifiedTime = aTime != 0 ? aTime : bTime;\n                    file.blobParsed = true;\n                    // file.size =\n                    \n                    return true;\n                }\n                \n            }\n        }\n        \n        return false;\n    }\n    \n    virtual ~MbdbITunesFileEnumerator()\n    {\n    }\n    \nprivate:\n    bool existsDomain(const std::string& domain) const\n    {\n        if (m_domains.empty())\n        {\n            return true;\n        }\n        \n        for (std::vector<std::string>::const_iterator it = m_domains.cbegin(); it != m_domains.cend(); ++it)\n        {\n            if (domain == *it)\n            {\n                return true;\n            }\n        }\n        return false;\n    }\n\nprivate:\n    MbdbReader                  m_reader;\n    bool                        m_valid;\n    std::vector<std::string>    m_domains;\n    bool                        m_onlyFile;\n    unsigned char               m_fixedData[40];\n};\n\nITunesDb::ITunesDb(const std::string& rootPath, const std::string& manifestFileName) : m_isMbdb(false), m_rootPath(rootPath), m_manifestFileName(manifestFileName)\n{\n    std::replace(m_rootPath.begin(), m_rootPath.end(), ALT_DIR_SEP, DIR_SEP);\n    \n    if (!endsWith(m_rootPath, DIR_SEP))\n    {\n        m_rootPath += DIR_SEP;\n    }\n    \n    m_version.clear();\n    BackupItem manifest;\n    if (ManifestParser::parseInfoPlist(m_rootPath, manifest, false))\n    {\n        m_version = manifest.getITunesVersion();\n        m_iOSVersion = manifest.getIOSVersion();\n    }\n    \n    std::string dbPath = combinePath(m_rootPath, \"Manifest.mbdb\");\n    if (existsFile(dbPath))\n    {\n        m_isMbdb = true;\n        \n    }\n}\n\nITunesDb::~ITunesDb()\n{\n    for (std::vector<ITunesFile *>::iterator it = m_files.begin(); it != m_files.end(); ++it)\n    {\n        delete *it;\n    }\n    m_files.clear();\n}\n\nbool ITunesDb::load()\n{\n    return load(\"\", false);\n}\n\nbool ITunesDb::load(const std::string& domain)\n{\n    return load(domain, false);\n}\n\nbool ITunesDb::load(const std::string& domain, bool onlyFile)\n{\n    std::vector<std::string> domains;\n    if (!domain.empty())\n    {\n        domains.push_back(domain);\n    }\n    \n#if !defined(NDEBUG) || defined(DBG_PERF)\n    printf(\"PERF: start.....%s\\r\\n\", getTimestampString(false, true).c_str());\n#endif\n    \n    std::unique_ptr<ITunesFileEnumerator> enumerator(buildEnumerator(domains, onlyFile));\n    if (enumerator->isInvalid())\n    {\n        return false;\n    }\n\n    bool hasFilter = (bool)m_loadingFilter;\n    \n    ITunesFile file;\n    m_files.reserve(2048);\n    while (enumerator->nextFile(file))\n    {\n        if (hasFilter && !m_loadingFilter(file.relativePath.c_str(), file.flags))\n        {\n            continue;\n        }\n        m_files.push_back(new ITunesFile(file));\n    }\n\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    printf(\"PERF: end.....%s, size=%lu\\r\\n\", getTimestampString(false, true).c_str(), m_files.size());\n#endif\n    \n    std::sort(m_files.begin(), m_files.end(), __string_less());\n\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    printf(\"PERF: after sort.....%s\\r\\n\", getTimestampString(false, true).c_str());\n#endif\n    return true;\n}\n\nITunesDb::ITunesFileEnumerator* ITunesDb::buildEnumerator(const std::vector<std::string>& domains, bool onlyFile) const\n{\n    std::string dbPath = combinePath(m_rootPath, m_isMbdb ? \"Manifest.mbdb\" : \"Manifest.db\");\n    ITunesFileEnumerator* enumerator = m_isMbdb ? (ITunesFileEnumerator*)(new MbdbITunesFileEnumerator(dbPath, domains, onlyFile)) : (ITunesFileEnumerator*)(new SqliteITunesFileEnumerator(dbPath, domains, onlyFile));\n    return enumerator;\n}\n\nITunesDb::ITunesFileEnumerator* ITunesDb::buildEnumerator(const std::string& dbPath, const std::vector<std::string>& domains, bool onlyFile) const\n{\n    ITunesFileEnumerator* enumerator = m_isMbdb ? (ITunesFileEnumerator*)(new MbdbITunesFileEnumerator(dbPath, domains, onlyFile)) : (ITunesFileEnumerator*)(new SqliteITunesFileEnumerator(dbPath, domains, onlyFile));\n    return enumerator;\n}\n\nbool ITunesDb::copy(const std::string& destPath, const std::string& backupId, std::vector<std::string>& domains, std::function<bool(const ITunesDb*, const ITunesFile*)>& func) const\n{\n    std::string destBackupPath = backupId.empty() ? combinePath(destPath, \"Backup\") : combinePath(destPath, \"Backup\", backupId);\n    if (!existsDirectory(destBackupPath))\n    {\n        makeDirectory(destBackupPath);\n    }\n    \n    // Copy control files\n    const char* files[] = {\"Info.plist\", (m_isMbdb ? \"Manifest.mbdb\" : \"Manifest.db\"), \"Manifest.plist\", \"Status.plist\"};\n    for (int idx = 0; idx < sizeof(files) / sizeof(const char *); ++idx)\n    {\n        ::copyFile(combinePath(m_rootPath, files[idx]), combinePath(destBackupPath, files[idx]));\n    }\n    \n    std::unique_ptr<ITunesFileEnumerator> enumerator;\n    if (m_isMbdb)\n    {\n        enumerator.reset(buildEnumerator(combinePath(destBackupPath, \"Manifest.mbdb\"), domains, false));\n    }\n    else\n    {\n        std::string dbPath = combinePath(destBackupPath, \"Manifest.db\");\n        \n        sqlite3 *db = NULL;\n        int rc = openSqlite3Database(dbPath, &db, false);\n        if (rc != SQLITE_OK)\n        {\n            // printf(\"Open database failed!\");\n            sqlite3_close(db);\n            return false;\n        }\n\n        sqlite3_exec(db, \"PRAGMA mmap_size=268435456;\", NULL, NULL, NULL); // 256M:268435456  2M 2097152\n        sqlite3_exec(db, \"PRAGMA synchronous=OFF;\", NULL, NULL, NULL);\n        \n        std::string sql = \"DELETE FROM Files\";\n        \n        if (domains.size() > 0)\n        {\n            sql += \" WHERE \";\n            // domain=?\";\n            std::vector<std::string> conditions(domains.size(), \"domain<>?\");\n                \n            sql += join(conditions, \" AND \");\n        }\n\n        sqlite3_stmt* stmt = NULL;\n        rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL);\n        if (rc != SQLITE_OK)\n        {\n            std::string error = sqlite3_errmsg(db);\n            sqlite3_close(db);\n            return false;\n        }\n\n        int idx = 1;\n        for (std::vector<std::string>::const_iterator it = domains.cbegin(); it != domains.cend(); ++it, ++idx)\n        {\n            rc = sqlite3_bind_text(stmt, idx, (*it).c_str(), (int)((*it).size()), NULL);\n            if (rc != SQLITE_OK)\n            {\n                sqlite3_finalize(stmt);\n                sqlite3_close(db);\n                return false;\n            }\n        }\n        \n    #if !defined(NDEBUG)\n    #ifdef __APPLE__\n        if (__builtin_available(macOS 10.12, *))\n        {\n    #endif\n            char *expandedSql = sqlite3_expanded_sql(stmt);\n            printf(\"PERF: %s sql=%s\\r\\n\", getTimestampString(false, true).c_str(), sqlite3_expanded_sql(stmt));\n    #ifdef __APPLE__\n        }\n    #endif\n    #endif\n        \n        if ((rc = sqlite3_step(stmt)) != SQLITE_DONE)\n        {\n    #ifndef NDEBUG\n            const char *errMsg = sqlite3_errmsg(db);\n            m_lastError = std::string(errMsg);\n    #endif\n            sqlite3_finalize(stmt);\n            sqlite3_close(db);\n            return false;\n        }\n        \n        sqlite3_finalize(stmt);\n        \n        sqlite3_exec(db, \"VACUUM;\", NULL, NULL, NULL);\n\t\tsqlite3_close(db);\n        \n        enumerator.reset(buildEnumerator(dbPath, std::vector<std::string>(), false));\n    }\n    \n    if (enumerator->isInvalid())\n    {\n        return false;\n    }\n    \n    std::string subPath;\n    std::string prefix;\n    std::string srcFilePath;\n    std::set<std::string> subFolders;\n    ITunesFile file;\n    while (enumerator->nextFile(file))\n    {\n        if (file.fileId.empty())\n        {\n            continue;\n        }\n        \n\t\tprefix = file.fileId.substr(0, 2);\n        \n        srcFilePath = combinePath(m_rootPath, prefix, file.fileId);\n        if (!existsFile(srcFilePath))\n        {\n#ifndef NDEBUG\n            // assert(!\"Source file not exists.\");\n#endif\n            continue;\n        }\n\n        subPath = combinePath(destBackupPath, prefix);\n        if (subFolders.find(prefix) == subFolders.cend())\n        {\n            if (!existsDirectory(subPath))\n            {\n                makeDirectory(subPath);\n            }\n            subFolders.insert(prefix);\n        }\n        bool ret = ::copyFile(srcFilePath, combinePath(subPath, file.fileId));\n#ifndef NDEBUG\n        if (!ret)\n        {\n            std::string msg = \"Failed to copy file\" + combinePath(subPath, file.fileId);\n            assert(!msg.c_str());\n        }\n#endif\n        \n        if (func)\n        {\n            if (!func(this, &file))\n            {\n                break;\n            }\n        }\n    }\n\n    return true;\n}\n\nunsigned int ITunesDb::parseModifiedTime(const std::vector<unsigned char>& data)\n{\n    if (data.empty())\n    {\n        return 0;\n    }\n    uint64_t val = 0;\n    plist_t node = NULL;\n    plist_from_memory(reinterpret_cast<const char *>(&data[0]), static_cast<uint32_t>(data.size()), &node);\n    if (NULL != node)\n    {\n        plist_t lastModified = plist_access_path(node, 3, \"$objects\", 1, \"LastModified\");\n        if (NULL != lastModified)\n        {\n            plist_get_uint_val(lastModified, &val);\n        }\n        \n        plist_free(node);\n    }\n\n    return static_cast<unsigned int>(val);\n}\n\nbool ITunesDb::parseFileInfo(const ITunesFile* file)\n{\n    if (NULL == file || file->blob.empty())\n    {\n        return false;\n    }\n    \n    if (file->blobParsed)\n    {\n        return true;\n    }\n    \n    file->blobParsed = true;\n    \n    uint64_t val = 0;\n    plist_t node = NULL;\n    plist_from_memory(reinterpret_cast<const char *>(&file->blob[0]), static_cast<uint32_t>(file->blob.size()), &node);\n    if (NULL != node)\n    {\n        plist_t lastModifiedNode = plist_access_path(node, 3, \"$objects\", 1, \"LastModified\");\n        if (NULL != lastModifiedNode)\n        {\n            plist_type pt = plist_get_node_type(lastModifiedNode);\n            plist_get_uint_val(lastModifiedNode, &val);\n            file->modifiedTime = (unsigned int)val;\n        }\n        \n        plist_t sizeNode = plist_access_path(node, 3, \"$objects\", 1, \"Size\");\n        if (NULL != sizeNode)\n        {\n            plist_type pt = plist_get_node_type(sizeNode);\n            val = 0;\n            plist_get_uint_val(sizeNode, &val);\n            file->size = val;\n        }\n        \n        plist_free(node);\n        return true;\n    }\n    \n    return false;\n}\n\nstd::string ITunesDb::findFileId(const std::string& relativePath) const\n{\n    const ITunesFile* file = findITunesFile(relativePath);\n    \n    if (NULL == file)\n    {\n        return std::string();\n    }\n    return file->fileId;\n}\n\nconst ITunesFile* ITunesDb::findITunesFile(const std::string& relativePath) const\n{\n    std::string formatedPath = relativePath;\n    std::replace(formatedPath.begin(), formatedPath.end(), '\\\\', '/');\n\n    typename std::vector<ITunesFile *>::iterator it = std::lower_bound(m_files.begin(), m_files.end(), formatedPath, __string_less());\n    \n    if (it == m_files.end() || (*it)->relativePath != formatedPath)\n    {\n        return NULL;\n    }\n    return *it;\n}\n\nstd::string ITunesDb::fileIdToRealPath(const std::string& fileId) const\n{\n    if (!fileId.empty())\n    {\n        return m_isMbdb ? combinePath(m_rootPath, fileId) : combinePath(m_rootPath, fileId.substr(0, 2), fileId);\n    }\n    \n    return std::string();\n}\n\nstd::string ITunesDb::getRealPath(const ITunesFile& file) const\n{\n    return fileIdToRealPath(file.fileId);\n}\n\nstd::string ITunesDb::getRealPath(const ITunesFile* file) const\n{\n    return fileIdToRealPath(file->fileId);\n}\n\nstd::string ITunesDb::findRealPath(const std::string& relativePath) const\n{\n    std::string fieldId = findFileId(relativePath);\n    return fileIdToRealPath(fieldId);\n}\n\nbool ITunesDb::copyFile(const std::string& vpath, const std::string& dest, bool overwrite/* = false*/) const\n{\n    std::string destPath = normalizePath(dest);\n    if (!overwrite && existsFile(destPath))\n    {\n        return true;\n    }\n    \n    const ITunesFile* file = findITunesFile(vpath);\n    if (NULL != file)\n    {\n        std::string srcPath = getRealPath(*file);\n        if (!srcPath.empty())\n        {\n            normalizePath(srcPath);\n            bool result = ::copyFile(srcPath, destPath, true);\n            if (result)\n            {\n                updateFileTime(dest, ITunesDb::parseModifiedTime(file->blob));\n            }\n            return result;\n        }\n    }\n    \n    return false;\n}\n\nbool ITunesDb::copyFile(const std::string& vpath, const std::string& destPath, const std::string& destFileName, bool overwrite/* = false*/) const\n{\n    std::string destFullPath = normalizePath(combinePath(destPath, destFileName));\n    if (!overwrite && existsFile(destFullPath))\n    {\n        return true;\n    }\n    \n    const ITunesFile* file = findITunesFile(vpath);\n    if (NULL != file)\n    {\n        std::string srcPath = getRealPath(*file);\n        if (!srcPath.empty())\n        {\n            normalizePath(srcPath);\n            if (!existsDirectory(destPath))\n            {\n                makeDirectory(destPath);\n            }\n            bool result = ::copyFile(srcPath, destFullPath, true);\n            if (result)\n            {\n                if (file->modifiedTime != 0)\n                {\n                    updateFileTime(destFullPath, static_cast<time_t>(file->modifiedTime));\n                }\n                else if (!file->blob.empty())\n                {\n                    updateFileTime(destFullPath, ITunesDb::parseModifiedTime(file->blob));\n                }\n            }\n            return result;\n        }\n    }\n    \n    return false;\n}\n\nDecodedWechatITunesDb::DecodedWechatITunesDb(const std::string& rootPath, const std::string& manifestFileName) : ITunesDb(rootPath, manifestFileName)\n{\n    \n}\n\nDecodedWechatITunesDb::~DecodedWechatITunesDb()\n{\n    \n}\n\nbool DecodedWechatITunesDb::load(const std::string& domain, bool onlyFile)\n{\n    return loadFiles(m_rootPath, onlyFile);\n}\n\nbool DecodedWechatITunesDb::loadFiles(const std::string& root, bool onlyFile)\n{\n#ifdef _WIN32\n\tstd::queue<CString> directories;\n\tdirectories.push(\"\");\n\t\n\tTCHAR szRoot[MAX_PATH] = { 0 };\n\t_tcscpy(szRoot, CW2T(CA2W(root.c_str(), CP_UTF8)));\n\tPathAddBackslash(szRoot);\n\t\n\tTCHAR szPath[MAX_PATH] = { 0 };\n\tTCHAR szRelativePath[MAX_PATH] = { 0 };\n\tULARGE_INTEGER ull;\n\n\twhile (!directories.empty())\n\t{\n\t\tconst CString& dirName = directories.front();\n\n\t\tPathCombine(szPath, szRoot, dirName);\n\t\tPathAddBackslash(szPath);\n\t\tPathAppend(szPath, TEXT(\"*.*\"));\n\n\t\tWIN32_FIND_DATA FindFileData;\n\t\tHANDLE hFind = INVALID_HANDLE_VALUE;\n\n\t\thFind = FindFirstFile((LPTSTR)szPath, &FindFileData);\n\t\tif (hFind == INVALID_HANDLE_VALUE)\n\t\t{\n\t\t\treturn false;\n\t\t}\n\t\tdo\n\t\t{\n\t\t\tif (_tcscmp(FindFileData.cFileName, TEXT(\".\")) == 0 || _tcscmp(FindFileData.cFileName, TEXT(\"..\")) == 0)\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbool isDir = ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0);\n\t\t\t\n\t\t\tPathCombine(szRelativePath, dirName, FindFileData.cFileName);\n\t\t\t\n\t\t\tif (!onlyFile || !isDir)\n\t\t\t{\n\t\t\t\tCString relativePath = szRelativePath;\n\t\t\t\trelativePath.Replace(DIR_SEP, ALT_DIR_SEP);\n\n\t\t\t\tCW2A pszU8(CT2W(szRelativePath), CP_UTF8);\n\n\t\t\t\tITunesFile *file = new ITunesFile();\n\t\t\t\tfile->relativePath = (LPCSTR)CW2A(CT2W(relativePath), CP_UTF8);;\n\t\t\t\tfile->fileId = (LPCSTR)pszU8;\n\t\t\t\tfile->flags = isDir ? 2 : 1;\n\n\t\t\t\tull.LowPart = FindFileData.ftLastWriteTime.dwLowDateTime;\n\t\t\t\tull.HighPart = FindFileData.ftLastWriteTime.dwHighDateTime;\n\n\t\t\t\tfile->modifiedTime = static_cast<unsigned int>(ull.QuadPart / 10000000ULL - 11644473600ULL);\n\t\t\t\t\n\t\t\t\tm_files.push_back(file);\n\t\t\t}\n\n\t\t\tif (isDir)\n\t\t\t{\n\t\t\t\tPathAddBackslash(szRelativePath);\n\t\t\t\tdirectories.emplace(szRelativePath);\n\t\t\t}\n\n\t\t} while (::FindNextFile(hFind, &FindFileData));\n\t\tFindClose(hFind);\n\n\t\tdirectories.pop();\n\t}\n    \n#else\n\tstd::queue<std::string> directories;\n\tdirectories.push(\"\");\n    struct stat statbuf;\n    \n    while (!directories.empty())\n    {\n        const std::string& dirName = directories.front();\n        std::string path = combinePath(root, dirName);\n        \n        struct dirent *entry = NULL;\n        DIR *dir = opendir(path.c_str());\n        if (dir == NULL)\n        {\n            return false;\n        }\n\n        while ((entry = readdir(dir)) != NULL)\n        {\n            if (strcmp(entry->d_name, \".\") == 0 || strcmp(entry->d_name, \"..\") == 0)\n            {\n                continue;\n            }\n            \n            if (filterFile(dirName, entry->d_name))\n            {\n                continue;\n            }\n            \n            bool isDir = false;\n            \n            std::string relativePath = dirName + entry->d_name;\n\n            lstat(combinePath(path, entry->d_name).c_str(), &statbuf);\n            isDir = S_ISDIR(statbuf.st_mode);\n            \n            if (!onlyFile || !isDir)\n            {\n\t\t\t\tstd::string fileId = relativePath;\n\n                ITunesFile *file = new ITunesFile();\n                file->relativePath = relativePath;\n                file->fileId = fileId;\n                file->flags = isDir ? 2 : 1;\n                file->modifiedTime = static_cast<unsigned int>(statbuf.st_mtimespec.tv_sec);\n                \n                m_files.push_back(file);\n            }\n            if (isDir)\n            {\n                directories.push(endsWith(relativePath, \"/\") ? relativePath : (relativePath + \"/\"));\n            }\n        }\n        closedir(dir);\n        directories.pop();\n    }\n    \n#endif\n    \n    std::sort(m_files.begin(), m_files.end(), __string_less());\n    \n    return true;\n}\n\nstd::string DecodedWechatITunesDb::fileIdToRealPath(const std::string& fileId) const\n{\n    if (!fileId.empty())\n    {\n        return combinePath(m_rootPath, fileId);\n    }\n    \n    return std::string();\n}\n\nbool DecodedWechatITunesDb::filterFile(const std::string& relativeDir, const std::string& fileName)\n{\n    return (relativeDir.empty() && fileName.compare(\"Shared\") == 0);\n}\n\nDecodedSharedWechatITunesDb::DecodedSharedWechatITunesDb(const std::string& rootPath, const std::string& manifestFileName) : DecodedWechatITunesDb(rootPath, manifestFileName)\n{\n    m_rootPath = combinePath(m_rootPath, \"Shared\", \"group.com.tencent.xin\");\n}\n\nDecodedSharedWechatITunesDb::~DecodedSharedWechatITunesDb()\n{\n}\n\nbool DecodedSharedWechatITunesDb::load(const std::string& domain, bool onlyFile)\n{\n    return loadFiles(m_rootPath, onlyFile);\n}\n\nbool DecodedSharedWechatITunesDb::filterFile(const std::string& relativeDir, const std::string& fileName)\n{\n    return false;\n}\n\nManifestParser::ManifestParser(const std::string& manifestPath, bool incudingApps) : m_manifestPath(manifestPath), m_incudingApps(incudingApps)\n{\n}\n\nstd::string ManifestParser::getLastError() const\n{\n\treturn m_lastError;\n}\n\nbool ManifestParser::parse(std::vector<BackupItem>& manifests) const\n{\n    bool res = false;\n    \n    std::string path = normalizePath(m_manifestPath);\n    if (endsWith(path, normalizePath(\"/MobileSync\")) || endsWith(path, normalizePath(\"/MobileSync/\")) || isValidMobileSync(path))\n    {\n        path = combinePath(path, \"Backup\");\n        res = parseDirectory(path, manifests);\n    }\n    else if (isValidBackupItem(path))\n    {\n        BackupItem manifest;\n        if (parse(path, manifest) && manifest.isValid())\n        {\n            manifests.push_back(manifest);\n            res = true;\n        }\n    }\n    else\n    {\n        // Assume the directory is ../../Backup/../\n        res = parseDirectory(path, manifests);\n    }\n    \n    return res;\n}\n\nbool ManifestParser::parseDirectory(const std::string& path, std::vector<BackupItem>& manifests) const\n{\n    std::vector<std::string> subDirectories;\n    if (!listSubDirectories(path, subDirectories))\n    {\n#ifndef NDEBUG\n#endif\n\t\tm_lastError += \"Failed to list subfolder in:\" + path + \"\\r\\n\";\n        return false;\n    }\n    \n    bool res = false;\n    for (std::vector<std::string>::const_iterator it = subDirectories.cbegin(); it != subDirectories.cend(); ++it)\n    {\n        std::string backupPath = combinePath(path, *it);\n\t\tif (!isValidBackupItem(backupPath))\n\t\t{\n\t\t\tcontinue;\n\t\t}\n        \n        BackupItem manifest;\n        manifest.setBackupId(*it);\n        if (parse(backupPath, manifest) && manifest.isValid())\n        {\n            manifests.push_back(manifest);\n            res = true;\n        }\n    }\n\n\tif (!res)\n\t{\n\t\tm_lastError += \"No valid backup id found in:\" + path + \"\\r\\n\";\n\t}\n\n    return res;\n}\n\nbool ManifestParser::isValidBackupItem(const std::string& path) const\n{\n    std::string fileName = combinePath(path, \"Info.plist\");\n    if (!existsFile(fileName))\n    {\n        m_lastError += \"Info.plist not found\\r\\n\";\n        return false;\n    }\n\n    fileName = combinePath(path, \"Manifest.plist\");\n    if (!existsFile(fileName))\n    {\n        m_lastError += \"Manifest.plist not found\\r\\n\";\n        return false;\n    }\n\n    // < iOS 10: Manifest.mbdb\n    // >= iOS 10: Manifest.db\n    if (!existsFile(combinePath(path, \"Manifest.db\")) && !existsFile(combinePath(path, \"Manifest.mbdb\")))\n    {\n        m_lastError += \"Manifest.db/Manifest.mbdb not found\\r\\n\";\n        return false;\n    }\n    \n    return true;\n}\n\nbool ManifestParser::isValidMobileSync(const std::string& path) const\n{\n    std::string backupPath = combinePath(path, \"Backup\");\n    if (!existsDirectory(backupPath))\n    {\n        m_lastError += \"Backup folder not found\\r\\n\";\n        return false;\n    }\n\n    return true;\n}\n\nbool ManifestParser::parse(const std::string& path, BackupItem& manifest) const\n{\n    //Info.plist is a xml file\n    if (!parseInfoPlist(path, manifest, m_incudingApps))\n    {\n\t\tm_lastError += \"Failed to parse xml: Info.plist\\r\\n\";\n        return false;\n    }\n    \n    std::string fileName = combinePath(path, \"Manifest.plist\");\n    std::vector<unsigned char> data;\n    if (readFile(fileName, data))\n    {\n        plist_t node = NULL;\n        plist_from_memory(reinterpret_cast<const char *>(&data[0]), static_cast<uint32_t>(data.size()), &node);\n        if (NULL != node)\n        {\n            plist_t isEncryptedNode = plist_access_path(node, 1, \"IsEncrypted\");\n            if (NULL != isEncryptedNode)\n            {\n                uint8_t val = 0;\n                plist_get_bool_val(isEncryptedNode, &val);\n                manifest.setEncrypted(val != 0);\n            }\n            \n            if (manifest.getIOSVersion().empty())\n            {\n                plist_t iOSVersionNode = plist_access_path(node, 2, \"Lockdown\", \"ProductVersion\");\n                manifest.setIOSVersion(getPlistStringValue(iOSVersionNode));\n            }\n            \n            plist_free(node);\n        }\n    }\n\telse\n\t{\n\t\tm_lastError = \"Failed to read Manifest.plist\\r\\n\";\n\t\treturn false;\n\t}\n\n    return true;\n}\n\nbool ManifestParser::parseInfoPlist(const std::string& backupIdPath, BackupItem& manifest, bool includingApps)\n{\n    std::string fileName = combinePath(backupIdPath, \"Info.plist\");\n    std::string contents = readFile(fileName);\n    plist_t node = NULL;\n    plist_from_memory(contents.c_str(), static_cast<uint32_t>(contents.size()), &node);\n    if (NULL == node)\n    {\n        return false;\n    }\n    \n    manifest.setPath(backupIdPath);\n    \n    const char* ptr = NULL;\n    uint64_t length = 0;\n    std::string val;\n\n    const char* ValueLastBackupDate = \"Last Backup Date\";\n    const char* ValueDisplayName = \"Display Name\";\n    const char* ValueDeviceName = \"Device Name\";\n    const char* ValueITunesVersion = \"iTunes Version\";\n    const char* ValueMacOSVersion = \"macOS Version\";\n    const char* ValueProductVersion = \"Product Version\";\n    const char* ValueInstalledApps = \"Installed Applications\";\n    const char* ValueUniqueIdentifier = \"Unique Identifier\";\n    const char* ValueTargetIdentifier = \"Target Identifier\";\n    \n    plist_t subNode = NULL;\n    \n    manifest.setDeviceName(getPlistStringValue(node, ValueDeviceName));\n    manifest.setDisplayName(getPlistStringValue(node, ValueDisplayName));\n    \n    subNode = plist_dict_get_item(node, ValueLastBackupDate);\n    if (NULL != subNode)\n    {\n        int32_t sec = 0, usec = 0;\n        plist_get_date_val(subNode, &sec, &usec);\n        manifest.setBackupTime(fromUnixTime(sec + 978278400, false));\n    }\n    \n    manifest.setITunesVersion(getPlistStringValue(node, ValueITunesVersion));\n    manifest.setIOSVersion(getPlistStringValue(node, ValueProductVersion));\n    manifest.setMacOSVersion(getPlistStringValue(node, ValueMacOSVersion));\n    \n    std::string uniqueId = getPlistStringValue(node, ValueUniqueIdentifier);\n    std::string targetId = getPlistStringValue(node, ValueTargetIdentifier);\n    std::string uniqueIdUpper = toUpper(uniqueId);\n    if (toUpper(manifest.getBackupId()) != uniqueIdUpper)\n    {\n        if (toUpper(targetId) != uniqueIdUpper)\n        {\n            manifest.setBackupId(targetId);\n        }\n        else\n        {\n            manifest.setBackupId(toLower(uniqueId));\n        }\n    }\n    \n    if (includingApps)\n    {\n        subNode = plist_dict_get_item(node, ValueInstalledApps);\n        if (NULL != subNode && PLIST_IS_ARRAY(subNode))\n        {\n            uint32_t arraySize = plist_array_get_size(subNode);\n            plist_t itemNode = NULL;\n            for (uint32_t idx = 0; idx < arraySize; ++idx)\n            {\n                itemNode = plist_array_get_item(subNode, idx);\n                if (itemNode == NULL)\n                {\n                    continue;\n                }\n                std::string bundleId = getPlistStringValue(itemNode);\n                if (!bundleId.empty())\n                {\n                    plist_t appNode = plist_access_path(node, 2, \"Applications\", bundleId.c_str());\n                    \n                    if (NULL != appNode)\n                    {\n                        plist_t appSubNode = NULL;\n                        \n                        appSubNode = plist_dict_get_item(appNode, \"iTunesMetadata\");\n                        if (NULL != appSubNode)\n                        {\n                            plist_type ptype = plist_get_node_type(appSubNode);\n                            ptr = plist_get_data_ptr(appSubNode, &length);\n                            if (ptr != NULL && length > 0)\n                            {\n                                std::string metadata(ptr, length);\n                                BackupItem::AppInfo appInfo;\n                                appInfo.bundleId = bundleId;\n                                parseITunesMetadata(metadata, appInfo);\n                                manifest.addApp(appInfo);\n                            }\n                            \n                        }\n                        \n                    }\n                }\n                \n            }\n        \n        }\n    }\n    \n    plist_free(node);\n\n    return true;\n}\n\nbool ManifestParser::parseITunesMetadata(const std::string& metadata, BackupItem::AppInfo& appInfo)\n{\n    plist_t node = NULL;\n    plist_from_memory(metadata.c_str(), static_cast<uint32_t>(metadata.size()), &node);\n    if (NULL == node)\n    {\n        return false;\n    }\n    \n    /*\n    plist_dict_iter it = NULL;\n    char *key = NULL;\n    plist_t plistVal = NULL;\n    \n    plist_dict_new_iter(node, &it);\n\n    while (1)\n    {\n        plist_dict_next_item(node, it, &key, &plistVal);\n        \n        if (NULL == key)\n        {\n            break;\n        }\n        std::string keyString = key;\n        int aa = 0;\n    }\n    */\n\n    appInfo.name = getPlistStringValue(node, \"itemName\");\n    appInfo.bundleShortVersion = getPlistStringValue(node, \"bundleShortVersionString\");\n    appInfo.bundleVersion = getPlistStringValue(node, \"bundleVersion\");\n    \n    plist_free(node);\n    \n    return true;\n}\n\nDecodedManifestParser::DecodedManifestParser(const std::string& manifestPath, bool includingApps) : ManifestParser(manifestPath, includingApps)\n{\n}\n\nbool DecodedManifestParser::parse(std::vector<BackupItem>& manifests) const\n{\n    bool res = false;\n    \n    std::string path = normalizePath(m_manifestPath);\n    if (isValidBackupItem(path))\n    {\n        BackupItem manifest;\n        if (parse(path, manifest) && manifest.isValid())\n        {\n            manifests.push_back(manifest);\n            res = true;\n        }\n    }\n    \n    return res;\n}\n\nbool DecodedManifestParser::isValidBackupItem(const std::string& path) const\n{\n    std::string fileName = combinePath(path, \"Documents\", \"LoginInfo2.dat\");\n    if (!existsFile(fileName))\n    {\n        m_lastError += \"LoginInfo2.dat not found:\" + fileName + \"\\r\\n\";\n        return false;\n    }\n\n    return true;\n}\n\nbool DecodedManifestParser::parse(const std::string& path, BackupItem& manifest) const\n{\n    std::string fileName = combinePath(path, \"Documents\", \"LoginInfo2.dat\");\n    if (!existsFile(fileName))\n    {\n        m_lastError += \"LoginInfo2.dat not found\\r\\n\";\n        return false;\n    }\n    \n\tmanifest.setPath(path);\n    \n#ifdef _WIN32\n\tstruct _stat statbuf;\n\tCA2W wpath(path.c_str(), CP_UTF8);\n\t_wstat((LPCWSTR)wpath, &statbuf);\n\tstd::time_t ts = statbuf.st_mtime;\n#else\n    struct stat statbuf;\n    lstat(fileName.c_str(), &statbuf);\n    std::time_t ts = statbuf.st_mtimespec.tv_sec;\n    \n#endif\n\tstd::tm * ptm = std::localtime(&ts);\n\n    char buffer[32];\n\tstd::strftime(buffer, 32, \"%Y-%m-%d %H:%M\", ptm);\n    \n    manifest.setDeviceName(\"localhost\");\n    manifest.setDisplayName(\"Wechat Backup\");\n    \n    manifest.setBackupTime(buffer);\n\n    return true;\n}\n"
  },
  {
    "path": "WechatExporter/core/ITunesParser.h",
    "content": "//\n//  ITunesParser.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/29.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include <string>\n#include <vector>\n#include <map>\n\n#include <sstream>\n#include <iomanip>\n#include <ctime>\n#include \"Utils.h\"\n\n#ifndef ITunesParser_h\n#define ITunesParser_h\n\nstruct ITunesFile\n{\n    std::string domain;\n    std::string fileId;\n    std::string relativePath;\n    unsigned int flags;\n    std::vector<unsigned char> blob;\n    mutable unsigned int modifiedTime;\n    mutable size_t size;\n    mutable bool blobParsed;\n\n    ITunesFile() : flags(0), modifiedTime(0), size(0), blobParsed(false)\n    {\n    }\n    \n    bool isDir() const\n    {\n        return flags == 2;\n    }\n};\n\nusing ITunesFileVector = std::vector<ITunesFile *>;\nusing ITunesFilesIterator = typename ITunesFileVector::iterator;\nusing ITunesFilesConstIterator = typename ITunesFileVector::const_iterator;\nusing ITunesFileRange = std::pair<ITunesFilesConstIterator, ITunesFilesConstIterator>;\n\nclass BackupItem\n{\npublic:\n    struct AppInfo\n    {\n        std::string bundleId;\n        std::string name;\n        std::string bundleShortVersion;\n        std::string bundleVersion;\n        \n    };\n    \nprotected:\n    std::string m_path;\n    std::string m_backupId;\n    std::string m_deviceName;\n    std::string m_displayName;\n    std::string m_backupTime;\n    std::string m_iTunesVersion;\n    std::string m_macOSVersion;\n    std::string m_iOSVersion;\n\tbool m_encrypted;\n    \n    std::vector<AppInfo> m_apps;    // Installed Applications\n    \npublic:\n    BackupItem() : m_encrypted(false)\n    {\n    }\n\n\tBackupItem(const std::string& path, const std::string& deviceName, const std::string& displayName, const std::string& backupTime) : m_path(path), m_deviceName(deviceName), m_displayName(displayName), m_encrypted(false)\n\t{\n\t}\n    \n    bool operator==(const BackupItem& rhs) const\n    {\n        if (this == &rhs)\n        {\n            return true;\n        }\n        \n        return m_path == rhs.m_path;\n    }\n\n\tvoid setPath(const std::string& path)\n\t{\n\t\tm_path = path;\n\t}\n\n    void setBackupId(const std::string& backupId)\n    {\n        m_backupId = backupId;\n    }\n\n\tvoid setDeviceName(const std::string& deviceName)\n\t{\n\t\tm_deviceName = deviceName;\n\t}\n\n\tvoid setDisplayName(const std::string& displayName)\n\t{\n\t\tm_displayName = displayName;\n\t}\n\n\tvoid setBackupTime(const std::string& backupTime)\n\t{\n\t\tm_backupTime = backupTime;\n\t}\n    \n    void setITunesVersion(const std::string& iTunesVersion)\n    {\n        m_iTunesVersion = iTunesVersion;\n    }\n    void setMacOSVersion(const std::string& macOSVersion)\n    {\n        m_macOSVersion = macOSVersion;\n    }\n    \n    void setIOSVersion(const std::string& iOSVersion)\n    {\n        m_iOSVersion = iOSVersion;\n    }\n    \n    std::string getIOSVersion() const\n    {\n        return m_iOSVersion;\n    }\n    \n    bool isITunesVersionEmpty() const\n    {\n        return m_iTunesVersion.empty();\n    }\n\n\tvoid setEncrypted(bool encrypted)\n\t{\n\t\tm_encrypted = encrypted;\n\t}\n    \n    void addApp(const AppInfo& appInfo)\n    {\n        m_apps.push_back(appInfo);\n    }\n\n\tconst std::vector<AppInfo>& getApps() const\n\t{\n\t\treturn m_apps;\n\t}\n    \n\tbool isEncrypted() const\n\t{\n\t\treturn m_encrypted;\n\t}\n\n    bool isValid() const\n    {\n        return !m_displayName.empty() && !m_backupTime.empty() && !m_deviceName.empty();\n    }\n    \n    std::string getITunesVersion() const\n    {\n        return m_iTunesVersion.empty() ? (m_macOSVersion.empty() ? \"\" : (\"Embedded iTunes on MacOS \" + m_macOSVersion)) : m_iTunesVersion;\n    }\n\n    std::string toString() const\n    {\n        return m_displayName + \" [\" + m_backupTime + \"] (\" + m_path + \")\" + (m_iTunesVersion.empty() ? (\" Embeded iTunes on MacOS:\" + m_macOSVersion) : (\" iTunes Version:\" + m_iTunesVersion));\n    }\n    \n    std::string getPath() const\n    {\n        return m_path;\n    }\n    \n    std::string getBackupId() const\n    {\n        return m_backupId;\n    }\n};\n\nclass ITunesDb\n{\npublic:\n    \n    class ITunesFileEnumerator\n    {\n    public:\n        virtual bool isInvalid() const = 0;\n        virtual bool nextFile(ITunesFile& file) = 0;\n        \n        virtual ~ITunesFileEnumerator() {}\n    };\n    \n    ITunesDb(const std::string& rootPath, const std::string& manifestFileName);\n    virtual ~ITunesDb();\n    \n    std::string getVersion() const\n    {\n        return m_version;\n    }\n    \n    std::string getIOSVersion() const\n    {\n        return m_iOSVersion;\n    }\n    \n    void setLoadingFilter(std::function<bool(const char *, int flags)> loadingFilter)\n    {\n        m_loadingFilter = std::move(loadingFilter);\n    }\n    \n    bool load();\n    bool load(const std::string& domain);\n    virtual bool load(const std::string& domain, bool onlyFile);\n    ITunesFileEnumerator* buildEnumerator(const std::vector<std::string>& domains, bool onlyFile) const;\n    bool copy(const std::string& destPath, const std::string& backupId, std::vector<std::string>& domains, std::function<bool(const ITunesDb*, const ITunesFile*)>& func) const;\n    \n    const ITunesFile* findITunesFile(const std::string& relativePath) const;\n    std::string findFileId(const std::string& relativePath) const;\n    std::string findRealPath(const std::string& relativePath) const;\n    template<class TFilter>\n    ITunesFileVector filter(TFilter f) const;\n    template<class THandler>\n    void enumFiles(THandler handler) const;\n    \n    bool isMbdb() const\n    {\n        return m_isMbdb;\n    }\n    \n    std::string getRealPath(const ITunesFile& file) const;\n    std::string getRealPath(const ITunesFile* file) const;\n    \n    static unsigned int parseModifiedTime(const std::vector<unsigned char>& data);\n    static bool parseFileInfo(const ITunesFile* file);\n    bool copyFile(const std::string& vpath, const std::string& dest, bool overwrite = false) const;\n    bool copyFile(const std::string& vpath, const std::string& destPath, const std::string& destFileName, bool overwrite = false) const;\n#ifndef NDEBUG\n    std::string getLastError() const { return m_lastError; }\n#endif\nprotected:\n    // bool copyMbdb(const std::string& destPath, const std::string& backupId, std::vector<std::string>& domains) const;\n    virtual std::string fileIdToRealPath(const std::string& fileId) const;\n    ITunesFileEnumerator* buildEnumerator(const std::string& dbPath, const std::vector<std::string>& domains, bool onlyFile) const;\nprotected:\n    bool m_isMbdb;\n    mutable std::vector<ITunesFile *> m_files;\n    std::string m_rootPath;\n    std::string m_manifestFileName;\n    std::string m_version;\n    std::string m_iOSVersion;\n    std::function<bool(const char *, int flags)> m_loadingFilter;\n    \n#ifndef NDEBUG\n    mutable std::string m_lastError;\n#endif\n};\n\nclass DecodedWechatITunesDb : public ITunesDb\n{\npublic:\n    DecodedWechatITunesDb(const std::string& rootPath, const std::string& manifestFileName);\n    ~DecodedWechatITunesDb();\n    \n    virtual bool load(const std::string& domain, bool onlyFile);\n    \nprotected:\n    bool loadFiles(const std::string& root, bool onlyFile);\n    virtual std::string fileIdToRealPath(const std::string& fileId) const;\n    virtual bool filterFile(const std::string& relativeDir, const std::string& fileName);\n};\n\nclass DecodedSharedWechatITunesDb : public DecodedWechatITunesDb\n{\npublic:\n    DecodedSharedWechatITunesDb(const std::string& rootPath, const std::string& manifestFileName);\n    ~DecodedSharedWechatITunesDb();\n    \n    virtual bool load(const std::string& domain, bool onlyFile);\n    \nprotected:\n    virtual bool filterFile(const std::string& relativeDir, const std::string& fileName);\n};\n\ntemplate<class TFilter>\nITunesFileVector ITunesDb::filter(TFilter f) const\n{\n    ITunesFileVector files;\n    ITunesFileRange range = std::equal_range(m_files.cbegin(), m_files.cend(), f, f);\n    if (range.first != range.second)\n    {\n        for (ITunesFilesConstIterator it = range.first; it != range.second; ++it)\n        {\n            if (f == *it)\n            {\n                files.push_back(*it);\n            }\n        }\n    }\n    \n    return files;\n}\n\ntemplate<class THandler>\nvoid ITunesDb::enumFiles(THandler handler) const\n{\n    for (ITunesFilesConstIterator it = m_files.cbegin(); it != m_files.cend(); ++it)\n    {\n        if (!handler(this, *it))\n        {\n            break;\n        }\n    }\n}\n\nclass ManifestParser\n{\nprotected:\n    std::string m_manifestPath;\n    bool m_incudingApps;\n\tmutable std::string m_lastError;\n\npublic:\n    ManifestParser(const std::string& manifestPath, bool includingApps);\n    virtual ~ManifestParser() {}\n    virtual bool parse(std::vector<BackupItem>& manifets) const;\n\tstd::string getLastError() const;\n\n    friend ITunesDb;\n    \nprotected:\n    bool parseDirectory(const std::string& path, std::vector<BackupItem>& manifests) const;\n    virtual bool parse(const std::string& path, BackupItem& manifest) const;\n    virtual bool isValidBackupItem(const std::string& path) const;\n    virtual bool isValidMobileSync(const std::string& path) const;\n    \n    static bool parseInfoPlist(const std::string& backupIdPath, BackupItem& manifest, bool includingApps);\n    static bool parseITunesMetadata(const std::string& metadata, BackupItem::AppInfo& appInfo);\n};\n\nclass DecodedManifestParser : public ManifestParser\n{\npublic:\n    DecodedManifestParser(const std::string& manifestPath, bool includingApps);\n    virtual bool parse(std::vector<BackupItem>& manifets) const;\n    \nprotected:\n    virtual bool parse(const std::string& path, BackupItem& manifest) const;\n    virtual bool isValidBackupItem(const std::string& path) const;\n};\n\n#endif /* ITunesParser_h */\n"
  },
  {
    "path": "WechatExporter/core/Logger.h",
    "content": "//\n//  Logger.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include <string>\n\n#ifndef Logger_h\n#define Logger_h\n\nclass Logger\n{\npublic:\n    virtual void write(const std::string& log) = 0;\n    virtual void debug(const std::string& log) = 0;\n    virtual ~Logger() {}\n};\n\n#endif /* Logger_h */\n"
  },
  {
    "path": "WechatExporter/core/MMKVReader.h",
    "content": "//\n//  MMKVReader.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/1/26.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef MMKVReader_h\n#define MMKVReader_h\n\nclass MMKVReader\n{\nprivate:\n    const unsigned char *m_ptr;\n\tsize_t m_size;\n    mutable size_t m_position;\n\npublic:\n    \n    MMKVReader(const unsigned char *ptr, size_t size) : m_ptr(ptr), m_size(size), m_position(0)\n    {\n    }\n    std::string readKey() const\n    {\n        std::string key;\n        uint32_t keyLength = 0;\n        const unsigned char* data = calcVarint32Ptr(m_ptr + m_position, m_ptr + m_size, &keyLength);\n        m_position += data - (m_ptr + m_position);\n#if !defined(NDEBUG)\n\t\tassert(m_position <= m_size);\n#endif\n        if (keyLength > 0)\n        {\n            auto s_size = static_cast<size_t>(keyLength);\n            if (s_size <= m_size - m_position)\n            {\n                key.assign((char *) (m_ptr + m_position), s_size);\n                m_position += s_size;\n            }\n            else\n            {\n                m_position = m_size;\n            }\n        }\n\n#if !defined(NDEBUG)\n\t\tassert(m_position <= m_size);\n#endif\n        \n        return key;\n    }\n    \n    void skipValue() const\n    {\n        uint32_t valueLength = 0;\n        const unsigned char* data = calcVarint32Ptr(m_ptr + m_position, m_ptr + m_size, &valueLength);\n        m_position += data - (m_ptr + m_position);\n        if (valueLength > 0)\n        {\n\t\t\tif ((m_position + valueLength) > m_size)\n\t\t\t{\n\t\t\t\tm_position = m_size;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tm_position += valueLength;\n\t\t\t}\n        }\n    }\n    \n    std::string readStringValue() const\n    {\n        // MMBuffer\n        std::string value;\n        uint32_t valueLength = 0;\n        const unsigned char* data = calcVarint32Ptr(m_ptr + m_position, m_ptr + m_size, &valueLength);\n        m_position += data - (m_ptr + m_position);\n        if (valueLength > 0)\n        {\n            // MMBuffer\n            uint32_t mbbLength = 0;\n            const unsigned char *ptr = m_ptr + m_position;\n            ptr = calcVarint32Ptr(ptr, m_ptr + m_size, &mbbLength);\n#if !defined(NDEBUG)\n\t\t\tassert((m_position + valueLength) <= m_size);\n\t\t\tassert(valueLength == (ptr - (m_ptr + m_position)) + mbbLength);\n#endif\n            if (mbbLength > 0)\n            {\n                auto s_size = static_cast<size_t>(mbbLength);\n                if (s_size <= m_size - m_position)\n                {\n                    value.assign((char *)(ptr), s_size);\n                    m_position += valueLength;\n                }\n                else\n                {\n                    m_position = m_size;\n                }\n            }\n            else\n            {\n                m_position += valueLength;\n            }\n        }\n\n#if !defined(NDEBUG)\n\t\tassert(m_position <= m_size);\n#endif\n        \n        return value;\n    }\n    \n    void seek(size_t position) const\n    {\n        m_position = position;\n    }\n    \n    size_t getPos() const\n    {\n        return m_position;\n    }\n    \n    bool isAtEnd() const\n    {\n#if !defined(NDEBUG)\n        assert(m_position <= m_size);\n#endif\n        return m_position >= m_size;\n    }\n};\n\n#endif /* MMKVReader_h */\n"
  },
  {
    "path": "WechatExporter/core/MbdbReader.h",
    "content": "//\n//  MbdbReader.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/6/25.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#include <fstream>\n\n#ifndef MbdbReader_h\n#define MbdbReader_h\n\n// Refer: https://www.theiphonewiki.com/wiki/ITunes_Backup#Manifest.mbdb\n\n// string Domain\n    // string Path\n    // string LinkTarget absolute path\n    // string DataHash SHA.1 (some files only)\n    // string unknown always N/A\n    // uint16 Mode same as mbdx.Mode\n    // uint32 unknown always 0\n    // uint32 unknown\n    // uint32 UserId\n    // uint32 GroupId mostly 501 for apps\n    // uint32 Time1 relative to unix epoch (e.g time_t)\n    // uint32 Time2 Time1 or Time2 is the former ModificationTime\n    // uint32 Time3\n    // uint64 FileLength always 0 for link or directory\n    // uint8 Flag 0 if special (link, directory), otherwise unknown\n    // uint8 PropertyCount number of properties following\n    //\n    // Property is a couple of strings:\n    //\n    // string name\n    // string value can be a string or a binary content\n\nclass MbdbReader {\n    \n    std::ifstream m_ifs;\n    std::vector<char> m_buffer;\n    \n#ifndef NDEBUG\n    std::ifstream::streampos m_pos;\n#endif\npublic:\n    \n    ~MbdbReader()\n    {\n        if (m_ifs.is_open())\n        {\n            m_ifs.close();\n        }\n    }\n    \n    bool open(const std::string& fileName)\n    {\n        m_ifs.open(fileName, std::ios_base::in | std::ios_base::binary);\n        if (!m_ifs.is_open())\n        {\n            return false;\n        }\n        \n        char signature[7] = { 0 };\n        m_ifs.read(&signature[0], 6);\n        if (strcmp(signature, \"mbdb\\5\\0\") != 0)\n        {\n            m_ifs.close();\n            return false;\n        }\n        \n        return true;\n    }\n    \n    bool hasMoreData()\n    {\n        return !m_ifs.eof();\n    }\n    \n    bool read(unsigned char *buffer, size_t length)\n    {\n        m_ifs.read(reinterpret_cast<char *>(buffer), length);\n\n        return true;\n    }\n    \n    bool read(std::string& str)\n    {\n        int b0 = m_ifs.get();\n        int b1 = m_ifs.get();\n        \n        if ((b0 == 255 && b1 == 255) || (b0 == 0 && b1 == 0))\n        {\n            str.clear();\n            return true;\n        }\n        \n        if (b0 == std::ifstream::traits_type::eof() || b1 == std::ifstream::traits_type::eof())\n        {\n            return false;\n        }\n        \n        int lengthOfString = b0 * 256 + b1;\n        m_buffer.resize(lengthOfString);\n        m_ifs.read(&m_buffer[0], lengthOfString);\n\n        str.clear();\n        std::copy(m_buffer.begin(), m_buffer.end(), std::back_inserter(str));\n        \n        return true;\n    }\n    \n    bool readD(std::string& str)\n    {\n        if (!read(str))\n        {\n            return false;\n        }\n        \n        // If only ASCII printable characters, return the string\n        size_t i = 0, length = str.size();\n        for (; i < length; ++i)\n        {\n            if (str[i] < 32 || str[i] >= 128)\n            {\n                break;\n            }\n        }\n        if (i == length)\n        {\n            return true;\n        }\n\n        // otherwise the hexadecimal dump\n        std::string result;\n        for (i = 0; i < length; ++i)\n        {\n            result.push_back(toHex(str[i] >> 4));\n            result.push_back(toHex(str[i] & 15));\n        }\n\n        str.swap(result);\n        return true;\n    }\n\n    bool skipString()\n    {\n        int b0 = m_ifs.get();\n        int b1 = m_ifs.get();\n        \n        if ((b0 == 255 && b1 == 255) || (b0 == 0 && b1 == 0))\n        {\n            return true;\n        }\n        \n        if (b0 == std::ifstream::traits_type::eof() || b1 == std::ifstream::traits_type::eof())\n        {\n            return false;\n        }\n        \n        int lengthOfString = b0 * 256 + b1;\n        m_ifs.seekg(lengthOfString, std::ios_base::cur);\n\n        return true;\n    }\n    // static size_t skipString(const unsigned char *data, size_t length);\n    \n    bool skip(size_t length)\n    {\n        m_ifs.seekg(length, std::ios_base::cur);\n        return true;\n    }\n    \nprotected:\n    char toHex(int value)\n    {\n        value &= 0xF;\n        return (value >= 0 && value <= 9) ? (char)('0' + value) : (char)('A' + (value - 10));\n    }\n\n    char toHexLow(int value)\n    {\n        value &= 0xF;\n        return (value >= 0 && value <= 9) ? (char)('0' + value) : (char)('a' + (value - 10));\n    }\n};\n\n\n#endif /* MbdbReader_h */\n"
  },
  {
    "path": "WechatExporter/core/MessageParser.cpp",
    "content": "//\n//  MessageParser.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2021/2/22.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#include \"MessageParser.h\"\n#include <libxml/parser.h>\n#include <libxml/tree.h>\n#include <libxml/xpath.h>\n#include <json/json.h>\n#include <plist/plist.h>\n#include \"XmlParser.h\"\n\n#define ALIGNMENT_LEFT  \"left\"\n#define ALIGNMENT_RIGHT  \"right\"\n\n#define DIR_ASSETS  \"Files\"\n\nMessageParser::MessageParser(const ITunesDb& iTunesDb, const ITunesDb& iTunesDbShare, TaskManager& taskManager, Friends& friends, Friend myself, const ExportOption& options, const std::string& resPath, const std::string& outputPath, const ResManager& resManager) : m_iTunesDb(iTunesDb), m_iTunesDbShare(iTunesDbShare), m_resManager(resManager), m_taskManager(taskManager), m_friends(friends), m_myself(myself), m_options(options), m_resPath(resPath), m_outputPath(outputPath)\n{\n    m_userBase = \"Documents/\" + m_myself.getHash();\n}\n\nbool MessageParser::parse(WXMSG& msg, const Session& session, std::vector<TemplateValues>& tvs) const\n{\n    TemplateValues& tv = *(tvs.emplace(tvs.end(), \"msg\"));\n    \n    tv[\"%%MSGID%%\"] = msg.msgId;\n    tv[\"%%NAME%%\"] = \"\";\n    tv[\"%%WXNAME%%\"] = \"\";\n    tv[\"%%TIME%%\"] = fromUnixTime(msg.createTime);\n    tv[\"%%MSGTYPE%%\"] = std::to_string(msg.type);\n    tv[\"%%MESSAGE%%\"] = \"\";\n\n    std::string forwardedMsg;\n    std::string forwardedMsgTitle;\n\n    std::string senderId = \"\";\n    if (session.isChatroom())\n    {\n        if (msg.des != 0)\n        {\n            std::string::size_type enter = msg.content.find(\":\\n\");\n            if (enter != std::string::npos && enter + 2 < msg.content.size())\n            {\n                senderId = msg.content.substr(0, enter);\n                msg.content = msg.content.substr(enter + 2);\n            }\n        }\n    }\n\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_type_\" + std::to_string(msg.type) + \".txt\"), msg.content);\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + msg.msgId + \".txt\"), msg.content);\n#endif\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"lastmsg.txt\"), msg.content);\n#endif\n    \n    bool res = false;\n    switch (msg.type)\n    {\n        case MSGTYPE_TEXT:  // 1\n            res = parseText(msg, session, tv);\n            break;\n        case MSGTYPE_IMAGE:  // 3\n            res = parseImage(msg, session, tv);\n            break;\n        case MSGTYPE_VOICE:  // 34\n            res = parseVoice(msg, session, tv);\n            break;\n        case MSGTYPE_PUSHMAIL:  // 35\n            res = parsePushMail(msg, session, tv);\n            break;\n        case MSGTYPE_VERIFYMSG: // 37\n            res = parseVerification(msg, session, tv);\n            break;\n        case MSGTYPE_POSSIBLEFRIEND:    // 40\n            res = parsePossibleFriend(msg, session, tv);\n            break;\n        case MSGTYPE_SHARECARD:  // 42\n        case MSGTYPE_IMCARD: // 66\n            res = parseCard(msg, session, tv);\n            break;\n        case MSGTYPE_VIDEO: // 43\n        case MSGTYPE_MICROVIDEO:    // 62\n            res = parseVideo(msg, session, senderId, tv);\n            break;\n        case MSGTYPE_EMOTICON:   // 47\n            res = parseEmotion(msg, session, tv);\n            break;\n        case MSGTYPE_LOCATION:   // 48\n            res = parseLocation(msg, session, tv);\n            break;\n        case MSGTYPE_APP:    // 49\n            res = parseAppMsg(msg, session, senderId, forwardedMsg, forwardedMsgTitle, tv);\n            break;\n        case MSGTYPE_VOIPMSG:   // 50\n            res = parseCall(msg, session, tv);\n            break;\n        case MSGTYPE_VOIPNOTIFY:    // 52\n        case MSGTYPE_VOIPINVITE:    // 53\n            res = parseSysNotice(msg, session, tv);\n            break;\n        case MSGTYPE_STATUSNOTIFY:  // 51\n            res = parseStatusNotify(msg, session, tv);\n            break;\n        case MSGTYPE_NOTICE: // 64\n            res = parseNotice(msg, session, tv);\n            break;\n        case MSGTYPE_SYSNOTICE: // 9999\n            res = parseNotice(msg, session, tv);\n            break;\n        case MSGTYPE_SYS:   // 10000\n        case MSGTYPE_RECALLED:  // 10002\n            res = parseSystem(msg, session, tv);\n            break;\n        default:\n#if !defined(NDEBUG) || defined(DBG_PERF)\n            writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_unknwn_type_\" + std::to_string(msg.type) + msg.msgId + \".txt\"), msg.content);\n#endif\n            res = parseText(msg, session, tv);\n            break;\n    }\n    \n#ifndef NDEBUG\n    if (m_resManager.hasEmojiTag(tv[\"%%MESSAGE%%\"]))\n    {\n        writeFile(combinePath(m_outputPath, \"../dbg\", \"wxemoji\" + std::to_string(msg.type) + \".txt\"), tv[\"%%MESSAGE%%\"] + \"\\r\\n\\r\\n\");\n        appendFile(combinePath(m_outputPath, \"../dbg\", \"wxemoji\" + std::to_string(msg.type) + \".txt\"), msg.content);\n    }\n    \n#endif\n\n    std::string portraitPath = (DIR_ASSETS DIR_SEP_STR \"Portrait\" DIR_SEP_STR);\n    std::string portraitUrlPath = (DIR_ASSETS \"/Portrait/\");\n\n    // std::string localPortrait;\n\n    const Friend* protraitUser = NULL;\n    if (session.isChatroom())\n    {\n        tv[\"%%ALIGNMENT%%\"] = (msg.des == 0) ? ALIGNMENT_RIGHT : ALIGNMENT_LEFT;\n        if (msg.des == 0)\n        {\n            tv[\"%%NAME%%\"] = m_myself.getDisplayName();    // CSS will prevent showing the name for self\n            tv[\"%%WXNAME%%\"] = m_myself.getWxName();\n            tv[\"%%AVATAR%%\"] = portraitUrlPath + m_myself.getLocalPortrait();\n            // remotePortrait = m_myself.getPortrait();\n            protraitUser = &m_myself;\n        }\n        else\n        {\n            if (!senderId.empty())\n            {\n                std::string senderHash = md5(senderId);\n                std::string senderDisplayName = session.getMemberName(senderId);\n                const Friend *f = m_friends.getFriend(senderHash);\n                if (senderDisplayName.empty() && NULL != f)\n                {\n                    senderDisplayName = f->getDisplayName();\n                }\n                tv[\"%%NAME%%\"] = senderDisplayName.empty() ? senderId : senderDisplayName;\n                if (NULL != f)\n                {\n                    protraitUser = f;\n                    tv[\"%%WXNAME%%\"] = f->getWxName();\n                }\n                if (NULL == f)\n                {\n                    ensureDefaultPortraitIconExisted(combinePath(session.getOutputFileName(), portraitPath));\n                }\n                tv[\"%%AVATAR%%\"] = portraitUrlPath + ((NULL != f) ? f->getLocalPortrait() : \"DefaultAvatar.png\");\n            }\n            else\n            {\n                tv[\"%%NAME%%\"] = senderId;\n                tv[\"%%AVATAR%%\"] = \"\";\n            }\n        }\n    }\n    else\n    {\n        if (msg.des == 0 || session.getUsrName() == m_myself.getUsrName())\n        {\n            tv[\"%%ALIGNMENT%%\"] = ALIGNMENT_RIGHT;\n            tv[\"%%NAME%%\"] = m_myself.getDisplayName();\n            tv[\"%%WXNAME%%\"] = m_myself.getWxName();\n            tv[\"%%AVATAR%%\"] = portraitUrlPath + m_myself.getLocalPortrait();\n            \n            protraitUser = &m_myself;\n        }\n        else\n        {\n            tv[\"%%ALIGNMENT%%\"] = ALIGNMENT_LEFT;\n\n            const Friend *f = m_friends.getFriend(session.getHash());\n            if (NULL == f)\n            {\n                tv[\"%%NAME%%\"] = session.getDisplayName();\n                tv[\"%%WXNAME%%\"] = session.getWxName();\n                if (session.isPortraitEmpty())\n                {\n                    ensureDefaultPortraitIconExisted(portraitPath);\n                }\n                // localPortrait = combinePath(session.getOutputFileName(), portraitPath + (session.isPortraitEmpty() ? \"DefaultAvatar.png\" : session.getLocalPortrait()));\n                // remotePortrait = session.getPortrait();\n                tv[\"%%AVATAR%%\"] = portraitUrlPath + (session.isPortraitEmpty() ? \"DefaultAvatar.png\" : session.getLocalPortrait());\n                \n                protraitUser = &session;\n            }\n            else\n            {\n                tv[\"%%NAME%%\"] = f->getDisplayName();\n                tv[\"%%WXNAME%%\"] = f->getWxName();\n                // localPortrait = portraitPath + f->getLocalPortrait();\n                // remotePortrait = f->getPortrait();\n                tv[\"%%AVATAR%%\"] = portraitUrlPath + f->getLocalPortrait();\n                \n                protraitUser = f;\n            }\n        }\n    }\n\n    if (!m_options.isTextMode())\n    {\n        if (NULL != protraitUser)\n        {\n            copyPortraitIcon(&session, *protraitUser, combinePath(m_outputPath, session.getOutputFileName(), portraitPath));\n            // m_downloader.addTask(remotePortrait, combinePath(m_outputPath, session.getOutputFileName(), localPortrait), msg.createTime);\n        }\n    }\n    \n    if (!m_options.isTextMode())\n    {\n        tv[\"%%NAME%%\"] = safeHTML(tv[\"%%NAME%%\"]);\n    }\n\n    if (!forwardedMsg.empty())\n    {\n        // This funtion will change tvs and causes tv invalid, so we do it at last\n        parseForwardedMsgs(session, msg, forwardedMsgTitle, forwardedMsg, tvs);\n    }\n    return res;\n}\n\nbool MessageParser::parsePortrait(const WXMSG& msg, const Session& session, const std::string& senderId, TemplateValues& tv) const\n{\n    return true;\n}\n\n/////////////////////////////////////\n\nbool MessageParser::parseText(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n    if (!m_options.isTextMode())\n    {\n        // std::string assetsDir = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS);\n        // tv[\"%%MESSAGE%%\"] = safeHTML(msg.content);\n        // tv[\"%%MESSAGE%%\"] = m_resManager.convertEmojis(safeHTML(msg.content), combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS);\n        tv[\"%%MESSAGE%%\"] = m_resManager.convertEmojis(msg.content, combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS);\n    }\n    else\n    {\n        tv[\"%%MESSAGE%%\"] = msg.content;\n    }\n    \n    return true;\n}\n\nbool MessageParser::parseImage(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n    std::string vFile = combinePath(m_userBase, \"Img\", session.getHash(), msg.msgId);\n    return parseImage(combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS, vFile + \".pic\", vFile + \".pic_hd\", msg.msgId + \".jpg\", vFile + \".pic_thum\", msg.msgId + \"_thumb.jpg\", tv);\n}\n\nbool MessageParser::parseVoice(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n    std::string audioSrc;\n    int voiceLen = -1;\n    std::string vLenStr;\n    std::string voiceFormat;\n    XmlParser xmlParser(msg.content);\n    if (xmlParser.parseAttributeValue(\"/msg/voicemsg\", \"voicelength\", vLenStr) && !vLenStr.empty())\n    {\n        voiceLen = std::stoi(vLenStr);\n    }\n    if (!xmlParser.parseAttributeValue(\"/msg/voicemsg\", \"voiceformat\", voiceFormat))\n    {\n        voiceFormat = \"4\";\n    }\n    const ITunesFile* audioSrcFile = NULL;\n    if (!m_options.isTextMode())\n    {\n        audioSrcFile = m_iTunesDb.findITunesFile(combinePath(m_userBase, \"Audio\", session.getHash(), msg.msgId + \".aud\"));\n        if (NULL != audioSrcFile)\n        {\n            audioSrc = m_iTunesDb.getRealPath(*audioSrcFile);\n        }\n    }\n    \n    bool result = false;\n    if (!audioSrc.empty())\n    {\n        // std::string audCopyPath = combinePath(m_outputPath, \"..\", \"dbg\");\n        // if (!existsDirectory(audCopyPath))\n        // {\n        //     makeDirectory(audCopyPath);\n        // }\n        // audCopyPath = combinePath(audCopyPath, msg.msgId + \".aud\");\n        // copyFile(audioSrc, combinePath(audCopyPath, msg.msgId + \".aud\"));\n        \n        // writeFile(combinePath(audCopyPath, msg.msgId + \".aud.xml\"), msg.content);\n        \n#ifdef USING_ASYNC_TASK_FOR_MP3\n        std::string assetsDir = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS);\n        ensureDirectoryExisted(assetsDir);\n        std::string mp3Path = combinePath(assetsDir, msg.msgId + \".mp3\");\n        m_taskManager.convertAudio(&session, audioSrc, mp3Path, (voiceFormat == \"0\") ? TaskManager::AUDIO_FORMAT_AMR : TaskManager::AUDIO_FORMAT_SILK, ITunesDb::parseModifiedTime(audioSrcFile->blob));\n        result = true;\n#else\n        std::string assetsDir = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS);\n        std::string mp3Path = combinePath(assetsDir, msg.msgId + \".mp3\");\n        ensureDirectoryExisted(assetsDir);\n        \n        m_pcmData.clear();\n        m_error.clear();\n        std::string err;\n        bool isSilk = false;\n\n        // SILK-v3\n        if (silkToPcm(audioSrc, m_pcmData, isSilk, &err) && !m_pcmData.empty())\n        {\n#ifndef NDEBUG\n            // std::string audCopyPath = combinePath(m_outputPath, \"..\", \"dbg\");\n            // audCopyPath = combinePath(audCopyPath, \"aud\", msg.msgId + \".pcm\");\n            // writeFile(audCopyPath, m_pcmData);\n#endif\n            if (pcmToMp3(m_pcmData, mp3Path, &err))\n            {\n                // copyFile(mp3Path, combinePath(audCopyPath, msg.msgId + \".slk.\" + voiceFormat + \".mp3\"));\n                result = true;\n            }\n        }\n        else if (!isSilk)\n        {\n            // AMR\n            if (amrToPcm(audioSrc, m_pcmData, &err) && !m_pcmData.empty())\n            {\n                if (amrPcmToMp3(m_pcmData, mp3Path, &err))\n                {\n                    // copyFile(mp3Path, combinePath(audCopyPath, msg.msgId + \".amr.\" + (voiceFormat != \"\" ? voiceFormat : \"empty\") + \".mp3\"));\n#ifndef NDEBUG\n                    // std::string audCopyPath = combinePath(m_outputPath, \"..\", \"dbg\");\n                    // audCopyPath = combinePath(audCopyPath, \"aud\", msg.msgId + \".amr.mp3\");\n                    // copyFile(mp3Path, audCopyPath);\n#endif\n                    result = true;\n                }\n            }\n        }\n        \n        if (result)\n        {\n            updateFileTime(mp3Path, ITunesDb::parseModifiedTime(audioSrcFile->blob));\n        }\n        else\n        {\n            m_error += err + \"\\r\\nvoiceFormat:*\" + voiceFormat + \"* \" + combinePath(m_userBase, \"Audio\", session.getHash(), msg.msgId + \".aud\");\n        }\n        \n        if (!result)\n        {\n#ifndef NDEBUG\n            std::string audCopyPath = combinePath(m_outputPath, \"..\", \"dbg\");\n            if (!existsDirectory(audCopyPath))\n            {\n                makeDirectory(audCopyPath);\n            }\n            \n            audCopyPath = combinePath(audCopyPath, msg.msgId + \".aud\");\n            copyFile(audioSrc, audCopyPath);\n            \n            writeFile(audCopyPath + \".xml\", msg.content);\n            \n#endif\n        }\n#endif\n    }\n    \n    if (result)\n    {\n        tv.setName(\"audio\");\n        tv[\"%%AUDIOPATH%%\"] = (DIR_ASSETS \"/\") + msg.msgId + \".mp3\";\n        tv[\"%%AUDIOTIME%%\"] = getDisplayTime(voiceLen);\n        tv[\"%%AUDIOLENGTH%%\"] = voiceLen == -1 ? \"\" : std::to_string((int)(voiceLen * 300 / 60000));    // max-width: 300px <=> 60s\n    }\n    else\n    {\n        tv.setName(\"msg\");\n        tv[\"%%MESSAGE%%\"] = voiceLen == -1 ? m_resManager.getLocaleString(\"[Audio]\") : formatString(m_resManager.getLocaleString(\"[Audio %s]\"), getDisplayTime(voiceLen).c_str());\n    }\n    \n    return result;\n}\n\nbool MessageParser::parsePushMail(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n    std::string subject;\n    std::string digest;\n    XmlParser xmlParser(msg.content);\n    xmlParser.parseNodeValue(\"/msg/pushmail/content/subject\", subject);\n    xmlParser.parseNodeValue(\"/msg/pushmail/content/digest\", digest);\n    \n    tv.setName(\"plainshare\");\n\n    tv[\"%%SHARINGURL%%\"] = \"##\";\n    tv[\"%%SHARINGTITLE%%\"] = subject;\n    tv[\"%%MESSAGE%%\"] = digest;\n    \n    return true;\n}\n\nbool MessageParser::parseVideo(const WXMSG& msg, const Session& session, std::string& senderId, TemplateValues& tv) const\n{\n    std::map<std::string, std::string> attrs = { {\"fromusername\", \"\"}, {\"cdnthumbwidth\", \"\"}, {\"cdnthumbheight\", \"\"} };\n    XmlParser xmlParser(msg.content);\n    if (xmlParser.parseAttributesValue(\"/msg/videomsg\", attrs))\n    {\n    }\n    \n    if (senderId.empty())\n    {\n        senderId = attrs[\"fromusername\"];\n    }\n    \n    std::string vfile = combinePath(m_userBase, \"Video\", session.getHash(), msg.msgId);\n    return parseVideo(combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS, vfile + \".mp4\", vfile + \"_raw.mp4\", msg.msgId + \".mp4\", vfile + \".video_thum\", msg.msgId + \"_thum.jpg\", attrs[\"cdnthumbwidth\"], attrs[\"cdnthumbheight\"], tv);\n}\n\nbool MessageParser::parseEmotion(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n    std::string url;\n    if (!m_options.isTextMode())\n    {\n        XmlParser xmlParser(msg.content);\n        if (!xmlParser.parseAttributeValue(\"/msg/emoji\", \"cdnurl\", url))\n        {\n            url.clear();\n        }\n        else\n        {\n            if (!startsWith(url, \"http\") && startsWith(url, \"https\"))\n            {\n                if (!xmlParser.parseAttributeValue(\"/msg/emoji\", \"thumburl\", url))\n                {\n                    url.clear();\n                }\n            }\n        }\n    }\n\n    if (startsWith(url, \"http\") || startsWith(url, \"https\"))\n    {\n        tv.setName(\"emoji\");\n        \n        if (!m_options.isUsingRemoteEmoji())\n        {\n            std::string emojiFile = url;\n            std::smatch sm2;\n            static std::regex pattern47_2(\"\\\\/(\\\\w+?)\\\\/\\\\w*$\");\n            if (std::regex_search(emojiFile, sm2, pattern47_2))\n            {\n                emojiFile = sm2[1];\n            }\n            else\n            {\n                static int uniqueFileName = 1000000000;\n                emojiFile = std::to_string(uniqueFileName++);\n            }\n            \n            std::string emojiPath = (DIR_ASSETS DIR_SEP_STR \"Emoji\" DIR_SEP_STR);\n            std::string emojiUrlPath = (DIR_ASSETS \"/Emoji/\");\n            std::string localEmojiPath = normalizePath((const std::string&)emojiPath);\n            std::string localEmojiFile = localEmojiPath + emojiFile + \".gif\";\n            ensureDirectoryExisted(combinePath(m_outputPath, session.getOutputFileName(), localEmojiPath));\n            \n#ifdef USING_DOWNLOADER\n            m_downloader.addTask(url, combinePath(m_outputPath, session.getOutputFileName(), localEmojiFile), msg.createTime, \"emoji\");\n#else\n            m_taskManager.download(&session, url, \"\", combinePath(m_outputPath, session.getOutputFileName(), localEmojiFile), msg.createTime, \"\", \"emoji\");\n#endif\n            \n            tv[\"%%EMOJIPATH%%\"] = emojiUrlPath + emojiFile + \".gif\";\n            tv[\"%%RAWEMOJIPATH%%\"] = url;\n        }\n        else\n        {\n            tv[\"%%EMOJIPATH%%\"] = url;\n            tv[\"%%RAWEMOJIPATH%%\"] = url;\n        }\n    }\n    else\n    {\n        tv.setName(\"msg\");\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Emoji]\");\n    }\n    \n    return true;\n}\n\nbool MessageParser::parseAppMsg(const WXMSG& msg, const Session& session, std::string& senderId, std::string& forwardedMsg, std::string& forwardedMsgTitle, TemplateValues& tv) const\n{\n    WXAPPMSG appMsg = {&msg, 0, std::string(), std::string(), senderId};\n    XmlParser xmlParser(msg.content, true);\n    if (senderId.empty())\n    {\n        xmlParser.parseNodeValue(\"/msg/fromusername\", senderId);\n    }\n    \n    std::string appMsgTypeStr;\n    if (!xmlParser.parseNodeValue(\"/msg/appmsg/type\", appMsgTypeStr))\n    {\n        // Failed to parse APPMSG type\n#ifndef NDEBUG\n        writeFile(combinePath(m_outputPath, \"../dbg\", \"msg\" + std::to_string(msg.type) + \"_app_invld_\" + msg.msgId + \".txt\"), msg.content);\n#endif\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Link]\");\n        return true;\n    }\n\n    tv.setName(\"plainshare\");\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg\" + std::to_string(msg.type) + \"_app_\" + appMsgTypeStr + \".txt\"), msg.content);\n#endif\n    appMsg.appMsgType = std::atoi(appMsgTypeStr.c_str());\n    xmlParser.parseAttributeValue(\"/msg/appmsg\", \"appid\", appMsg.appId);\n    if (!appMsg.appId.empty())\n    {\n        xmlParser.parseNodeValue(\"/msg/appinfo/appname\", appMsg.appName);\n        tv[\"%%APPNAME%%\"] = appMsg.appName;\n        std::string vFile = combinePath(m_userBase, \"appicon\", appMsg.appId + \".png\");\n        std::string portraitDir = (DIR_ASSETS DIR_SEP_STR \"Portrait\");\n\n        if (m_iTunesDb.copyFile(vFile, combinePath(m_outputPath, session.getOutputFileName(), portraitDir), \"appicon_\" + appMsg.appId + \".png\"))\n        {\n            std::string portraitUrlDir = (DIR_ASSETS \"/Portrait\");\n            appMsg.localAppIcon = portraitUrlDir + \"/appicon_\" + appMsg.appId + \".png\";\n            tv[\"%%APPICONPATH%%\"] = appMsg.localAppIcon;\n        }\n    }\n\n    bool res = false;\n    switch (appMsg.appMsgType)\n    {\n        case APPMSGTYPE_TEXT: // 1\n            res = parseAppMsgText(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_IMG: // 2\n            res = parseAppMsgImage(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_AUDIO: // 3\n            res = parseAppMsgAudio(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_VIDEO: // 4\n            res = parseAppMsgVideo(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_URL: // 5\n            res = parseAppMsgUrl(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_ATTACH: // 6\n            res = parseAppMsgAttachment(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_OPEN: // 7\n            res = parseAppMsgOpen(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_EMOJI: // 8\n            res = parseAppMsgEmoji(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_VOICE_REMIND: // 9\n            res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_SCAN_GOOD: // 10\n            res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_GOOD: // 13\n            res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_EMOTION: // 15\n            res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_CARD_TICKET: // 16\n            res = parseAppMsgDefault(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_REALTIME_LOCATION: // 17\n            res = parseAppMsgRtLocation(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_FWD_MSG: // 19\n            res = parseAppMsgFwdMsg(appMsg, xmlParser, session, forwardedMsg, forwardedMsgTitle, tv);\n            break;\n        case APPMSGTYPE_NOTE:    // 24\n            res = parseAppMsgNote(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_CHANNEL_CARD:   // 50\n            res = parseAppMsgChannelCard(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_CHANNELS:   // 51\n            res = parseAppMsgChannels(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_REFER:  // 57\n            res = parseAppMsgRefer(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_PAT:    // 62\n            res = parseAppMsgPat(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_TRANSFERS: // 2000\n            res = parseAppMsgTransfer(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_RED_ENVELOPES: // 2001\n            res = parseAppMsgRedPacket(appMsg, xmlParser, session, tv);\n            break;\n        case APPMSGTYPE_READER_TYPE: // 100001\n            res = parseAppMsgReaderType(appMsg, xmlParser, session, tv);\n            break;\n        default:\n            res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv);\n            break;\n    }\n    \n#ifndef NDEBUG\n    if (m_resManager.hasEmojiTag(tv[\"%%MESSAGE%%\"]))\n    {\n        writeFile(combinePath(m_outputPath, \"../dbg\", \"wxemoji_app_\" + std::to_string(msg.type) + \"_\" + std::to_string(appMsg.appMsgType) + \".txt\"), tv[\"%%MESSAGE%%\"] + \"\\r\\n\\r\\n\");\n        appendFile(combinePath(m_outputPath, \"../dbg\", \"wxemoji_app_\" + std::to_string(msg.type) + \"_\" + std::to_string(appMsg.appMsgType) + \".txt\"), msg.content);\n    }\n#endif\n    \n#ifndef NDEBUG\n    std::string vThumbFile = m_userBase + \"/OpenData/\" + session.getHash() + \"/\" + appMsg.msg->msgId + \".pic_thum\";\n    std::string destPath = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS, appMsg.msg->msgId + \"_thum.jpg\");\n    \n    std::string fileId = m_iTunesDb.findFileId(vThumbFile);\n    if (!fileId.empty() && !existsFile(destPath))\n    {\n        if (appMsg.appMsgType != 19)\n        {\n            // assert(false);\n        }\n    }\n#endif\n    \n    return res;\n}\n\nbool MessageParser::parseCall(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n    tv.setName(\"msg\");\n    tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Video/Audio Call]\");\n    return true;\n}\n\nbool MessageParser::parseLocation(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n    std::map<std::string, std::string> attrs = { {\"x\", \"\"}, {\"y\", \"\"}, {\"label\", \"\"}, {\"poiname\", \"\"} };\n    \n    XmlParser xmlParser(msg.content);\n    xmlParser.parseAttributesValue(\"/msg/location\", attrs);\n    \n    std::string location = (!attrs[\"poiname\"].empty() && !attrs[\"label\"].empty()) ? (attrs[\"poiname\"] + \" - \" + attrs[\"label\"]) : (attrs[\"poiname\"] + attrs[\"label\"]);\n    if (!location.empty())\n    {\n        tv[\"%%MESSAGE%%\"] = formatString(m_resManager.getLocaleString(\"[Location] %s (%s,%s)\"), location.c_str(), attrs[\"x\"].c_str(), attrs[\"y\"].c_str());\n    }\n    else\n    {\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Location]\");\n    }\n    tv.setName(\"msg\");\n    \n    return true;\n}\n\nbool MessageParser::parseStatusNotify(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(msg.type) + msg.msgId + \".txt\"), msg.content);\n#endif\n    return parseText(msg, session, tv);\n}\n\nbool MessageParser::parsePossibleFriend(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(msg.type) + msg.msgId + \".txt\"), msg.content);\n#endif\n    return parseText(msg, session, tv);\n}\n\nbool MessageParser::parseVerification(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(msg.type) + msg.msgId + \".txt\"), msg.content);\n#endif\n    return parseText(msg, session, tv);\n}\n\nbool MessageParser::parseCard(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n    std::string portraitDir = (DIR_ASSETS DIR_SEP_STR \"Portrait\");\n    std::string portraitUrlDir = (DIR_ASSETS \"/Portrait\");\n    return parseCard(session, m_outputPath, portraitDir, portraitUrlDir, msg.content, tv);\n}\n\nbool MessageParser::parseNotice(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(msg.type) + msg.msgId + \".txt\"), msg.content);\n#endif\n    tv.setName(\"notice\");\n\n    Json::CharReaderBuilder builder;\n    std::unique_ptr<Json::CharReader> reader(builder.newCharReader());\n\n    Json::Value root;\n    if (reader->parse(msg.content.c_str(), msg.content.c_str() + msg.content.size(), &root, NULL))\n    {\n        tv[\"%%MESSAGE%%\"] = root[\"msgContent\"].asString();\n    }\n    return true;\n}\n\nbool MessageParser::parseSysNotice(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(msg.type) + msg.msgId + \".txt\"), msg.content);\n#endif\n    tv.setName(\"notice\");\n    std::string sysMsg = msg.content;\n    removeHtmlTags(sysMsg);\n    tv[\"%%MESSAGE%%\"] = sysMsg;\n    return true;\n}\n\nbool MessageParser::parseSystem(const WXMSG& msg, const Session& session, TemplateValues& tv) const\n{\n    tv.setName(\"notice\");\n    if (startsWith(msg.content, \"<sysmsg\"))\n    {\n        XmlParser xmlParser(msg.content, true);\n        std::string sysMsgType;\n        xmlParser.parseAttributeValue(\"/sysmsg\", \"type\", sysMsgType);\n        if (sysMsgType == \"sysmsgtemplate\")\n        {\n            std::string plainText;\n            std::string templateContent;\n            std::string templateType;\n            xmlParser.parseAttributeValue(\"/sysmsg/sysmsgtemplate/content_template\", \"type\", templateType);\n#ifndef NDEBUG\n            writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(msg.type) + \"_\" + sysMsgType + \".txt\"), msg.content);\n#endif\n            if (startsWith(templateType, \"tmpl_type_profile\") || templateType == \"tmpl_type_admin_explain\" || templateType == \"new_tmpl_type_succeed_contact\")\n            {\n                // tmpl_type_profilewithrevokeqrcode\n                // tmpl_type_profilewithrevoke\n                // tmpl_type_profile\n                xmlParser.parseNodeValue(\"/sysmsg/sysmsgtemplate/content_template/plain\", plainText);\n                if (plainText.empty())\n                {\n                    xmlParser.parseNodeValue(\"/sysmsg/sysmsgtemplate/content_template/template\", templateContent);\n                    WechatTemplateHandler handler(xmlParser, templateContent);\n                    if (xmlParser.parseWithHandler(\"/sysmsg/sysmsgtemplate/content_template/link_list/link\", handler))\n                    {\n                        tv[\"%%MESSAGE%%\"] = handler.getText();\n                    }\n                }\n                else\n                {\n                    tv[\"%%MESSAGE%%\"] = msg.content;\n                }\n            }\n            else\n            {\n#ifndef NDEBUG\n                writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(msg.type) + \"_\" + sysMsgType + \".txt\"), msg.content);\n                assert(false);\n#endif\n            }\n        }\n        else if (sysMsgType == \"editrevokecontent\")\n        {\n            std::string content;\n            xmlParser.parseNodeValue(\"/sysmsg/\" + sysMsgType + \"/text\", content);\n            tv[\"%%MESSAGE%%\"] = content;\n        }\n        else if (sysMsgType == \"paymsg\")\n        {\n            std::string content;\n            xmlParser.parseNodeValue(\"/sysmsg/\" + sysMsgType + \"/appmsgcontent\", content);\n            content = decodeUrl(content);\n            removeHtmlTags(content);\n            tv[\"%%MESSAGE%%\"] = content;\n        }\n        else\n        {\n            // Try to find plain first\n            std::string plainText;\n            if (xmlParser.parseNodeValue(\"/sysmsg/\" + sysMsgType + \"/plain\", plainText) && !plainText.empty())\n            {\n                tv[\"%%MESSAGE%%\"] = plainText;\n            }\n            else\n            {\n#ifndef NDEBUG\n                writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(msg.type) + \"_\" + sysMsgType + \".txt\"), msg.content);\n                assert(false);\n#endif\n            }\n        }\n    }\n    else if (startsWith(msg.content, \"<_wc_custom_link_\"))\n    {\n        // <_wc_custom_link_ href=\"weixin://me/profile/mypatsuffix\">设置拍一拍</_wc_custom_link_>，朋友拍了拍你的头像后会出现设置的内容。\n        \n        std::string plainText = msg.content;\n        removeHtmlTags(plainText);\n        tv[\"%%MESSAGE%%\"] = plainText;\n#ifndef NDEBUG\n        plainText.clear();\n        auto pos = msg.content.find(\"</_wc_custom_link_>\");\n        if (pos != std::string::npos)\n        {\n            auto pos2 = msg.content.find(\">\", 17);    // length of <_wc_custom_link_\n            if (pos2 != std::string::npos)\n            {\n                plainText = msg.content.substr(pos2 + 1, pos - (pos2 + 1));\n            }\n            plainText += msg.content.substr(pos + 19);   //\n        }\n        if (plainText != tv[\"%%MESSAGE%%\"])\n        {\n            // int aa = 0;\n        }\n#endif\n    }\n    else\n    {\n#ifndef NDEBUG\n        if (startsWith(msg.content, \"<\") && !startsWith(msg.content, \"<img\"))\n        {\n            writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(msg.type) + \"_unkwn_fmt_\" + msg.msgId + \".txt\"), msg.content);\n            assert(false);\n        }\n#endif\n        // Plain Text\n        std::string sysMsg = msg.content;\n        removeHtmlTags(sysMsg);\n        tv[\"%%MESSAGE%%\"] = sysMsg;\n    }\n    \n    return true;\n}\n\n////////////////////////////////\n\nbool MessageParser::parseAppMsgText(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    std::string title;\n    xmlParser.parseNodeValue(\"/msg/appmsg/title\", title);\n    xmlParser.parseNodeValue(\"/msg/appmsg/title\", title);\n    tv[\"%%MESSAGE%%\"] = title.empty() ? m_resManager.getLocaleString(\"[Link]\") : title;\n    \n    return true;\n}\n\nbool MessageParser::parseAppMsgImage(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    return parseAppMsgDefault(appMsg, xmlParser, session, tv);\n}\n\nbool MessageParser::parseAppMsgAudio(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    return parseAppMsgDefault(appMsg, xmlParser, session, tv);\n}\n\nbool MessageParser::parseAppMsgVideo(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    return parseAppMsgDefault(appMsg, xmlParser, session, tv);\n}\n\nbool MessageParser::parseAppMsgEmotion(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    return parseAppMsgDefault(appMsg, xmlParser, session, tv);\n}\n\nbool MessageParser::parseAppMsgUrl(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    std::string title;\n    std::string desc;\n    std::string url;\n    std::string thumbUrl;\n    xmlParser.parseNodeValue(\"/msg/appmsg/title\", title);\n    xmlParser.parseNodeValue(\"/msg/appmsg/des\", desc);\n    xmlParser.parseNodeValue(\"/msg/appmsg/url\", url);\n    \n    // Check Local File\n    std::string vThumbFile = m_userBase + \"/OpenData/\" + session.getHash() + \"/\" + appMsg.msg->msgId + \".pic_thum\";\n    std::string destPath = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS);\n    if (m_iTunesDb.copyFile(vThumbFile, destPath, appMsg.msg->msgId + \"_thum.jpg\"))\n    {\n        thumbUrl = (DIR_ASSETS \"/\") + appMsg.msg->msgId + \"_thum.jpg\";\n    }\n    else\n    {\n        xmlParser.parseNodeValue(\"/msg/appmsg/thumburl\", thumbUrl);\n        if (thumbUrl.empty())\n        {\n            thumbUrl = appMsg.localAppIcon;\n        }\n    }\n    \n    tv.setName(thumbUrl.empty() ? \"plainshare\" : \"share\");\n    tv[\"%%SHARINGIMGPATH%%\"] = thumbUrl;\n    tv[\"%%SHARINGTITLE%%\"] = title;\n    tv[\"%%SHARINGURL%%\"] = url;\n    tv[\"%%MESSAGE%%\"] = desc;\n    \n    return true;\n}\n\nbool MessageParser::parseAppMsgAttachment(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg_\" + std::to_string(appMsg.msg->type) + \"_attach_\" + appMsg.msg->msgId + \".txt\"), appMsg.msg->content);\n#endif\n    std::string title;\n    std::string attachFileExtName;\n    xmlParser.parseNodeValue(\"/msg/appmsg/title\", title);\n    xmlParser.parseNodeValue(\"/msg/appmsg/appattach/fileext\", attachFileExtName);\n    \n    std::string attachFileName = m_userBase + \"/OpenData/\" + session.getHash() + \"/\" + appMsg.msg->msgId;\n    std::string attachOutputFileName = appMsg.msg->msgId;\n    if (!attachFileExtName.empty())\n    {\n        attachFileName += \".\" + attachFileExtName;\n        attachOutputFileName += \".\" + attachFileExtName;\n    }\n    return parseFile(combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS, attachFileName, attachOutputFileName, title, tv);\n}\n\nbool MessageParser::parseAppMsgOpen(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    return parseAppMsgDefault(appMsg, xmlParser, session, tv);\n}\n\nbool MessageParser::parseAppMsgEmoji(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    // Can't parse the detail info of emoji as the url is encrypted\n    return parseAppMsgDefault(appMsg, xmlParser, session, tv);\n}\n\nbool MessageParser::parseAppMsgRtLocation(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Real-time Location]\");\n    return true;\n}\n\nbool MessageParser::parseAppMsgFwdMsg(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, std::string& forwardedMsg, std::string& forwardedMsgTitle, TemplateValues& tv) const\n{\n    std::string title;\n    xmlParser.parseNodeValue(\"/msg/appmsg/title\", title);\n    xmlParser.parseNodeValue(\"/msg/appmsg/recorditem\", forwardedMsg);\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg\" + std::to_string(appMsg.msg->type) + \"_app_19.txt\"), forwardedMsg);\n#endif\n    tv.setName(\"msg\");\n    tv[\"%%MESSAGE%%\"] = title;\n\n    forwardedMsgTitle = title;\n    return true;\n}\n\nbool MessageParser::parseAppMsgCard(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    return parseAppMsgDefault(appMsg, xmlParser, session, tv);\n}\n\nbool MessageParser::parseAppMsgChannelCard(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    // Channel Card\n    std::map<std::string, std::string> nodes = { {\"username\", \"\"}, {\"avatar\", \"\"}, {\"nickname\", \"\"}};\n    xmlParser.parseNodesValue(\"/msg/appmsg/findernamecard/*\", nodes);\n    \n    std::string portraitDir = (DIR_ASSETS DIR_SEP_STR \"Portrait\");\n    std::string portraitUrlDir = (DIR_ASSETS \"/Portrait\");\n    \n    return parseChannelCard(session, portraitDir, portraitUrlDir, nodes[\"username\"], nodes[\"avatar\"], \"\", nodes[\"nickname\"], tv);\n}\n\nbool MessageParser::parseAppMsgChannels(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg\" + std::to_string(appMsg.msg->type) + \"_app_\" + std::to_string(appMsg.appMsgType) + \"_\" + appMsg.msg->msgId + \".txt\"), appMsg.msg->content);\n#endif\n    \n    return parseChannels(appMsg.msg->msgId, xmlParser, NULL, \"/msg/appmsg/finderFeed\", session, tv);\n}\n\nbool MessageParser::parseAppMsgRefer(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    // Refer Message\n    std::string title;\n    xmlParser.parseNodeValue(\"/msg/appmsg/title\", title);\n    std::map<std::string, std::string> nodes = { {\"displayname\", \"\"}, {\"content\", \"\"}, {\"type\", \"\"}};\n    if (xmlParser.parseNodesValue(\"/msg/appmsg/refermsg/*\", nodes))\n    {\n#ifndef NDEBUG\n        writeFile(combinePath(m_outputPath, \"../dbg\", \"msg\" + std::to_string(appMsg.msg->type) + \"_app_\" + std::to_string(APPMSGTYPE_REFER) + \"_ref_\" + nodes[\"type\"] + \" .txt\"), nodes[\"content\"]);\n#endif\n        tv.setName(\"refermsg\");\n        tv[\"%%MESSAGE%%\"] = m_resManager.convertEmojis(title, combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS);\n        tv[\"%%REFERNAME%%\"] = nodes[\"displayname\"];\n        if (nodes[\"type\"] == \"43\")\n        {\n            tv[\"%%REFERMSG%%\"] = m_resManager.getLocaleString(\"[Video]\");\n        }\n        else if (nodes[\"type\"] == \"1\")\n        {\n            if (!m_options.isTextMode())\n            {\n                tv[\"%%REFERMSG%%\"] = m_resManager.convertEmojis(nodes[\"content\"], combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS);\n            }\n            else\n            {\n                tv[\"%%REFERMSG%%\"] = nodes[\"content\"];\n            }\n        }\n        else if (nodes[\"type\"] == \"3\")\n        {\n            tv[\"%%REFERMSG%%\"] = m_resManager.getLocaleString(\"[Photo]\");\n        }\n        else if (nodes[\"type\"] == \"49\")\n        {\n            // APPMSG\n            XmlParser subAppMsgXmlParser(nodes[\"content\"], true);\n            std::string subAppMsgTitle;\n            subAppMsgXmlParser.parseNodeValue(\"/msg/appmsg/title\", subAppMsgTitle);\n            tv[\"%%REFERMSG%%\"] = subAppMsgTitle;\n        }\n        else\n        {\n            tv[\"%%REFERMSG%%\"] = nodes[\"content\"];\n        }\n    }\n    else\n    {\n        tv.setName(\"msg\");\n        tv[\"%%MESSAGE%%\"] = title;\n    }\n    \n    return true;\n}\n\nbool MessageParser::parseAppMsgTransfer(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n#ifndef NDEBUG\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg\" + std::to_string(appMsg.msg->type) + \"_app_transfer_\" + appMsg.msg->msgId + \".xml\"), appMsg.msg->content);\n#endif\n    std::map<std::string, std::string> nodes = { {\"title\", \"\"}, {\"type\", \"\"}, {\"des\", \"\"}, {\"url\", \"\"}, {\"thumburl\", \"\"}, {\"recorditem\", \"\"} };\n    xmlParser.parseNodesValue(\"/msg/appmsg/*\", nodes);\n    \n    std::map<std::string, std::string> transferNode = { {\"paysubtype\", \"\"}, {\"feedesc\", \"\"}, {\"pay_memo\", \"\"}, {\"receiver_username\", \"\"}, {\"payer_username\", \"\"} };\n    xmlParser.parseNodesValue(\"/msg/appmsg/wcpayinfo/*\", transferNode);\n    \n    // paysubtype:\n    //\n    // 3,4 received receive rejected\n    // 8,9 send send rejected\n    std::string content = transferNode[\"feedesc\"];\n    const std::string& memo = transferNode[\"pay_memo\"];\n    const std::string& paySubType = transferNode[\"paysubtype\"];\n    std::string paySubTypeKey = (session.isChatroom() ? \"Group_Transfer_Subtype_\" : \"Transfer_Subtype_\") + paySubType;\n    std::string paySubTypeDesc = m_resManager.getLocaleString(paySubTypeKey);\n\n    if (paySubTypeDesc == paySubTypeKey)\n    {\n        paySubTypeDesc = \"\";\n    }\n    else\n    {\n        if (session.isChatroom())\n        {\n            if (paySubType == \"1\" || paySubType == \"8\" || paySubType == \"9\")\n            {\n                // Pay to\n                std::string displayName = getMemberDisplayName(transferNode[\"receiver_username\"], session);\n                paySubTypeDesc = formatString(paySubTypeDesc, displayName.c_str());\n            }\n            else if (paySubType == \"3\" || paySubType == \"4\" || paySubType == \"5\")\n            {\n                // 3 Accepted\n                // 5 Accepted and waiting for ...\n                // From\n                std::string displayName = getMemberDisplayName(transferNode[\"payer_username\"], session);\n                paySubTypeDesc = formatString(paySubTypeDesc, displayName.c_str());\n            }\n        }\n    }\n    if (!paySubTypeDesc.empty())\n    {\n        content += \" - \";\n        content += paySubTypeDesc;\n    }\n    if (!memo.empty())\n    {\n        content += \"\\r\\n\";\n        content += memo;\n    }\n    tv[\"%%MESSAGE%%\"] = content;\n    \n    tv.setName(\"transfer\");\n    return true;\n}\n\nbool MessageParser::parseAppMsgRedPacket(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Red Packet]\");\n    return true;\n}\n\nbool MessageParser::parseAppMsgPat(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    // int recordNum = 0;\n    // xmlParser.parseNodeValue(\"/msg/appmsg/patMsg/records/record/fromUser\", fromUser);\n    \n    std::string fromUser;\n    std::string temp;\n    std::string pattedUser;\n    xmlParser.parseNodeValue(\"/msg/appmsg/patMsg/records/record/fromUser\", fromUser);\n    xmlParser.parseNodeValue(\"/msg/appmsg/patMsg/records/record/templete\", temp);\n    xmlParser.parseNodeValue(\"/msg/appmsg/patMsg/records/record/pattedUser\", pattedUser);\n    \n    std::string fromUserName;\n    std::string pattedUserName;\n    \n    Friend* f = m_friends.getFriendByUid(fromUser);\n    if (NULL != f)\n    {\n        fromUserName = f->getDisplayName();\n    }\n    else\n    {\n        fromUserName = session.getMemberName(fromUser);\n    }\n    \n    f = m_friends.getFriendByUid(pattedUser);\n    if (NULL != f)\n    {\n        pattedUserName = f->getDisplayName();\n    }\n    else\n    {\n        pattedUserName = session.getMemberName(pattedUser);\n    }\n    \n    replaceAll(temp, \"${\" + fromUser + \"}\", fromUserName);\n    replaceAll(temp, \"${\" + pattedUser + \"}\", pattedUserName);\n    \n    tv[\"%%MESSAGE%%\"] = temp;\n    \n    tv.setName(\"system\");\n    \n    return true;\n}\n\nbool MessageParser::parseAppMsgReaderType(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    return parseAppMsgDefault(appMsg, xmlParser, session, tv);\n}\n\nbool MessageParser::parseAppMsgNote(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg\" + std::to_string(appMsg.msg->type) + \"_app_24_\" + appMsg.msg->msgId + \".txt\"), appMsg.msg->content);\n#endif\n    std::map<std::string, std::string> nodes = { {\"title\", \"\"}, {\"type\", \"\"}, {\"des\", \"\"}, {\"url\", \"\"}, {\"thumburl\", \"\"}, {\"recorditem\", \"\"} };\n    xmlParser.parseNodesValue(\"/msg/appmsg/*\", nodes);\n    removeSupportUrl(nodes[\"url\"]);\n    if (nodes[\"title\"].empty() && !nodes[\"des\"].empty())\n    {\n        nodes[\"title\"].swap(nodes[\"des\"]);\n    }\n    \n    if (!nodes[\"recorditem\"].empty())\n    {\n        XmlParser xmlParser2(nodes[\"recorditem\"], true);\n        \n        std::map<std::string, std::string> nodes2 = { {\"info\", \"\"}, {\"desc\", \"\"}, {\"edittime\", \"\"}, {\"favusername\", \"\"} };\n        xmlParser2.parseNodesValue(\"/recordinfo/*\", nodes2);\n        \n        if (!nodes2[\"info\"].empty())\n        {\n            nodes[\"title\"] = nodes2[\"info\"];\n        }\n        else if (!nodes2[\"desc\"].empty())\n        {\n            nodes[\"title\"] = nodes2[\"desc\"];\n        }\n    }\n\n    if (!nodes[\"title\"].empty() && !nodes[\"url\"].empty())\n    {\n        bool isPlainShare = nodes[\"thumburl\"].empty();\n        tv.setName(isPlainShare ? \"plainshare\" : \"share\");\n\n        tv[\"%%SHARINGIMGPATH%%\"] = nodes[\"thumburl\"];\n        tv[\"%%SHARINGURL%%\"] = nodes[\"url\"];\n        tv[\"%%SHARINGTITLE%%\"] = nodes[\"title\"];\n        tv[\"%%MESSAGE%%\"] = nodes[\"des\"];\n    }\n    else if (!nodes[\"title\"].empty())\n    {\n        tv[\"%%MESSAGE%%\"] = nodes[\"title\"];\n    }\n    else\n    {\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Link]\");\n    }\n    \n    return true;\n}\n\nbool MessageParser::parseAppMsgUnknownType(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    writeFile(combinePath(m_outputPath, \"../dbg\", \"msg\" + std::to_string(appMsg.msg->type) + \"_app_unknwn_\" + std::to_string(appMsg.appMsgType) + \".txt\"), appMsg.msg->content);\n#endif\n    return parseAppMsgDefault(appMsg, xmlParser, session, tv);\n}\n\nbool MessageParser::parseAppMsgDefault(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const\n{\n    std::map<std::string, std::string> nodes = { {\"title\", \"\"}, {\"type\", \"\"}, {\"des\", \"\"}, {\"url\", \"\"}, {\"thumburl\", \"\"}, {\"recorditem\", \"\"} };\n    xmlParser.parseNodesValue(\"/msg/appmsg/*\", nodes);\n    \n    if (!nodes[\"title\"].empty() && !nodes[\"url\"].empty())\n    {\n        tv.setName(nodes[\"thumburl\"].empty() ? \"plainshare\" : \"share\");\n\n        tv[\"%%SHARINGIMGPATH%%\"] = nodes[\"thumburl\"];\n        tv[\"%%SHARINGURL%%\"] = nodes[\"url\"];\n        tv[\"%%SHARINGTITLE%%\"] = nodes[\"title\"];\n        tv[\"%%MESSAGE%%\"] = nodes[\"des\"];\n    }\n    else if (!nodes[\"title\"].empty())\n    {\n        tv[\"%%MESSAGE%%\"] = nodes[\"title\"];\n    }\n    else\n    {\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Link]\");\n    }\n    \n    return true;\n}\n\n////////////////////////////////\n\nbool MessageParser::parseFwdMsgText(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNode *itemNode, const Session& session, TemplateValues& tv) const\n{\n    std::string message;\n    xmlParser.getChildNodeContent(itemNode, \"datadesc\", message);\n    static std::vector<std::pair<std::string, std::string>> replaces = { {\"\\r\\n\", \"<br />\"}, {\"\\r\", \"<br />\"}, {\"\\n\", \"<br />\"}};\n    replaceAll(message, replaces);\n    tv[\"%%MESSAGE%%\"] = message;\n    \n    return true;\n}\n\nbool MessageParser::parseFwdMsgImage(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNode *itemNode, const Session& session, TemplateValues& tv) const\n{\n    std::string fileExtName = fwdMsg.dataFormat.empty() ? \"\" : (\".\" + fwdMsg.dataFormat);\n    std::string vfile = m_userBase + \"/OpenData/\" + session.getHash() + \"/\" + fwdMsg.msg->msgId + \"/\" + fwdMsg.dataId;\n    return parseImage(combinePath(m_outputPath, session.getOutputFileName()), (DIR_ASSETS \"/\") + fwdMsg.msg->msgId, (DIR_ASSETS \"/\") + fwdMsg.msg->msgId, vfile + fileExtName, vfile + fileExtName + \"_pre3\", fwdMsg.dataId + \".jpg\", vfile + \".record_thumb\", fwdMsg.dataId + \"_thumb.jpg\", tv);\n}\n\nbool MessageParser::parseFwdMsgVideo(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const\n{\n    std::string fileExtName = fwdMsg.dataFormat.empty() ? \"\" : (\".\" + fwdMsg.dataFormat);\n    std::string vfile = m_userBase + \"/OpenData/\" + session.getHash() + \"/\" + fwdMsg.msg->msgId + \"/\" + fwdMsg.dataId;\n    return parseVideo(combinePath(m_outputPath, session.getOutputFileName()), (DIR_ASSETS \"/\") + fwdMsg.msg->msgId, (DIR_ASSETS \"/\") + fwdMsg.msg->msgId, vfile + fileExtName, \"\", fwdMsg.dataId + fileExtName, vfile + \".record_thumb\", fwdMsg.dataId + \"_thumb.jpg\", \"\", \"\", tv);\n}\n\nbool MessageParser::parseFwdMsgLink(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const\n{\n    std::string link;\n    std::string title;\n    std::string thumbUrl;\n    std::string message;\n    xmlNodePtr urlItemNode = xmlParser.getChildNode(itemNode, \"weburlitem\");\n    if (NULL != urlItemNode)\n    {\n        XmlParser::getChildNodeContent(urlItemNode, \"title\", title);\n        XmlParser::getChildNodeContent(urlItemNode, \"link\", link);\n        XmlParser::getChildNodeContent(urlItemNode, \"thumburl\", thumbUrl);\n        XmlParser::getChildNodeContent(urlItemNode, \"desc\", message);\n    }\n\n    bool hasThumb = false;\n    if (!m_options.isTextMode())\n    {\n        std::string vfile = m_userBase + \"/OpenData/\" + session.getHash() + \"/\" + fwdMsg.msg->msgId + \"/\" + fwdMsg.dataId + \".record_thumb\";\n        hasThumb = m_iTunesDb.copyFile(vfile, combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS, fwdMsg.msg->msgId), fwdMsg.dataId + \"_thumb.jpg\");\n    }\n    \n    if (!(link.empty()))\n    {\n        tv.setName(hasThumb ? \"share\" : \"plainshare\");\n\n        tv[\"%%SHARINGIMGPATH%%\"] = (DIR_ASSETS \"/\") + fwdMsg.msg->msgId + \"/\" + fwdMsg.dataId + \"_thumb.jpg\";\n        tv[\"%%SHARINGURL%%\"] = link;\n        tv[\"%%SHARINGTITLE%%\"] = title;\n        tv[\"%%MESSAGE%%\"] = message;\n    }\n    else\n    {\n        tv[\"%%MESSAGE%%\"] = title;\n    }\n    \n    return true;\n}\n\nbool MessageParser::parseFwdMsgLocation(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const\n{\n    std::string label;\n    std::string message;\n    std::string lng;\n    std::string lat;\n    xmlNodePtr locItemNode = xmlParser.getChildNode(itemNode, \"locitem\");\n    if (NULL != locItemNode)\n    {\n        XmlParser::getChildNodeContent(locItemNode, \"label\", label);\n        XmlParser::getChildNodeContent(locItemNode, \"poiname\", message);\n        XmlParser::getChildNodeContent(locItemNode, \"lat\", lat);\n        XmlParser::getChildNodeContent(locItemNode, \"lng\", lng);\n    }\n\n    std::string location = (!message.empty() && !label.empty()) ? (message + \" - \" + label) : (message + label);\n    if (!location.empty())\n    {\n        tv[\"%%MESSAGE%%\"] = formatString(m_resManager.getLocaleString(\"[Location] %s (%s,%s)\"), location.c_str(), lat.c_str(), lng.c_str());\n    }\n    else\n    {\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Location]\");\n    }\n    tv.setName(\"msg\");\n    \n    return true;\n}\n\nbool MessageParser::parseFwdMsgAttach(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const\n{\n    std::string message;\n    xmlParser.getChildNodeContent(itemNode, \"datatitle\", message);\n    \n    std::string fileExtName = fwdMsg.dataFormat.empty() ? \"\" : (\".\" + fwdMsg.dataFormat);\n    std::string vfile = m_userBase + \"/OpenData/\" + session.getHash() + \"/\" + fwdMsg.msg->msgId + \"/\" + fwdMsg.dataId;\n    return parseFile(combinePath(m_outputPath, session.getOutputFileName()), (DIR_ASSETS \"/\") + fwdMsg.msg->msgId, (DIR_ASSETS \"/\") + fwdMsg.msg->msgId, vfile + fileExtName, fwdMsg.dataId + fileExtName, message, tv);\n}\n\nbool MessageParser::parseFwdMsgCard(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const\n{\n    std::string cardContent;\n    xmlParser.getChildNodeContent(itemNode, \"datadesc\", cardContent);\n    \n    std::string portraitDir = (DIR_ASSETS DIR_SEP_STR \"Portrait\");\n    std::string portraitUrlDir = (DIR_ASSETS \"/Portrait\");\n    return parseCard(session, m_outputPath, portraitDir, portraitUrlDir, cardContent, tv);\n}\n\nbool MessageParser::parseFwdMsgNestedFwdMsg(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, std::string& nestedFwdMsg, std::string& nestedFwdMsgTitle, TemplateValues& tv) const\n{\n    xmlParser.getChildNodeContent(itemNode, \"datadesc\", nestedFwdMsgTitle);\n    xmlNodePtr nodeRecordInfo = XmlParser::getChildNode(itemNode, \"recordinfo\");\n    if (NULL != nodeRecordInfo)\n    {\n        nestedFwdMsg = XmlParser::getNodeOuterXml(nodeRecordInfo);\n    }\n    \n    tv[\"%%MESSAGE%%\"] = nestedFwdMsgTitle;\n    \n    return true;\n}\n\nbool MessageParser::parseFwdMsgMiniProgram(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const\n{\n    std::string title;\n    xmlParser.getChildNodeContent(itemNode, \"datatitle\", title);\n    tv[\"%%MESSAGE%%\"] = title;\n    \n    return true;\n}\n\nbool MessageParser::parseFwdMsgChannels(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const\n{\n    return parseChannels(fwdMsg.msg->msgId, xmlParser, itemNode, \"./finderFeed\", session, tv);\n}\n\nbool MessageParser::parseFwdMsgChannelCard(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const\n{\n    std::string usrName;\n    std::string avatar;\n    std::string nickName;\n    xmlNodePtr cardItemNode = xmlParser.getChildNode(itemNode, \"finderShareNameCard\");\n    if (NULL != cardItemNode)\n    {\n        XmlParser::getChildNodeContent(cardItemNode, \"username\", usrName);\n        XmlParser::getChildNodeContent(cardItemNode, \"avatar\", avatar);\n        XmlParser::getChildNodeContent(cardItemNode, \"nickname\", nickName);\n    }\n    \n    std::string portraitDir = (DIR_ASSETS DIR_SEP_STR \"Portrait\");\n    std::string portraitUrlDir = (DIR_ASSETS \"/Portrait\");\n    return parseChannelCard(session, portraitDir, portraitUrlDir, usrName, avatar, \"\", nickName, tv);\n}\n\n///////////////////////////////\n// Implementation\n\nbool MessageParser::parseVideo(const std::string& sessionPath, const std::string& sessionAssetsPath, const std::string& sessionAssetsUrlPath, const std::string& srcVideo, const std::string& srcRawVideo, const std::string& destVideo, const std::string& srcThumb, const std::string& destThumb, const std::string& width, const std::string& height, TemplateValues& tv) const\n{\n    bool hasThumb = false;\n    bool hasVideo = false;\n    \n    if (!m_options.isTextMode())\n    {\n        std::string fullAssetsPath = combinePath(sessionPath, sessionAssetsPath);\n        ensureDirectoryExisted(fullAssetsPath);\n        hasThumb = m_iTunesDb.copyFile(srcThumb, combinePath(fullAssetsPath, destThumb));\n        if (!srcRawVideo.empty())\n        {\n            hasVideo = m_iTunesDb.copyFile(srcRawVideo, combinePath(fullAssetsPath, destVideo), true);\n        }\n        if (!hasVideo)\n        {\n            hasVideo = m_iTunesDb.copyFile(srcVideo, combinePath(fullAssetsPath, destVideo));\n        }\n    }\n\n    if (hasVideo)\n    {\n        tv.setName(\"video\");\n        tv[\"%%THUMBPATH%%\"] = hasThumb ? (sessionAssetsUrlPath + \"/\" + destThumb) : \"\";\n        tv[\"%%VIDEOPATH%%\"] = sessionAssetsUrlPath + \"/\" + destVideo;\n        tv[\"%%MSGTYPE%%\"] = \"video\";\n    }\n    else if (hasThumb)\n    {\n        tv.setName(\"thumb\");\n        tv[\"%%IMGTHUMBPATH%%\"] = sessionAssetsUrlPath + \"/\" + destThumb;\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"(Video Missed)\");\n    }\n    else\n    {\n        tv.setName(\"msg\");\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Video]\");\n    }\n    \n    tv[\"%%VIDEOWIDTH%%\"] = width;\n    tv[\"%%VIDEOHEIGHT%%\"] = height;\n    \n    return true;\n}\n\nbool MessageParser::parseImage(const std::string& sessionPath, const std::string& sessionAssetsPath, const std::string& sessionAssetsUrlPath, const std::string& src, const std::string& srcHdOrPre, const std::string& dest, const std::string& srcThumb, const std::string& destThumb, TemplateValues& tv) const\n{\n    bool hasThumb = false;\n    bool hasImage = false;\n    if (!m_options.isTextMode())\n    {\n        std::string fullAssetsPath = combinePath(sessionPath, sessionAssetsPath);\n        hasThumb = m_iTunesDb.copyFile(srcThumb, fullAssetsPath, destThumb);\n        if (!srcHdOrPre.empty())\n        {\n            hasImage = m_iTunesDb.copyFile(srcHdOrPre, fullAssetsPath, dest);\n        }\n        if (!hasImage)\n        {\n            hasImage = m_iTunesDb.copyFile(src, fullAssetsPath, dest);\n        }\n    }\n\n    if (hasImage)\n    {\n        tv.setName(\"image\");\n        tv[\"%%IMGPATH%%\"] = sessionAssetsUrlPath + \"/\" + dest;\n        // If it is PDF mode, use the raw image directly for print quaility\n        tv[\"%%IMGTHUMBPATH%%\"] = sessionAssetsUrlPath + \"/\" + (((!hasThumb) || m_options.isPdfMode()) ? dest : destThumb);\n        tv[\"%%MSGTYPE%%\"] = \"image\";\n        tv[\"%%EXTRA_CLS%%\"] = \"raw-img\";\n    }\n    else if (hasThumb)\n    {\n        tv.setName(\"thumb\");\n        tv[\"%%IMGTHUMBPATH%%\"] = sessionAssetsUrlPath + \"/\" + destThumb;\n        tv[\"%%MESSAGE%%\"] = \"\";\n        tv[\"%%MSGTYPE%%\"] = \"image\";\n    }\n    else\n    {\n        tv.setName(\"msg\");\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Photo]\");\n    }\n    \n    return true;\n}\n\nbool MessageParser::parseFile(const std::string& sessionPath, const std::string& sessionAssetsPath, const std::string& sessionAssetsUrlPath, const std::string& src, const std::string& dest, const std::string& fileName, TemplateValues& tv) const\n{\n    bool hasFile = false;\n    if (!m_options.isTextMode())\n    {\n        hasFile = m_iTunesDb.copyFile(src, combinePath(sessionPath, sessionAssetsPath), dest);\n    }\n\n    if (hasFile)\n    {\n        tv.setName(\"plainshare\");\n        tv[\"%%SHARINGURL%%\"] = sessionAssetsUrlPath + \"/\" + dest;\n        tv[\"%%SHARINGTITLE%%\"] = fileName;\n        tv[\"%%MESSAGE%%\"] = \"\";\n        tv[\"%%MSGTYPE%%\"] = \"file\";\n    }\n    else\n    {\n        tv.setName(\"msg\");\n        tv[\"%%MESSAGE%%\"] = formatString(m_resManager.getLocaleString(\"[File: %s]\"), fileName.c_str());\n    }\n    \n    return true;\n}\n\nbool MessageParser::parseCard(const Session& session, const std::string& sessionPath, const std::string& portraitDir, const std::string& portraitUrlDir, const std::string& cardMessage, TemplateValues& tv) const\n{\n    // static std::regex pattern42_1(\"nickname ?= ?\\\"(.+?)\\\"\");\n    // static std::regex pattern42_2(\"smallheadimgurl ?= ?\\\"(.+?)\\\"\");\n    std::map<std::string, std::string> attrs;\n    if (!m_options.isTextMode())\n    {\n        attrs = { {\"nickname\", \"\"}, {\"smallheadimgurl\", \"\"}, {\"bigheadimgurl\", \"\"}, {\"username\", \"\"} };\n    }\n    else\n    {\n        attrs = { {\"nickname\", \"\"}, {\"username\", \"\"} };\n    }\n\n    tv[\"%%CARDTYPE%%\"] = m_resManager.getLocaleString(\"[Contact Card]\");\n    XmlParser xmlParser(cardMessage, true);\n    if (xmlParser.parseAttributesValue(\"/msg\", attrs) && !attrs[\"nickname\"].empty())\n    {\n        std::string portraitUrl = attrs[\"bigheadimgurl\"].empty() ? attrs[\"smallheadimgurl\"] : attrs[\"bigheadimgurl\"];\n        bool hasPortrait = !attrs[\"bigheadimgurl\"].empty() || !attrs[\"smallheadimgurl\"].empty();\n        if (!attrs[\"username\"].empty() && hasPortrait)\n        {\n            tv.setName(\"card\");\n            // Some username is too long to be created on windows, have to use its md5 string\n\t\t\tstd::string imgFileName = startsWith(attrs[\"username\"], \"wxid_\") ? attrs[\"username\"] : md5(attrs[\"username\"]);\n            tv[\"%%CARDNAME%%\"] = attrs[\"nickname\"];\n            tv[\"%%CARDIMGPATH%%\"] = portraitUrlDir + \"/\" + imgFileName + \".jpg\";\n\t\t\tstd::string localPortraitDir = combinePath(session.getOutputFileName(), normalizePath(portraitDir));\n            std::string localFile = combinePath(localPortraitDir, imgFileName + \".jpg\");\n            ensureDirectoryExisted(combinePath(sessionPath, localPortraitDir));\n\t\t\tstd::string output = combinePath(sessionPath, localFile);\n#ifdef USING_DOWNLOADER\n            m_downloader.addTask(portraitUrl, combinePath(sessionPath, localFile), 0, \"card\");\n#else\n            m_taskManager.download(&session, attrs[\"bigheadimgurl\"], attrs[\"smallheadimgurl\"], combinePath(sessionPath, localFile), 0, \"\", \"card\");\n#endif\n        }\n        else if (!attrs[\"nickname\"].empty())\n        {\n            tv[\"%%MESSAGE%%\"] = formatString(m_resManager.getLocaleString(\"[Contact Card] %s\"), attrs[\"nickname\"].c_str());\n        }\n        else\n        {\n            tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Contact Card]\");\n        }\n    }\n    else\n    {\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Contact Card]\");\n    }\n    tv[\"%%EXTRA_CLS%%\"] = \"contact-card\";\n    \n    return true;\n}\n\nbool MessageParser::parseChannelCard(const Session& session, const std::string& portraitDir, const std::string& portraitUrlDir, const std::string& usrName, const std::string& avatar, const std::string& avatarLD, const std::string& name, TemplateValues& tv) const\n{\n    bool hasImg = false;\n    if (!m_options.isTextMode())\n    {\n        hasImg = (!usrName.empty() && !avatar.empty());\n    }\n    tv[\"%%CARDTYPE%%\"] = m_resManager.getLocaleString(\"[Channel Card]\");\n    if (!name.empty())\n    {\n        if (hasImg)\n        {\n            tv.setName(\"card\");\n            tv[\"%%CARDNAME%%\"] = name;\n            tv[\"%%CARDIMGPATH%%\"] = portraitUrlDir + \"/\" + usrName + \".jpg\";\n\t\t\tstd::string localPortraitDir = normalizePath(portraitDir);\n            std::string localFile = combinePath(localPortraitDir, usrName + \".jpg\");\n            ensureDirectoryExisted(combinePath(m_outputPath, session.getOutputFileName(), localPortraitDir));\n#ifdef USING_DOWNLOADER\n            m_downloader.addTask(avatar, combinePath(m_outputPath, localfile), 0, \"card\");\n#else\n            m_taskManager.download(&session, avatar, avatarLD, combinePath(m_outputPath, localFile), 0, \"\", \"card\");\n#endif\n        }\n        else\n        {\n            tv.setName(\"msg\");\n            tv[\"%%MESSAGE%%\"] = formatString(m_resManager.getLocaleString(\"[Channel Card] %s\"), name.c_str());\n        }\n    }\n    else\n    {\n        tv[\"%%MESSAGE%%\"] = m_resManager.getLocaleString(\"[Channel Card]\");\n    }\n    tv[\"%%EXTRA_CLS%%\"] = \"channel-card\";\n    \n    return true;\n}\n\nbool MessageParser::parseChannels(const std::string& msgId, const XmlParser& xmlParser, xmlNodePtr parentNode, const std::string& finderFeedXPath, const Session& session, TemplateValues& tv) const\n{\n    // Channels SHI PIN HAO\n    std::map<std::string, std::string> nodes = { {\"objectId\", \"\"}, {\"nickname\", \"\"}, {\"avatar\", \"\"}, {\"desc\", \"\"}, {\"mediaCount\", \"\"}, {\"feedType\", \"\"}, {\"desc\", \"\"}, {\"username\", \"\"}};\n    std::map<std::string, std::string> videoNodes = { {\"mediaType\", \"\"}, {\"url\", \"\"}, {\"thumbUrl\", \"\"}, {\"coverUrl\", \"\"}, {\"videoPlayDuration\", \"\"}};\n    \n    if (NULL == parentNode)\n    {\n        xmlParser.parseNodesValue(finderFeedXPath + \"/*\", nodes);\n        xmlParser.parseNodesValue(finderFeedXPath + \"/mediaList/media/*\", videoNodes);\n    }\n    else\n    {\n        xmlParser.parseChildNodesValue(parentNode, finderFeedXPath + \"/*\", nodes);\n        xmlParser.parseChildNodesValue(parentNode, finderFeedXPath + \"/mediaList/media/*\", videoNodes);\n    }\n#ifndef NDEBUG\n    if (nodes[\"mediaCount\"] == \"\")\n    {\n        int aa = 0;\n    }\n#endif\n    std::string thumbUrl;\n    if (!m_options.isTextMode())\n    {\n        thumbUrl = videoNodes[\"thumbUrl\"].empty() ? videoNodes[\"coverUrl\"] : videoNodes[\"thumbUrl\"];\n    }\n    \n    const std::string portraitDir = (DIR_ASSETS DIR_SEP_STR \"Portrait\");\n    const std::string portraitUrlDir = (DIR_ASSETS \"/Portrait\");\n    \n    tv[\"%%CARDNAME%%\"] = nodes[\"nickname\"];\n    tv[\"%%CHANNELS%%\"] = m_resManager.getLocaleString(\"Channels\");\n    tv[\"%%MESSAGE%%\"] = nodes[\"desc\"];\n    tv[\"%%EXTRA_CLS%%\"] = \"channels\";\n    \n    if (!thumbUrl.empty())\n    {\n        tv.setName(\"channels\");\n        tv[\"%%MSGTYPE%%\"] = \"channels\";\n        std::string thumbFile = (DIR_ASSETS \"/\") + msgId + \".jpg\";\n        std::string localThumbFile = (DIR_ASSETS DIR_SEP_STR) + msgId + \".jpg\";\n        tv[\"%%CHANNELTHUMBPATH%%\"] = thumbFile;\n        ensureDirectoryExisted(combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS));\n\n#ifdef USING_DOWNLOADER\n        m_downloader.addTask(thumbUrl, combinePath(m_outputPath, session.getOutputFileName(), localThumbFile), 0, \"thumb\");\n#else\n        m_taskManager.download(&session, thumbUrl, \"\", combinePath(m_outputPath, session.getOutputFileName(), localThumbFile), 0, \"\", \"thumb\");\n#endif\n        \n        if (!nodes[\"avatar\"].empty())\n        {\n            std::string fileName = nodes[\"username\"].empty() ? nodes[\"objectId\"] : nodes[\"username\"];\n            tv[\"%%CARDIMGPATH%%\"] = portraitUrlDir + \"/\" + fileName + \".jpg\";\n\t\t\tstd::string localPortraitDir = normalizePath(portraitDir);\n            std::string localFile = combinePath(localPortraitDir, fileName + \".jpg\");\n            ensureDirectoryExisted(combinePath(m_outputPath, session.getOutputFileName(), localPortraitDir));\n#ifdef USING_DOWNLOADER\n            m_downloader.addTask(nodes[\"avatar\"], combinePath(m_outputPath, session.getOutputFileName(), localFile), 0, \"card\");\n#else\n            m_taskManager.download(&session, nodes[\"avatar\"], \"\", combinePath(m_outputPath, session.getOutputFileName(), localFile), 0, \"\", \"card\");\n#endif\n        }\n\n        tv[\"%%CHANNELURL%%\"] = videoNodes[\"url\"];\n    }\n    \n    return true;\n}\n\nbool MessageParser::parseForwardedMsgs(const Session& session, const WXMSG& msg, const std::string& title, const std::string& message, std::vector<TemplateValues>& tvs) const\n{\n    std::string portraitPath = (DIR_ASSETS DIR_SEP_STR \"Portrait\" DIR_SEP_STR);\n    std::string portraitUrlPath = (DIR_ASSETS \"/Portrait/\");\n    \n    tvs.push_back(TemplateValues(\"notice\"));\n    TemplateValues& beginTv = tvs.back();\n    beginTv[\"%%MESSAGE%%\"] = formatString(m_resManager.getLocaleString(\"<< %s\"), title.c_str());\n    beginTv[\"%%EXTRA_CLS%%\"] = \"fmsgtag\";   // tag for forwarded msg\n    \n    XmlParser xmlParser(message);\n    XmlParser::XPathEnumerator enumerator(xmlParser, \"/recordinfo/datalist/dataitem\");\n    while (enumerator.hasNext())\n    {\n        xmlNodePtr node = enumerator.nextNode();\n        if (NULL != node)\n        {\n            WXFWDMSG fmsg = { &msg };\n\n            XmlParser::getNodeAttributeValue(node, \"datatype\", fmsg.dataType);\n            XmlParser::getNodeAttributeValue(node, \"dataid\", fmsg.dataId);\n            XmlParser::getNodeAttributeValue(node, \"subtype\", fmsg.subType);\n            \n            XmlParser::getChildNodeContent(node, \"sourcename\", fmsg.displayName);\n            XmlParser::getChildNodeContent(node, \"sourcetime\", fmsg.msgTime);\n            XmlParser::getChildNodeContent(node, \"srcMsgCreateTime\", fmsg.srcMsgTime);\n            XmlParser::getChildNodeContent(node, \"datafmt\", fmsg.dataFormat);\n            xmlNodePtr srcNode = XmlParser::getChildNode(node, \"dataitemsource\");\n            if (NULL != srcNode)\n            {\n                if (!XmlParser::getChildNodeContent(srcNode, \"realchatname\", fmsg.usrName))\n                {\n                    XmlParser::getChildNodeContent(srcNode, \"fromusr\", fmsg.usrName);\n                }\n            }\n            \n#if !defined(NDEBUG) || defined(DBG_PERF)\n            fmsg.rawMessage = xmlParser.getNodeOuterXml(node);\n            writeFile(combinePath(m_outputPath, \"../dbg\", \"fwdmsg_\" + fmsg.dataType + \".txt\"), fmsg.rawMessage);\n#endif\n            TemplateValues& tv = *(tvs.emplace(tvs.end(), \"msg\"));\n            tv[\"%%ALIGNMENT%%\"] = \"left\";\n            tv[\"%%EXTRA_CLS%%\"] = \"fmsg\";   // forwarded msg\n            \n            std::string nestedFwdMsgTitle;\n            std::string nestedFwdMsg;\n            int dataType = fmsg.dataType.empty() ? 0 : std::atoi(fmsg.dataType.c_str());\n            switch (dataType)\n            {\n                case FWDMSG_DATATYPE_TEXT:  // 1\n                    parseFwdMsgText(fmsg, xmlParser, node, session, tv);\n                    break;\n                case FWDMSG_DATATYPE_IMAGE: // 2\n                    parseFwdMsgImage(fmsg, xmlParser, node, session, tv);\n                    break;\n                case FWDMSG_DATATYPE_VIDEO: // 4\n                    parseFwdMsgVideo(fmsg, xmlParser, node, session, tv);\n                    break;\n                case FWDMSG_DATATYPE_LINK: //\n                    parseFwdMsgLink(fmsg, xmlParser, node, session, tv);\n                    break;\n                case FWDMSG_DATATYPE_LOCATION: //\n                    parseFwdMsgLocation(fmsg, xmlParser, node, session, tv);\n                    break;\n                case FWDMSG_DATATYPE_ATTACH: //\n                    parseFwdMsgAttach(fmsg, xmlParser, node, session, tv);\n                    break;\n                case FWDMSG_DATATYPE_CARD: //\n                    parseFwdMsgCard(fmsg, xmlParser, node, session, tv);\n                    break;\n                case FWDMSG_DATATYPE_NESTED_FWD_MSG: //\n                    parseFwdMsgNestedFwdMsg(fmsg, xmlParser, node, session, nestedFwdMsg, nestedFwdMsgTitle, tv);\n                    break;\n                case FWDMSG_DATATYPE_MINI_PROGRAM: //   19\n                    parseFwdMsgMiniProgram(fmsg, xmlParser, node, session, tv);\n                    break;\n                case FWDMSG_DATATYPE_CHANNELS: //   22\n                    parseFwdMsgChannels(fmsg, xmlParser, node, session, tv);\n                    break;\n                case FWDMSG_DATATYPE_CHANNEL_CARD: //    26\n                    parseFwdMsgChannelCard(fmsg, xmlParser, node, session, tv);\n                    break;\n                default:\n                    parseFwdMsgText(fmsg, xmlParser, node, session, tv);\n#if !defined(NDEBUG) || defined(DBG_PERF)\n                    writeFile(combinePath(m_outputPath, \"../dbg\", \"fwdmsg_unknwn_\" + fmsg.dataType + \".txt\"), fmsg.rawMessage);\n#endif\n                    break;\n            }\n            \n            tv[\"%%NAME%%\"] = fmsg.displayName;\n            tv[\"%%MSGID%%\"] = msg.msgId + \"_\" + fmsg.dataId;\n            tv[\"%%TIME%%\"] = fmsg.srcMsgTime.empty() ? fmsg.msgTime : fromUnixTime(static_cast<unsigned int>(std::atoi(fmsg.srcMsgTime.c_str())));\n\n            // std::string localPortrait;\n            // bool hasPortrait = false;\n            // localPortrait = combinePath(portraitPath, fmsg.usrName + \".jpg\");\n            if (copyPortraitIcon(&session, fmsg.usrName, fmsg.portrait, fmsg.portraitLD, combinePath(m_outputPath, session.getOutputFileName(), portraitPath)))\n            {\n                tv[\"%%AVATAR%%\"] = portraitUrlPath + \"/\" + fmsg.usrName + \".jpg\";\n            }\n            else\n            {\n                ensureDefaultPortraitIconExisted(portraitPath);\n                tv[\"%%AVATAR%%\"] = portraitUrlPath + \"DefaultAvatar.png\";\n            }\n\n            if ((dataType == FWDMSG_DATATYPE_NESTED_FWD_MSG) && !nestedFwdMsg.empty())\n            {\n                parseForwardedMsgs(session, msg, nestedFwdMsgTitle, nestedFwdMsg, tvs);\n            }\n        }\n    }\n    \n    tvs.push_back(TemplateValues(\"notice\"));\n    TemplateValues& endTv = tvs.back();\n    endTv[\"%%MESSAGE%%\"] = formatString(m_resManager.getLocaleString(\"%s Ends >>\"), title.c_str());\n    endTv[\"%%EXTRA_CLS%%\"] = \"fmsgtag\";   // tag for forwarded msg\n    \n    return true;\n}\n\n/////////////////////////////\n\n\n/////////////////////////\n\nstd::string MessageParser::getDisplayTime(int ms) const\n{\n    if (ms < 1000) return \"1\\\"\";\n    ms /= 1000;\n    if (ms < 60) return std::to_string(ms) + \"\\\"\";\n    int seconds = ms % 60;\n    return seconds != 0 ? (std::to_string((int)(std::round((double)ms / 60))) + \"\\'\" + std::to_string(seconds) + \"\\\"\") : (std::to_string((int)(std::round((double)ms / 60))) + \"\\'\");\n}\n\nbool MessageParser::copyPortraitIcon(const Session* session, const std::string& usrName, const std::string& portraitUrl, const std::string& portraitUrlLD, const std::string& destPath) const\n{\n    return copyPortraitIcon(session, usrName, md5(usrName), portraitUrl, portraitUrlLD, destPath);\n}\n\nbool MessageParser::copyPortraitIcon(const Session* session, const Friend& f, const std::string& destPath) const\n{\n    return copyPortraitIcon(session, f.getUsrName(), f.getHash(), f.getPortrait(), f.getSecondaryPortrait(), destPath);\n}\n\nbool MessageParser::copyPortraitIcon(const Session* session, const std::string& usrName, const std::string& usrNameHash, const std::string& portraitUrl, const std::string& portraitUrlLD, const std::string& destPath) const\n{\n    std::string destFileName = usrName + \".jpg\";\n    std::string destFullPath = combinePath(destPath, destFileName);\n    if (existsFile(destFullPath))\n    {\n        return true;\n    }\n    \n    if (!existsDirectory(destPath))\n    {\n        makeDirectory(destPath);\n    }\n    \n    bool hasPortrait = false;\n    std::string avatarPath = \"share/\" + m_myself.getHash() + \"/session/headImg/\" + usrNameHash + \".pic\";\n    const ITunesFile* file = m_iTunesDbShare.findITunesFile(avatarPath);\n    if (NULL != file)\n    {\n        ITunesDb::parseFileInfo(file);\n        \n        std::string srcPath = m_iTunesDbShare.getRealPath(*file);\n        if (!srcPath.empty() && !Friend::isDefaultAvatar(file->size, srcPath))\n        {\n            normalizePath(srcPath);\n            \n            bool result = ::copyFile(srcPath, destFullPath, true);\n            if (result)\n            {\n                if (file->modifiedTime != 0)\n                {\n                    updateFileTime(destFullPath, static_cast<time_t>(file->modifiedTime));\n                }\n                else if (!file->blob.empty())\n                {\n                    updateFileTime(destFullPath, ITunesDb::parseModifiedTime(file->blob));\n                }\n                hasPortrait = true;\n            }\n        }\n    }\n\n    // bool hasPortrait = m_iTunesDbShare.copyFile(avatarPath, destPath, destFileName);\n    if (!hasPortrait)\n    {\n        if (portraitUrl.empty() && portraitUrlLD.empty())\n        {\n            const Friend *f = (m_myself.getUsrName() == usrName) ? &m_myself : m_friends.getFriend(usrNameHash);\n            if (NULL != f)\n            {\n                std::string url = f->getPortrait();\n                std::string urlLD = f->getSecondaryPortrait();\n                if (!url.empty() || !urlLD.empty())\n                {\n\t\t\t\t\tstd::string localDestPath = normalizePath(destPath);\n#ifdef USING_DOWNLOADER\n                    m_downloader.addTask(url, combinePath(localDestPath, destFileName), 0, \"avatar\");\n#else\n                    m_taskManager.download(session, url, urlLD, combinePath(localDestPath, destFileName), 0, m_resManager.getDefaultAvatarPath(), \"avatar\");\n#endif\n                    hasPortrait = true;\n                }\n            }\n        }\n        else\n        {\n\t\t\tstd::string localDestPath = normalizePath(destPath);\n#ifdef USING_DOWNLOADER\n            m_downloader.addTask(portraitUrl, combinePath(localDestPath, destFileName), 0, \"avatar\");\n#else\n            m_taskManager.download(session, portraitUrl, portraitUrlLD, combinePath(localDestPath, destFileName), 0, m_resManager.getDefaultAvatarPath(), \"avatar\");\n#endif\n            hasPortrait = true;\n        }\n    }\n    \n    if (!hasPortrait)\n    {\n        std::string localDestPath = normalizePath(destPath);\n        hasPortrait = ::copyFile(m_resManager.getDefaultAvatarPath(), combinePath(localDestPath, destFileName));\n    }\n    \n    return hasPortrait;\n}\n\nvoid MessageParser::ensureDefaultPortraitIconExisted(const std::string& portraitPath) const\n{\n    std::string dest = combinePath(m_outputPath, portraitPath);\n    ensureDirectoryExisted(dest);\n    dest = combinePath(dest, \"DefaultAvatar.png\");\n    if (!existsFile(dest))\n    {\n        copyFile(combinePath(m_resPath, \"res\", \"DefaultAvatar.png\"), dest, false);\n    }\n}\n\nbool MessageParser::removeSupportUrl(std::string& url)\n{\n    if (startsWith(url, \"https://support.weixin.qq.com\"))\n    {\n        url.clear();\n        return true;\n    }\n    \n    return false;\n}\n\nstd::string MessageParser::getMemberDisplayName(const std::string& usrName, const Session& session) const\n{\n    std::string displayName;\n    if (session.isChatroom())\n    {\n        // Should fix the priority later:\n        // 1 nick name of chatroom\n        // 2 nick name of friend\n        // 3: display name in chatroom\n        // 4: display name of friend\n        \n        displayName = session.getMemberName(usrName);\n        if (displayName.empty())\n        {\n            const Friend *f = usrName == m_myself.getUsrName() ? &m_myself : m_friends.getFriend(md5(usrName));\n            if (NULL != f)\n            {\n                displayName = f->getDisplayName();\n            }\n            else\n            {\n                displayName = usrName;\n            }\n        }\n    }\n    else\n    {\n        if (usrName == m_myself.getUsrName())\n        {\n            displayName = m_myself.getDisplayName();\n        }\n        else if (usrName == session.getUsrName())\n        {\n            displayName = session.getDisplayName();\n        }\n        else\n        {\n            const Friend *f = m_friends.getFriend(md5(usrName));\n            if (NULL != f)\n            {\n                displayName = f->getDisplayName();\n            }\n            else\n            {\n                displayName = usrName;\n            }\n        }\n    }\n    \n    return displayName;\n}\n"
  },
  {
    "path": "WechatExporter/core/MessageParser.h",
    "content": "//\n//  MessageParser.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/2/22.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef MessageParser_h\n#define MessageParser_h\n\n#include <string>\n#ifndef NDEBUG\n#include <cassert>\n#endif\n#include \"WechatObjects.h\"\n#include \"ExportOption.h\"\n#include \"ResManager.h\"\n#include \"Downloader.h\"\n#include \"TaskManager.h\"\n#include \"ITunesParser.h\"\n#include \"FileSystem.h\"\n#include \"XmlParser.h\"\n#include \"Utils.h\"\n\nclass TemplateValues\n{\nprivate:\n    std::map<std::string, std::string> m_values;\n    std::string m_name;\n    \npublic:\n    using const_iterator = std::map<std::string, std::string>::const_iterator;\n\npublic:\n    TemplateValues()\n    {\n    }\n    TemplateValues(const std::string& name) : m_name(name)\n    {\n    }\n    std::string getName() const\n    {\n        return m_name;\n    }\n    void setName(const std::string& name)\n    {\n        m_name = name;\n    }\n    std::string& operator[](const std::string& k)\n    {\n        return m_values[k];\n    }\n    bool hasValue(const std::string& key) const\n    {\n        return m_values.find(key) != m_values.cend();\n    }\n    \n    const_iterator cbegin() const\n    {\n        return m_values.cbegin();\n    }\n    \n    const_iterator cend() const\n    {\n        return m_values.cend();\n    }\n    \n    void clear()\n    {\n        m_values.clear();\n    }\n    \n    void clearName()\n    {\n        m_name.clear();\n    }\n    \n    const std::map<std::string, std::string>& getValues() const\n    {\n        return m_values;\n    }\n};\n\nstruct WechatTemplateHandler\n{\n    XmlParser& m_xmlParser;\n    std::string m_templateText;\n\n    WechatTemplateHandler(XmlParser& xmlParser, const std::string& templateText) : m_xmlParser(xmlParser), m_templateText(templateText)\n    {\n    }\n    \n    std::string getText() const\n    {\n        return m_templateText;\n    }\n    \n    bool operator() (xmlNodeSetPtr xpathNodes)\n    {\n        std::string linkName;\n        std::string linkType;\n        std::string linkHidden;\n        \n        for (int idx = 0; idx < xpathNodes->nodeNr; ++idx)\n        {\n            xmlNode *cur = xpathNodes->nodeTab[idx];\n            \n            XmlParser::getNodeAttributeValue(cur, \"name\", linkName);\n            XmlParser::getNodeAttributeValue(cur, \"type\", linkType);\n            XmlParser::getNodeAttributeValue(cur, \"hidden\", linkHidden);\n            \n            std::string linkValue;\n            if (linkHidden == \"1\")\n            {\n                replaceAll(m_templateText, \"$\" + linkName + \"$\", linkValue);\n                continue;\n            }\n            \n            if (linkType == \"link_plain\")\n            {\n                XmlParser::getChildNodeContent(cur, \"plain\", linkValue);\n            }\n            else if (linkType == \"link_profile\")\n            {\n                xmlNodePtr nodeMemberList = XmlParser::getChildNode(cur, \"memberlist\");\n                std::vector<std::string> linkValues;\n                if (NULL != nodeMemberList)\n                {\n                    xmlNodePtr nodeMember = XmlParser::getChildNode(nodeMemberList, \"member\");\n                    while (NULL != nodeMember)\n                    {\n                        XmlParser::getChildNodeContent(nodeMember, \"nickname\", linkValue);\n                        linkValues.push_back(linkValue);\n                        nodeMember = XmlParser::getNextNodeSibling(nodeMember);\n                    }\n                }\n                std::string linkSep;\n                XmlParser::getChildNodeContent(cur, \"separator\", linkSep);\n                \n                linkValue = join(linkValues, linkSep.c_str());\n            }\n            else if (linkType == \"link_admin_explain\")\n            {\n                XmlParser::getChildNodeContent(cur, \"title\", linkValue);\n            }\n            else if (linkType == \"link_revoke_qrcode\")\n            {\n                XmlParser::getChildNodeContent(cur, \"title\", linkValue);\n            }\n            else if (linkType == \"new_link_succeed_contact\")\n            {\n                XmlParser::getChildNodeContent(cur, \"title\", linkValue);\n            }\n            else\n            {\n                // Try to find value of title\n                if (!XmlParser::getChildNodeContent(cur, \"title\", linkValue))\n                {\n#ifndef NDEBUG\n                    assert(false);\n#endif\n                }\n            }\n            replaceAll(m_templateText, \"$\" + linkName + \"$\", linkValue);\n            \n        }\n        \n        return true;\n    }\n};\n\nclass MessageParser\n{\npublic:\n    static const int MSGTYPE_TEXT = 1;\n    static const int MSGTYPE_IMAGE = 3;\n    static const int MSGTYPE_VOICE = 34;\n    static const int MSGTYPE_PUSHMAIL = 35;\n    static const int MSGTYPE_VERIFYMSG = 37;\n    static const int MSGTYPE_POSSIBLEFRIEND = 40;\n    static const int MSGTYPE_SHARECARD = 42;\n    static const int MSGTYPE_VIDEO = 43;\n    static const int MSGTYPE_EMOTICON = 47;\n    static const int MSGTYPE_LOCATION = 48;\n    static const int MSGTYPE_APP = 49;\n    static const int MSGTYPE_VOIPMSG = 50;\n    static const int MSGTYPE_VOIPNOTIFY = 52;\n    static const int MSGTYPE_VOIPINVITE = 53;\n    \n    static const int MSGTYPE_STATUSNOTIFY = 51;\n    \n    static const int MSGTYPE_MICROVIDEO = 62;\n    \n    static const int MSGTYPE_NOTICE = 64;\n    \n    static const int MSGTYPE_IMCARD = 66;\n    \n    // Transfer          = 2000, // 转账\n    //   RedEnvelope       = 2001, // 红包\n    //   MiniProgram       = 2002, // 小程序\n    //   GroupInvite       = 2003, // 群邀请\n    //  File              = 2004, // 文件消息\n    \n    static const int MSGTYPE_SYSNOTICE = 9999;\n    static const int MSGTYPE_SYS = 10000;\n    static const int MSGTYPE_RECALLED = 10002;\n\n    static const int APPMSGTYPE_TEXT = 1;\n    static const int APPMSGTYPE_IMG = 2;\n    static const int APPMSGTYPE_AUDIO = 3;\n    static const int APPMSGTYPE_VIDEO = 4;\n    static const int APPMSGTYPE_URL = 5;\n    static const int APPMSGTYPE_ATTACH = 6;\n    static const int APPMSGTYPE_OPEN = 7;\n    static const int APPMSGTYPE_EMOJI = 8;\n    static const int APPMSGTYPE_VOICE_REMIND = 9;\n    static const int APPMSGTYPE_SCAN_GOOD = 10;\n    static const int APPMSGTYPE_GOOD = 13;\n    static const int APPMSGTYPE_EMOTION = 15;\n    static const int APPMSGTYPE_CARD_TICKET = 16;\n    static const int APPMSGTYPE_REALTIME_LOCATION = 17;\n    static const int APPMSGTYPE_FWD_MSG = 19;\n    static const int APPMSGTYPE_NOTE = 24;\n    static const int APPMSGTYPE_CHANNEL_CARD = 50;\n    static const int APPMSGTYPE_CHANNELS = 51;\n    static const int APPMSGTYPE_REFER = 57;\n    static const int APPMSGTYPE_PAT = 62;\n    static const int APPMSGTYPE_TRANSFERS = 2000;\n    static const int APPMSGTYPE_RED_ENVELOPES = 2001;\n    static const int APPMSGTYPE_READER_TYPE = 100001;\n    \n    static const int FWDMSG_DATATYPE_TEXT = 1;\n    static const int FWDMSG_DATATYPE_IMAGE = 2;\n    static const int FWDMSG_DATATYPE_VIDEO = 4;\n    static const int FWDMSG_DATATYPE_LINK = 5;\n    static const int FWDMSG_DATATYPE_LOCATION = 6;\n    static const int FWDMSG_DATATYPE_ATTACH = 8;\n    static const int FWDMSG_DATATYPE_CARD = 16;\n    static const int FWDMSG_DATATYPE_NESTED_FWD_MSG = 17;\n    static const int FWDMSG_DATATYPE_MINI_PROGRAM = 19;\n    static const int FWDMSG_DATATYPE_CHANNELS = 22;\n    static const int FWDMSG_DATATYPE_CHANNEL_CARD = 26;\n    \n    MessageParser(const ITunesDb& iTunesDb, const ITunesDb& iTunesDbShare, TaskManager& taskManager, Friends& friends, Friend myself, const ExportOption& options, const std::string& resPath, const std::string& outputPath, const ResManager& resManager);\n    \n    bool parse(WXMSG& msg, const Session& session, std::vector<TemplateValues>& tvs) const;\n    \n    bool copyPortraitIcon(const Session* session, const std::string& usrName, const std::string& portraitUrl, const std::string& portraitUrlLD, const std::string& destPath) const;\n    bool copyPortraitIcon(const Session* session, const std::string& usrName, const std::string& usrNameHash, const std::string& portraitUrl, const std::string& portraitUrlLD, const std::string& destPath) const;\n    bool copyPortraitIcon(const Session* session, const Friend& f, const std::string& destPath) const;\n    \n    std::string getError() const\n    {\n        return m_error;\n    }\n    bool hasError() const\n    {\n        return !m_error.empty();\n    }\nprotected:\n    \n    bool parsePortrait(const WXMSG& msg, const Session& session, const std::string& senderId, TemplateValues& tv) const;\n    \n    bool parseText(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parseImage(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parseVoice(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parsePushMail(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parseVideo(const WXMSG& msg, const Session& session, std::string& senderId, TemplateValues& tv) const;\n    bool parseEmotion(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsg(const WXMSG& msg, const Session& session, std::string& senderId, std::string& fwdMsg, std::string& fwdMsgTitle, TemplateValues& tv) const;\n    bool parseCall(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parseLocation(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parseStatusNotify(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parsePossibleFriend(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parseVerification(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parseCard(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    bool parseNotice(const WXMSG& msg, const Session& session, TemplateValues& tv) const;  // 64\n    bool parseSysNotice(const WXMSG& msg, const Session& session, TemplateValues& tv) const;   // 9999\n    bool parseSystem(const WXMSG& msg, const Session& session, TemplateValues& tv) const;\n    \n    // APP MSG\n    bool parseAppMsgText(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgImage(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgAudio(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgVideo(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgEmotion(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgUrl(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgAttachment(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgOpen(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgEmoji(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgRtLocation(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgFwdMsg(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, std::string& fwdMsg, std::string& fwdMsgTitle, TemplateValues& tv) const;\n    bool parseAppMsgCard(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgChannelCard(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgChannels(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgRefer(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgTransfer(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgRedPacket(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgPat(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgReaderType(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgNote(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgUnknownType(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n    bool parseAppMsgDefault(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const;\n\n    // FORWARDEWD MSG\n    bool parseFwdMsgText(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    bool parseFwdMsgImage(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    bool parseFwdMsgVideo(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    bool parseFwdMsgLink(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    bool parseFwdMsgLocation(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    bool parseFwdMsgAttach(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    bool parseFwdMsgCard(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    bool parseFwdMsgNestedFwdMsg(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, std::string& nestedFwdMsg, std::string& nestedFwdMsgTitle, TemplateValues& tv) const;\n    bool parseFwdMsgMiniProgram(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    bool parseFwdMsgChannels(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    bool parseFwdMsgChannelCard(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const;\n    \n    // Implementation\n    bool parseImage(const std::string& sessionPath, const std::string& sessionAssetsPath, const std::string& sessionAssetsUrlPath, const std::string& src, const std::string& srcHdOrPre, const std::string& dest, const std::string& srcThumb, const std::string& destThumb, TemplateValues& tv) const;\n    bool parseVideo(const std::string& sessionPath, const std::string& sessionAssetsPath, const std::string& sessionAssetsUrlPath, const std::string& src, const std::string& srcRaw, const std::string& dest, const std::string& srcThumb, const std::string& destThumb, const std::string& width, const std::string& height, TemplateValues& tv) const;\n    bool parseFile(const std::string& sessionPath, const std::string& sessionAssetsPath, const std::string& sessionAssetsUrlPath, const std::string& src, const std::string& dest, const std::string& fileName, TemplateValues& tv) const;\n    bool parseCard(const Session& session, const std::string& sessionPath, const std::string& portraitDir, const std::string& portraitUrlDir, const std::string& cardMessage, TemplateValues& tv) const;\n    bool parseChannelCard(const Session& session, const std::string& portraitDir, const std::string& portraitUrlDir, const std::string& usrName, const std::string& avatar, const std::string& avatarLD, const std::string& name, TemplateValues& tv) const;\n    bool parseChannels(const std::string& msgId, const XmlParser& xmlParser, xmlNodePtr parentNode, const std::string& finderFeedXPath, const Session& session, TemplateValues& tv) const;\n    bool parseForwardedMsgs(const Session& session, const WXMSG& msg, const std::string& title, const std::string& message, std::vector<TemplateValues>& tvs) const;\n    \n    std::string getDisplayTime(int ms) const;\n    \n    std::string getMemberDisplayName(const std::string& usrName, const Session& session) const;\n    \n    void ensureDirectoryExisted(const std::string& path) const\n    {\n        if (!existsDirectory(path))\n        {\n            makeDirectory(path);\n        }\n    }\n    \n    void ensureDefaultPortraitIconExisted(const std::string& portraitPath) const;\n    \n    static bool removeSupportUrl(std::string& url);\nprotected:\n    const ITunesDb& m_iTunesDb;\n    const ITunesDb& m_iTunesDbShare;\n    const ResManager& m_resManager;\n    TaskManager& m_taskManager;\n\n    Friends& m_friends;\n    Friend m_myself;\n    ExportOption m_options;\n    \n    const std::string m_resPath;\n    const std::string m_outputPath;\n    std::string m_userBase;\n    mutable std::string m_error;\n\nprotected:\n#ifndef USING_ASYNC_TASK_FOR_MP3\n    mutable std::vector<unsigned char> m_pcmData;  // buffer\n#endif\n};\n\n#endif /* MessageParser_h */\n"
  },
  {
    "path": "WechatExporter/core/PdfConverter.h",
    "content": "//\n//  PdfConverter.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/4/14.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef PdfConverter_h\n#define PdfConverter_h\n\nclass PdfConverter\n{\npublic:\n    virtual bool makeUserDirectory(const std::string& dirName) = 0;\n    virtual bool convert(const std::string& htmlPath, const std::string& pdfPath) = 0;\n    virtual ~PdfConverter() {}\n};\n\n#endif /* PdfConverter_h */\n"
  },
  {
    "path": "WechatExporter/core/RawMessage.cpp",
    "content": "//\n//  RawMessage.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/10/13.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include \"RawMessage.h\"\n#ifdef _WIN32\r\n#include <atlstr.h>\r\n#endif\n\nbool convertUnknownField(const UnknownField &uf, std::string& value)\n{\n    if (uf.type() == UnknownField::TYPE_LENGTH_DELIMITED)\n    {\n        value.assign(uf.length_delimited().c_str(), uf.GetLengthDelimitedSize());\n        return true;\n    }\n    else if (uf.type() == UnknownField::TYPE_VARINT)\n    {\n        value = std::to_string(uf.varint());\n        return true;\n    }\n    else if (uf.type() == UnknownField::TYPE_FIXED32)\n    {\n        value = std::to_string(uf.fixed32());\n        return true;\n    }\n    else if (uf.type() == UnknownField::TYPE_FIXED64)\n    {\n        value = std::to_string(uf.fixed64());\n        return true;\n    }\n    \n    return false;\n}\n\nbool convertUnknownField(const UnknownField &uf, int& value)\n{\n    if (uf.type() == UnknownField::TYPE_VARINT)\n    {\n        value = static_cast<int>(uf.varint());\n        return true;\n    }\n    else if (uf.type() == UnknownField::TYPE_FIXED32)\n    {\n        value = static_cast<int>(uf.fixed32());\n        return true;\n    }\n    else if (uf.type() == UnknownField::TYPE_FIXED64)\n    {\n        value = static_cast<int>(uf.fixed64());\n        return true;\n    }\n    \n    return false;\n}\n\nstd::string RawMessage::toUtf8String(const std::string& str)\n{\n    return UnescapeCEscapeString(str);\n}\n\nRawMessage::RawMessage() : m_pool(NULL), m_factory(NULL), m_message(NULL)\n{\n}\n\nRawMessage::~RawMessage()\n{\n    release();\n}\n\nvoid RawMessage::release()\n{\n    if (NULL != m_message)\n    {\n        delete m_message;\n        m_message = NULL;\n    }\n    if (NULL != m_factory)\n    {\n        delete m_factory;\n        m_factory = NULL;\n    }\n    if (NULL != m_pool)\n    {\n        delete m_pool;\n        m_pool = NULL;\n    }\n}\n\nbool RawMessage::merge(const char *data, int length)\n{\n    release();\n    \n    m_pool = new DescriptorPool();\n    FileDescriptorProto file;\n    file.set_name(\"empty_message.proto\");\n    file.add_message_type()->set_name(\"EmptyMessage\");\n    GOOGLE_CHECK(m_pool->BuildFile(file) != NULL);\n\n    const Descriptor *descriptor = m_pool->FindMessageTypeByName(\"EmptyMessage\");\n    if (NULL == descriptor)\n    {\n        // FormatError(outputString, lengthOfOutputString, ERROR_NO_MESSAGE_TYPE, src->messageTypeName);\n        return false;\n    }\n\n    m_factory = new DynamicMessageFactory(m_pool);\n    const Message *message = m_factory->GetPrototype(descriptor);\n    if (NULL == message)\n    {\n        return false;\n    }\n    \n    m_message = message->New();\n    if (NULL == m_message)\n    {\n        return false;\n    }\n    \n    if (!m_message->ParseFromArray(reinterpret_cast<const void *>(data), length))\n    {\n        delete m_message;\n        m_message = NULL;\n        return false;\n    }\n\n    return true;\n}\n\nbool RawMessage::mergeFile(const std::string& path)\n{\n    release();\n    \n#ifdef _WIN32\n    CA2W pszW(path.c_str(), CP_UTF8);\n    std::ifstream file(pszW, std::ios::in|std::ios::binary|std::ios::ate);\n#else\n    std::ifstream file(path.c_str(), std::ios::in|std::ios::binary|std::ios::ate);\n#endif\n    if (file.is_open())\n    {\n        std::streampos size = file.tellg();\n        std::vector<char> buffer;\n        buffer.resize(size);\n        \n        file.seekg (0, std::ios::beg);\n        file.read((char *)(&buffer[0]), size);\n        file.close();\n\n        return merge(&buffer[0], static_cast<int>(size));\n    }\n    \n    return false;\n}\n\n/*\nbool RawMessage::parse(int fieldNumber, std::string& value)\n{\n    if (NULL == m_message)\n    {\n        return false;\n    }\n    \n    const Reflection* reflection = m_message->GetReflection();\n    const UnknownFieldSet& ufs = reflection->GetUnknownFields(*m_message);\n    \n    for (int fieldIdx = 0; fieldIdx < ufs.field_count(); ++fieldIdx)\n    {\n        const UnknownField uf = ufs.field(fieldIdx);\n        if (uf.number() == fieldNumber)\n        {\n            value = uf.length_delimited();\n            return true;\n        }\n    }\n\n    return false;\n}\n\nbool RawMessage::parseFile(const std::string& path, std::string& fields, std::string& value)\n{\n    std::string data = readFile(path);\n    return parse(data, fields, value);\n}\n\nbool RawMessage::parse(const std::string& data, std::string& fields, std::string& value)\n{\n    std::queue<int> fieldNumbers;\n    \n    std::string::size_type start = 0;\n    std::string::size_type end = fields.find('.');\n    while (end != std::string::npos)\n    {\n        std::string field = fields.substr(start, end - start);\n        if (field.empty())\n        {\n            return false;\n        }\n        \n        int fieldNumber = std::stoi(field);\n        fieldNumbers.push(fieldNumber);\n        start = end + 1;\n        end = fields.find('.', start);\n    }\n    std::string field = fields.substr(start, end);\n    if (field.empty())\n    {\n        return false;\n    }\n    int fieldNumber = std::stoi(field);\n    fieldNumbers.push(fieldNumber);\n    \n    return parse(data, fieldNumbers, value);\n}\n\nbool RawMessage::parse(const std::string& data, std::queue<int>& fieldNumbers, std::string& value)\n{\n    if (fieldNumbers.empty())\n    {\n        return false;\n    }\n\n    UnknownFieldSet unknownFields;\n    if (unknownFields.ParseFromString(data))\n    {\n        int fieldNumber = fieldNumbers.front();\n        fieldNumbers.pop();\n        \n        for (int fieldIdx = 0; fieldIdx < unknownFields.field_count(); ++fieldIdx)\n        {\n            const UnknownField uf = unknownFields.field(fieldIdx);\n            if (uf.number() == fieldNumber)\n            {\n                if (fieldNumbers.empty())\n                {\n                    value = uf.length_delimited();\n                    return true;\n                }\n                else\n                {\n                    std::string embeded_data = uf.length_delimited();\n                    return parse(embeded_data, fieldNumbers, value);\n                }\n\n                break;\n            }\n        }\n    }\n\n    return false;\n}\n*/\n"
  },
  {
    "path": "WechatExporter/core/RawMessage.h",
    "content": "//\n//  RawMessage.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/10/13.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include <string>\n#include <queue>\n#include <iostream>\n#include <fstream>\n#include <iomanip>\n\n#include <google/protobuf/descriptor.h>\n#include <google/protobuf/descriptor.pb.h>\n#include <google/protobuf/descriptor_database.h>\n#include <google/protobuf/dynamic_message.h>\n#include <google/protobuf/io/coded_stream.h>\n#include <google/protobuf/io/zero_copy_stream_impl.h>\n#include <google/protobuf/io/tokenizer.h>\n#include <google/protobuf/compiler/parser.h>\n// #include <google/protobuf/compiler/importer.h>\n#include <google/protobuf/stubs/strutil.h>\n#include <google/protobuf/text_format.h>\n\n#include \"Utils.h\"\n\n\nusing namespace google::protobuf;\nusing namespace google::protobuf::io;\nusing namespace google::protobuf::compiler;\n\n#ifndef RawMessage_h\n#define RawMessage_h\n\nbool convertUnknownField(const UnknownField &uf, std::string& value);\nbool convertUnknownField(const UnknownField &uf, int& value);\n\nclass RawMessage\n{\npublic:\n    RawMessage();\n    ~RawMessage();\n    \n    bool merge(const char *data, int length);\n    bool mergeFile(const std::string& path);\n    // bool parse(int fieldNumber, std::string& value);\n    \n    template<class T>\n    bool parse(const std::string& fields, T& value);\n    \n    // static bool parse(const std::string& data, std::string& fields, std::string& value);\n    // static bool parse(const std::string& data, std::string& fields, int& value);\n    // static bool parseFile(const std::string& path, std::string& fields, std::string& value);\n    \n    static std::string toUtf8String(const std::string& str);\n    \nprivate:\n    \n    void release();\n    // static bool parse(const std::string& data, std::queue<int>& fieldNumbers, std::string& value);\n\n    google::protobuf::DescriptorPool* m_pool;\n    google::protobuf::DynamicMessageFactory* m_factory;\n    google::protobuf::Message* m_message;\n};\n\ntemplate<class T>\ninline bool RawMessage::parse(const std::string& fields, T& value)\n{\n    if (NULL == m_message)\n    {\n        return false;\n    }\n    \n    std::vector<unsigned int> fieldNumbers;\n    \n    std::string::size_type start = 0;\n    std::string::size_type end = fields.find('.');\n    while (end != std::string::npos)\n    {\n        std::string field = fields.substr(start, end - start);\n        if (field.empty())\n        {\n            return false;\n        }\n        \n        int fieldNumber = std::stoi(field);\n        fieldNumbers.push_back(fieldNumber);\n        start = end + 1;\n        end = fields.find('.', start);\n    }\n    std::string field = fields.substr(start, end);\n    if (field.empty())\n    {\n        return false;\n    }\n    int fieldNumber = std::stoi(field);\n    fieldNumbers.push_back(fieldNumber);\n    \n    const Reflection* reflection = m_message->GetReflection();\n    const UnknownFieldSet& ufs = reflection->GetUnknownFields(*m_message);\n    \n    const UnknownFieldSet* pUfs = &ufs;\n    \n    std::vector<std::unique_ptr<UnknownFieldSet>> ufsList;\n\n    for (int idx = 0; idx < fieldNumbers.size(); ++idx)\n    {\n        bool found = false;\n        for (int fieldIdx = 0; fieldIdx < pUfs->field_count(); ++fieldIdx)\n        {\n            const UnknownField &uf = pUfs->field(fieldIdx);\n            if (uf.number() == fieldNumbers[idx])\n            {\n                found = true;\n                if (idx == fieldNumbers.size() - 1)\n                {\n                    return convertUnknownField(uf, value);\n                }\n                else\n                {\n                    if (uf.type() == UnknownField::TYPE_GROUP)\n                    {\n                        pUfs = &(uf.group());\n                    }\n                    else if (uf.type() == UnknownField::TYPE_LENGTH_DELIMITED)\n                    {\n                        std::unique_ptr<UnknownFieldSet> ufs(new UnknownFieldSet());\n                        \n                        if (ufs->ParseFromString(uf.length_delimited()))\n                        {\n                            pUfs = ufs.get();\n                        }\n                        \n                        ufsList.push_back(std::move(ufs));\n                    }\n                }\n                break;\n            }\n        }\n        \n        if (!found || NULL == pUfs)\n        {\n            break;\n        }\n    }\n    \n    return false;\n}\n\n#endif /* RawMessage_h */\n"
  },
  {
    "path": "WechatExporter/core/ResManager.cpp",
    "content": "//\n//  BaseResConverter.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2021/10/27.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#include \"ResManager.h\"\n#include <algorithm>\n#include <json/json.h>\n#include \"FileSystem.h\"\n#include \"Utils.h\"\n\n#define TEMPLATE_NAMES {\"frame\", \"msg\", \"video\", \"notice\", \"system\", \"audio\", \"image\", \"card\", \"emoji\", \"plainshare\", \"share\", \"thumb\", \"listframe\", \"listitem\", \"scripts\", \"filter\", \"refermsg\", \"channels\", \"wxemoji\", \"transfer\"}\n\n#define TEMPLATE_FORMATS {\"template\", \"template_txt\"}\n\nResManager::ResManager()\n{\n}\n\nResManager::~ResManager()\n{\n}\n\nbool ResManager::initLocaleResource(const std::string& resDir, const std::string& languageCode)\n{\n    m_resDir = resDir;\n    \n    bool res = true;\n    if (!loadLocaleStrings(resDir, languageCode))\n    {\n        res = false;\n    }\n\n    return res;\n}\n\nbool ResManager::initResources(const std::string& resDir, const std::string& languageCode, const std::string& templateName)\n{\n    m_resDir = resDir;\n    \n    bool res = true;\n    if (!loadLocaleStrings(resDir, languageCode))\n    {\n        res = false;\n    }\n    if (!loadTemplates(resDir, templateName))\n    {\n        res = false;\n    }\n    if (!loadEmojis(resDir))\n    {\n        res = false;\n    }\n\n    return res;\n}\n\nstd::string ResManager::getDefaultAvatarPath() const\n{\n    return combinePath(m_resDir, \"res\", \"DefaultAvatar.png\");\n}\n\nbool ResManager::loadLocaleStrings(const std::string& resDir, const std::string& languageCode)\n{\n    m_localeStrings.clear();\n    \n    std::string path = combinePath(resDir, \"res\", languageCode + \".txt\");\n    if (!existsFile(path))\n    {\n        return false;\n    }\n    std::string contents = readFile(path);\n    if (contents.empty())\n    {\n        return false;\n    }\n    \n    Json::CharReaderBuilder builder;\n    std::unique_ptr<Json::CharReader> reader(builder.newCharReader());\n\n    Json::Value value;\n    if (reader->parse(contents.c_str(), contents.c_str() + contents.size(), &value, NULL))\n    {\n        int sz = value.size();\n        for (int idx = 0; idx < sz; ++idx)\n        {\n            std::string k = value[idx][\"key\"].asString();\n            std::string v = value[idx][\"value\"].asString();\n            if (m_localeStrings.find(k) != m_localeStrings.cend())\n            {\n                // return false;\n            }\n            m_localeStrings[k] = v;\n        }\n    }\n\n    return true;\n}\n\nbool ResManager::loadTemplates(const std::string& resDir, const std::string& templateName)\n{\n    m_templates.clear();\n    \n    const char* names[] = TEMPLATE_NAMES;\n    \n    std::string preTag = \"%%ML:\";\n    std::string::size_type preTagLen = preTag.size();\n    std::string postTag = \"%%\";\n    std::string::size_type postTagLen = postTag.size();\n    \n    bool res = true;\n    for (int idx = 0; idx < sizeof(names) / sizeof(const char*); idx++)\n    {\n        std::string name = names[idx];\n        std::string path = combinePath(resDir, \"res\", templateName, name + \".html\");\n        if (!existsFile(path))\n        {\n            res = false;\n            continue;\n        }\n        std::string contents = readFile(path);\n        \n        // Localization\n        std::string::size_type tailPos = 0;\n        std::string::size_type pos = std::string::npos;\n        while ((pos = contents.find(preTag, tailPos)) != std::string::npos)\n        {\n            std::string::size_type tailPos = contents.find(postTag, pos + preTagLen);\n            if (tailPos == std::string::npos)\n            {\n                break;\n            }\n            \n            std::string str = contents.substr(pos + preTagLen, tailPos - pos - preTagLen);\n            std::string localizedStr = getLocaleString(str);\n            \n            contents.replace(pos, tailPos + postTagLen - pos, localizedStr);\n            \n            tailPos += postTagLen + localizedStr.size() - str.size() - preTagLen - postTagLen;\n        }\n        \n        m_templates[name] = contents;\n        \n        m_newTemplates[name] = Template(contents);\n        \n        \n#ifndef NDEBUG\n        writeFile(\"/Users/matthew/Documents/WechatHistory/test/templates/\" + name + \".html\", contents);\n#endif\n    }\n\n    return res;\n}\n\nbool ResManager::loadEmojis(const std::string& resDir)\n{\n    m_emojiTags.clear();\n\n    std::string path = combinePath(resDir, \"res\", \"emoji\", \"emoji.json\");\n    if (!existsFile(path))\n    {\n        return false;\n    }\n    \n    std::string contents = readFile(path);\n    if (contents.empty())\n    {\n        return false;\n    }\n\n    Json::CharReaderBuilder builder;\n    std::unique_ptr<Json::CharReader>reader(builder.newCharReader());\n\n    Json::Value value;\n    if (reader->parse(contents.c_str(), contents.c_str() + contents.size(), &value, NULL))\n    {\n        if (value.isArray())\n        {\n            for (Json::Value::const_iterator it = value.begin(); it != value.end(); ++it)\n            {\n                if (!it->isObject()) continue;\n                \n                Json::Value preTagValue = it->get(\"preTag\", Json::Value::null);\n                std::string preTag = preTagValue.isNull() ? \"\" : preTagValue.asCString();\n                \n                if (preTag.empty()) continue;\n\n                Json::Value postTagValue = it->get(\"postTag\", Json::Value::null);\n                std::string postTag = postTagValue.isNull() ? \"\" : postTagValue.asCString();\n                \n                std::vector<EmojiTag>::iterator itEmojiTag = m_emojiTags.emplace(m_emojiTags.end(), preTag, postTag);\n                \n                Json::Value keysValue = it->get(\"keys\", Json::Value::null);\n                if (keysValue.isArray())\n                {\n                    for (Json::Value::const_iterator itKey = keysValue.begin(); itKey != keysValue.end(); ++itKey)\n                    {\n                        if (!itKey->isObject()) continue;\n                        \n                        Json::Value keyValue = itKey->get(\"key\", Json::Value::null);\n                        std::string key = keyValue.isNull() ? \"\" : keyValue.asCString();\n                        \n                        Json::Value fileValue = itKey->get(\"file\", Json::Value::null);\n                        std::string file = fileValue.isNull() ? \"\" : fileValue.asCString();\n                        \n                        if (key.empty() || file.empty()) continue;\n                        \n                        std::string fullTag = preTag + key + postTag;\n                        \n                        itEmojiTag->m_items.emplace(itEmojiTag->m_items.end(), key, fullTag, fullTag, file);\n                    }\n                    \n                    std::sort(itEmojiTag->m_items.begin(), itEmojiTag->m_items.end());\n                }\n            }\n        }\n        int sz = value.size();\n        for (int idx = 0; idx < sz; ++idx)\n        {\n            std::string k = value[idx][\"key\"].asString();\n            std::string v = value[idx][\"value\"].asString();\n            if (m_localeStrings.find(k) != m_localeStrings.cend())\n            {\n                // return false;\n            }\n            m_localeStrings[k] = v;\n        }\n    }\n\n    return true;\n}\n\nstd::string ResManager::getTemplate(const std::string& key) const\n{\n    std::map<std::string, std::string>::const_iterator it = m_templates.find(key);\n    return (it == m_templates.cend()) ? \"\" : it->second;\n}\n\nconst std::string& ResManager::buildFromTemplate(const std::string& key, const std::map<std::string, std::string>& values) const\n{\n    auto it = m_newTemplates.find(key);\n    if (it == m_newTemplates.cend())\n    {\n        return m_emptyString;\n    }\n    \n    return it->second.build(values);\n}\n\nstd::string ResManager::checkEmptyTemplates() const\n{\n    std::vector<std::string> keys;\n    for (std::map<std::string, std::string>::const_iterator it = m_templates.cbegin(); it != m_templates.cend(); ++it)\n    {\n        if (it->second.empty())\n        {\n            keys.push_back(it->second);\n        }\n    }\n    \n    return join(keys, \",\");\n}\n\nstd::string ResManager::getLocaleString(const std::string& key) const\n{\n    // std::string value = key;\n    std::map<std::string, std::string>::const_iterator it = m_localeStrings.find(key);\n    return it == m_localeStrings.cend() ? key : it->second;\n}\n\n// Emoji\nbool ResManager::hasEmojiTag(const std::string& msg) const\n{\n    if (msg.empty())\n    {\n        return false;\n    }\n    \n    bool existed = false;\n    EmojiItemCompare comp;\n    for (std::vector<EmojiTag>::const_iterator itTag = m_emojiTags.cbegin(); itTag != m_emojiTags.cend(); ++itTag)\n    {\n        std::string::size_type pos = 0;\n        std::string::size_type tailPos = 0;\n        while ((pos = msg.find(itTag->m_headTag, pos)) != std::string::npos)\n        {\n            if (itTag->hasTailTag())\n            {\n                tailPos = msg.find(itTag->m_tailTag, pos + itTag->m_headTag.size());\n                if (tailPos == std::string::npos)\n                {\n                    break;\n                }\n                \n                std::string tag = msg.substr(pos + itTag->m_headTag.size(), tailPos - pos - itTag->m_headTag.size());\n                if (!tag.empty())\n                {\n                    std::vector<EmojiItem>::const_iterator it = std::lower_bound(itTag->m_items.cbegin(), itTag->m_items.cend(), tag, comp);\n                    if (it != itTag->m_items.cend() && it->equals(tag))\n                    {\n                        existed = true;\n                        break;\n                    }\n                }\n                \n                pos = tailPos + itTag->m_tailTag.size();\n            }\n            else\n            {\n                pos += itTag->m_headTag.size();\n                for (std::vector<EmojiItem>::const_iterator it = itTag->m_items.cbegin(); it != itTag->m_items.cend(); ++it)\n                {\n                    if (msg.compare(pos, it->getTag().size(), it->getTag()) == 0)\n                    {\n                        existed = true;\n                        break;\n                    }\n                }\n                \n                if (existed) break;\n            }\n        }\n        \n        if (existed) break;\n    }\n   \n    return existed;\n}\n\nstd::string ResManager::convertEmojis(const std::string& msg, const std::string& localRootPath, const std::string& emojiPath, const std::string& emojiUrlPath) const\n{\n    struct FindEmojiItem\n    {\n        std::string::size_type  pos;\n        std::string::size_type  length;\n        const EmojiItem*        emojiItem;\n        \n        FindEmojiItem(std::string::size_type p, std::string::size_type l, const EmojiItem* ei) : pos(p), length(l), emojiItem(ei)\n        {\n        }\n        inline bool operator<(const FindEmojiItem &rhs) const\n        {\n            return pos < rhs.pos;\n        }\n        \n        inline bool operator>(const FindEmojiItem &rhs) const\n        {\n            return pos > rhs.pos;\n        }\n    };\n    \n    // greatter to less\n    struct FindEmojiItemCompare\n    {\n        inline bool operator() ( const FindEmojiItem& lhs, const FindEmojiItem& rhs) const\n        {\n            return lhs.pos > rhs.pos;\n        }\n    };\n\n    std::string emojiTemplate = getTemplate(\"wxemoji\");\n    if (emojiTemplate.empty())\n    {\n        return msg;\n    }\n    \n    EmojiItemCompare comp;\n    std::vector<FindEmojiItem> findEmojiItems;\n    bool matched = false;\n    for (std::vector<EmojiTag>::const_iterator itTag = m_emojiTags.cbegin(); itTag != m_emojiTags.cend(); ++itTag)\n    {\n        std::string::size_type pos = 0;\n        std::string::size_type tailPos = 0;\n        while ((pos = msg.find(itTag->m_headTag, pos)) != std::string::npos)\n        {\n            if (itTag->hasTailTag())\n            {\n                tailPos = msg.find(itTag->m_tailTag, pos + itTag->m_headTag.size());\n                if (tailPos == std::string::npos)\n                {\n                    // break while loop\n                    break;\n                }\n                \n                matched = false;\n                std::string tag = msg.substr(pos + itTag->m_headTag.size(), tailPos - pos - itTag->m_headTag.size());\n                if (!tag.empty())\n                {\n                    std::vector<EmojiItem>::const_iterator it = std::lower_bound(itTag->m_items.cbegin(), itTag->m_items.cend(), tag, comp);\n                    if (it != itTag->m_items.cend() && it->equals(tag))\n                    {\n                        findEmojiItems.emplace(findEmojiItems.end(), pos, tailPos + itTag->m_tailTag.size() - pos, &(*it));\n                        matched = true;\n                    }\n                }\n                matched ? (pos += itTag->m_headTag.size()) : (pos = tailPos + itTag->m_tailTag.size());\n            }\n            else\n            {\n                std::string::size_type oldPos = pos;\n                pos += itTag->m_headTag.size();\n                for (std::vector<EmojiItem>::const_iterator it = itTag->m_items.cbegin(); it != itTag->m_items.cend(); ++it)\n                {\n                    if (msg.compare(pos, it->getTag().size(), it->getTag()) == 0)\n                    {\n                        findEmojiItems.emplace(findEmojiItems.end(), oldPos, itTag->m_headTag.size() + it->getTag().size(), &(*it));\n                        pos += it->getTag().size();\n                        break;\n                    }\n                }\n            }\n        }\n        \n    }\n        \n    if (findEmojiItems.empty())\n    {\n        return msg;\n    }\n        \n    std::sort(findEmojiItems.begin(), findEmojiItems.end(), FindEmojiItemCompare());\n\n    std::string newMsg = msg;\n    std::string srcEmojiPath = combinePath(m_resDir, \"res\", \"emoji\", \"images\");\n    std::string destEmojiPath = combinePath(localRootPath, emojiPath, \"Emoji\", \"wx\");\n    bool destEmojiPathExisted = existsDirectory(destEmojiPath);\n    for (std::vector<FindEmojiItem>::const_iterator it = findEmojiItems.cbegin(); it != findEmojiItems.cend(); ++it)\n    {\n        std::string emojiStr = emojiTemplate;\n\n        replaceAll(emojiStr, \"%%EMOJI_PATH%%\", emojiUrlPath + \"/Emoji/wx/\" + it->emojiItem->m_fileName + \".png\");\n        replaceAll(emojiStr, \"%%EMOJI_TITLE%%\", it->emojiItem->getTitle());\n        replaceAll(emojiStr, \"%%EMOJI_RAW%%\", it->emojiItem->m_fullTag);\n        \n        newMsg.replace(it->pos, it->length, emojiStr);\n        \n        std::string destFileName = combinePath(destEmojiPath, it->emojiItem->m_fileName + \".png\");\n        if (!existsFile(destFileName))\n        {\n            if (!destEmojiPathExisted)\n            {\n                destEmojiPathExisted = makeDirectory(destEmojiPath);\n            }\n            \n            copyFile(combinePath(srcEmojiPath, it->emojiItem->m_fileName + \".png\"), destFileName);\n        }\n    }\n\n    return newMsg;\n}\n\nbool ResManager::validateResources(const std::string& resDir, std::string& error)\n{\n    const char* names[] = TEMPLATE_NAMES;\n    const char* formats[] = TEMPLATE_FORMATS;\n    \n    bool res = true;\n    for (int idx1 = 0; idx1 < sizeof(formats) / sizeof(const char*); idx1++)\n    {\n        std::string format = formats[idx1];\n        for (int idx2 = 0; idx2 < sizeof(names) / sizeof(const char*); idx2++)\n        {\n            std::string name = names[idx2];\n            std::string path = combinePath(resDir, \"res\", format, name + \".html\");\n            if (!existsFile(path))\n            {\n                error = \"Templagte file doesn't exist: res\";\n                error += DIR_SEP_STR + format + DIR_SEP_STR + name + \".html\";\n                res = false;\n                break;\n            }\n            \n            if (getFileSize(path) == 0)\n            {\n                error = \"Templagte file is empty: res\";\n                error += DIR_SEP_STR + format + DIR_SEP_STR + name + \".html\";\n                res = false;\n                break;\n            }\n        }\n    }\n    \n    return res;\n}\n"
  },
  {
    "path": "WechatExporter/core/ResManager.h",
    "content": "//\n//  BaseResConverter.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/10/26.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef ResManager_h\n#define ResManager_h\n\n#include <string>\n#include <vector>\n#include <map>\n\n#include \"Template.h\"\n\nclass ResManager\n{\npublic:\n    \n    struct EmojiItem\n    {\n        std::string m_tag;\n        std::string m_fullTag;\n        std::string m_title;\n        std::string m_fileName;\n\n        EmojiItem()\n        {\n        }\n        \n        EmojiItem(const std::string& tag, const std::string& fullTag, const std::string& title, const std::string& fileName) : m_tag(tag), m_fullTag(fullTag), m_title(title), m_fileName(fileName)\n        {\n        }\n        \n        inline bool operator<(const EmojiItem &rhs) const\n        {\n            return m_tag.compare(rhs.m_tag) < 0;\n        }\n        \n        inline bool operator>(const EmojiItem &rhs) const\n        {\n            return m_tag.compare(rhs.m_tag) > 0;\n        }\n        \n        const std::string& getTag() const\n        {\n            return m_tag;\n        }\n        \n        const std::string& getTitle() const\n        {\n            return m_title;\n        }\n        \n        const std::string& getFileName() const\n        {\n            return m_fileName;\n        }\n        \n        bool equals(const std::string& tag) const\n        {\n            return m_tag.compare(tag) == 0;\n        }\n    };\n    \n    struct EmojiItemCompare\n    {\n        inline bool operator() ( const EmojiItem& lhs, const std::string& rhs) const\n        {\n            return lhs.getTag().compare(rhs) < 0;\n        }\n    };\n    \n    struct EmojiItemCompareN\n    {\n        inline bool operator() ( const EmojiItem& lhs, const std::string& rhs) const\n        {\n            return lhs.getTag().compare(rhs) < 0;\n        }\n    };\n    \n    struct EmojiTag\n    {\n        std::string m_headTag;\n        std::string m_tailTag;\n        \n        std::vector<EmojiItem> m_items;\n        \n        EmojiTag(const std::string& headTag, const std::string& tailTag) : m_headTag(headTag), m_tailTag(tailTag)\n        {\n        }\n        \n        bool hasTailTag() const\n        {\n            return !m_tailTag.empty();\n        }\n    };\n    \n    \npublic:\n    ResManager();\n    ~ResManager();\n    \n    bool initLocaleResource(const std::string& resDir, const std::string& languageCode);\n    bool initResources(const std::string& resDir, const std::string& languageCode, const std::string& templateName);\n    \n    // Default Resource\n    std::string getDefaultAvatarPath() const;\n    std::string getDefaultAppIconPath() const;\n\n    // Localization\n    std::string getLocaleString(const std::string& key) const;\n    \n    // Template\n    std::string getTemplate(const std::string& key) const;\n    // const Template& getNewTemplate(const std::string& key) const;\n    std::string checkEmptyTemplates() const;\n    \n    const std::string& buildFromTemplate(const std::string& key, const std::map<std::string, std::string>& values) const;\n\n    // Emoji\n    bool hasEmojiTag(const std::string& msg) const;\n    std::string convertEmojis(const std::string& msg, const std::string& localRootPath, const std::string& emojiPath, const std::string& emojiUrlPath) const;\n\n    static bool validateResources(const std::string& resDir, std::string& error);\n    \nprotected:\n    bool loadLocaleStrings(const std::string& resDir, const std::string& languageCode);\n    bool loadTemplates(const std::string& resDir, const std::string& templateName);\n    bool loadEmojis(const std::string& resDir);\n\nprotected:\n    std::string m_resDir;\n    std::map<std::string, std::string> m_templates;\n    std::map<std::string, Template> m_newTemplates;\n\n    std::map<std::string, std::string> m_localeStrings;\n    std::vector<EmojiTag> m_emojiTags;\n    \n    std::string m_emptyString;\n};\n\n#endif /* ResManager_h */\n"
  },
  {
    "path": "WechatExporter/core/TaskManager.cpp",
    "content": "//\n//  TaskManager.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2021/4/20.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#include \"TaskManager.h\"\n#include \"AsyncTask.h\"\n#include \"FileSystem.h\"\n\nTaskManager::TaskManager(Logger* logger) : m_logger(logger), m_downloadExecutor(NULL)\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    , m_audioExecutor(NULL)\n#endif\n{\n    m_downloadExecutor = new AsyncExecutor(2, 4, this);\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    m_audioExecutor = new AsyncExecutor(1, 1, this);\n#endif\n    // m_audioExecutor = m_downloadExecutor;\n    \n#if !defined(NDEBUG) || defined(DBG_PERF)\n    m_downloadExecutor->setTag(\"dl\");\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor)\n    {\n        m_audioExecutor->setTag(\"audio\");\n    }\n#endif\n#endif\n}\n\nTaskManager::~TaskManager()\n{\n    shutdownExecutors();\n    m_logger = NULL;\n}\n\nvoid TaskManager::shutdown()\n{\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor)\n    {\n        m_audioExecutor->shutdown();\n    }\n#endif\n    if (NULL != m_downloadExecutor)\n    {\n        m_downloadExecutor->shutdown();\n    }\n}\n\nvoid TaskManager::shutdownExecutors()\n{\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor)\n    {\n        delete m_audioExecutor;\n        m_audioExecutor = NULL;\n    }\n#endif\n    if (NULL != m_downloadExecutor)\n    {\n        delete m_downloadExecutor;\n        m_downloadExecutor = NULL;\n    }\n}\n\n// true: completed, false: timeout\nbool TaskManager::waitForCompltion(unsigned int ms)\n{\n    /*\n    bool hasMoreTasks = true;\n    while (hasMoreTasks)\n    {\n        {\n            std::unique_lock<std::mutex> lock(m_mutex);\n            hasMoreTasks = !(m_copyTaskQueue.empty() && m_pdfTaskQueue.empty());\n        }\n        \n        if (!hasMoreTasks)\n        {\n            break;\n        }\n\n        std::this_thread::sleep_for(std::chrono::milliseconds(ms == 0 ? 512 : ms));\n        if (ms > 0)\n        {\n            return false;\n        }\n    }\n     */\n    \n    if (!m_downloadExecutor->waitForCompltion(ms))\n    {\n        return false;\n    }\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor)\n    {\n        if (!m_audioExecutor->waitForCompltion(ms))\n        {\n            return false;\n        }\n    }\n#endif\n\n    return true;\n}\n\nvoid TaskManager::cancel()\n{\n    std::map<uint32_t, std::set<AsyncExecutor::Task *>> copyTaskQueue;\n    \n    {\n        std::unique_lock<std::mutex> lock(m_mutex);\n        copyTaskQueue.swap(m_copyTaskQueue);\n    }\n    \n    m_downloadExecutor->cancel();\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor)\n    {\n        m_audioExecutor->cancel();\n    }\n#endif\n    \n    for (std::map<uint32_t, std::set<AsyncExecutor::Task *>>::iterator it = copyTaskQueue.begin(); it != copyTaskQueue.end(); ++it)\n    {\n        for (std::set<AsyncExecutor::Task *>::iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2)\n        {\n            delete (*it2);\n        }\n        it->second.clear();\n    }\n    copyTaskQueue.clear();\n}\n\nsize_t TaskManager::getNumberOfQueue(std::string& queueDesc) const\n{\n    size_t numberOfDownloads = m_downloadExecutor->getNumberOfQueue();\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    size_t numberOfAudio = 0;\n    if (m_audioExecutor != m_downloadExecutor)\n    {\n        numberOfAudio = m_audioExecutor->getNumberOfQueue();\n    }\n#endif\n    \n    {\n        std::unique_lock<std::mutex> lock(m_mutex);\n        numberOfDownloads += m_copyTaskQueue.size();\n    }\n    \n    queueDesc = \"\";\n    if (numberOfDownloads > 0)\n    {\n        queueDesc += std::to_string(numberOfDownloads) + \" downloads\";\n    }\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    if (numberOfAudio > 0)\n    {\n        if (!queueDesc.empty())\n        {\n            queueDesc += \", \";\n        }\n        queueDesc += std::to_string(numberOfAudio) + \" audios\";\n    }\n#endif\n\n    return numberOfDownloads\n#ifdef USING_ASYNC_TASK_FOR_MP3\n\t\t+ numberOfAudio\n#endif\n    ;\n}\n\nvoid TaskManager::setUserAgent(const std::string& userAgent)\n{\n    m_userAgent = userAgent;\n}\n\nvoid TaskManager::onTaskStart(const AsyncExecutor* executor, const AsyncExecutor::Task *task)\n{\n    if (NULL != m_logger && task->getType() != TASK_TYPE_AUDIO)\n    {\n        if (task->getType() == TASK_TYPE_PDF)\n        {\n            m_logger->write(\"Task Starts: \" + task->getName());\n        }\n    }\n}\n\nvoid TaskManager::onTaskComplete(const AsyncExecutor* executor, const AsyncExecutor::Task *task, bool succeeded)\n{\n    if (NULL != m_logger)\n    {\n        if (!succeeded || task->hasError())\n        {\n            m_logger->write(task->getError());\n        }\n#ifndef NDEBUG\n        else\n        {\n            if (task->getType() == TASK_TYPE_DOWNLOAD)\n            {\n                // m_logger->write(\"Task Ends: \" + task->getName());\n            }\n            if (task->getType() == TASK_TYPE_PDF)\n            {\n                // m_logger->write(\"Task Ends: \" + task->getName());\n            }\n        }\n#endif\n    }\n    \n    // const Session* session = task->getUserData() == NULL ? NULL : reinterpret_cast<const Session *>(task->getUserData());\n    if (/*executor == m_downloadExecutor && */task->getType() == TASK_TYPE_DOWNLOAD)\n    {\n        const DownloadTask* downloadTask = dynamic_cast<const DownloadTask *>(task);\n        \n        std::unique_lock<std::mutex> lock(m_mutex);\n        std::map<std::string, uint32_t>::const_iterator it = m_downloadingTasks.find(downloadTask->getUrl());\n#ifndef NDEBUG\n        assert(it != m_downloadingTasks.cend());\n#endif\n        if (it != m_downloadingTasks.cend())\n        {\n            m_downloadingTasks.erase(it);\n        }\n        \n        std::set<AsyncExecutor::Task *> copyTasks = dequeueCopyTasks(task->getTaskId());\n        lock.unlock();\n        \n#ifndef NDEBUG\n        if (succeeded\n#ifdef FAKE_DOWNLOAD\n            && task->getType() != TASK_TYPE_DOWNLOAD\n#endif\n            )\n        {\n            assert(existsFile(downloadTask->getOutput()));\n        }\n#endif\n        for (std::set<AsyncExecutor::Task *>::iterator it = copyTasks.begin(); it != copyTasks.end(); ++it)\n        {\n            m_downloadExecutor->addTask(*it);\n        }\n    }\n    else if ((task->getType() == TASK_TYPE_COPY) || (task->getType() == TASK_TYPE_AUDIO))\n    {\n        // check copy task\n    }\n}\n\nvoid TaskManager::download(const Session* session, const std::string &url, const std::string &backupUrl, const std::string& output, time_t mtime, const std::string& defaultFile/* = \"\"*/, std::string type/* = \"\"*/)\n{\n#ifndef NDEBUG\n    if (url == \"\")\n    {\n        assert(!\"url shouldn't be empty\");\n    }\n\tif (output.find(ALT_DIR_SEP) != std::string::npos)\n\t{\n\t\tassert(!\"Directory is invalid\");\n\t}\n#endif\n    \n    {\n        std::unique_lock<std::mutex> lock(m_mutex);\n        std::set<std::string>::iterator it = m_downloadedFiles.find(output);\n        if (it != m_downloadedFiles.end())\n        {\n            return;\n        }\n        m_downloadedFiles.insert(output);\n    }\n    \n    std::map<std::string, std::string>::iterator it = m_downloadTasks.find(url);\n    if (it != m_downloadTasks.end() && it->second == output)\n    {\n        // Existed and same output path, skip it\n        return;\n    }\n    \n    bool downloadFile = false;\n    uint32_t taskId = AsyncExecutor::genNextTaskId();\n    AsyncExecutor::Task *task = NULL;\n    if (it != m_downloadTasks.end())\n    {\n        // Existed and different output path, copy it\n        task = new CopyTask(it->second, output, \"CP: \" + url + \" => \" + output + \" <= \" + it->second);\n    }\n    else\n    {\n        DownloadTask* downloadTask = new DownloadTask(url, output, defaultFile, mtime, \"DL: \" + url + \" => \" + output);\n        downloadTask->setUserAgent(m_userAgent);\n        task = downloadTask;\n        downloadFile = true;\n        m_downloadTasks.insert(std::pair<std::string, std::string>(url, output));\n    }\n    task->setTaskId(taskId);\n    task->setUserData(reinterpret_cast<const void *>(session));\n    \n    std::unique_lock<std::mutex> lock(m_mutex);\n    if (downloadFile)\n    {\n        m_downloadingTasks.insert(std::pair<std::string, uint32_t>(url, taskId));\n    }\n    else\n    {\n        std::map<std::string, uint32_t>::const_iterator it3 = m_downloadingTasks.find(url);\n        if (it3 != m_downloadingTasks.cend())\n        {\n            // Downloading is running, add it into waiting queue\n            std::map<uint32_t, std::set<AsyncExecutor::Task *>>::iterator it4 = m_copyTaskQueue.find(it3->second);\n            if (it4 == m_copyTaskQueue.end())\n            {\n                it4 = m_copyTaskQueue.insert(it4, std::pair<uint32_t, std::set<AsyncExecutor::Task *>>(it3->second, std::set<AsyncExecutor::Task *>()));\n            }\n            it4->second.insert(task);\n            task = NULL;\n        }\n    }\n\n    lock.unlock();\n    if (NULL != task)\n    {\n        m_downloadExecutor->addTask(task);\n    }\n}\n\n#ifdef USING_ASYNC_TASK_FOR_MP3\nvoid TaskManager::convertAudio(const Session* session, const std::string& pcmPath, const std::string& mp3Path, TaskManager::AUDIO_FORMAT format, unsigned int mtime)\n{\n    if (NULL == session)\n    {\n        return;\n    }\n    \n    Mp3Task *task = new Mp3Task(pcmPath, mp3Path, mtime);\n    if (NULL != task)\n    {\n        task->setTaskId(AsyncExecutor::genNextTaskId());\n        task->setUserData(reinterpret_cast<const void *>(session));\n        \n        m_audioExecutor->addTask(task);\n    }\n    \n}\n#endif\n"
  },
  {
    "path": "WechatExporter/core/TaskManager.h",
    "content": "//\n//  SessionTaskManager.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/4/20.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef TaskManager_h\n#define TaskManager_h\n\n#include <stdio.h>\n#include <map>\n#include <set>\n#include \"WechatObjects.h\"\n#include \"AsyncExecutor.h\"\n#include \"PdfConverter.h\"\n#include \"Logger.h\"\n\nclass TaskManager : public AsyncExecutor::Callback\n{\nprivate:\n    Logger* m_logger;\n    \n    AsyncExecutor   *m_downloadExecutor;\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    AsyncExecutor   *m_audioExecutor;\n#endif\n    std::map<std::string, std::string> m_downloadTasks;\n    \n    std::string m_userAgent;\n    \n    mutable std::mutex m_mutex;\n    std::set<std::string> m_downloadedFiles;\n    std::map<std::string, uint32_t> m_downloadingTasks;\n    \n    std::map<uint32_t, std::set<AsyncExecutor::Task *>> m_copyTaskQueue;\n    \n#ifdef USING_ASYNC_TASK_FOR_MP3\n    std::queue<std::vector<unsigned char>> m_Buffers;\n#endif\n    \npublic:\n    \n    TaskManager(Logger* logger);\n    ~TaskManager();\n    \n    virtual void onTaskStart(const AsyncExecutor* executor, const AsyncExecutor::Task *task);\n    virtual void onTaskComplete(const AsyncExecutor* executor, const AsyncExecutor::Task *task, bool succeeded);\n    \n    void setUserAgent(const std::string& userAgent);\n    \n    size_t getNumberOfQueue(std::string& queueDesc) const;\n    void cancel();\n    void shutdown();\n    // true: completed, false: timeout\n    bool waitForCompltion(unsigned int ms);\n\n    void download(const Session* session, const std::string &url, const std::string &backupUrl, const std::string& output, time_t mtime, const std::string& defaultFile = \"\", std::string type = \"\");\n#ifdef USING_ASYNC_TASK_FOR_MP3\n    enum AUDIO_FORMAT\n    {\n        AUDIO_FORMAT_AMR,\n        AUDIO_FORMAT_SILK\n    };\n    void convertAudio(const Session* session, const std::string& pcmPath, const std::string& mp3Path, AUDIO_FORMAT format, unsigned int mtime);\n#endif\n    \nprivate:\n    \n    void shutdownExecutors();\n    \n    inline std::set<AsyncExecutor::Task *> dequeueCopyTasks(uint32_t taskId)\n    {\n        std::set<AsyncExecutor::Task *> tasks;\n        std::map<uint32_t, std::set<AsyncExecutor::Task *>>::iterator it = m_copyTaskQueue.find(taskId);\n        if (it != m_copyTaskQueue.end())\n        {\n            tasks.swap(it->second);\n            m_copyTaskQueue.erase(it);\n        }\n        \n        return tasks;\n    }\n\n};\n\n#endif /* SessionTaskManager_h */\n"
  },
  {
    "path": "WechatExporter/core/Template.cpp",
    "content": "//\n//  Template.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2022/4/21.\n//  Copyright © 2022 Matthew. All rights reserved.\n//\n\n#include \"Template.h\"\n\n#define TEMPLATE_TAG \"%%\"\n#define TEMPLATE_TAG_LENGTH 2\n\n\nTemplate::Template()\n{\n}\n\nTemplate::Template(const std::string& tmpl) : m_template(tmpl)\n{\n    size_t pos = 0;\n    size_t posEnd = 0;\n    while (1)\n    {\n        pos = m_template.find(TEMPLATE_TAG, pos);\n        if (pos == std::string::npos)\n        {\n            break;\n        }\n        posEnd = m_template.find(TEMPLATE_TAG, pos + TEMPLATE_TAG_LENGTH);\n        if (posEnd == std::string::npos)\n        {\n            break;\n        }\n        \n        size_t length = posEnd - pos + TEMPLATE_TAG_LENGTH;\n        std::string tag = m_template.substr(pos, length);\n        \n        m_tags.emplace_back(tag, pos, length);\n        \n        pos = posEnd + TEMPLATE_TAG_LENGTH;\n    }\n}\n\nTemplate::Template(const Template& rhs) : m_template(rhs.m_template), m_tags(rhs.m_tags)\n{\n}\n\nTemplate& Template::operator=(const Template& rhs)\n{\n    m_template = rhs.m_template;\n    m_result = rhs.m_result;\n    m_tags = rhs.m_tags;\n    \n    return *this;\n}\n\nconst std::string& Template::build(const std::map<std::string, std::string>& values) const\n{\n    m_result.assign(m_template);\n    \n    for (auto it = m_tags.crbegin(); it != m_tags.crend(); ++it)\n    {\n        auto itVal = values.find(it->tag);\n        if (itVal == values.cend())\n        {\n            m_result.erase(m_result.begin() + it->pos, m_result.begin() + it->pos + it->length);\n        }\n        else\n        {\n            m_result.replace(it->pos, it->length, itVal->second);\n        }\n    }\n\n    return m_result;\n}\n"
  },
  {
    "path": "WechatExporter/core/Template.h",
    "content": "//\n//  Template.h\n//  WechatExporter\n//\n//  Created by Matthew on 2022/4/21.\n//  Copyright © 2022 Matthew. All rights reserved.\n//\n\n#ifndef Template_h\n#define Template_h\n\n#include <string>\n#include <vector>\n#include <map>\n\nstruct TEMPLATE_TAG\n{\n    std::string tag;\n    size_t pos;\n    size_t length;\n    \n    TEMPLATE_TAG(const std::string& t, size_t p, size_t l) : tag(t), pos(p), length(l) {}\n};\n\nclass Template\n{\npublic:\n    Template();\n    Template(const std::string& tmpl);\n    \n    Template(const Template& rhs);\n    \n    Template& operator=(const Template& rhs);\n    \n    std::string build(const std::vector<std::pair<std::string, std::string>>& values) const;\n    \n    const std::string& build(const std::map<std::string, std::string>& values) const;\n    \nprotected:\n    std::string m_template;\n    mutable std::string m_result;\n    std::vector<TEMPLATE_TAG> m_tags;\n};\n\n#endif /* Template_h */\n"
  },
  {
    "path": "WechatExporter/core/Updater.cpp",
    "content": "//\n//  Updater.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2021/3/6.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#include \"Updater.h\"\n#include \"Downloader.h\"\n#include \"AsyncTask.h\"\n#include \"Utils.h\"\n\nUpdater::Updater(const std::string& currentVersion) : m_currentVersion(currentVersion)\n{\n}\n\nUpdater::~Updater()\n{\n}\n\nvoid Updater::setUserAgent(const std::string& userAgent)\n{\n    m_userAgent = userAgent;\n}\n\nstd::string Updater::getNewVersion() const\n{\n    return m_latestVersion;\n}\n\nstd::string Updater::getUpdateUrl() const\n{\n    return m_updateUrl;\n}\n\nbool Updater::checkUpdate()\n{\n    m_latestVersion.clear();\n    m_updateUrl.clear();\n    \n    std::vector<unsigned char> body;\n    std::vector<std::pair<std::string, std::string>> headers;\n    if (!m_userAgent.empty())\n    {\n        headers.push_back(std::pair<std::string, std::string>(\"User-Agent\", m_userAgent));\n    }\n    \n    std::string url = \"https://src.wakin.org/github/wxexp/update.conf?v=\" + encodeUrl(m_currentVersion);\n#ifndef NDEBUG\n    headers.push_back(std::pair<std::string, std::string>(\"RESOLVE\", \"src.wakin.org:443:127.0.0.1\"));\n    url += \"&dbg=1\";\n#endif\n    long httpStatus = 0;\n#ifdef USING_DOWNLOADER\n    if (!Downloader::httpGet(url, headers, httpStatus, body) || httpStatus != 200 || body.empty())\n#else\n    if (!DownloadTask::httpGet(url, headers, httpStatus, body) || httpStatus != 200 || body.empty())\n#endif\n    {\n        return false;\n    }\n    \n    std::string bodyStr;\n    bodyStr.assign(reinterpret_cast<char *>(&body[0]), body.size());\n    replaceAll(bodyStr, \"\\r\\n\", \"\\n\");\n    replaceAll(bodyStr, \"\\r\", \"\\n\");\n\n    std::vector<std::string> parts = split(bodyStr, \"\\n\");\n    if (parts.empty() || parts[0].empty())\n    {\n        return false;\n    }\n    \n    std::vector<std::string> versionParts = split(parts[0], \".\");\n    if (versionParts.size() != 4 || !isNumber(versionParts[0]) || !isNumber(versionParts[1]) || !isNumber(versionParts[2]) || !isNumber(versionParts[3]))\n    {\n        return false;\n    }\n    m_latestVersion = parts[0];\n    \n    std::vector<std::string> curVersionParts = split(m_currentVersion, \".\");\n    if (curVersionParts.size() != 4 || !isNumber(curVersionParts[0]) || !isNumber(curVersionParts[1]) || !isNumber(curVersionParts[2]) || !isNumber(curVersionParts[3]))\n    {\n        return false;\n    }\n    \n    if (parts.size() > 1 && !parts[1].empty())\n    {\n        m_updateUrl = parts[1];\n    }\n\n    for (int idx = 0; idx < 4; ++idx)\n    {\n        int v = std::atoi(versionParts[idx].c_str());\n        int cv = std::atoi(curVersionParts[idx].c_str());\n        if (v > cv)\n        {\n            return true;\n        }\n    }\n    \n    return false;\n}\n"
  },
  {
    "path": "WechatExporter/core/Updater.h",
    "content": "//\n//  Updater.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/3/6.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef Updater_h\n#define Updater_h\n\n#include <string>\n\nclass Updater\n{\npublic:\n    Updater(const std::string& currentVersion);\n    ~Updater();\n    \n    void setUserAgent(const std::string& userAgent);\n    \n    bool checkUpdate();\n    std::string getNewVersion() const;\n    std::string getUpdateUrl() const;\n    \nprivate:\n    std::string m_currentVersion;\n    std::string m_latestVersion;\n    std::string m_updateUrl;\n    std::string m_userAgent;\n};\n\n#endif /* Updater_h */\n"
  },
  {
    "path": "WechatExporter/core/Utils.cpp",
    "content": "//\r\n//  Utils.cpp\r\n//  WechatExporter\r\n//\r\n//  Created by Matthew on 2020/9/30.\r\n//  Copyright © 2020 Matthew. All rights reserved.\r\n//\r\n\r\n#include \"Utils.h\"\r\n#include <ctime>\r\n#include <vector>\r\n#include <sstream>\r\n#include <iomanip>\r\n#include <fstream>\r\n#include <algorithm>\r\n#include <string>\r\n#include <codecvt>\r\n#include <locale>\r\n#include <cstdio>\r\n#include <chrono>\r\n#ifdef _WIN32\r\n#include <direct.h>\r\n#include <atlstr.h>\r\n#include <sys/utime.h>\r\n#include \"Shlwapi.h\"\r\n#ifndef NDEBUG\r\n#include <cassert>\r\n#endif\r\n#else\r\n#include <utime.h>\r\n#endif\r\n#include <sys/types.h>\r\n#include <sys/stat.h>\r\n#include <time.h>\r\n#include <sqlite3.h>\r\n#include <curl/curl.h>\r\n#include \"FileSystem.h\"\r\n\r\n#ifdef _WIN32\r\n#include <Rpc.h>\r\n#else\r\n#include <uuid/uuid.h>\r\n#endif\r\n\r\n\r\nint replaceAll(std::string& input, const std::string& search, const std::string& replace)\r\n{\r\n    int matched = 0;\r\n    size_t pos = 0;\r\n    while((pos = input.find(search, pos)) != std::string::npos)\r\n    {\r\n        input.replace(pos, search.length(), replace);\r\n        pos += replace.length();\r\n        ++matched;\r\n    }\r\n    \r\n    return matched;\r\n}\r\n\r\nint replaceAll(std::string& input, const std::vector<std::pair<std::string, std::string>>& pairs)\r\n{\r\n    int matched = 0;\r\n    for (std::vector<std::pair<std::string, std::string>>::const_iterator it = pairs.cbegin(); it != pairs.cend(); ++it)\r\n    {\r\n        size_t pos = 0;\r\n        while((pos = input.find(it->first, pos)) != std::string::npos)\r\n        {\r\n            input.replace(pos, it->first.length(), it->second);\r\n            pos += it->second.length();\r\n            ++matched;\r\n        }\r\n    }\r\n    \r\n    return matched;\r\n}\r\n\r\nstd::string replaceAll(const std::string& input, const std::string& search, const std::string& replace)\r\n{\r\n    std::string result = input;\r\n    replaceAll(result, search, replace);\r\n    return result;\r\n}\r\n\r\nstd::string replaceAll(const std::string& input, const std::vector<std::pair<std::string, std::string>>& pairs)\r\n{\r\n    std::string result = input;\r\n    replaceAll(result, pairs);\r\n\r\n    return result;\r\n}\r\n\r\nbool endsWith(const std::string& str, const std::string& suffix)\r\n{\r\n    return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix);\r\n}\r\n\r\nbool endsWith(const std::string& str, std::string::value_type ch)\r\n{\r\n    return !str.empty() && str[str.size() - 1] == ch;\r\n}\r\n\r\nbool startsWith(const std::string& str, const std::string& prefix, int pos/* = 0*/)\r\n{\r\n    return str.size() >= prefix.size() && 0 == str.compare(pos, prefix.size(), prefix);\r\n}\r\n\r\nbool startsWith(const std::string& str, std::string::value_type ch)\r\n{\r\n    return !str.empty() && str[0] == ch;\r\n}\r\n\r\nstd::string toUpper(const std::string& str)\r\n{\r\n    std::string res = str;\r\n    std::transform(res.begin(), res.end(), res.begin(), ::toupper);\r\n    return res;\r\n}\r\n\r\nstd::string toLower(const std::string& str)\r\n{\r\n    std::string res = str;\r\n    std::transform(res.begin(), res.end(), res.begin(), ::tolower);\r\n    return res;\r\n}\r\n\r\nstd::vector<std::string> split(const std::string& str, const std::string& delimiter)\r\n{\r\n    std::vector<std::string> tokens;\r\n    size_t prev = 0, pos = 0;\r\n    do\r\n    {\r\n        pos = str.find(delimiter, prev);\r\n        if (pos == std::string::npos) pos = str.length();\r\n        std::string token = str.substr(prev, pos-prev);\r\n        if (!token.empty()) tokens.push_back(token);\r\n        prev = pos + delimiter.length();\r\n    }\r\n    while (pos < str.length() && prev < str.length());\r\n    return tokens;\r\n}\r\n\r\nstd::string join(const std::vector<std::string>& elements, const char *const delimiter)\r\n{\r\n    return join(std::cbegin(elements), std::cend(elements), delimiter);\r\n}\r\n\r\nstd::string join(std::vector<std::string>::const_iterator b, std::vector<std::string>::const_iterator e, const char *const delimiter)\r\n{\r\n    std::ostringstream os;\r\n    if (b != e)\r\n    {\r\n        auto pe = prev(e);\r\n        for (std::vector<std::string>::const_iterator it = b; it != pe; ++it)\r\n        {\r\n            os << *it;\r\n            os << delimiter;\r\n        }\r\n        b = pe;\r\n    }\r\n    if (b != e)\r\n    {\r\n        os << *b;\r\n    }\r\n\r\n    return os.str();\r\n}\r\n\r\nstd::string safeHTML(const std::string& s)\r\n{\r\n    static std::vector<std::pair<std::string, std::string>> replaces =\r\n    { {\"&\", \"&amp;\"}, /*{\" \", \"&nbsp;\"}, */{\"<\", \"&lt;\"}, {\">\", \"&gt;\"}, {\"\\r\\n\", \"<br/>\"}, {\"\\r\", \"<br/>\"}, {\"\\n\", \"<br/>\"} };\r\n    return replaceAll(s, replaces);\r\n}\r\n\r\nvoid removeHtmlTags(std::string& html)\r\n{\r\n    std::string::size_type startpos = 0;\r\n    while ((startpos = html.find(\"<\", startpos)) != std::string::npos)\r\n    {\r\n        // auto startpos = html.find(\"<\");\r\n        auto endpos = html.find(\">\", startpos + 1);\r\n        if (endpos == std::string::npos)\r\n        {\r\n            break;\r\n        }\r\n        html.erase(startpos, endpos - startpos + 1);\r\n    }\r\n}\r\n\r\nstd::string removeCdata(const std::string& str)\r\n{\r\n    if (startsWith(str, \"<![CDATA[\") && endsWith(str, \"]]>\")) return str.substr(9, str.size() - 12);\r\n    return str;\r\n}\r\n\r\nstd::string fromUnixTime(unsigned int unixtime, bool localTime/* = true*/)\r\n{\r\n    std::time_t ts = unixtime;\r\n    std::tm* t1 = std::localtime(&ts);\r\n    if (!localTime)\r\n    {\r\n        std::time_t local_secs = std::mktime(t1);\r\n\r\n        struct tm *t2 = gmtime(&ts);\r\n        std::time_t gmt_secs = mktime(t2);\r\n        \r\n        ts -= gmt_secs - local_secs;\r\n        t1 = std::localtime(&ts);\r\n    }\r\n    \r\n    char buf[30] = { 0 };\r\n\tstd::strftime(buf, 30, \"%Y-%m-%d %H:%M:%S\", t1);\r\n\r\n    // std::stringstream ss; // or if you're going to print, just input directly into the output stream\r\n    // ss << std::put_time(t, \"%Y-%m-%d %H:%M:%S\");\r\n\r\n    return std::string(buf);\r\n}\r\n\r\nuint32_t getUnixTimeStamp()\r\n{\r\n\ttime_t rawTime = 0;\r\n\ttime(&rawTime);\r\n\tstruct tm *localTm = localtime(&rawTime);\r\n\treturn mktime(localTm);\r\n}\r\n/*\r\nbool existsFile(const std::string &path)\r\n{\r\n#ifdef _WIN32\r\n    struct stat buffer;\r\n\tCW2A pszA(CA2W(path.c_str(), CP_UTF8));\r\n    return (stat ((LPCSTR)pszA, &buffer) == 0);\r\n#else\r\n\tstruct stat buffer;\r\n\treturn (stat(path.c_str(), &buffer) == 0);\r\n#endif\r\n}\r\n*/\r\n\r\n/*\r\nint makePathImpl(const std::string::value_type *path, mode_t mode)\r\n{\r\n    struct stat st;\r\n    int status = 0;\r\n\r\n    if (stat(path, &st) != 0)\r\n    {\r\n        // Directory does not exist. EEXIST for race condition \r\n        if (mkdir(path, mode) != 0 && errno != EEXIST)\r\n            status = -1;\r\n    }\r\n    else if (!S_ISDIR(st.st_mode))\r\n    {\r\n        // errno = ENOTDIR;\r\n        status = -1;\r\n    }\r\n\r\n    return status;\r\n}\r\n\r\nint makePath(const std::string& path, mode_t mode)\r\n{\r\n    std::vector<std::string::value_type> copypath;\r\n    copypath.reserve(path.size() + 1);\r\n    std::copy(path.begin(), path.end(), std::back_inserter(copypath));\r\n    copypath.push_back('\\0');\r\n    std::replace(copypath.begin(), copypath.end(), '\\\\', '/');\r\n    \r\n    std::vector<std::string::value_type>::iterator itStart = copypath.begin();\r\n    std::vector<std::string::value_type>::iterator it;\r\n    \r\n    int status = 0;\r\n    while (status == 0 && (it = std::find(itStart, copypath.end(), '/')) != copypath.end())\r\n    {\r\n        if (it != copypath.begin())\r\n        {\r\n            // Neither root nor double slash in path\r\n            *it = '\\0';\r\n            status = makePathImpl(&copypath[0], mode);\r\n            *it = '/';\r\n        }\r\n        itStart = it + 1;\r\n    }\r\n    if (status == 0)\r\n    {\r\n        status = makePathImpl(&copypath[0], mode);\r\n    }\r\n    \r\n    return status;\r\n}\r\n*/\r\n/*\r\nbool moveFile(const std::string& src, const std::string& dest, bool overwrite)\r\n{\r\n#ifdef _WIN32\r\n\tCW2T pszSrc(CA2W(src.c_str(), CP_UTF8));\r\n\tCW2T pszDest(CA2W(dest.c_str(), CP_UTF8));\r\n\tif (overwrite)\r\n\t{\r\n\t\t::DeleteFile(pszDest);\r\n\t}\r\n\tBOOL bErrorFlag = ::MoveFile(pszSrc, pszDest);\r\n\treturn (TRUE == bErrorFlag);\r\n#else\r\n\tif (overwrite)\r\n\t{\r\n\t\tremove(dest.c_str());\r\n\t}\r\n\treturn 0 == rename(src.c_str(), dest.c_str());\r\n#endif\r\n}\r\n\r\nbool copyFile(const std::string& src, const std::string& dest)\r\n{\r\n#ifdef _WIN32\r\n\tCW2T pszSrc(CA2W(src.c_str(), CP_UTF8));\r\n\tCW2T pszDest(CA2W(dest.c_str(), CP_UTF8));\r\n\r\n\tBOOL bErrorFlag = ::CopyFile(pszSrc, pszDest, FALSE);\r\n\treturn (TRUE == bErrorFlag);\r\n#else\r\n\tstd::ifstream  ss(src, std::ios::binary);\r\n\tstd::ofstream  ds(dest, std::ios::binary);\r\n\r\n\tds << ss.rdbuf();\r\n\r\n\treturn true;\r\n#endif\r\n}\r\n*/\r\n\r\n#ifdef _WIN32\r\nstd::string utf8ToLocalAnsi(const std::string& utf8Str)\r\n{\r\n\tCW2A pszA(CA2W(utf8Str.c_str(), CP_UTF8));\r\n\treturn std::string((LPCSTR)pszA);\r\n}\r\n#else\r\n#endif\r\n\r\nvoid updateFileTime(const std::string& path, time_t mtime)\r\n{\r\n#ifdef _WIN32\r\n    CW2T pszT(CA2W(path.c_str(), CP_UTF8));\r\n\tstruct _stat st;\r\n\tstruct _utimbuf new_times;\r\n\r\n    _tstat((LPCTSTR)pszT, &st);\r\n\r\n\tnew_times.actime = st.st_atime; /* keep atime unchanged */\r\n\tnew_times.modtime = mtime;\r\n\r\n\t_tutime((LPCTSTR)pszT, &new_times);\r\n#else\r\n\tstruct stat st;\r\n\tstruct utimbuf new_times;\r\n\r\n    stat(path.c_str(), &st);\r\n\r\n\tnew_times.actime = st.st_atime; /* keep atime unchanged */\r\n\tnew_times.modtime = mtime;\r\n\r\n\tutime(path.c_str(), &new_times);\r\n#endif\r\n}\r\n\r\n/*\r\nbool deleteFile(const std::string& fileName)\r\n{\r\n    return 0 == std::remove(fileName.c_str());\r\n}\r\n*/\r\n\r\nint openSqlite3Database(const std::string& path, sqlite3 **ppDb, bool readOnly/* = true*/)\r\n{\r\n    std::string encodedPath;\r\n#ifdef _WIN32\r\n    TCHAR szDriver[_MAX_DRIVE] = { 0 };\r\n    TCHAR szDir[_MAX_DIR] = { 0 };\r\n    \r\n    CW2T pszT(CA2W(normalizePath(path).c_str(), CP_UTF8));\r\n    \r\n    _tsplitpath(pszT, szDriver, szDir, NULL, NULL);\r\n    size_t driveLen = _tcslen(szDriver);\r\n    if (driveLen == 0)\r\n    {\r\n        // NO driver\r\n        encodedPath = path;\r\n    }\r\n    else\r\n    {\r\n        CW2A pszU8(CT2W(&pszT[driveLen]), CP_UTF8);\r\n        encodedPath = pszU8;\r\n    }\r\n#else\r\n    encodedPath = normalizePath(path);\r\n#endif\r\n\r\n    std::vector<std::string> parts = split(encodedPath, DIR_SEP_STR);\r\n    std::vector<std::string> encodedParts;\r\n    encodedPath.reserve(parts.size() + 1);\r\n\r\n    CURL *curl = curl_easy_init();\r\n    if (curl)\r\n    {\r\n        for (std::vector<std::string>::const_iterator it = parts.cbegin(); it != parts.cend(); ++it)\r\n        {\r\n            char *ptr = curl_easy_escape(curl, it->c_str(), static_cast<int>(it->size()));\r\n            if (ptr)\r\n            {\r\n                encodedParts.push_back(std::string(ptr));\r\n                curl_free(ptr);\r\n            }\r\n        }\r\n\r\n        curl_easy_cleanup(curl);\r\n\r\n        encodedPath = join(encodedParts, DIR_SEP_STR);\r\n    }\r\n\r\n#ifdef _WIN32\r\n    if (driveLen == 0)\r\n    {\r\n        if (_tcslen(szDir) > 0 && szDir[0] == DIR_SEP)\r\n        {\r\n            encodedPath = DIR_SEP_STR + encodedPath;\r\n        }\r\n    }\r\n    else\r\n    {\r\n        CW2A pszU8(CT2W(szDriver), CP_UTF8);\r\n\t\tencodedPath = std::string((LPCSTR)pszU8) + DIR_SEP_STR + encodedPath;\r\n    }\r\n#else\r\n    if (startsWith(path, DIR_SEP_STR))\r\n    {\r\n        encodedPath = DIR_SEP_STR + encodedPath;\r\n    }\r\n#endif\r\n\r\n#ifdef _WIN32\r\n    std::string pathWithQuery = \"file:///\" + encodedPath;\r\n#else\r\n\tstd::string pathWithQuery = \"file://\" + encodedPath;\r\n#endif\r\n    // std::string pathWithQuery = \"file:\" + path;\r\n    pathWithQuery += readOnly ? \"?immutable=1&mode=ro\" : \"?mode=rwc\";\r\n\r\n    return sqlite3_open_v2(pathWithQuery.c_str(), ppDb, readOnly ? (SQLITE_OPEN_READONLY | SQLITE_OPEN_URI) : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI), NULL);\r\n}\r\n\r\n\r\nbool isBigEndian()\r\n{\r\n    short int number = 0x1;\r\n    char *numPtr = (char*)&number;\r\n    return (numPtr[0] != 1);\r\n}\r\n\r\ntemplate<typename T>\r\nT swapEndian(T u)\r\n{\r\n    union ET\r\n    {\r\n        T u;\r\n        unsigned char u8[sizeof(T)];\r\n    } src, dest;\r\n\r\n    src.u = u;\r\n\r\n    for (size_t i = 0; i < sizeof(T); ++i)\r\n        dest.u8[i] = src.u8[sizeof(T) - i - 1];\r\n\r\n    return dest.u;\r\n}\r\n\r\nint GetBigEndianInteger(const unsigned char* data, int startIndex/* = 0*/)\r\n{\r\n    if (isBigEndian())\r\n    {\r\n        return *((int *)(data + startIndex));\r\n    }\r\n    \r\n#ifndef NDEBUG\r\n    int aa =  (data[startIndex] << 24)\r\n         | (data[startIndex + 1] << 16)\r\n         | (data[startIndex + 2] << 8)\r\n         | data[startIndex + 3];\r\n    \r\n    int bb = swapEndian(*((int *)(data + startIndex)));\r\n    if (aa == bb)\r\n    {\r\n        aa = bb;\r\n    }\r\n#endif\r\n    return swapEndian(*((int *)&data[startIndex]));\r\n}\r\n\r\nint16_t bigEndianToNative(int16_t n)\r\n{\r\n    return isBigEndian() ? n : swapEndian(n);\r\n}\r\n\r\nint32_t bigEndianToNative(int32_t n)\r\n{\r\n    return isBigEndian() ? n : swapEndian(n);\r\n}\r\n\r\nint64_t bigEndianToNative(int64_t n)\r\n{\r\n    return isBigEndian() ? n : swapEndian(n);\r\n}\r\n\r\nuint16_t bigEndianToNative(uint16_t n)\r\n{\r\n    return isBigEndian() ? n : swapEndian(n);\r\n}\r\n\r\nuint32_t bigEndianToNative(uint32_t n)\r\n{\r\n    return isBigEndian() ? n : swapEndian(n);\r\n}\r\n\r\nuint64_t bigEndianToNative(uint64_t n)\r\n{\r\n    return isBigEndian() ? n : swapEndian(n);\r\n}\r\n\r\n\r\nint GetLittleEndianInteger(const unsigned char* data, int startIndex/* = 0*/)\r\n{\r\n    return (data[startIndex + 3] << 24)\r\n         | (data[startIndex + 2] << 16)\r\n         | (data[startIndex + 1] << 8)\r\n         | data[startIndex];\r\n}\r\n\r\nstd::string encodeUrl(const std::string& url)\r\n{\r\n    std::string encodedUrl = url;\r\n    CURL *curl = curl_easy_init();\r\n    if(curl)\r\n    {\r\n        char *output = curl_easy_escape(curl, url.c_str(), static_cast<int>(url.size()));\r\n        if(output)\r\n        {\r\n            encodedUrl = output;\r\n            curl_free(output);\r\n        }\r\n        \r\n        curl_easy_cleanup(curl);\r\n    }\r\n    \r\n    return encodedUrl;\r\n}\r\n\r\nstd::string decodeUrl(const std::string& url)\r\n{\r\n    std::string decodedUrl = url;\r\n    CURL *curl = curl_easy_init();\r\n    if(curl)\r\n    {\r\n        int outlength = 0;\r\n        char *output = curl_easy_unescape(curl, url.c_str(), static_cast<int>(url.size()), &outlength);\r\n        if(output)\r\n        {\r\n            decodedUrl = output;\r\n            curl_free(output);\r\n        }\r\n        \r\n        curl_easy_cleanup(curl);\r\n    }\r\n    \r\n    return decodedUrl;\r\n}\r\n\r\nstd::string getTimestampString(bool includingYMD/* = false*/, bool includingMs/* = false*/)\r\n{\r\n    using std::chrono::system_clock;\r\n    auto currentTime = std::chrono::system_clock::now();\r\n    char buffer[80];\r\n\r\n    std::time_t tt;\r\n    tt = system_clock::to_time_t ( currentTime );\r\n\tauto timeinfo = localtime (&tt);\r\n\tstrftime (buffer, 80, includingYMD ? \"%F %H:%M:%S\" : \"%H:%M:%S\", timeinfo);\r\n    if (includingMs)\r\n    {\r\n        auto transformed = currentTime.time_since_epoch().count() / 1000000;\r\n        auto millis = transformed % 1000;\r\n        sprintf(buffer, \"%s.%03d\", buffer, (int)millis);\r\n    }\r\n\r\n    return std::string(buffer);\r\n}\r\n\r\nbool isNumber(const std::string &s)\r\n{\r\n    return !s.empty() && std::all_of(s.begin(), s.end(), ::isdigit);\r\n}\r\n\r\n\r\nstd::string makeUuid()\r\n{\r\n#ifdef WIN32\r\n    UUID uuid;\r\n    UuidCreate ( &uuid );\r\n\r\n\tRPC_CSTR str = NULL;\r\n    UuidToStringA ( &uuid, &str );\r\n\r\n    std::string s((LPCSTR)CW2A(CA2W((LPCSTR)str), CP_UTF8));\r\n\r\n    RpcStringFreeA(&str);\r\n#else\r\n    uuid_t uuid;\r\n    uuid_generate_random ( uuid );\r\n    char s[37];\r\n    uuid_unparse ( uuid, s );\r\n#endif\r\n    return s;\r\n}\r\n\r\nstd::string toHex(unsigned char* data, size_t length)\r\n{\r\n    std::stringstream stream;\r\n    stream << std::setfill ('0') << std::hex;\r\n    \r\n    for (int idx = 0; idx < length; idx++)\r\n    {\r\n        stream << std::setw(2) << ((unsigned int) data[idx]);\r\n    }\r\n    return stream.str();\r\n}\r\n\r\nstd::string toHex(char* data, size_t length)\r\n{\r\n    return toHex((unsigned char *)data, length);\r\n}\r\n"
  },
  {
    "path": "WechatExporter/core/Utils.h",
    "content": "//\n//  util.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include <string>\n#include <vector>\n#include <map>\n#include <thread>\n#include <locale>\n\n#ifdef _WIN32\n#include <io.h>\ntypedef int mode_t;\n#endif\n\n#define ENABLE_AUDIO_CONVERTION\n\n#ifndef Utils_h\n#define Utils_h\n\nint replaceAll(std::string& input, const std::string& search, const std::string& replace);\nint replaceAll(std::string& input, const std::vector<std::pair<std::string, std::string>>& pairs);\n// std::string replaceAll(const std::string& input, const std::string& search, const std::string& replace);\n// std::string replaceAll(const std::string& input, const std::vector<std::pair<std::string, std::string>>& pairs);\n\n\nbool endsWith(const std::string& str, const std::string& suffix);\nbool endsWith(const std::string& str, std::string::value_type ch);\nbool startsWith(const std::string& str, const std::string& prefix, int pos = 0);\nbool startsWith(const std::string& str, const std::string::value_type ch);\n\nstd::string toUpper(const std::string& str);\nstd::string toLower(const std::string& str);\n\nstd::vector<std::string> split(const std::string& str, const std::string& delim);\nstd::string join(const std::vector<std::string>& elements, const char *const delimiter);\nstd::string join(std::vector<std::string>::const_iterator b, std::vector<std::string>::const_iterator e, const char *const delimiter);\n\ntemplate <typename ...Args>\nstd::string formatString(const std::string& format, Args && ...args)\n{\n    auto size = std::snprintf(nullptr, 0, format.c_str(), std::forward<Args>(args)...);\n    std::string output(size, '\\0');\n    std::sprintf(&output[0], format.c_str(), std::forward<Args>(args)...);\n    return output;\n}\n\n// bool existsFile(const std::string &path);\n// int makePath(const std::string& path, mode_t mode);\n\nstd::string md5(const std::string& s);\nstd::string sha1(const std::string& s);\nstd::string md5File(const std::string& path);\n\nstd::string safeHTML(const std::string& s);\nvoid removeHtmlTags(std::string& html);\n\nstd::string removeCdata(const std::string& str);\n\nstd::string fromUnixTime(unsigned int unixtime, bool localTime = true);\nuint32_t getUnixTimeStamp();\n\nconst char* calcVarint32Ptr(const char* p, const char* limit, uint32_t* value);\nconst unsigned char* calcVarint32Ptr(const unsigned char* p, const unsigned char* limit, uint32_t* value);\n\n// bool moveFile(const std::string& src, const std::string& dest, bool overwrite = true);\n// bool copyFile(const std::string& src, const std::string& dest);\n\n#ifdef _WIN32\nstd::string utf8ToLocalAnsi(const std::string& utf8Str);\n#else\n#define utf8ToLocalAnsi(utf8Str) utf8Str\n#endif\nvoid updateFileTime(const std::string& path, time_t mtime);\n// bool deleteFile(const std::string& fileName);\n\nbool isBigEndian();\nint GetBigEndianInteger(const unsigned char* data, int startIndex = 0);\nint GetLittleEndianInteger(const unsigned char* data, int startIndex = 0);\n\nint16_t bigEndianToNative(int16_t n);\nint32_t bigEndianToNative(int32_t n);\nint64_t bigEndianToNative(int64_t n);\nuint16_t bigEndianToNative(uint16_t n);\nuint32_t bigEndianToNative(uint32_t n);\nuint64_t bigEndianToNative(uint64_t n);\n\nstruct sqlite3;\nint openSqlite3Database(const std::string& path, sqlite3 **ppDb, bool readOnly = true);\n\nstd::string encodeUrl(const std::string& url);\nstd::string decodeUrl(const std::string& url);\n\n// std::string utcToLocal(const std::string& utcTime);\nstd::string getTimestampString(bool includingYMD = false, bool includingMs = false);\n\nbool amrToPcm(const std::string& silkPath, std::vector<unsigned char>& pcmData, std::string* error = NULL);\nbool amrToPcm(const std::string& silkPath, const std::string& pcmPath, std::string* error = NULL);\n\nbool silkToPcm(const std::string& silkPath, std::vector<unsigned char>& pcmData, bool& isSilk, std::string* error = NULL);\nbool silkToPcm(const std::string& silkPath, const std::string& pcmPath, bool& isSilk, std::string* error = NULL);\n\nbool pcmToMp3(const std::string& pcmPath, const std::string& mp3Path, std::string* error = NULL);\nbool pcmToMp3(const std::vector<unsigned char>& pcmData, const std::string& mp3Path, std::string* error = NULL);\n\nbool amrPcmToMp3(const std::string& pcmPath, const std::string& mp3Path, std::string* error = NULL);\nbool amrPcmToMp3(const std::vector<unsigned char>& pcmData, const std::string& mp3Path, std::string* error = NULL);\n\nvoid setThreadName(const char* threadName);\nbool isNumber(const std::string &s);\n\nstd::string makeUuid();\n\nstd::string toHex(unsigned char* data, size_t length);\nstd::string toHex(char* data, size_t length);\n\n#endif /* Utils_h */\n"
  },
  {
    "path": "WechatExporter/core/Utils_audio.cpp",
    "content": "//\n//  Utils_audio.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//  Refer: https://www.programmersought.com/article/2635152445/\n//\n\nextern \"C\"\n{\n#include <lame/lame.h>\n}\n#include \"Utils.h\"\n#include \"FileSystem.h\"\n#ifdef _WIN32\n#include <atlstr.h>\n#ifndef NDEBUG\n#include <cassert>\n#endif\n#endif\n\nbool pcmToMp3(const std::string& pcmPath, const std::string& mp3Path, std::string* error/* = NULL*/)\n{\n\tstd::vector<unsigned char> pcmData;\n\tif (!readFile(pcmPath, pcmData))\n\t{\n\t\treturn false;\n\t}\n\n    return pcmToMp3(pcmData, mp3Path, error);\n}\n\nbool pcmToMp3(const std::vector<unsigned char>& pcmData, const std::string& mp3Path, std::string* error/* = NULL*/)\n{\n#ifndef NDEBUG\n    assert(!pcmData.empty());\n#endif\n#ifdef ENABLE_AUDIO_CONVERTION\n    const int MP3_SIZE = 4096;\n    const int num_of_channels = 1;\n\n    lame_global_flags *gfp = NULL;\n    gfp = lame_init();\n    if (NULL == gfp)\n    {\n        if (NULL != error)\n        {\n            *error = \"lame_init failed.\";\n        }\n        return false;\n    }\n\n    lame_set_in_samplerate(gfp, 24000);\n    lame_set_preset(gfp, 56);\n    lame_set_mode(gfp, MONO);\n    // RG is enabled by default\n    lame_set_findReplayGain(gfp, 1);\n    // lame_set_quality(gfp, 7);\n    //Setting Channels\n    lame_set_num_channels(gfp, num_of_channels);\n\n    unsigned long fsize = (unsigned long) (pcmData.size() / (2 * num_of_channels));\n    lame_set_num_samples(gfp, fsize);\n    \n    int samples_to_read = lame_get_framesize(gfp);\n    int samples_of_channel = 576;\n    samples_to_read = samples_of_channel * num_of_channels;    //\n    // int framesize = samples_to_read;\n    // std::assert(framesize <= 1152);\n    // int bytes_per_sample = sizeof(short int);\n    \n    lame_set_out_samplerate(gfp, 24000);\n\n    if(lame_init_params(gfp) == -1)\n    {\n        //lame initialization failed\n        lame_close(gfp);\n        if (NULL != error)\n        {\n            *error = \"lame_init_params failed.\";\n        }\n        return false;\n    }\n    \n    std::vector<short int> pcm_buffer;\n    pcm_buffer.resize(samples_to_read, 0);\n    int bytesOfPcmBuffer = static_cast<int>(pcm_buffer.size() * sizeof(short int));\n    unsigned char mp3_buffer[MP3_SIZE];\n    int count = static_cast<int>((pcmData.size() + bytesOfPcmBuffer - 1) / bytesOfPcmBuffer);\n    \n#ifdef _WIN32\n\tCA2W pszW(mp3Path.c_str(), CP_UTF8);\n    // CW2A pszA(pszW);\n    FILE *mp3 = _wfopen((LPCWSTR)pszW, L\"wb\" );\n\t// FILE *mp3 = fopen(mp3Path.c_str(), \"wb,ccs=UTF-8\");\n#else\n    FILE *mp3 = fopen(mp3Path.c_str(), \"wb\");\n#endif\n    \n    if(mp3 == NULL)\n    {\n        lame_close(gfp);\n        if (NULL != error)\n        {\n            *error = \"Failed to open file for writing: \" + mp3Path;\n        }\n        return false;\n    }\n    for (int idx = 0; idx < count; ++idx)\n    {\n        if (idx == (count - 1) && (pcmData.size() % bytesOfPcmBuffer) != 0)\n        {\n            pcm_buffer.assign(pcm_buffer.size(), 0);\n            samples_to_read = ((pcmData.size() % bytesOfPcmBuffer) + sizeof(short int) - 1) / sizeof(short int);\n        }\n        memcpy(reinterpret_cast<void *>(&(pcm_buffer[0])), reinterpret_cast<const void *>(&(pcmData[idx * bytesOfPcmBuffer])), samples_to_read * sizeof(short int));\n        \n        int write = lame_encode_buffer(gfp, &pcm_buffer[0], NULL, samples_of_channel, mp3_buffer, MP3_SIZE);\n        fwrite(mp3_buffer, write, sizeof(char), mp3);\n    }\n\n    fclose(mp3);\n    lame_close(gfp);\n\n#endif // ENABLE_AUDIO_CONVERTION\n    \n    return true;\n}\n\nbool amrPcmToMp3(const std::string& pcmPath, const std::string& mp3Path, std::string* error/* = NULL*/)\n{\n    std::vector<unsigned char> pcmData;\n    if (!readFile(pcmPath, pcmData))\n    {\n        return false;\n    }\n\n    return amrPcmToMp3(pcmData, mp3Path, error);\n}\n\nbool amrPcmToMp3(const std::vector<unsigned char>& pcmData, const std::string& mp3Path, std::string* error/* = NULL*/)\n{\n#ifndef NDEBUG\n    assert(!pcmData.empty());\n#endif\n#ifdef ENABLE_AUDIO_CONVERTION\n    const int MP3_SIZE = 4096;\n    const int num_of_channels = 1;\n    const int bits_per_sample = 16;\n\n    lame_global_flags *gfp = NULL;\n    gfp = lame_init();\n    if (NULL == gfp)\n    {\n        if (NULL != error)\n        {\n            *error = \"lame_init failed.\";\n        }\n        return false;\n    }\n\n    //Setting Channels\n    lame_set_num_channels(gfp, num_of_channels);\n    lame_set_in_samplerate(gfp, 8000);\n    // lame_set_preset(gfp, 56);\n    // lame_set_mode(gfp, MONO);\n    // RG is enabled by default\n    lame_set_findReplayGain(gfp, 1);\n    // lame_set_quality(gfp, 7);\n    \n    \n    /*\n    if (lame_get_VBR(gfp) == vbr_off)\n    {\n        lame_set_VBR(gfp, vbr_default);\n    }\n    lame_set_VBR_quality(gfp, 2);\n     */\n\n    unsigned long fsize = (unsigned long) (pcmData.size() / (num_of_channels * ((bits_per_sample + 7) / 8)));\n    // lame_set_num_samples(gfp, fsize);\n    lame_set_num_samples(gfp, pcmData.size() / (num_of_channels * ((bits_per_sample + 7) / 8)));\n    \n    int samples_to_read = lame_get_framesize(gfp);\n    // int samples_of_channel = 576;\n    int samples_of_channel = 320;\n    samples_to_read = samples_of_channel * num_of_channels;    //\n    // int framesize = samples_to_read;\n    // std::assert(framesize <= 1152);\n    // int bytes_per_sample = sizeof(short int);\n    \n    lame_set_out_samplerate(gfp, 24000);\n\n    if(lame_init_params(gfp) == -1)\n    {\n        //lame initialization failed\n        lame_close(gfp);\n        if (NULL != error)\n        {\n            *error = \"lame_init_params failed.\";\n        }\n        return false;\n    }\n    \n    std::vector<short int> pcm_buffer;\n    pcm_buffer.resize(samples_to_read, 0);\n    int bytesOfPcmBuffer = static_cast<int>(pcm_buffer.size() * sizeof(short int));\n    unsigned char mp3_buffer[MP3_SIZE];\n    int count = static_cast<int>((pcmData.size() + bytesOfPcmBuffer - 1) / bytesOfPcmBuffer);\n    \n#ifdef _WIN32\n    CA2W pszW(mp3Path.c_str(), CP_UTF8);\n    // CW2A pszA(pszW);\n    FILE *mp3 = _wfopen((LPCWSTR)pszW, L\"wb\" );\n    // FILE *mp3 = fopen(mp3Path.c_str(), \"wb,ccs=UTF-8\");\n#else\n    FILE *mp3 = fopen(mp3Path.c_str(), \"wb\");\n#endif\n    \n    if(mp3 == NULL)\n    {\n        lame_close(gfp);\n        if (NULL != error)\n        {\n            *error = \"Failed to open file for writing: \" + mp3Path;\n        }\n        return false;\n    }\n    for (int idx = 0; idx < count; ++idx)\n    {\n        if (idx == (count - 1) && (pcmData.size() % bytesOfPcmBuffer) != 0)\n        {\n            pcm_buffer.assign(pcm_buffer.size(), 0);\n            samples_to_read = ((pcmData.size() % bytesOfPcmBuffer) + sizeof(short int) - 1) / sizeof(short int);\n        }\n        memcpy(reinterpret_cast<void *>(&(pcm_buffer[0])), reinterpret_cast<const void *>(&(pcmData[idx * bytesOfPcmBuffer])), samples_to_read * sizeof(short int));\n        \n        int write = lame_encode_buffer(gfp, &pcm_buffer[0], &pcm_buffer[1], samples_of_channel, mp3_buffer, MP3_SIZE);\n        fwrite(mp3_buffer, write, sizeof(char), mp3);\n    }\n\n    fclose(mp3);\n    lame_close(gfp);\n\n#endif // ENABLE_AUDIO_CONVERTION\n    \n    return true;\n}\n"
  },
  {
    "path": "WechatExporter/core/Utils_md5.cpp",
    "content": "//\n//  Utils.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include <string>\n#include <sstream>\n#include <iomanip>\n\n#if defined(_WIN32)\n#include <windows.h>\n#include <wincrypt.h>\n\n#define MD5_DIGEST_LENGTH 16\n#define SHA_DIGEST_LENGTH 20\n\n#elif defined(__APPLE__)\n#import <CommonCrypto/CommonDigest.h>\n#else\n\n#endif\n\n#include \"FileSystem.h\"\n\nstd::string md5Impl(const void* data, size_t dataSize)\n{\n    std::stringstream stream;\n    stream << std::setfill ('0') << std::hex;\n    \n#if defined(_WIN32)\n    \n    HCRYPTPROV hCryptProv = NULL;\n    HCRYPTHASH hHash = NULL;\n    BYTE bHash[0x7f] = {0};\n    DWORD dwHashLen= MD5_DIGEST_LENGTH; // The MD5 algorithm always returns 16 bytes.\n\n    if(CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET))\n    {\n        if(CryptCreateHash(hCryptProv, CALG_MD5, 0, 0, &hHash))\n        {\n            if(CryptHashData(hHash, reinterpret_cast<const BYTE*>(data), static_cast<DWORD>(dataSize), 0))\n            {\n                if(CryptGetHashParam(hHash, HP_HASHVAL, bHash, &dwHashLen, 0))\n                {\n                    // Make a string version of the numeric digest value\n                    for (int idx = 0; idx < 16; idx++)\n                    {\n                        stream << std::setw(2) << ((unsigned int) bHash[idx]);\n                    }\n                }\n            }\n        }\n    }\n\n    CryptDestroyHash(hHash);\n    CryptReleaseContext(hCryptProv, 0);\n\n#elif defined(__APPLE__)\n    unsigned char digest[CC_MD5_DIGEST_LENGTH] = {0};\n    CC_MD5(data, (CC_LONG)dataSize, digest); // This is the md5 call\n\n    for (int idx = 0; idx < CC_MD5_DIGEST_LENGTH; idx++)\n    {\n        stream << std::setw(2) << ((unsigned int) digest[idx]);\n    }\n#else\n#error \"Md5 Not implemented.\"\n#endif\n\n    return stream.str();\n}\n\nstd::string md5(const std::string& s)\n{\n    return md5Impl(s.c_str(), s.size());\n}\n\nstd::string md5File(const std::string& path)\n{\n    std::vector<unsigned char> data;\n    \n    if (readFile(path, data) && !data.empty())\n    {\n        return md5Impl(&data[0], data.size());\n    }\n    \n    return \"\";\n}\n\nstd::string sha1(const std::string& s)\n{\n    std::stringstream stream;\n    stream << std::setfill ('0') << std::hex;\n    \n#if defined(_WIN32)\n    \n    HCRYPTPROV hCryptProv = NULL;\n    HCRYPTHASH hHash = NULL;\n    BYTE bHash[0x7f] = {0};\n    DWORD dwHashLen= SHA_DIGEST_LENGTH ; // The SHA1 algorithm always returns 20 bytes.\n\n    if(CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET))\n    {\n        if(CryptCreateHash(hCryptProv, CALG_SHA1, 0, 0, &hHash))\n        {\n            if(CryptHashData(hHash, reinterpret_cast<const BYTE*>(s.c_str()), static_cast<DWORD>(s.size()), 0))\n            {\n                if(CryptGetHashParam(hHash, HP_HASHVAL, bHash, &dwHashLen, 0))\n                {\n                    // Make a string version of the numeric digest value\n                    for (int idx = 0; idx < dwHashLen; idx++)\n                    {\n                        stream << std::setw(2) << ((unsigned int) bHash[idx]);\n                    }\n                }\n            }\n        }\n    }\n\n    CryptDestroyHash(hHash);\n    CryptReleaseContext(hCryptProv, 0);\n\n#elif defined(__APPLE__)\n    unsigned char digest[CC_SHA1_DIGEST_LENGTH] = {0};\n    CC_SHA1(s.c_str(), (CC_LONG)s.size(), digest); // This is the md5 call\n\n    for (int idx = 0; idx < CC_SHA1_DIGEST_LENGTH; idx++)\n    {\n        stream << std::setw(2) << ((unsigned int) digest[idx]);\n    }\n#else\n#error \"SHA1 Not implemented.\"\n#endif\n\n    return stream.str();\n}\n"
  },
  {
    "path": "WechatExporter/core/Utils_protobuf.cpp",
    "content": "//\n//  Utils.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include \"Utils.h\"\n#include <string>\n#include <vector>\n#include <iostream>\n#include <fstream>\n#include <iomanip>\n\nconst char* GetVarint64Ptr(const char* p, const char* limit, uint64_t* value) {\n  uint64_t result = 0;\n  for (uint32_t shift = 0; shift <= 63 && p < limit; shift += 7) {\n    uint64_t byte = *(reinterpret_cast<const uint8_t*>(p));\n    p++;\n    if (byte & 0x80) {\n      // More bytes are present\n      result |= ((byte & 0x7F) << shift);\n    } else {\n      result |= (byte << shift);\n      *value = result;\n      return reinterpret_cast<const char*>(p);\n    }\n  }\n  return nullptr;\n}\n\nconst char* GetVarint32PtrFallback(const char* p, const char* limit, uint32_t* value)\n{\n    uint32_t result = 0;\n    for (uint32_t shift = 0; shift <= 28 && p < limit; shift += 7)\n    {\n        uint32_t byte = *(reinterpret_cast<const unsigned char*>(p));\n        p++;\n        if (byte & 128)\n        {\n            // More bytes are present\n            result |= ((byte & 127) << shift);\n        }\n        else\n        {\n            result |= (byte << shift);\n            *value = result;\n            return reinterpret_cast<const char*>(p);\n        }\n    }\n    return NULL;\n}\n\nconst char* calcVarint32Ptr(const char* p, const char* limit, uint32_t* value)\n{\n    if (p < limit)\n    {\n        uint32_t result = *(reinterpret_cast<const unsigned char*>(p));\n        if ((result & 0x80) == 0)\n        {\n            *value = result;\n            return p + 1;\n        }\n    }\n    return GetVarint32PtrFallback(p, limit, value);\n}\n\nconst unsigned char* calcVarint32Ptr(const unsigned char* p, const unsigned char* limit, uint32_t* value)\n{\n    const char* p1 = calcVarint32Ptr(reinterpret_cast<const char*>(p), reinterpret_cast<const char*>(limit), value);\n    return reinterpret_cast<const unsigned char*>(p1);\n}\n\n/*\nclass Protobuf2JsonErrorCollector : public google::protobuf::compiler::MultiFileErrorCollector\n{\n    virtual void AddError(const std::string & filename, int line, int column, const std::string & message) {\n        // define import error collector\n        printf(\"%s, %d, %d, %s\\n\", filename.c_str(), line, column, message.c_str());\n    }\n};\n*/\n\n/*\nbool parseFieldValueFromProtobuf(const unsigned char *data, size_t length, const std::string &fields, std::string& value)\n{\n    std::vector<unsigned int> fieldNumbers;\n    \n    std::string::size_type start = 0;\n    std::string::size_type end = fields.find('.');\n    while (end != std::string::npos)\n    {\n        std::string field = fields.substr(start, end - start);\n        if (field.empty())\n        {\n            return false;\n        }\n        \n        int fieldNumber = std::stoi(field);\n        fieldNumbers.push_back(fieldNumber);\n        start = end + 1;\n        end = fields.find('.', start);\n    }\n    std::string field = fields.substr(start, end);\n    if (field.empty())\n    {\n        return false;\n    }\n    int fieldNumber = std::stoi(field);\n    fieldNumbers.push_back(fieldNumber);\n\n    DescriptorPool pool;\n    FileDescriptorProto file;\n    file.set_name(\"empty_message.proto\");\n    file.add_message_type()->set_name(\"EmptyMessage\");\n    GOOGLE_CHECK(pool.BuildFile(file) != NULL);\n\n    const Descriptor *descriptor = pool.FindMessageTypeByName(\"EmptyMessage\");\n    if (NULL == descriptor)\n    {\n        // FormatError(outputString, lengthOfOutputString, ERROR_NO_MESSAGE_TYPE, src->messageTypeName);\n        return false;\n    }\n\n    DynamicMessageFactory factory(&pool);\n    const Message *message = factory.GetPrototype(descriptor);\n    \n    std::unique_ptr<Message> msg(message->New());\n    // Message *msg = message->New();\n    if (NULL == msg)\n    {\n        // FormatError(outputString, lengthOfOutputString, ERROR_NEW_MESSAGE, src->messageTypeName);\n        return false;\n    }\n    \n    \n    // ZeroCopyInputStream* in_stream = new FileInputStream(infd, 128);\n    \n    if (!msg->ParseFromArray(reinterpret_cast<const void *>(data), static_cast<int>(length)))\n    {\n        return false;\n    }\n    \n    const UnknownFieldSet& ufs = msg->GetReflection()->GetUnknownFields(*msg);\n    \n    const UnknownFieldSet* pUfs = &ufs;\n    \n    for (int idx = 0; idx < fieldNumbers.size(); ++idx)\n    {\n        bool found = false;\n        for (int fieldIdx = 0; fieldIdx < pUfs->field_count(); ++fieldIdx)\n        {\n            const UnknownField uf = pUfs->field(fieldIdx);\n            if (uf.number() == fieldNumbers[idx])\n            {\n                found = true;\n                if (idx == fieldNumbers.size() - 1)\n                {\n                    value = uf.length_delimited();\n                    return true;\n                }\n                else\n                {\n                    pUfs = &(uf.group());\n                }\n                break;\n            }\n        }\n        \n        if (!found)\n        {\n            break;\n        }\n    }\n\n    return false;\n}\n\nbool parseFieldValueFromProtobuf(const std::string& path, const std::string &fields, std::string& value)\n{\n    std::ifstream file(path.c_str(), std::ios::in|std::ios::binary|std::ios::ate);\n    if (file.is_open())\n    {\n        std::streampos size = file.tellg();\n        std::vector<unsigned char> buffer;\n        buffer.resize(size);\n        \n        file.seekg (0, std::ios::beg);\n        file.read((char *)(&buffer[0]), size);\n        file.close();\n\n        return parseFieldValueFromProtobuf(&buffer[0], size, fields, value);\n    }\n    \n    return false;\n}\n\n\n*/\n\n"
  },
  {
    "path": "WechatExporter/core/Utils_silk.cpp",
    "content": "#ifdef _WIN32\n#define _CRT_SECURE_NO_DEPRECATE    1\n#endif\n\n#include \"Utils.h\"\n#include \"FileSystem.h\"\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <string>\n#include <vector>\n#include <silk/SKP_Silk_SDK_API.h>\n#include <silk/SKP_Silk_SigProc_FIX.h>\n\n#include <opencore-amrnb/interf_dec.h>\n\n#ifdef _WIN32\n#include <atlstr.h>\n#endif\n\n#include \"Utils.h\"\n\n/* Define codec specific settings should be moved to h file */\n#define MAX_BYTES_PER_FRAME     1024\n#define MAX_INPUT_FRAMES        5\n#define MAX_FRAME_LENGTH        480\n#define FRAME_LENGTH_MS         20\n#define MAX_API_FS_KHZ          48\n#define MAX_LBRR_DELAY          2\n\n\n/* From WmfDecBytesPerFrame in dec_input_format_tab.cpp */\nconst int amr_frame_sizes[] = { 12, 13, 15, 17, 19, 20, 26, 31, 5, 6, 5, 5, 0, 0, 0, 0 };\n\n\n#ifdef _SYSTEM_IS_BIG_ENDIAN\n/* Function to convert a little endian int16 to a */\n/* big endian int16 or vica verca                 */\nvoid swap_endian(\n    SKP_int16       vec[],\n    SKP_int         len\n)\n{\n    SKP_int i;\n    SKP_int16 tmp;\n    SKP_uint8 *p1, *p2;\n\n    for( i = 0; i < len; i++ ){\n        tmp = vec[ i ];\n        p1 = (SKP_uint8 *)&vec[ i ]; p2 = (SKP_uint8 *)&tmp;\n        p1[ 0 ] = p2[ 1 ]; p1[ 1 ] = p2[ 0 ];\n    }\n}\n#endif\n\n#if (defined(_WIN32) || defined(_WINCE))\n#include <windows.h>    /* timer */\n#else    // Linux or Mac\n#include <sys/time.h>\n#endif\n\n#ifdef _WIN32\n\nunsigned long GetHighResolutionTime() /* O: time in usec*/\n{\n    /* Returns a time counter in microsec    */\n    /* the resolution is platform dependent */\n    /* but is typically 1.62 us resolution  */\n    LARGE_INTEGER lpPerformanceCount;\n    LARGE_INTEGER lpFrequency;\n    QueryPerformanceCounter(&lpPerformanceCount);\n    QueryPerformanceFrequency(&lpFrequency);\n    return (unsigned long)((1000000*(lpPerformanceCount.QuadPart)) / lpFrequency.QuadPart);\n}\n#else    // Linux or Mac\nunsigned long GetHighResolutionTime() /* O: time in usec*/\n{\n    struct timeval tv;\n    gettimeofday(&tv, 0);\n    return((tv.tv_sec*1000000)+(tv.tv_usec));\n}\n#endif // _WIN32\n\n/* Seed for the random number generator, which is used for simulating packet loss */\nstatic SKP_int32 rand_seed = 1;\n\nbool silkToPcm(const std::string& silkPath, std::vector<unsigned char>& pcmData, bool& isSilk, std::string* error/* = NULL*/)\n{\n    pcmData.clear();\n    isSilk = false;\n    \n#ifdef ENABLE_AUDIO_CONVERTION\n    unsigned long tottime, starttime;\n    size_t    counter;\n    SKP_int32 totPackets, i, k;\n    SKP_int16 ret, len, tot_len;\n    SKP_int16 nBytes;\n    SKP_uint8 payload[    MAX_BYTES_PER_FRAME * MAX_INPUT_FRAMES * ( MAX_LBRR_DELAY + 1 ) ];\n    SKP_uint8 *payloadEnd = NULL, *payloadToDec = NULL;\n    SKP_uint8 FECpayload[ MAX_BYTES_PER_FRAME * MAX_INPUT_FRAMES ], *payloadPtr;\n    SKP_int16 nBytesFEC;\n    SKP_int16 nBytesPerPacket[ MAX_LBRR_DELAY + 1 ], totBytes;\n    SKP_int16 out[ ( ( FRAME_LENGTH_MS * MAX_API_FS_KHZ ) << 1 ) * MAX_INPUT_FRAMES ], *outPtr;\n    FILE      *bitInFile;\n    SKP_int32 packetSize_ms=0, API_Fs_Hz = 0;\n    SKP_int32 decSizeBytes;\n    void      *psDec;\n    SKP_float loss_prob;\n    SKP_int32 frames, lost, quiet;\n    SKP_SILK_SDK_DecControlStruct DecControl;\n\n    /* default settings */\n    loss_prob = 0.0f;\n\n    /* Open files */\n#ifdef _WIN32\n    CA2W pszW(silkPath.c_str(), CP_UTF8);\n    bitInFile = _wfopen((LPCWSTR)pszW, L\"rb\" );\n#else\n    bitInFile = fopen(silkPath.c_str(), \"rb\" );\n#endif\n    \n    std::unique_ptr<FILE, decltype(std::fclose) *> file(bitInFile, std::fclose);\n    \n    if( bitInFile == NULL )\n    {\n        if (NULL != error)\n        {\n            error->assign(\"Failed to open file: \" + silkPath);\n        }\n        return false;\n    }\n\n    /* Check Silk header */\n    {\n        char header_buf[ 50 ];\n        if (fread(header_buf, sizeof(char), 1, bitInFile) != 1)\n        {\n            fclose(bitInFile);\n            if (NULL != error)\n            {\n                error->assign(\"Can't read header: \" + silkPath);\n            }\n            return false;\n        }\n        // header_buf[ strlen( \"\u0002\" ) ] = '\\0'; /* Terminate with a null character */\n        if( header_buf[0] != 0x02 )\n        {\n           counter = fread( header_buf, sizeof( char ), strlen( \"!SILK_V3\" ), bitInFile );\n           header_buf[ strlen( \"!SILK_V3\" ) ] = '\\0'; /* Terminate with a null character */\n           if( strcmp( header_buf, \"!SILK_V3\" ) != 0 ) {\n               /* Non-equal strings */\n               if (NULL != error)\n               {\n                   error->assign(\"SILK Error: Wrong Header \" + silkPath + \": \" + toHex(header_buf, strlen( \"!SILK_V3\" )));\n               }\n               // printf( \"SILK Error: Wrong Header %s: %s\\n\", silkPath.c_str(), header_buf );\n               // exit( 0 );\n               fclose(bitInFile);\n               return false;\n           }\n            else\n            {\n                isSilk = true;\n            }\n        }\n        else\n        {\n            counter = fread( header_buf, sizeof( char ), strlen( \"#!SILK_V3\" ), bitInFile );\n            header_buf[ strlen( \"#!SILK_V3\" ) ] = '\\0'; /* Terminate with a null character */\n            if( strcmp( header_buf, \"#!SILK_V3\" ) != 0 ) {\n                /* Non-equal strings */\n                if (NULL != error)\n                {\n                    error->assign(\"SILK Error: Wrong Header \" + silkPath + \": \" + toHex(header_buf, strlen( \"!SILK_V3\" )));\n                }\n                // printf( \"SILK Error: Wrong Header %s: %s\\n\", silkPath.c_str(), header_buf );\n                // exit( 0 );\n                fclose(bitInFile);\n                return false;\n            }\n            else\n            {\n                isSilk = true;\n            }\n        }\n    }\n\n    // speechOutFile = fopen( speechOutFileName, \"wb\" );\n    \n    /* Set the samplingrate that is requested for the output */\n    if( API_Fs_Hz == 0 ) {\n        DecControl.API_sampleRate = 24000;\n    } else {\n        DecControl.API_sampleRate = API_Fs_Hz;\n    }\n\n    /* Initialize to one frame per packet, for proper concealment before first packet arrives */\n    DecControl.framesPerPacket = 1;\n\n    /* Create decoder */\n    ret = SKP_Silk_SDK_Get_Decoder_Size( &decSizeBytes );\n    if( ret ) {\n        // printf( \"\\nSKP_Silk_SDK_Get_Decoder_Size returned %d\", ret );\n    }\n    std::vector<unsigned char> bufferDec;\n    bufferDec.resize(decSizeBytes, 0);\n    // psDec = malloc( decSizeBytes );\n    psDec = reinterpret_cast<void *>(&(bufferDec[0]));\n\n    /* Reset decoder */\n    ret = SKP_Silk_SDK_InitDecoder( psDec );\n    if( ret ) {\n        // printf( \"\\nSKP_Silk_InitDecoder returned %d\", ret );\n    }\n\n    totPackets = 0;\n    tottime    = 0;\n    payloadEnd = payload;\n\n    /* Simulate the jitter buffer holding MAX_FEC_DELAY packets */\n    for( i = 0; i < MAX_LBRR_DELAY; i++ ) {\n        /* Read payload size */\n        counter = fread( &nBytes, sizeof( SKP_int16 ), 1, bitInFile );\n#ifdef _SYSTEM_IS_BIG_ENDIAN\n        swap_endian( &nBytes, 1 );\n#endif\n        /* Read payload */\n        counter = fread( payloadEnd, sizeof( SKP_uint8 ), nBytes, bitInFile );\n\n        if( ( SKP_int16 )counter < nBytes ) {\n            break;\n        }\n        nBytesPerPacket[ i ] = nBytes;\n        payloadEnd          += nBytes;\n        totPackets++;\n    }\n\n    while( 1 ) {\n        /* Read payload size */\n        counter = fread( &nBytes, sizeof( SKP_int16 ), 1, bitInFile );\n#ifdef _SYSTEM_IS_BIG_ENDIAN\n        swap_endian( &nBytes, 1 );\n#endif\n        if( nBytes < 0 || counter < 1 ) {\n            break;\n        }\n\n        /* Read payload */\n        counter = fread( payloadEnd, sizeof( SKP_uint8 ), nBytes, bitInFile );\n        if( ( SKP_int16 )counter < nBytes ) {\n            break;\n        }\n\n        /* Simulate losses */\n        rand_seed = SKP_RAND( rand_seed );\n        if( ( ( ( float )( ( rand_seed >> 16 ) + ( 1 << 15 ) ) ) / 65535.0f >= ( loss_prob / 100.0f ) ) && ( counter > 0 ) ) {\n            nBytesPerPacket[ MAX_LBRR_DELAY ] = nBytes;\n            payloadEnd                       += nBytes;\n        } else {\n            nBytesPerPacket[ MAX_LBRR_DELAY ] = 0;\n        }\n\n        if( nBytesPerPacket[ 0 ] == 0 ) {\n            /* Indicate lost packet */\n            lost = 1;\n\n            /* Packet loss. Search after FEC in next packets. Should be done in the jitter buffer */\n            payloadPtr = payload;\n            for( i = 0; i < MAX_LBRR_DELAY; i++ ) {\n                if( nBytesPerPacket[ i + 1 ] > 0 ) {\n                    starttime = GetHighResolutionTime();\n                    SKP_Silk_SDK_search_for_LBRR( payloadPtr, nBytesPerPacket[ i + 1 ], ( i + 1 ), FECpayload, &nBytesFEC );\n                    tottime += GetHighResolutionTime() - starttime;\n                    if( nBytesFEC > 0 ) {\n                        payloadToDec = FECpayload;\n                        nBytes = nBytesFEC;\n                        lost = 0;\n                        break;\n                    }\n                }\n                payloadPtr += nBytesPerPacket[ i + 1 ];\n            }\n        } else {\n            lost = 0;\n            nBytes = nBytesPerPacket[ 0 ];\n            payloadToDec = payload;\n        }\n\n        /* Silk decoder */\n        outPtr = out;\n        tot_len = 0;\n        starttime = GetHighResolutionTime();\n\n        if( lost == 0 ) {\n            /* No Loss: Decode all frames in the packet */\n            frames = 0;\n            do {\n                /* Decode 20 ms */\n                ret = SKP_Silk_SDK_Decode( psDec, &DecControl, 0, payloadToDec, nBytes, outPtr, &len );\n                if( ret ) {\n                    // printf( \"\\nSKP_Silk_SDK_Decode returned %d\", ret );\n                }\n\n                frames++;\n                outPtr  += len;\n                tot_len += len;\n                if( frames > MAX_INPUT_FRAMES ) {\n                    /* Hack for corrupt stream that could generate too many frames */\n                    outPtr  = out;\n                    tot_len = 0;\n                    frames  = 0;\n                }\n                /* Until last 20 ms frame of packet has been decoded */\n            } while( DecControl.moreInternalDecoderFrames );\n        } else {\n            /* Loss: Decode enough frames to cover one packet duration */\n            for( i = 0; i < DecControl.framesPerPacket; i++ ) {\n                /* Generate 20 ms */\n                ret = SKP_Silk_SDK_Decode( psDec, &DecControl, 1, payloadToDec, nBytes, outPtr, &len );\n                if( ret ) {\n                    // printf( \"\\nSKP_Silk_Decode returned %d\", ret );\n                }\n                outPtr  += len;\n                tot_len += len;\n            }\n        }\n\n        packetSize_ms = tot_len / ( DecControl.API_sampleRate / 1000 );\n        tottime += GetHighResolutionTime() - starttime;\n        totPackets++;\n\n        /* Write output to file */\n#ifdef _SYSTEM_IS_BIG_ENDIAN\n        swap_endian( out, tot_len );\n#endif\n        // fwrite( out, sizeof( SKP_int16 ), tot_len, speechOutFile );\n        unsigned char *p = reinterpret_cast<unsigned char *>(out);\n        std::copy(p, p + sizeof( SKP_int16 ) * tot_len, std::back_inserter(pcmData));\n\n        /* Update buffer */\n        totBytes = 0;\n        for( i = 0; i < MAX_LBRR_DELAY; i++ ) {\n            totBytes += nBytesPerPacket[ i + 1 ];\n        }\n        /* Check if the received totBytes is valid */\n        if (totBytes < 0 || totBytes > sizeof(payload))\n        {\n            if (NULL != error)\n            {\n                *error += \"\\rPackets decoded:             \" + std::to_string(totPackets);\n            }\n            // fprintf( stderr, \"\\rPackets decoded:             %d\", totPackets );\n            return false;\n        }\n        SKP_memmove( payload, &payload[ nBytesPerPacket[ 0 ] ], totBytes * sizeof( SKP_uint8 ) );\n        payloadEnd -= nBytesPerPacket[ 0 ];\n        SKP_memmove( nBytesPerPacket, &nBytesPerPacket[ 1 ], MAX_LBRR_DELAY * sizeof( SKP_int16 ) );\n        \n    }\n\n    /* Empty the recieve buffer */\n    for( k = 0; k < MAX_LBRR_DELAY; k++ ) {\n        if( nBytesPerPacket[ 0 ] == 0 ) {\n            /* Indicate lost packet */\n            lost = 1;\n\n            /* Packet loss. Search after FEC in next packets. Should be done in the jitter buffer */\n            payloadPtr = payload;\n            for( i = 0; i < MAX_LBRR_DELAY; i++ ) {\n                if( nBytesPerPacket[ i + 1 ] > 0 ) {\n                    starttime = GetHighResolutionTime();\n                    SKP_Silk_SDK_search_for_LBRR( payloadPtr, nBytesPerPacket[ i + 1 ], ( i + 1 ), FECpayload, &nBytesFEC );\n                    tottime += GetHighResolutionTime() - starttime;\n                    if( nBytesFEC > 0 ) {\n                        payloadToDec = FECpayload;\n                        nBytes = nBytesFEC;\n                        lost = 0;\n                        break;\n                    }\n                }\n                payloadPtr += nBytesPerPacket[ i + 1 ];\n            }\n        } else {\n            lost = 0;\n            nBytes = nBytesPerPacket[ 0 ];\n            payloadToDec = payload;\n        }\n\n        /* Silk decoder */\n        outPtr  = out;\n        tot_len = 0;\n        starttime = GetHighResolutionTime();\n\n        if( lost == 0 ) {\n            /* No loss: Decode all frames in the packet */\n            frames = 0;\n            do {\n                /* Decode 20 ms */\n                ret = SKP_Silk_SDK_Decode( psDec, &DecControl, 0, payloadToDec, nBytes, outPtr, &len );\n                if( ret ) {\n                    if (NULL != error)\n                    {\n                        *error += \"\\nSKP_Silk_SDK_Decode returned \" + std::to_string(ret);\n                    }\n                    // printf( \"\\nSKP_Silk_SDK_Decode returned %d\", ret );\n                }\n\n                frames++;\n                outPtr  += len;\n                tot_len += len;\n                if( frames > MAX_INPUT_FRAMES ) {\n                    /* Hack for corrupt stream that could generate too many frames */\n                    outPtr  = out;\n                    tot_len = 0;\n                    frames  = 0;\n                }\n            /* Until last 20 ms frame of packet has been decoded */\n            } while( DecControl.moreInternalDecoderFrames );\n        } else {\n            /* Loss: Decode enough frames to cover one packet duration */\n\n            /* Generate 20 ms */\n            for( i = 0; i < DecControl.framesPerPacket; i++ ) {\n                ret = SKP_Silk_SDK_Decode( psDec, &DecControl, 1, payloadToDec, nBytes, outPtr, &len );\n                if( ret ) {\n                    if (NULL != error)\n                    {\n                        *error += \"\\nSKP_Silk_Decode returned \" + std::to_string(ret);\n                    }\n                    // printf( \"\\nSKP_Silk_Decode returned %d\", ret );\n                }\n                outPtr  += len;\n                tot_len += len;\n            }\n        }\n\n        packetSize_ms = tot_len / ( DecControl.API_sampleRate / 1000 );\n        tottime += GetHighResolutionTime() - starttime;\n        totPackets++;\n\n        /* Write output to file */\n#ifdef _SYSTEM_IS_BIG_ENDIAN\n        swap_endian( out, tot_len );\n#endif\n        // fwrite( out, sizeof( SKP_int16 ), tot_len, speechOutFile );\n        unsigned char *p = reinterpret_cast<unsigned char *>(out);\n        std::copy(p, p + sizeof( SKP_int16 ) * tot_len, std::back_inserter(pcmData));\n\n        /* Update Buffer */\n        totBytes = 0;\n        for( i = 0; i < MAX_LBRR_DELAY; i++ ) {\n            totBytes += nBytesPerPacket[ i + 1 ];\n        }\n\n        /* Check if the received totBytes is valid */\n        if (totBytes < 0 || totBytes > sizeof(payload))\n        {\n            if (NULL != error)\n            {\n                *error += \"\\rPackets decoded:              \" + std::to_string(totPackets);\n            }\n            // fprintf( stderr, \"\\rPackets decoded:              %d\", totPackets );\n            return false;\n        }\n        \n        SKP_memmove( payload, &payload[ nBytesPerPacket[ 0 ] ], totBytes * sizeof( SKP_uint8 ) );\n        payloadEnd -= nBytesPerPacket[ 0 ];\n        SKP_memmove( nBytesPerPacket, &nBytesPerPacket[ 1 ], MAX_LBRR_DELAY * sizeof( SKP_int16 ) );\n    }\n\n    /* Free decoder */\n    // free( psDec );\n\n    /* Close files */\n    // fclose( bitInFile );\n\n    // filetime = totPackets * 1e-3 * packetSize_ms;\n    \n#endif // ENABLE_AUDIO_CONVERTION\n    \n    return true;\n}\n\nbool silkToPcm(const std::string& silkPath, const std::string& pcmPath, bool& isSilk, std::string* error/* = NULL*/)\n{\n    std::vector<unsigned char> pcmData;\n    bool result = silkToPcm(silkPath, pcmData, isSilk, error);\n    if (result)\n    {\n        result = writeFile(pcmPath, pcmData);\n    }\n    return result;\n}\n\nsize_t skipAmrnbHeader(FILE* fp)\n{\n    const char szFileHeader[] = \"#!AMR\\n\";\n    size_t headerLen = strlen(szFileHeader);\n    \n    unsigned char cData[32];\n    size_t bytesRead = fread(cData, (size_t)1, headerLen, fp);\n    if (bytesRead < headerLen || strncmp((const char *)cData, szFileHeader, bytesRead) != 0)\n    {\n        fseek(fp, 0, SEEK_SET);\n        return 0;\n    }\n    \n    return headerLen;\n}\n\nbool amrToPcm(const std::string& amrPath, std::vector<unsigned char>& pcmData, std::string* error/* = NULL*/)\n{\n#ifdef _WIN32\n    CA2W pszW(amrPath.c_str(), CP_UTF8);\n    FILE *fp = _wfopen((LPCWSTR)pszW, L\"rb\" );\n#else\n    FILE *fp = fopen(amrPath.c_str(), \"rb\" );\n#endif\n    if (NULL == fp)\n    {\n        return false;\n    }\n\n    skipAmrnbHeader(fp);\n    \n    void *amrnb_dec = Decoder_Interface_init();\n    if (NULL == amrnb_dec)\n    {\n        fclose(fp);\n        return false;\n    }\n        \n    uint8_t buffer[500], littleendian[320], *ptr;\n    int16_t outbuffer[160];\n    size_t n = 0;\n    \n    while (1)\n    {\n        int size, i;\n        \n        /* Read the mode byte */\n        n = fread(buffer, 1, 1, fp);\n        if (n <= 0)\n            break;\n        /* Find the packet size */\n        size = amr_frame_sizes[(buffer[0] >> 3) & 0x0f];\n        \n        n = fread(buffer + 1, 1, size, fp);\n        if (n != size)\n            break;\n\n        /* Decode the packet */\n        Decoder_Interface_Decode(amrnb_dec, buffer, outbuffer, 0);\n\n        /* Convert to little endian and write to wav */\n        ptr = littleendian;\n        for (i = 0; i < 160; i++)\n        {\n            *ptr++ = (outbuffer[i] >> 0) & 0xff;\n            *ptr++ = (outbuffer[i] >> 8) & 0xff;\n        }\n        pcmData.insert(pcmData.end(), littleendian, littleendian + 320);\n    }\n        \n    Decoder_Interface_exit(amrnb_dec);\n    \n    fclose(fp);\n    \n    return true;\n}\n\nbool amrToPcm(const std::string& amrPath, const std::string& pcmPath, std::string* error/* = NULL*/)\n{\n    std::vector<unsigned char> pcmData;\n    bool result = amrToPcm(amrPath, pcmData, error);\n    if (result)\n    {\n        result = writeFile(pcmPath, pcmData);\n    }\n    return result;\n}\n"
  },
  {
    "path": "WechatExporter/core/Utils_thread.cpp",
    "content": "//\n//  Utils_thread.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n#if 1\n#include \"Utils.h\"\n\n#ifdef _WIN32\n\n// https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2015/debugger/how-to-set-a-thread-name-in-native-code?view=vs-2015&redirectedfrom=MSDN\n// https://stackoverflow.com/questions/10121560/stdthread-naming-your-thread\n\n#include <windows.h>\nconst DWORD MS_VC_EXCEPTION = 0x406D1388;\n\n#pragma pack(push, 8)\ntypedef struct tagTHREADNAME_INFO\n{\n   DWORD dwType; // Must be 0x1000.\n   LPCSTR szName; // Pointer to name (in user addr space).\n   DWORD dwThreadID; // Thread ID (-1=caller thread).\n   DWORD dwFlags; // Reserved for future use, must be zero.\n} THREADNAME_INFO;\n#pragma pack(pop)\n\nvoid setThreadName(uint32_t dwThreadID, const char* threadName)\n{\n   THREADNAME_INFO info;\n   info.dwType = 0x1000;\n   info.szName = threadName;\n   info.dwThreadID = dwThreadID;\n   info.dwFlags = 0;\n\n   __try\n   {\n      RaiseException(MS_VC_EXCEPTION, 0, sizeof(info)/sizeof(ULONG_PTR), (ULONG_PTR*)&info );\n   }\n   __except(EXCEPTION_EXECUTE_HANDLER)\n   {\n   }\n}\n\nvoid setThreadName( const char* threadName)\n{\n    setThreadName(GetCurrentThreadId(), threadName);\n}\n\nvoid setThreadName(std::thread* thread, const char* threadName)\n{\n    DWORD threadId = ::GetThreadId(static_cast<HANDLE>(thread->native_handle() ) );\n    setThreadName(threadId, threadName);\n}\n\n#elif defined(__linux__)\n#include <sys/prctl.h>\nvoid setThreadName(const char* threadName)\n{\n    prctl(PR_SET_NAME, threadName, 0, 0, 0);\n}\n\n#else\n#include <pthread.h>\nvoid setThreadName(const char* threadName)\n{\n    pthread_setname_np(threadName);\n}\n#endif\n\n#endif // 0\n"
  },
  {
    "path": "WechatExporter/core/Utils_xml.cpp",
    "content": "//\n//  Utils.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include <string>\n#include \"Utils.h\"\n#include <libxml/parser.h>\n#include <libxml/tree.h>\n#include <libxml/xpath.h>\n\nbool getXmlNodeValue(const std::string& xml, const std::string& xpath, std::string& value)\n{\n\tbool result = false;\n\tvalue.clear();\n\n\txmlDocPtr doc = NULL;\n\txmlXPathContextPtr xpathCtx = NULL;\n\txmlXPathObjectPtr xpathObj = NULL;\n\txmlNodeSetPtr xpathNodes = NULL;\n\n\tdoc = xmlParseMemory(xml.c_str(), static_cast<int>(xml.size()));\n\tif (doc == NULL) { goto end; }\n\n\txpathCtx = xmlXPathNewContext(doc);\n\tif (xpathCtx == NULL) { goto end; }\n\n\txpathObj = xmlXPathEvalExpression(BAD_CAST(xpath.c_str()), xpathCtx);\n\tif (xpathObj == NULL) { goto end; }\n\n\txpathNodes = xpathObj->nodesetval;\n\tif ((xpathNodes) && (xpathNodes->nodeNr > 0))\n\t{\n\t\txmlNode *cur = xpathNodes->nodeTab[0];\n\t\txmlChar* sz = xmlNodeGetContent(cur);\n\t\tif (sz != NULL)\n\t\t{\n\t\t\tvalue = reinterpret_cast<char *>(sz);;\n\t\t\txmlFree(sz);\n\t\t}\n\t\tresult = true;\n\t}\n\nend:\n\tif (xpathObj) { xmlXPathFreeObject(xpathObj); }\n\tif (xpathCtx) { xmlXPathFreeContext(xpathCtx); }\n\tif (doc) { xmlFreeDoc(doc); }\n\n\treturn result;\n}\n\nbool getXmlNodeAttributeValue(const std::string& xml, const std::string& xpath, const std::string& attributeName, std::string& value)\n{\n\tbool result = false;\n\tvalue.clear();\n\n\txmlDocPtr doc = NULL;\n\txmlXPathContextPtr xpathCtx = NULL;\n\txmlXPathObjectPtr xpathObj = NULL;\n\txmlNodeSetPtr xpathNodes = NULL;\n\n\tdoc = xmlParseMemory(xml.c_str(), static_cast<int>(xml.size()));\n\tif (doc == NULL) { goto end; }\n\n\txpathCtx = xmlXPathNewContext(doc);\n\tif (xpathCtx == NULL) { goto end; }\n\n\txpathObj = xmlXPathEvalExpression(BAD_CAST(xpath.c_str()), xpathCtx);\n\tif (xpathObj == NULL) { goto end; }\n\n\txpathNodes = xpathObj->nodesetval;\n\tif ((xpathNodes) && (xpathNodes->nodeNr > 0))\n\t{\n\t\txmlNode *cur = xpathNodes->nodeTab[0];\n\t\txmlChar* attr = xmlGetProp(cur, reinterpret_cast<const xmlChar *>(attributeName.c_str()));\n\t\tif (NULL != attr)\n\t\t{\n\t\t\tvalue = reinterpret_cast<char *>(attr);\n\t\t\txmlFree(attr);\n\t\t}\n\t\t\n\t\tresult = true;\n\t}\n\nend:\n\tif (xpathObj) { xmlXPathFreeObject(xpathObj); }\n\tif (xpathCtx) { xmlXPathFreeContext(xpathCtx); }\n\tif (doc) { xmlFreeDoc(doc); }\n\n\treturn result;\n}"
  },
  {
    "path": "WechatExporter/core/WechatObjects.h",
    "content": "//\n//  WechatObjects.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include <string>\n#include <vector>\n#include <regex>\n#include <map>\n#include <algorithm>\n#include <cmath>\n#ifndef NDEBUG\n#include <cassert>\n#endif\n#include \"Utils.h\"\n#include \"FileSystem.h\"\n\n#ifndef WechatObjects_h\n#define WechatObjects_h\n\n// https://www.theiphonewiki.com/wiki/Kernel#iOS\n// https://en.wikipedia.org/wiki/Darwin_(operating_system)\n// Major * 10000 + Minor * 100 + Patch\n/*\n6.0     13.0.0\n7.0     14.0.0\n9.0     15.0.0\n9.3     15.4.0\n9.3.2   15.5.0\n9.3.3   15.6.0\n10.0    16.0.0\n10.1    16.1.0\n10.2    16.3.0\n10.3    16.5.0\n10.3.2  16.6.0\n10.3.3  16.7.0\n11.0    17.0.0\n11.1    17.2.0\n11.2    17.3.0\n11.2.5  17.4.0\n11.3    17.5.0\n11.4    17.6.0\n11.4.1  17.7.0\n12.0    18.0.0\n12.1    18.2.0\n12.2    18.5.0\n12.3    18.6.0\n12.4    18.7.0\n13.0    19.0.0\n13.3    19.2.0\n13.3.1  19.3.0\n13.4    19.4.0\n13.4.5  19.5.0\n13.5.5  19.6.0\n14.0    20.0.0\n14.2    20.1.0\n14.3    20.2.0\n14.4    20.3.0\n14.5    20.4.0\n*/\n\n// CFNetwork - Darwin\n// https://user-agents.net/applications/cfnetwork\n\nclass WechatInfo\n{\nprivate:\n    std::string m_version;\n    std::string m_osVersion;\n    std::string m_shortVersion;\n    std::string m_cellDataVersion;\n    \n    struct __less\n    {\n        bool operator()(const std::pair<int, std::string>& x, int y) const {return x.first < y;}\n    };\n    \npublic:\n    void setVersion(const std::string& version)\n    {\n        m_version = version;\n        m_shortVersion = version;\n        \n        std::vector<std::string> parts = split(version, \".\");\n        if (parts.size() > 3)\n        {\n            parts.erase(parts.begin() + 3, parts.end());\n            m_shortVersion = join(parts, \".\");\n        }\n    }\n    \n    void setOSVersion(const std::string& osVersion)\n    {\n        size_t pos = osVersion.find_first_not_of(\"0123456789.\");\n        if (pos == std::string::npos)\n        {\n            m_osVersion = osVersion;\n        }\n        else\n        {\n            m_osVersion = osVersion.substr(0, pos);\n        }\n    }\n    \n    std::string getVersion() const\n    {\n        return m_version;\n    }\n    \n    std::string getShortVersion() const\n    {\n        return m_shortVersion;\n    }\n    void setCellDataVersion(const std::string& cellDataVersion)\n    {\n        m_cellDataVersion = cellDataVersion;\n    }\n    \n    std::string getCellDataVersion() const\n    {\n        return m_cellDataVersion;\n    }\n    \n    std::string buildUserAgent() const\n    {\n        std::vector<std::pair<int, std::string>> versionMapping = {\n            {0, \"11.0.0 485.12.7\"},\n            {60000, \"13.0.0 609.1.4\"},\n            {70000, \"14.0.0 711.3.18\"},\n            {90000, \"15.0.0 758.2.8\"},\n            {90300, \"15.4.0 758.3.15\"},\n            {90302, \"15.5.0 758.4.3\"},\n            {90303, \"15.6.0 758.5.3\"},\n            {100000, \"16.0.0 808.0.2\"},\n            {100100, \"16.1.0 808.1.4\"},\n            {100200, \"16.3.0 808.3\"},\n            {100300, \"16.5.0 811.4.18\"},\n            {100302, \"16.6.0 811.5.4\"},\n            {100303, \"16.7.0 811.5.4\"},\n            {110000, \"17.0.0 887\"},\n            {110100, \"17.2.0 889.9\"},\n            {110200, \"17.3.0 893.14.2\"},\n            {110205, \"17.4.0 894\"},\n            {110300, \"17.5.0 897.15\"},\n            {110400, \"17.6.0 901.1\"},\n            {110401, \"17.7.0 902.2\"},\n            {120000, \"18.0.0 974.2.1\"},\n            {120100, \"18.2.0 975.0.3\"},\n            {120200, \"18.5.0 978.0.7\"},\n            {120300, \"18.6.0 978.0.7\"},\n            {120400, \"18.7.0 978.0.7\"},\n            {130000, \"19.0.0 1120\"},\n            {130300, \"19.2.0 1121.2.2\"},\n            {130301, \"19.3.0 1121.2.2\"},\n            {130400, \"19.4.0 978.0.7\"},\n            {130405, \"19.5.0 1126\"},\n            {130505, \"19.6.0 1128.0.1\"},\n            {140000, \"20.0.0 1197\"},\n            {140200, \"20.1.0 1206\"},\n            {140300, \"20.2.0 1209\"},\n            {140400, \"20.3.0 1220.1\"},\n            {140500, \"20.4.0 1237\"},\n            {140600, \"20.5.0 1240.0.4\"},\n            {140700, \"20.6.0 1240.0.4\"},\n            {150000, \"21.0.0 1300.1\"},\n        };\n        \n        int osVersion = getOSVersionNumber();\n        std::vector<std::pair<int, std::string>>::iterator it = std::lower_bound(versionMapping.begin(), versionMapping.end(), osVersion, __less());\n        if (it == versionMapping.cend() || it->first != osVersion)\n        {\n            --it;\n        }\n        size_t pos = it->second.find(' ');\n        if (pos != std::string::npos)\n        {\n            std::string darwinVersion = it->second.substr(0, pos);\n            std::string cfVersion = it->second.substr(pos + 1);\n            return \"WeChat/\" + (m_version.empty() ? \"7.0.15.33\" : m_version) +\n                \" CFNetwork/\" + (cfVersion.empty() ? \"978.0.7\" : cfVersion) +\n                \" Darwin/\" + (darwinVersion.empty() ? \"18.6.0\" : darwinVersion);\n        }\n        \n        return \"WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0\"; // default\n    }\n    \nprotected:\n    int getOSVersionNumber() const\n    {\n        int versionNumber = 0;\n        \n        if (!m_osVersion.empty())\n        {\n            std::vector<std::string> parts = split(m_osVersion, \".\");\n            for (int idx = 0; idx < std::min(3, static_cast<int>(parts.size())); ++idx)\n            {\n                if (!parts[idx].empty())\n                {\n                    versionNumber += std::pow(100, (2 - idx)) * std::stoi(parts[idx]);\n                }\n            }\n        }\n        return versionNumber;\n    }\n};\n\nclass Friend\n{\n#ifndef NDEBUG\npublic:\n#else\nprotected:\n#endif\n    std::string m_usrName;\n    std::string m_wxName;   // WeiXin Hao\n    std::string m_uidHash;\n    std::string m_displayName;\n    int m_userType;\n    bool m_isChatroom;\n\tbool m_deleted;\n    std::string m_portrait;\n    std::string m_portraitHD;\n\n    std::string m_outputFileName; // Use displayName first and then usrName\n    std::string m_encodedOutputFileName; // Use displayName first and then usrName\n    \n    // std::map<std::string, std::pair<std::string, std::string>> m_members; // uidHash => <uid,NickName>\n    std::map<std::string, std::string> m_members; // uid => NickName\n    std::vector<std::string> m_memberUsrNames; // uid\n    std::vector<std::string> m_tags;\n    \npublic:\n    \n    Friend() : m_isChatroom(false), m_deleted(false)\n    {\n    }\n    \n    Friend(const std::string& uid, const std::string& hash) : m_usrName(uid), m_uidHash(hash), m_isChatroom(false), m_deleted(false)\n    {\n        m_isChatroom = isChatroom(uid);\n    }\n    \n    static bool isSubscription(const std::string& usrName);\n    static bool isChatroom(const std::string& usrName);\n    \n    static bool isDefaultAvatar(const std::string& path);\n    static bool isDefaultAvatar(size_t fileSize, const std::string& path);\n    \n    inline bool isSubscription() const\n    {\n        return isSubscription(m_usrName);\n    }\n    \n    inline std::string getUsrName() const { return m_usrName; }\n    inline std::string getWxName() const { return m_wxName.empty() ? m_usrName : m_wxName; }\n    inline std::string getHash() const { return m_uidHash; }\n    void setUsrName(const std::string& usrName) { this->m_usrName = usrName; m_uidHash = md5(usrName);  m_outputFileName = m_uidHash; m_isChatroom = isChatroom(usrName); }\n    inline bool isUsrNameEmpty() const\n    {\n        return m_usrName.empty();\n    }\n    inline bool isHashEmpty() const\n    {\n        return m_uidHash.empty();\n    }\n\n\tvoid setEmptyUsrName(const std::string& usrName) { this->m_usrName = usrName; }\n\t\n\tbool isDeleted() const\n\t{\n\t\treturn m_deleted;\n\t}\n\n\tvoid setDeleted(bool deleted)\n\t{\n\t\tm_deleted = deleted;\n\t}\n\n    bool containMember(const std::string& usrName) const\n    {\n        auto it = m_members.find(usrName);\n        return it != m_members.cend();\n    }\n    \n    std::string getMemberName(const std::string& usrName) const\n    {\n        auto it = m_members.find(usrName);\n        return it != m_members.cend() ? it->second : \"\";\n    }\n\n    void addMember(const std::string& usrName, const std::string& displayName)\n    {\n        auto it = m_members.find(usrName);\n        if (it != m_members.end())\n        {\n            it->second = displayName;\n        }\n        else\n        {\n            m_members[usrName] = displayName;\n            m_memberUsrNames.push_back(usrName);\n        }\n    }\n    \n    std::vector<std::string> getMemberUsrNames() const\n    {\n        return m_memberUsrNames;\n    }\n    \n    inline std::string getDisplayName() const\n    {\n        return m_displayName.empty() ? (m_wxName.empty() ? m_usrName : m_wxName) : m_displayName;\n    }\n    \n    inline bool isDisplayNameEmpty() const\n    {\n        return m_displayName.empty();\n    }\n    \n    inline bool isWxNameEmpty() const\n    {\n        return m_wxName.empty();\n    }\n    \n    inline void setWxName(const std::string& wxName)\n    {\n        m_wxName = wxName;\n    }\n    \n    inline void setDisplayName(const std::string& displayName)\n    {\n        m_displayName = displayName;\n    }\n    \n    inline void setUserType(int userType)\n    {\n        m_userType = userType;\n    }\n    \n    static bool isInvalidPortrait(const std::string& portrait);\n    \n    inline void setPortrait(const std::string& portrait)\n    {\n#ifndef NDEBUG\n        if (isInvalidPortrait(portrait))\n        {\n            assert(false);\n        }\n#endif\n        m_portrait = portrait;\n    }\n    \n    inline void setPortraitHD(const std::string& portraitHD)\n    {\n        m_portraitHD = portraitHD;\n    }\n    \n    inline std::string getOutputFileName() const\n    {\n        return m_outputFileName;\n    }\n    \n    inline void setOutputFileName(const std::string& outputFileName)\n    {\n        m_outputFileName = outputFileName;\n        m_encodedOutputFileName = encodeUrl(outputFileName);\n    }\n    \n    inline std::string getEncodedOutputFileName() const\n    {\n        return m_encodedOutputFileName;\n    }\n    \n    inline bool isChatroom() const\n    {\n        return m_isChatroom;\n    }\n    \n    inline bool isPortraitEmpty() const\n    {\n        return m_portrait.empty() && m_portraitHD.empty();\n    }\n    \n    inline std::string getPortrait() const\n    {\n        return m_portraitHD.empty() ? m_portrait : m_portraitHD;\n    }\n    \n    inline std::string getSecondaryPortrait() const\n    {\n        return (m_portraitHD.empty() || m_portrait.empty()) ? \"\" : m_portrait;\n    }\n    \n    inline std::string getLocalPortrait() const\n    {\n        return m_usrName + \".jpg\";\n    }\n    \n    void clearTags()\n    {\n        m_tags.clear();\n    }\n    \n    void swapTags(std::vector<std::string> tags)\n    {\n        m_tags.swap(tags);\n    }\n    \n    std::string buildTagDesc(const std::map<uint64_t, std::string>& tags) const\n    {\n        if (m_tags.empty())\n        {\n            return \"\";\n        }\n        \n        std::vector<std::string> tagDesc(m_tags.size());\n        for (auto it = m_tags.cbegin(); it != m_tags.cend(); ++it)\n        {\n            auto it2 = tags.find(std::stoull(*it));\n            if (it2 != tags.cend())\n            {\n                tagDesc.push_back(it2->second);\n            }\n        }\n        return join(tagDesc, \" \");\n    }\n    \nprotected:\n    bool update(const Friend& f)\n    {\n        if (m_usrName.empty() && m_uidHash.empty())\n        {\n            // Can't compare the object\n            return false;\n        }\n        if (!m_usrName.empty())\n        {\n            if (m_usrName != f.m_usrName)\n            {\n                return false;\n            }\n        }\n        else\n        {\n            if (m_uidHash != f.m_uidHash)\n            {\n                return false;\n            }\n        }\n        \n        bool result = false;\n        \n        if (isUsrNameEmpty())\n        {\n            setUsrName(f.getUsrName());\n            result = true;\n        }\n        if (m_wxName.empty() && !f.m_wxName.empty())\n        {\n            m_wxName = f.m_wxName;\n            result = true;\n        }\n        \n        if (m_displayName.empty() && !f.m_displayName.empty())\n        {\n            m_displayName = f.m_displayName;\n            result = true;\n        }\n        if (m_portrait.empty() && !f.m_portrait.empty())\n        {\n            m_portrait = f.m_portrait;\n            result = true;\n        }\n        if (m_portraitHD.empty() && !f.m_portraitHD.empty())\n        {\n            m_portraitHD = f.m_portraitHD;\n            result = true;\n        }\n        \n        if (m_members.empty())\n        {\n            if (!f.m_members.empty())\n            {\n                m_members = f.m_members;\n                m_memberUsrNames = f.m_memberUsrNames;\n            }\n        }\n        else\n        {\n            for (auto it = m_members.begin(); it != m_members.end(); ++it)\n            {\n                if (it->second.empty())\n                {\n                    auto it2 = f.m_members.find(it->first);\n                    if (it2 != f.m_members.cend())\n                    {\n                        it->second = it2->second;\n                        result = true;\n                    }\n                }\n            }\n        }\n\n        return result;\n    }\n    \n};\n\ninline bool Friend::isSubscription(const std::string& usrName)\n{\n    /*\n     newsapp,fmessage,filehelper,weibo,qqmail,fmessage,tmessage,qmessage,qqsync,floatbottle,lbsapp,shakeapp,medianote,qqfriend,readerapp,blogapp,facebookapp,masssendapp,meishiapp,feedsapp,voip,blogappweixin,weixin,brandsessionholder,weixinreminder,wxid_novlwrv3lqwv11,gh_22b87fa7cb3c,officialaccounts,notification_messages,wxid_novlwrv3lqwv11,gh_22b87fa7cb3c,wxitil,userexperience_alarm,notification_messages\n     */\n    return startsWith(usrName, \"gh_\")\n        || (usrName.compare(\"brandsessionholder\") == 0)\n        || (usrName.compare(\"newsapp\") == 0)\n        || (usrName.compare(\"weixin\") == 0)\n        || (usrName.compare(\"notification_messages\") == 0);\n}\n\ninline bool Friend::isChatroom(const std::string& usrName)\n{\n    return endsWith(usrName, \"@chatroom\")\n        || endsWith(usrName, \"@im.chatroom\");\n}\n\ninline bool Friend::isDefaultAvatar(const std::string& path)\n{\n    return isDefaultAvatar(getFileSize(path), path);\n}\n\ninline bool Friend::isDefaultAvatar(size_t fileSize, const std::string& path)\n{\n    return (fileSize == 5875) && (md5File(path) == \"e3e807760b01d24760eba724bac616d6\");\n}\n\ninline bool Friend::isInvalidPortrait(const std::string& portrait)\n{\n    return !portrait.empty() && (!startsWith(portrait, \"http://\") && !startsWith(portrait, \"https://\") && !startsWith(portrait, \"file://\"));\n}\n\nstruct FriendDisplayNameCompare\n{\n    bool operator()(const Friend& f1, const Friend& f2) const\n    {\n        return f1.getDisplayName().compare(f2.getDisplayName()) < 0;\n    }\n    \n    bool operator()(const Friend& f1, const std::string& s2) const\n    {\n        return f1.getDisplayName().compare(s2) < 0;\n    }\n    \n    bool operator()(const Friend* f1, const Friend* f2) const\n    {\n        return f1->getDisplayName().compare(f2->getDisplayName()) < 0;\n    }\n    \n    bool operator()(const Friend* f1, const std::string& s2) const\n    {\n        return f1->getDisplayName().compare(s2) < 0;\n    }\n};\n\nclass Friends\n{\npublic:\n    std::map<std::string, Friend> friends;  // uidHash => Friend\n    std::map<std::string, std::string> hashes;  // uid => Hash\n    \n    /*\n    template <class THandler>\n    void handleFriend(THandler handler)\n    {\n        for (std::map<std::string, Friend>::iterator it = friends.begin(); it != friends.end(); ++it)\n        {\n            handler(it->second);\n        }\n    }\n     */\n    \n    void toArraySortedByDisplayName(std::vector<const Friend *>& friends) const\n    {\n        friends.reserve(this->friends.size());\n        for (auto it = this->friends.cbegin(); it != this->friends.cend(); ++it)\n        {\n            friends.push_back(&(it->second));\n        }\n        \n        std::sort(friends.begin(), friends.end(), FriendDisplayNameCompare());\n    }\n    \n    bool hasFriend(const std::string& hash) const { return friends.find(hash) != friends.end(); }\n    const Friend* getFriend(const std::string& uidHash) const\n    {\n        std::map<std::string, Friend>::const_iterator it = friends.find(uidHash);\n        if (it == friends.cend())\n        {\n            return NULL;\n        }\n        return &(it->second);\n    }\n    Friend* getFriend(const std::string& uidHash)\n    {\n        std::map<std::string, Friend>::iterator it = friends.find(uidHash);\n        if (it == friends.end())\n        {\n            return NULL;\n        }\n        return &(it->second);\n    }\n    const Friend* getFriendByUid(const std::string& uid) const\n    {\n        std::map<std::string, std::string>::const_iterator it = hashes.find(uid);\n        std::string hash = it == hashes.cend() ? md5(uid) : it->second;\n        \n        std::map<std::string, Friend>::const_iterator it2 = friends.find(hash);\n        if (it2 == friends.cend())\n        {\n            return NULL;\n        }\n        return &(it2->second);\n        // return getFriend(hash);\n    }\n    Friend* getFriendByUid(const std::string& uid)\n    {\n        std::map<std::string, std::string>::const_iterator it = hashes.find(uid);\n        std::string hash = it == hashes.cend() ? md5(uid) : it->second;\n        \n        std::map<std::string, Friend>::iterator it2 = friends.find(hash);\n        if (it2 == friends.end())\n        {\n            return NULL;\n        }\n        return &(it2->second);\n        // return getFriend(hash);\n    }\n    \n    Friend& addFriend(const std::string& uid)\n    {\n        std::map<std::string, std::string>::const_iterator it = hashes.find(uid);\n        std::string hash = it == hashes.cend() ? md5(uid) : it->second;\n        if (it == hashes.cend())\n        {\n            hashes[uid] = hash;\n        }\n        \n        friends[hash] = Friend(uid, hash);\n        return friends[hash];\n    }\n    \n    void addHash(const std::string& uid)\n    {\n        std::map<std::string, std::string>::const_iterator it = hashes.find(uid);\n        if (it == hashes.cend())\n        {\n            hashes[uid] = md5(uid);\n        }\n    }\n    \n};\n\nclass Session : public Friend\n{\nprotected:\n    int m_unreadCount;\n    int m_recordCount;\n    \n    unsigned int m_createTime;\n    unsigned int m_lastMessageTime;\n    std::string m_lastMessage;\n    std::string m_lastMessageUsrName;\n    std::string m_lastMessageUserDisplayName;\n    std::string m_extFileName;\n    std::string m_dbFile;\n    std::string m_memberIds;\n    void *m_data;\n    const Friend* m_owner;\n\npublic:\n    Session(const Friend* owner) : Friend(), m_unreadCount(0), m_recordCount(0), m_createTime(0), m_lastMessageTime(0), m_data(NULL), m_owner(owner)\n    {\n    }\n    \n    Session(const std::string& uid, const std::string& hash, const Friend* owner) : Friend(uid, hash), m_unreadCount(0), m_recordCount(0), m_createTime(0), m_lastMessageTime(0), m_data(NULL), m_owner(owner)\n    {\n    }\n    \n    inline unsigned int getCreateTime() const\n    {\n        return m_createTime;\n    }\n    \n    inline void setCreateTime(unsigned int createTime)\n    {\n        m_createTime = createTime;\n    }\n    \n    inline unsigned int getLastMessageTime() const\n    {\n        return m_lastMessageTime;\n    }\n    \n    inline void setLastMessageTime(unsigned int lastMessageTime)\n    {\n        m_lastMessageTime = lastMessageTime;\n    }\n    \n    inline std::string getLastMessage() const\n    {\n        return m_lastMessage;\n    }\n    \n    inline void setLastMessage(const std::string& lastMessage)\n    {\n        m_lastMessage = lastMessage;\n    }\n    \n    inline void setLastMessage(const std::string& lastMessage, const std::string& lastMessageUsrName, const Friends& friends)\n    {\n        if (startsWith(lastMessage, lastMessageUsrName + \":\\n\"))\n        {\n            m_lastMessage = lastMessage.substr(lastMessageUsrName.size() + 2);\n        }\n        else\n        {\n            m_lastMessage = lastMessage;\n        }\n        setLastMessageUsrName(lastMessageUsrName, friends);\n    }\n    \n    inline std::string getLastMessageUsrName() const\n    {\n        return m_lastMessageUsrName;\n    }\n    \n    bool isTextMessage() const\n    {\n        return !m_lastMessage.empty() && !startsWith(m_lastMessage, \"<?xml\") && !startsWith(m_lastMessage, \"<msg>\") && !startsWith(m_lastMessage, \"<voipinvitemsg>\") && !startsWith(m_lastMessage, \"{\\\"msgLocalID\\\":\") && !startsWith(m_lastMessage, \"<sysmsg\");\n    }\n    \n    inline void setLastMessageUsrName(const std::string& lastMessageUsrName, const Friends& friends)\n    {\n        m_lastMessageUsrName = lastMessageUsrName;\n        // std::string uidHash = md5(m_lastMessageUsrName);\n        auto it = m_members.find(m_lastMessageUsrName);\n        // std::map<std::string, std::pair<std::string, std::string>> m_members; // uidHash => <uid,NickName>\n        if (it != m_members.cend() && !it->second.empty())  // \"Empty display name\" means there is no specified nick name for chat group\n        {\n            m_lastMessageUserDisplayName = it->second;\n        }\n        else\n        {\n            const Friend* f = friends.getFriendByUid(m_lastMessageUsrName);\n            if (NULL != f)\n            {\n                m_lastMessageUserDisplayName = f->getDisplayName();\n            }\n        }\n    }\n    \n    inline void setLastMessageUsrName(const std::string& lastMessageUsrName, const std::string& lastMessageUserDsiplayName)\n    {\n        m_lastMessageUsrName = lastMessageUsrName;\n        m_lastMessageUserDisplayName = lastMessageUserDsiplayName;\n    }\n    \n    inline bool hasLastMessageUserDisplayName() const\n    {\n        return !m_lastMessageUserDisplayName.empty();\n    }\n    \n    inline std::string getLastMessageUserDisplayName() const\n    {\n        return m_lastMessageUserDisplayName;\n    }\n    \n    inline void setLastMessageUserDisplayName(const std::string& lastMessageUserDispalayName)\n    {\n        m_lastMessageUserDisplayName = lastMessageUserDispalayName;\n    }\n    \n    inline bool isExtFileNameEmpty() const\n    {\n        return m_extFileName.empty();\n    }\n    \n    inline std::string getExtFileName() const\n    {\n        return m_extFileName;\n    }\n    \n    inline void setExtFileName(const std::string& extFileName)\n    {\n        m_extFileName = extFileName;\n    }\n    \n    inline bool isMemberIdsEmpty() const\n    {\n        return m_members.empty() && m_memberUsrNames.empty();\n    }\n    \n    inline std::string getMemberIds() const\n    {\n        return m_memberIds;\n    }\n    \n    inline void setMemberIds(const std::string& memberIds)\n    {\n        m_memberIds = memberIds;\n    }\n    \n    inline int getUnreadCount() const\n    {\n        return m_unreadCount;\n    }\n    \n    inline void setUnreadCount(int unreadCount)\n    {\n        m_unreadCount = unreadCount;\n    }\n    \n    inline int getRecordCount() const\n    {\n        return m_recordCount;\n    }\n    \n    inline void setRecordCount(int rc)\n    {\n        m_recordCount = rc;\n    }\n    \n    inline bool isDbFileEmpty() const\n    {\n        return m_dbFile.empty();\n    }\n    \n    inline std::string getDbFile() const\n    {\n        return m_dbFile;\n    }\n    \n    inline void setDbFile(const std::string& dbFile)\n    {\n        m_dbFile = dbFile;\n    }\n    \n    void *getData() const\n    {\n        return m_data;\n    }\n    \n    void setData(void *data)\n    {\n        m_data = data;\n    }\n\n    bool update(const Friend& f)\n    {\n        bool result = Friend::update(f);\n        if (!m_lastMessageUsrName.empty() && m_lastMessageUserDisplayName.empty())\n        {\n            std::string memberName = f.getMemberName(m_lastMessageUsrName);\n            if (!memberName.empty())\n            {\n                m_lastMessageUserDisplayName = memberName;\n                result = true;\n            }\n        }\n\n        return result;\n    }\n    \n    const Friend* getOwner() const\n    {\n        return m_owner;\n    }\n\n};\n\nstruct SessionUsrNameCompare\n{\n    bool operator()(const Session& s1, const Session& s2) const\n    {\n        return s1.getUsrName().compare(s2.getUsrName()) < 0;\n    }\n    \n    bool operator()(const Session& s1, const std::string& s2) const\n    {\n        return s1.getUsrName().compare(s2) < 0;\n    }\n};\n\nstruct SessionHashCompare\n{\n    bool operator()(const Session& s1, const Session& s2) const\n    {\n        return s1.getHash().compare(s2.getHash()) < 0;\n    }\n    \n    bool operator()(const Session& s1, const std::string& s2) const\n    {\n        return s1.getHash().compare(s2) < 0;\n    }\n};\n\nstruct SessionLastMsgTimeCompare\n{\n    bool operator()(const Session& s1, const Session& s2) const\n    {\n        return s1.getLastMessageTime() > s2.getLastMessageTime();\n    }\n};\n\nstruct WXMSG\n{\n    unsigned int createTime;\n    std::string content;\n    int des;\n    int type;\n    std::string msgId;\n    int64_t msgIdValue;\n    uint64_t msgSvrId;\n    int status;\n    int tableVersion;\n};\n\nstruct WXAPPMSG\n{\n    const WXMSG *msg;\n    int appMsgType;\n    std::string appId;\n    std::string appName;\n    std::string localAppIcon;\n    std::string senderUsrName;\n};\n\nstruct WXFWDMSG\n{\n    const WXMSG *msg;\n    std::string usrName;\n    std::string displayName;\n    std::string portrait;\n    std::string portraitLD;\n    std::string dataType;\n    std::string subType;\n    std::string dataId;\n    std::string dataFormat;\n    std::string msgTime;\n    std::string srcMsgTime;\n#if !defined(NDEBUG) || defined(DBG_PERF)\n    std::string rawMessage;\n#endif\n};\n\n\n#endif /* WechatObjects_h */\n"
  },
  {
    "path": "WechatExporter/core/WechatParser.cpp",
    "content": "//\n//  WechatParser.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include \"WechatParser.h\"\n#include <cstdio>\n#include <iostream>\n#include <fstream>\n#include <cmath>\n#include <algorithm>\n#include <cstdio>\n#include <cstring>\n#include <sqlite3.h>\n#include <libxml/parser.h>\n#include <libxml/tree.h>\n#include <libxml/xpath.h>\n#include <json/json.h>\n#include <plist/plist.h>\n\n#include \"WechatObjects.h\"\n#include \"RawMessage.h\"\n#include \"XmlParser.h\"\n#include \"MMKVReader.h\"\n\n#ifdef _WIN32\n#include <atlconv.h>\n#endif\n\ntemplate<class T>\nbool parseMembers(const std::string& xml, T& f)\n{\n    bool result = false;\n    xmlDocPtr doc = NULL;\n    xmlXPathContextPtr xpathCtx = NULL;\n    xmlXPathObjectPtr xpathObj = NULL;\n    xmlNodeSetPtr xpathNodes = NULL;\n\n    doc = xmlParseMemory(xml.c_str(), static_cast<int>(xml.size()));\n    if (doc == NULL) { goto end; }\n\n    xpathCtx = xmlXPathNewContext(doc);\n    if(xpathCtx == NULL) { goto end; }\n    \n    xpathObj = xmlXPathEvalExpression(BAD_CAST(\"//RoomData/Member\"), xpathCtx);\n    if(xpathObj == NULL) { goto end; }\n    \n    xpathNodes = xpathObj->nodesetval; //从xpath object中得到node set\n    if ((xpathNodes) && (xpathNodes->nodeNr > 0))\n    {\n        for (int i = 0; i < xpathNodes->nodeNr; i++)\n        {\n            xmlNode *cur = xpathNodes->nodeTab[i];\n            xmlChar* uidPtr = xmlGetProp(cur, reinterpret_cast<const xmlChar *>(\"UserName\"));\n            if (NULL == uidPtr)\n            {\n                continue;\n            }\n            \n            std::string uid = reinterpret_cast<char *>(uidPtr);\n            xmlFree(uidPtr);\n            \n            std::string displayName;\n            \n            cur = cur->xmlChildrenNode;\n            while (cur != NULL)\n            {\n                if ((!xmlStrcmp(cur->name, reinterpret_cast<const xmlChar *>(\"DisplayName\"))))\n                {\n                    xmlChar *dnPtr = xmlNodeGetContent(cur);\n                    if (NULL != dnPtr)\n                    {\n                        displayName = reinterpret_cast<char *>(dnPtr);\n                        xmlFree(dnPtr);\n                    }\n                    break;\n                }\n                cur = cur->next;\n            }\n\n            f.addMember(uid, displayName);\n        }\n    }\n    \n    result = true;\n    \n    end:\n    if (xpathObj) { xmlXPathFreeObject(xpathObj); }\n    if (xpathCtx) { xmlXPathFreeContext(xpathCtx); }\n    if (doc) { xmlFreeDoc(doc); }\n    \n    return result;\n}\n\nLoginInfo2Parser::LoginInfo2Parser(ITunesDb *iTunesDb, Logger* logger) : m_iTunesDb(iTunesDb), m_logger(logger)\n{\n}\n\nvoid LoginInfo2Parser::debugLog(const std::string& log)\n{\n    if (NULL != m_logger)\n    {\n        m_logger->debug(log);\n    }\n}\n\nstd::string LoginInfo2Parser::getError() const\n{\n    return m_error;\n}\n\nbool LoginInfo2Parser::parse(std::vector<Friend>& users)\n{\n    std::string loginInfo2 = \"Documents/LoginInfo2.dat\";\n    std::string realPath = m_iTunesDb->findRealPath(loginInfo2);\n    \n    if (!realPath.empty())\n    {\n        parse(realPath, users);\n    }\n    else\n    {\n        debugLog(\"Documents/LoginInfo2.dat not exists.\");\n        // return false;\n    }\n    \n    parseUserFromFolder(users);\n    \n    if (users.empty())\n    {\n        return false;\n    }\n\n    MMSettingInMMappedKVFilter filter;\n    ITunesFileVector mmsettings = m_iTunesDb->filter(filter);\n    \n    std::map<std::string, std::string> mmsettingFiles;  // hash => usrName\n    for (ITunesFilesConstIterator it = mmsettings.cbegin(); it != mmsettings.cend(); ++it)\n    {\n        debugLog(\"mmsetting: \" + (*it)->relativePath  + \" => \" + (*it)->fileId);\n        std::string fileName = filter.parse((*it));\n        fileName = fileName.substr(filter.getPrefix().size());\n        if (fileName.empty())\n        {\n            continue;\n        }\n        \n        std::string usrNameHash = md5(fileName);\n        mmsettingFiles[usrNameHash] = fileName;\n    }\n    \n    for (std::vector<Friend>::iterator it = users.begin(); it != users.end(); ++it)\n    {\n        debugLog(\"Parse user:\" + it->getUsrName() + \"(\" + it->getHash() + \") => \" + it->getDisplayName());\n        MMSettingParser mmsettingParser(m_iTunesDb);\n        if (mmsettingParser.parse(it->getHash()))\n        {\n            debugLog(\"Succeeded to parse mmsetting:\" + it->getUsrName());\n            \n            it->setUsrName(mmsettingParser.getUsrName());\n            it->setWxName(mmsettingParser.getWxName());\n            if (it->isDisplayNameEmpty())\n            {\n                it->setDisplayName(mmsettingParser.getDisplayName());\n            }\n            it->setPortrait(mmsettingParser.getPortrait());\n            it->setPortraitHD(mmsettingParser.getPortraitHD());\n        }\n        else\n        {\n            debugLog(\"Failed to parse mmsetting:\" + it->getUsrName());\n\n            // Check mmsettings.archive in MMappedKV folde\n            if (it->getUsrName().empty())\n            {\n                std::map<std::string, std::string>::const_iterator it2 = mmsettingFiles.find(it->getHash());\n                if (it2 != mmsettingFiles.cend())\n                {\n                    it->setUsrName(it2->second);\n                }\n            }\n            if (!(it->getUsrName().empty()))\n            {\n                std::string realPath = m_iTunesDb->findRealPath(\"Documents/MMappedKV/mmsetting.archive.\" + it->getUsrName());\n                std::string realCrcPath = m_iTunesDb->findRealPath(\"Documents/MMappedKV/mmsetting.archive.\" + it->getUsrName() + \".crc\");\n                if (!realPath.empty() && !realCrcPath.empty())\n                {\n                    MMKVParser parser(m_logger);\n\n                    debugLog(\"Parse MMKV file: Documents/MMappedKV/mmsetting.archive.\" + it->getUsrName() + \" => \" + realPath);\n                    debugLog(\"Parse MMKV file: Documents/MMappedKV/mmsetting.archive.\" + it->getUsrName() + \".crc => \" + realCrcPath);\n\n                    if (parser.parse(realPath, realCrcPath))\n                    {\n                        debugLog(\"Succeeded to parse mmkv:\" + it->getUsrName());\n                        \n                        if (it->isDisplayNameEmpty())\n                        {\n                            it->setDisplayName(parser.getDisplayName());\n                        }\n                        it->setPortrait(parser.getPortrait());\n                        it->setPortraitHD(parser.getPortraitHD());\n                    }\n                    else\n                    {\n                        debugLog(\"Failed to parse MMKV: Documents/MMappedKV/mmsetting.archive.\" + it->getUsrName());\n                    }\n                }\n                else\n                {\n                    debugLog(\"MMKV file not exists: Documents/MMappedKV/mmsetting.archive.\" + it->getUsrName());\n                }\n            }\n        }\n    }\n    \n    auto it = users.begin();\n    while (it != users.end())\n    {\n        if (it->getUsrName().empty())\n        {\n            debugLog(\"Erase: md5=\" + it->getHash() + \"* dn=\" + it->getDisplayName() + \"*\");\n            m_error += \"Erase: md5=\" + it->getHash() + \"* dn=\" + it->getDisplayName() + \"* \";\n            // erase() invalidates the iterator, use returned iterator\n            it = users.erase(it);\n\n        }\n        else\n        {\n            ++it;\n        }\n    }\n    \n    return true;\n}\n\nbool LoginInfo2Parser::parse(const std::string& loginInfo2Path, std::vector<Friend>& users)\n{\n    RawMessage msg;\n    if (!msg.mergeFile(loginInfo2Path))\n    {\n        debugLog(\"Failed to parse Documents/LoginInfo2.dat(pb).\");\n        return false;\n    }\n    \n    std::string value1;\n    if (!msg.parse(\"1\", value1))\n    {\n        debugLog(\"Failed to parse field 1 in Documents/LoginInfo2.dat.\");\n        return false;\n    }\n    \n    users.clear();\n    int offset = 0;\n    std::string::size_type length = value1.size();\n    debugLog(\"Length of field 1 in Documents/LoginInfo2.dat = \" + std::to_string(length));\n    while (offset < length)\n    {\n        debugLog(\"Offset of field 1 in Documents/LoginInfo2.dat = \" + std::to_string(offset) + \"/\" + std::to_string(length));\n\n        int res = parseUser(value1.c_str() + offset, static_cast<int>(length - offset), users);\n        if (res < 0)\n        {\n            break;\n        }\n        \n        offset += res;\n    }\n    \n    if (NULL != m_logger)\n    {\n        for (std::vector<Friend>::iterator it = users.begin(); it != users.end(); ++it)\n        {\n            debugLog(\"User from Documents/LoginInfo2.dat:\" + it->getUsrName() + \"(\" + it->getHash() + \") => \" + it->getDisplayName());\n        }\n    }\n    \n    return true;\n}\n\nint LoginInfo2Parser::parseUser(const char* data, int length, std::vector<Friend>& users)\n{\n    uint32_t userBufferLen = 0;\n    \n    const char* p = calcVarint32Ptr(data, data + length, &userBufferLen);\n    if (NULL == p || 0 == userBufferLen)\n    {\n        return -1;\n    }\n    \n#ifndef NDEBUG\n    writeFile(\"/Users/matthew/Documents/WxExp/LoginInfo2.user.data\", (const unsigned char* )p, userBufferLen);\n#endif\n    RawMessage msg;\n    if (!msg.merge(p, userBufferLen))\n    {\n        return -1;\n    }\n    \n    Friend user;\n    \n    std::string value;\n    if (msg.parse(\"1\", value))\n    {\n        debugLog(\"UsrName from Documents/LoginInfo2.dat = \" + value);\n        user.setUsrName(value);\n    }\n    if (msg.parse(\"2\", value))\n    {\n        user.setWxName(value);\n    }\n    if (msg.parse(\"3\", value))\n    {\n        debugLog(\"DisplayName from Documents/LoginInfo2.dat = \" + value);\n        user.setDisplayName(value);\n    }\n#ifndef NDEBUG\n    if (msg.parse(\"10.1.2\", value))\n    {\n    }\n    if (msg.parse(\"10.2.2.2\", value))\n    {\n    }\n#endif\n    users.push_back(user);\n\n    m_error += \"LoginInfo2.dat: *\" + user.getDisplayName() + \"* \";\n    \n    return static_cast<int>(userBufferLen + (p - data));\n}\n\nbool LoginInfo2Parser::parseUserFromFolder(std::vector<Friend>& users)\n{\n    debugLog(\"parseUserFromFolder starts...\");\n\n    UserFolderFilter filter;\n    ITunesFileVector folders = m_iTunesDb->filter(filter);\n    \n    for (ITunesFilesConstIterator it = folders.cbegin(); it != folders.cend(); ++it)\n    {\n        std::string fileName = filter.parse((*it));\n        m_error += \"User Folder: *\" + fileName + \"*  \";\n        if (fileName == \"00000000000000000000000000000000\")\n        {\n            continue;\n        }\n\n        debugLog(\"Find User Folder:\" + fileName);\n        \n        bool existing = false;\n        std::vector<Friend>::const_iterator it2 = users.cbegin();\n        for (; it2 != users.cend(); ++it2)\n        {\n            if (it2->getHash() == fileName)\n            {\n                existing = true;\n                break;\n            }\n        }\n        \n        if (!existing)\n        {\n            debugLog(\"New User Folder:\" + fileName);\n            users.emplace(users.end(), \"\", fileName);\n            m_error += \"New User From Folder: *\" + fileName + \"*  \";\n        }\n    }\n\n    return true;\n}\n\nvoid MMSettings::clear()\n{\n    m_usrName.clear();\n    m_displayName.clear();\n    m_portrait.clear();\n    m_portraitHD.clear();\n}\n\nstd::string MMSettings::getUsrName() const\n{\n    return m_usrName;\n}\n\nstd::string MMSettings::getWxName() const\n{\n    return m_wxName;\n}\n\nstd::string MMSettings::getDisplayName() const\n{\n    return m_displayName;\n}\n\nstd::string MMSettings::getPortrait() const\n{\n    return m_portrait;\n}\n\nstd::string MMSettings::getPortraitHD() const\n{\n    return m_portraitHD;\n}\n\nMMKVParser::MMKVParser(Logger *logger) : m_logger(logger)\n{\n}\n\nvoid MMKVParser::debugLog(const std::string& log)\n{\n    if (NULL != m_logger)\n    {\n        m_logger->debug(log);\n    }\n}\n\nbool MMKVParser::parse(const std::string& path, const std::string& crcPath)\n{\n    std::vector<unsigned char> contents;\n    uint32_t lastActualSize = 0;\n    // 86: usrName\n    // 87: wxName\n    // 88: DisplayName\n    if (readFile(crcPath, contents))\n    {\n        if (contents.size() >= 36)\n        {\n            memcpy(&lastActualSize, &contents[32], 4);\n            debugLog(\"MMKV lastActualSize from crc: \" + std::to_string(lastActualSize) + \" crc file size:\" + std::to_string(contents.size()));\n        }\n        else\n        {\n            debugLog(\"MMKV crc file size:\" + std::to_string(contents.size()));\n        }\n        contents.clear();\n    }\n    else\n    {\n        debugLog(\"Failed to read MMKV crc file:\" + crcPath);\n    }\n    \n    if (!readFile(path, contents))\n    {\n        debugLog(\"Failed to read MMKV file:\" + path);\n        return false;\n    }\n    \n    debugLog(\"MMKV file size:\" + std::to_string(contents.size()));\n    \n    uint32_t actualSize = 0;\n    if (contents.size() >= 4)\n    {\n        memcpy(&actualSize, &contents[0], 4);\n    }\n    if (actualSize <= 0)\n    {\n        actualSize = lastActualSize;\n    }\n    \n    if (actualSize <= 0)\n    {\n        debugLog(\"MMKV actualSize is less than 0: \" + std::to_string(actualSize));\n        return false;\n    }\n\n    actualSize += 4;\n    if (contents.size() < actualSize)\n    {\n        debugLog(\"MMKV contents size < actualSize: \" + std::to_string(contents.size()) + \"/\" + std::to_string(actualSize));\n    }\n    \n    MMKVReader reader(&contents[0], actualSize);\n    reader.seek(8);\n    \n    while (!reader.isAtEnd())\n    {\n        // debugLog(\"MMKV offset: \" + std::to_string(reader.getPos()) + \"/\" + std::to_string(actualSize));\n\n        const auto k = reader.readKey();\n        if (k.empty())\n        {\n            debugLog(\"MMKV exception: empty key\");\n            break;\n        }\n        \n        if (k == \"86\")\n        {\n            m_usrName = reader.readStringValue();\n            // debugLog(\"MMKV usrName: \" + m_usrName);\n        }\n        else if (k == \"87\")\n        {\n            m_wxName = reader.readStringValue();\n        }\n        else if (k == \"88\")\n        {\n            m_displayName = reader.readStringValue();\n            // debugLog(\"MMKV displayName: \" + m_displayName);\n        }\n        else if (k == \"headimgurl\")\n        {\n            m_portrait = reader.readStringValue();\n        }\n        else if (k == \"headhdimgurl\")\n        {\n            m_portraitHD = reader.readStringValue();\n        }\n        else\n        {\n            reader.skipValue();\n        }\n    }\n    \n    return true;\n}\n\nMMSettingParser::MMSettingParser(ITunesDb *iTunesDb) : m_iTunesDb(iTunesDb)\n{\n}\n\nbool MMSettingParser::parse(const std::string& usrNameHash)\n{\n    clear();\n    \n    std::string vpath = \"Documents/\" + usrNameHash + \"/mmsetting.archive\";\n    std::string mmsettingPath = m_iTunesDb->findRealPath(vpath);\n    if (mmsettingPath.empty())\n    {\n        return false;\n    }\n    \n    std::vector<unsigned char> data;\n    if (!readFile(mmsettingPath, data))\n    {\n        return false;\n    }\n    \n    plist_t node = NULL;\n    plist_from_memory(reinterpret_cast<const char *>(&data[0]), static_cast<uint32_t>(data.size()), &node);\n    if (NULL == node)\n    {\n        return false;\n    }\n    \n    std::unique_ptr<void, decltype(&plist_free)> nodePtr(node, &plist_free);\n    plist_t objectsNode = plist_access_path(node, 1, \"$objects\");\n    if (NULL == objectsNode || !PLIST_IS_ARRAY(objectsNode))\n    {\n        return false;\n    }\n\n    plist_t keyedUidNodes = plist_array_get_item(objectsNode, 1);\n    \n    const char* keys[] = {\"UsrName\", \"NickName\", \"AliasName\"};\n    for (int idx = 0; idx < sizeof(keys) / sizeof(const char *); ++idx)\n    {\n        plist_t keyedUidNode = plist_dict_get_item(keyedUidNodes, keys[idx]);\n        if (keyedUidNode != NULL && PLIST_IS_UID(keyedUidNode))\n        {\n            uint64_t uid = 0;\n            plist_get_uid_val(keyedUidNode, &uid);\n            plist_t keyedItemNode = plist_array_get_item(objectsNode, static_cast<uint32_t>(uid));\n            \n            uint64_t valueLength = 0;\n            const char* pValue = plist_get_string_ptr(keyedItemNode, &valueLength);\n            if (pValue == NULL || valueLength == 0)\n            {\n                continue;\n            }\n            \n            if (idx == 0)\n            {\n                m_usrName.assign(pValue, valueLength);\n            }\n            else if (idx == 1)\n            {\n                m_displayName.assign(pValue, valueLength);\n            }\n            else if (idx == 2)\n            {\n                m_wxName.assign(pValue, valueLength);\n            }\n        }\n    }\n    \n    // \"new_dicsetting\"\n    plist_t keyedUidNode = plist_dict_get_item(keyedUidNodes, \"new_dicsetting\");\n    if (keyedUidNode != NULL && PLIST_IS_UID(keyedUidNode))\n    {\n        uint64_t uid = 0;\n        plist_get_uid_val(keyedUidNode, &uid);\n        \n        plist_t settingNode = plist_array_get_item(objectsNode, static_cast<uint32_t>(uid));\n        if (keyedUidNode != NULL && PLIST_IS_DICT(settingNode))\n        {\n            plist_t settingKeysNode = plist_dict_get_item(settingNode, \"NS.keys\");\n            plist_t settingValuesNode = plist_dict_get_item(settingNode, \"NS.objects\");\n            \n            if (settingKeysNode != NULL && PLIST_IS_ARRAY(settingKeysNode) && settingValuesNode != NULL && PLIST_IS_ARRAY(settingValuesNode))\n            {\n                uint32_t settingKeysNodeSize = plist_array_get_size(settingKeysNode);\n                uint32_t settingValuesNodeSize = plist_array_get_size(settingValuesNode);\n                uint32_t minSize = std::min(settingKeysNodeSize, settingValuesNodeSize);\n                for (uint32_t idx = 0; idx < minSize; ++idx)\n                {\n                    plist_t keyedUidNode = plist_array_get_item(settingKeysNode, idx);\n                    if (keyedUidNode == NULL || !PLIST_IS_UID(keyedUidNode))\n                    {\n                        continue;\n                    }\n                    \n                    uint64_t uid = 0;\n                    plist_get_uid_val(keyedUidNode, &uid);\n                    plist_t keyedItemNode = plist_array_get_item(objectsNode, static_cast<uint32_t>(uid));\n                    if (keyedItemNode == NULL || !PLIST_IS_STRING(keyedItemNode))\n                    {\n                        continue;\n                    }\n                    \n                    uint64_t valueLength = 0;\n                    const char* pValue = plist_get_string_ptr(keyedItemNode, &valueLength);\n                    \n                    if (pValue == NULL || valueLength == 0)\n                    {\n                        continue;\n                    }\n                    \n                    std::string settingKey(pValue, valueLength);\n                    \n                    if (settingKey != \"headimgurl\" && settingKey != \"headhdimgurl\")\n                    {\n                        continue;\n                    }\n                    \n                    keyedUidNode = plist_array_get_item(settingValuesNode, idx);\n                    if (keyedUidNode == NULL || !PLIST_IS_UID(keyedUidNode))\n                    {\n                        continue;\n                    }\n                    \n                    uid = 0;\n                    plist_get_uid_val(keyedUidNode, &uid);\n                    keyedItemNode = plist_array_get_item(objectsNode, static_cast<uint32_t>(uid));\n                    if (keyedItemNode == NULL || !PLIST_IS_STRING(keyedItemNode))\n                    {\n                        continue;\n                    }\n                    \n                    valueLength = 0;\n                    pValue = plist_get_string_ptr(keyedItemNode, &valueLength);\n                    if (pValue == NULL || valueLength == 0)\n                    {\n                        continue;\n                    }\n                    \n                    if (settingKey == \"headimgurl\")\n                    {\n                        m_portrait.assign(pValue, valueLength);\n                    }\n                    else if (settingKey == \"headhdimgurl\")\n                    {\n                        m_portraitHD.assign(pValue, valueLength);\n                    }\n                    \n                    if (!m_portrait.empty() && !m_portraitHD.empty())\n                    {\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    return true;\n}\n\nFriendsParser::FriendsParser(bool detailedInfo/* = true*/) : m_detailedInfo(detailedInfo)\n{\n}\n\n#ifndef NDEBUG\nvoid FriendsParser::setOutputPath(const std::string& outputPath)\n{\n    m_outputPath = outputPath;\n}\n#endif\n\nbool FriendsParser::parseWcdb(const std::string& mmPath, Friends& friends)\n{\n    sqlite3 *db = NULL;\n    int rc = openSqlite3Database(mmPath, &db);\n    if (rc != SQLITE_OK)\n    {\n        sqlite3_close(db);\n        return false;\n    }\n    \n    std::string sql = \"SELECT userName,dbContactRemark,dbContactChatRoom,dbContactHeadImage,type FROM Friend\";\n    sqlite3_stmt* stmt = NULL;\n    rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL);\n    if (rc != SQLITE_OK)\n    {\n        std::string error = sqlite3_errmsg(db);\n        sqlite3_close(db);\n        return false;\n    }\n\n    while (sqlite3_step(stmt) == SQLITE_ROW)\n    {\n        int userType = sqlite3_column_int(stmt, 4);\n        const char* val = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));\n        if (NULL == val)\n        {\n            continue;\n        }\n        std::string uid = val;\n        /*\n        if (Friend::isSubscription(uid))\n        {\n            continue;\n        }\n         */\n        \n        Friend& f = friends.addFriend(uid);\n        f.setUserType(userType);\n        \n        parseRemark(sqlite3_column_blob(stmt, 1), sqlite3_column_bytes(stmt, 1), f);\n        parseChatroom(sqlite3_column_blob(stmt, 2), sqlite3_column_bytes(stmt, 2), f);\n        if (m_detailedInfo)\n        {\n            parseAvatar(sqlite3_column_blob(stmt, 3), sqlite3_column_bytes(stmt, 3), f);\n        }\n    }\n    \n    sqlite3_finalize(stmt);\n    \n    sql = \"SELECT userName,dbContactRemark,dbContactChatRoom,dbContactHeadImage,type FROM OpenIMContact\";\n    stmt = NULL;\n    rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL);\n    if (rc != SQLITE_OK)\n    {\n        std::string error = sqlite3_errmsg(db);\n        sqlite3_close(db);\n        return false;\n    }\n\n    while (sqlite3_step(stmt) == SQLITE_ROW)\n    {\n        int userType = sqlite3_column_int(stmt, 4);\n        const char* val = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));\n        if (NULL == val)\n        {\n            continue;\n        }\n        std::string uid = val;\n        /*\n        if (Friend::isSubscription(uid))\n        {\n            continue;\n        }\n         */\n        \n        Friend& f = friends.addFriend(uid);\n        f.setUserType(userType);\n        \n        parseRemark(sqlite3_column_blob(stmt, 1), sqlite3_column_bytes(stmt, 1), f);\n        parseChatroom(sqlite3_column_blob(stmt, 2), sqlite3_column_bytes(stmt, 2), f);\n        if (m_detailedInfo)\n        {\n            parseAvatar(sqlite3_column_blob(stmt, 3), sqlite3_column_bytes(stmt, 3), f);\n        }\n    }\n    \n    sqlite3_finalize(stmt);\n    \n    sqlite3_close(db);\n    \n#ifndef NDEBUG\n    \n    if (!m_outputPath.empty())\n    {\n        std::string data = \"usrName\\tWxId\\tAlias\\r\\n\";\n        for (std::map<std::string, Friend>::const_iterator it = friends.friends.cbegin(); it != friends.friends.cend(); ++it)\n        {\n            data += it->second.m_usrName + \"\\t\" + it->second.m_wxName + \"\\t\" + it->second.m_displayName + \"\\r\\n\";\n        }\n        writeFile(combinePath(m_outputPath, \"Friends.txt\"), data);\n    }\n    \n#endif\n\n    return true;\n}\n\nbool FriendsParser::parseRemark(const void *data, int length, Friend& f)\n{\n    RawMessage msg;\n    if (!msg.merge(reinterpret_cast<const char *>(data), length))\n    {\n        return false;\n    }\n    \n    std::string value;\n    // Remark Name\n    if (msg.parse(\"3\", value))\n    {\n        f.setDisplayName(value);\n    }\n    if (f.isDisplayNameEmpty() && msg.parse(\"1\", value))\n    {\n        f.setDisplayName(value);\n    }\n    if (msg.parse(\"2\", value))\n    {\n        f.setWxName(value);\n    }\n\n    /*\n    if (msg.parse(\"6\", value))\n    {\n    }\n    */\n    \n    if (m_detailedInfo)\n    {\n        // 8: Tags\n        // 8: \"18,42\"\n        f.clearTags();\n        if (msg.parse(\"8\", value))\n        {\n            std::vector<std::string> tags = split(value, \",\");\n            f.swapTags(tags);\n        }\n    }\n\n    return true;\n}\n\nbool FriendsParser::parseAvatar(const void *data, int length, Friend& f)\n{\n    RawMessage msg;\n    if (!msg.merge(reinterpret_cast<const char *>(data), length))\n    {\n        return false;\n    }\n    \n    std::string value;\n    if (msg.parse(\"2\", value))\n    {\n        if (!Friend::isInvalidPortrait(value))\n        {\n            f.setPortrait(value);\n        }\n    }\n    if (msg.parse(\"3\", value))\n    {\n        if (!Friend::isInvalidPortrait(value))\n        {\n            f.setPortraitHD(value);\n        }\n    }\n\n    return true;\n}\n\nbool FriendsParser::parseChatroom(const void *data, int length, Friend& f)\n{\n    RawMessage msg;\n    if (!msg.merge(reinterpret_cast<const char *>(data), length))\n    {\n        return false;\n    }\n    \n    std::string value;\n    if (msg.parse(\"6\", value))\n    {\n        parseMembers(value, f);\n    }\n\n    return true;\n}\n\nbool FriendsParser::parseFriendTags(ITunesDb *iTunesDb, const std::string& uidHash, std::map<uint64_t, std::string>& tags)\n{\n    std::string tagFilePath = \"Documents/\" + uidHash + \"/contactlabel.list\";\n    tagFilePath = iTunesDb->findRealPath(tagFilePath);\n    if (tagFilePath.empty())\n    {\n        return false;\n    }\n    \n    std::vector<unsigned char> data;\n    if (!readFile(tagFilePath, data))\n    {\n        return false;\n    }\n    \n    plist_t node = NULL;\n    plist_from_memory(reinterpret_cast<const char *>(&data[0]), static_cast<uint32_t>(data.size()), &node);\n    if (NULL == node)\n    {\n        return false;\n    }\n\n    // std::unique_ptr<plist_t, decltype(::plist_free)> auotNode(node, plist_free);\n    bool res = false;\n    std::unique_ptr<void, decltype(&plist_free)> nodePtr(node, &plist_free);\n\n    plist_t topNode = plist_access_path(node, 1, \"$top\");\n    if (!PLIST_IS_DICT(topNode))\n    {\n        return false;\n    }\n    plist_t rootNode = plist_dict_get_item(topNode, \"root\");\n    if (!PLIST_IS_UID(rootNode))\n    {\n        return false;\n    }\n    uint64_t topIdx = 0;\n    plist_get_uid_val(rootNode, &topIdx);\n    \n    \n    plist_t arrayNode = plist_access_path(node, 1, \"$objects\");\n    \n    plist_type pt  = plist_get_node_type(arrayNode);\n    if (PLIST_IS_ARRAY(arrayNode))\n    {\n        plist_t keysAndObjectsNode = plist_array_get_item(arrayNode, (uint32_t)topIdx);\n        plist_t keysNode = plist_dict_get_item(keysAndObjectsNode, \"NS.keys\");\n        plist_t objectsNode = plist_dict_get_item(keysAndObjectsNode, \"NS.objects\");\n        \n        uint32_t numberOfKeys = plist_array_get_size(keysNode);\n        uint32_t numberOfObjects = plist_array_get_size(objectsNode);\n        \n        uint32_t numberOfCount = std::min(numberOfKeys, numberOfObjects);\n        uint64_t keyIdx = 0;\n        uint64_t objectIdx = 0;\n        uint64_t objValIdx = 0;\n        uint64_t strLength = 0;\n        uint64_t keyVal = 0;\n        \n        for (uint32_t idx = 0; idx < numberOfCount; ++idx)\n        {\n            plist_t keyIdxNode = plist_array_get_item(keysNode, idx);\n            plist_t objectIdxNode = plist_array_get_item(objectsNode, idx);\n            pt = plist_get_node_type(keyIdxNode);\n            pt = plist_get_node_type(objectIdxNode);\n\n            plist_get_uid_val(keyIdxNode, &keyIdx);\n            plist_get_uid_val(objectIdxNode, &objectIdx);\n            \n            plist_t keyNode = plist_array_get_item(arrayNode, keyIdx);\n            plist_t objectNode = plist_array_get_item(arrayNode, objectIdx);\n            \n            pt = plist_get_node_type(keyNode);\n            pt = plist_get_node_type(objectNode);\n            if (!PLIST_IS_UINT(keyNode))\n            {\n                // continue;\n            }\n            if (!PLIST_IS_DICT(objectNode))\n            {\n                continue;\n            }\n            \n            plist_t objValIdxNode = plist_dict_get_item(objectNode, \"m_nsName\");\n            if (!PLIST_IS_UID(objValIdxNode))\n            {\n                continue;\n            }\n            plist_get_uid_val(objValIdxNode, &objValIdx);\n            \n            plist_t objValNode = plist_array_get_item(arrayNode, (uint32_t)objValIdx);\n            if (!PLIST_IS_STRING(objValNode))\n            {\n                continue;\n            }\n\n            plist_get_uint_val(keyNode, &keyVal);\n            \n            strLength = 0;\n            const char* ptr = plist_get_string_ptr(objValNode, &strLength);\n            \n            std::string val;\n            if (NULL != ptr && strLength > 0)\n            {\n                val.assign(ptr, strLength);\n            }\n            \n            tags[keyVal] = val;\n            \n        }\n    }\n    \n    return res;\n}\n\nWechatInfoParser::WechatInfoParser(ITunesDb *iTunesDb) : m_iTunesDb(iTunesDb)\n{\n}\n\nbool WechatInfoParser::parse(WechatInfo& wechatInfo)\n{\n    wechatInfo.setOSVersion(m_iTunesDb->getIOSVersion());\n    return parsePreferences(wechatInfo);\n}\n\nbool WechatInfoParser::parsePreferences(WechatInfo& wechatInfo)\n{\n    std::string preferencesPath = m_iTunesDb->findRealPath(\"Library/Preferences/com.tencent.xin.plist\");\n    if (preferencesPath.empty())\n    {\n        return false;\n    }\n    \n    std::vector<unsigned char> data;\n    if (!readFile(preferencesPath, data))\n    {\n        return false;\n    }\n    \n    plist_t node = NULL;\n    plist_from_memory(reinterpret_cast<const char *>(&data[0]), static_cast<uint32_t>(data.size()), &node);\n    if (NULL == node)\n    {\n        return false;\n    }\n    \n    plist_t startupVersions = plist_access_path(node, 1, \"prevStartupVersions\");\n    if (NULL != startupVersions && PLIST_IS_ARRAY(startupVersions))\n    {\n        uint32_t startupVersionsSize = plist_array_get_size(startupVersions);\n        if (startupVersionsSize > 0)\n        {\n            plist_t currentVersion = plist_array_get_item(startupVersions, startupVersionsSize - 1);\n            if (NULL != currentVersion)\n            {\n                uint64_t versionLength = 0;\n                const char* pVersion = plist_get_string_ptr(currentVersion, &versionLength);\n                if (NULL != pVersion && versionLength > 0)\n                {\n                    std::string version;\n                    version.assign(pVersion, versionLength);\n                    wechatInfo.setVersion(version);\n                }\n            }\n        }\n    }\n    \n    plist_t cellDataVersion = plist_access_path(node, 1, \"FrameCellDataVersion\");\n    if (NULL != cellDataVersion)\n    {\n        uint64_t versionLength = 0;\n        const char* pVersion = plist_get_string_ptr(cellDataVersion, &versionLength);\n        if (NULL != pVersion && versionLength > 0)\n        {\n            std::string cellDataVersion;\n            cellDataVersion.assign(pVersion, versionLength);\n            wechatInfo.setCellDataVersion(cellDataVersion);\n        }\n    }\n    \n    plist_free(node);\n    \n    return true;\n}\n\nSessionsParser::SessionsParser(ITunesDb *iTunesDb, ITunesDb *iTunesDbShare, const Friends& friends, const std::string& cellDataVersion, Logger* logger, bool detailedInfo/* = true*/) : m_iTunesDb(iTunesDb), m_iTunesDbShare(iTunesDbShare), m_friends(friends), m_cellDataVersion(cellDataVersion), m_detailedInfo(detailedInfo), m_logger(logger)\n{\n    if (cellDataVersion.empty())\n    {\n        m_cellDataVersion = \"V\";\n    }\n}\n\nvoid SessionsParser::debugLog(const std::string& log)\n{\n    if (NULL != m_logger)\n    {\n        m_logger->debug(log);\n    }\n}\n\nbool SessionsParser::parse(const Friend& user, std::vector<Session>& sessions)\n{\n    std::string usrNameHash = user.getHash();\n    std::string userRoot = \"Documents/\" + usrNameHash;\n    std::string sessionDbPath = m_iTunesDb->findRealPath(combinePath(userRoot, \"session\", \"session.db\"));\n    if (sessionDbPath.empty())\n    {\n        return false;\n    }\n\n    sqlite3 *db = NULL;\n    int rc = openSqlite3Database(sessionDbPath, &db);\n    if (rc != SQLITE_OK)\n    {\n        sqlite3_close(db);\n        return false;\n    }\n    \n    std::string sql = \"SELECT ConStrRes1,CreateTime,unreadcount,UsrName FROM SessionAbstract\";\n\n    sqlite3_stmt* stmt = NULL;\n    rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL);\n    if (rc != SQLITE_OK)\n    {\n        std::string error = sqlite3_errmsg(db);\n        sqlite3_close(db);\n        return false;\n    }\n\n    while (sqlite3_step(stmt) == SQLITE_ROW)\n    {\n        const char *usrName = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));\n        if (NULL == usrName/* || Session::isSubscription(usrName)*/)\n        {\n            continue;\n        }\n\n        std::vector<Session>::iterator it = sessions.emplace(sessions.cend(), &user);\n        Session& session = (*it);\n\n        session.setUsrName(usrName);\n        session.setCreateTime(static_cast<unsigned int>(sqlite3_column_int(stmt, 1)));\n        const char* extFileName = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));\n        if (NULL != extFileName)\n        {\n            session.setExtFileName(extFileName);\n        }\n        else\n        {\n            // Guess ext file name\n            // /session/data/c3/2488b928e0bf604ec1cb02b53f18a7\n            std::string relativePath = combinePath(userRoot, \"session/data\", session.getHash().substr(0, 2), session.getHash().substr(2));\n            const ITunesFile* file = m_iTunesDb->findITunesFile(relativePath);\n            if (NULL != file)\n            {\n                session.setExtFileName(file->relativePath.substr(userRoot.size())); // file->relativePath is formatted\n            }\n            \n        }\n        session.setUnreadCount(sqlite3_column_int(stmt, 2));\n        \n        const Friend* f = m_friends.getFriend(session.getHash());\n        if (NULL != f)\n        {\n            it->update(*f);\n        }\n    }\n    \n    sqlite3_finalize(stmt);\n    sqlite3_close(db);\n    \n    parseUniversalSessions(user, userRoot, sessions);\n\n    // if (m_detailedInfo)\n    {\n        parseMessageDbs(user, userRoot, sessions);\n    }\n    \n#if !defined(NDEBUG) || defined(DBG_PERF)\n    std::set<std::string> displayNames;\n#endif\n    for (std::vector<Session>::iterator it = sessions.begin(); it != sessions.end(); )\n    {\n        if (!it->isExtFileNameEmpty())\n        {\n            parseCellData(userRoot, *it);\n        }\n#if !defined(NDEBUG) || defined(DBG_PERF)\n        if (!it->isDisplayNameEmpty())\n        {\n            std::set<std::string>::iterator itDN = displayNames.find(it->getDisplayName());\n            if (itDN == displayNames.end())\n            {\n                displayNames.insert(it->getDisplayName());\n            }\n            else\n            {\n                debugLog(\"Duplicate ChatGroup Name:\" + it->getDisplayName() + \"\\t \" + it->getUsrName());\n            }\n        }\n#endif\n        /*\n        if (!it->isUsrNameEmpty() && it->isSubscription())\n        {\n            it = sessions.erase(it);\n            continue;\n        }\n         */\n\n        \n        ++it;\n    }\n    \n    // Check displayName and avatar\n    std::string shareUserRoot = \"share/\" + usrNameHash;\n    parseSessionsInGroupApp(shareUserRoot, sessions);\n\n    int sessionId = 1;\n    for (std::vector<Session>::iterator it = sessions.begin(); it != sessions.end(); ++it)\n    {\n        if (it->isUsrNameEmpty())\n        {\n            it->setEmptyUsrName(\"wxid_unknwn_\" + std::to_string(sessionId++));\n        }\n\n        if (it->isDisplayNameEmpty() && !it->isMemberIdsEmpty())\n        {\n            // Combine the display name from member list\n            debugLog(\"ChatGroup Name of \" + it->getUsrName() + \" is empty. Build it from members\");\n            parseDisplayNameFromMembers(user, *it);\n        }\n    }\n\n#ifndef NDEBUG\n    // Invalidate db path\n    int cnt = 0;\n    for (std::vector<Session>::const_iterator it = sessions.cbegin(); it != sessions.cend(); ++it)\n    {\n        if (it->isDbFileEmpty())\n        {\n            cnt++;\n        }\n    }\n    if (cnt > 0)\n    {\n        // assert(false);\n    }\n#endif\n    \n#if !defined(NDEBUG) || defined(DBG_PERF)\n    displayNames.clear();\n    int emptyDNCount = 0;\n    int duplicateDNCount = 0;\n    for (std::vector<Session>::iterator it = sessions.begin(); it != sessions.end(); )\n    {\n        if (!it->isDisplayNameEmpty())\n        {\n            std::set<std::string>::iterator itDN = displayNames.find(it->getDisplayName());\n            if (itDN == displayNames.end())\n            {\n                displayNames.insert(it->getDisplayName());\n            }\n            else\n            {\n                duplicateDNCount++;\n            }\n        }\n        else\n        {\n            emptyDNCount++;\n        }\n        ++it;\n    }\n    \n    debugLog(\"Duplicate ChatGroup Name:\" + std::to_string(duplicateDNCount) + \", Empty ChatGroup Name:\" + std::to_string(emptyDNCount));\n#endif\n\n    return true;\n}\n\nbool SessionsParser::parseDisplayNameFromMembers(const Friend& user, Session& session)\n{\n    std::string memberIds = session.getMemberIds();\n    std::vector<std::string> members = memberIds.empty() ? session.getMemberUsrNames() : split(memberIds, \";\");\n    // std::vector<std::string displayName;\n    for (auto it = members.begin(); it != members.end();)\n    {\n        if (user.getUsrName() == *it)\n        {\n            it = members.erase(it);\n            continue;\n        }\n        \n        const Friend* member = m_friends.getFriendByUid(*it);\n        if (NULL != member)\n        {\n            std::string displayName = member->getDisplayName();\n            it->swap(displayName);\n        }\n        \n        ++it;\n    }\n    \n    session.setDisplayName(join(members, \",\"));\n\n    return true;\n}\n\nbool SessionsParser::parseUniversalSessions(const Friend& user, const std::string& userRoot, std::vector<Session>& sessions)\n{\n    SessionUsrNameCompare comp;\n    std::sort(sessions.begin(), sessions.end(), comp);\n\n    std::map<std::string, Session> newSessions;\n    \n    std::string items[] = {\"BottleSession\", \"SubscribeSession\", \"SubscribeSessionList\", \"WASession\"};\n    size_t numberOfItems = sizeof(items) / sizeof(std::string);\n    for (size_t idx = 0; idx < numberOfItems; ++idx)\n    {\n        std::string sessionDbPath = m_iTunesDb->findRealPath(combinePath(userRoot, \"UniversalSession\", items[idx], \"session.db\"));\n        if (sessionDbPath.empty())\n        {\n            continue;\n        }\n\n        sqlite3 *db = NULL;\n        int rc = openSqlite3Database(sessionDbPath, &db);\n        if (rc != SQLITE_OK)\n        {\n            sqlite3_close(db);\n            continue;\n        }\n        \n        std::string sql = \"SELECT sessionId,lastMsgUpdateTime,unreadCount FROM SessionTable\";\n        sqlite3_stmt* stmt = NULL;\n        rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL);\n        if (rc != SQLITE_OK)\n        {\n            std::string error = sqlite3_errmsg(db);\n            sqlite3_close(db);\n            continue;\n        }\n\n        while (sqlite3_step(stmt) == SQLITE_ROW)\n        {\n            const char *usrNamePtr = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));\n            if (NULL == usrNamePtr/* || Session::isSubscription(usrName)*/)\n            {\n                continue;\n            }\n            std::string usrName = usrNamePtr;\n            if (newSessions.find(usrName) != newSessions.cend())\n            {\n                continue;\n            }\n            \n            std::vector<Session>::iterator it = std::lower_bound(sessions.begin(), sessions.end(), usrName, comp);\n            if (it == sessions.end() || it->getUsrName() != usrName)\n            {\n                Session session(usrName, md5(usrName), &user);\n                session.setUsrName(usrName);\n                session.setLastMessageTime(static_cast<unsigned int>(sqlite3_column_int(stmt, 1)));\n                session.setUnreadCount(sqlite3_column_int(stmt, 2));\n                // /session/data/c3/2488b928e0bf604ec1cb02b53f18a7\n                std::string relativePath = combinePath(userRoot, \"session/data\", session.getHash().substr(0, 2), session.getHash().substr(2));\n                const ITunesFile* file = m_iTunesDb->findITunesFile(relativePath);\n                if (NULL != file)\n                {\n                    session.setExtFileName(file->relativePath.substr(userRoot.size())); // file->relativePath is formatted\n                }\n                session.setDeleted(true);\n                \n                newSessions.insert(newSessions.end(), std::pair<std::string, Session>(usrName, session));\n            }\n        }\n        \n        sqlite3_finalize(stmt);\n        sqlite3_close(db);\n    }\n    \n    for (std::map<std::string, Session>::const_iterator it = newSessions.cbegin(); it != newSessions.cend(); ++it)\n    {\n        sessions.push_back(it->second);\n    }\n    \n    return true;\n}\n\nbool SessionsParser::parseMessageDbs(const Friend& user, const std::string& userRoot, std::vector<Session>& sessions)\n{\n    SessionHashCompare comp;\n    std::sort(sessions.begin(), sessions.end(), comp);\n\n    MessageDbFilter filter(userRoot);\n    ITunesFileVector dbs = m_iTunesDb->filter(filter);\n    const ITunesFile* file = m_iTunesDb->findITunesFile(combinePath(userRoot, \"DB\", \"MM.sqlite\"));\n    if (NULL != file)\n    {\n        dbs.push_back(const_cast<ITunesFile *>(file));\n    }\n\n    std::vector<Session> deletedSessions;\n    std::vector<std::pair<std::string, int>> sessionIds;\n    for (ITunesFilesConstIterator it = dbs.cbegin(); it != dbs.cend(); ++it)\n    {\n        std::string mmPath = m_iTunesDb->getRealPath(*it);\n        parseMessageDb(user, mmPath, sessions, deletedSessions);\n    }\n    \n    // Append deletedSessions at last as parseMessageDb needs SORTED sessions\n    for (std::vector<Session>::iterator it = deletedSessions.begin(); it != deletedSessions.end(); ++it)\n    {\n        // /session/data/c3/2488b928e0bf604ec1cb02b53f18a7\n        std::string relativePath = combinePath(userRoot, \"/session/data/\", it->getHash().substr(0, 2), it->getHash().substr(2));\n        const ITunesFile* file = m_iTunesDb->findITunesFile(relativePath);\n        if (NULL != file)\n        {\n            it->setExtFileName(file->relativePath); // it->relativePath is formatted\n        }\n        \n        it->setDeleted(true);\n        sessions.push_back(*it);\n    }\n\n    return true;\n}\n\nbool SessionsParser::parseMessageDb(const Friend& user, const std::string& mmPath, std::vector<Session>& sessions, std::vector<Session>& deletedSessions)\n{\n    sqlite3 *db = NULL;\n    int rc = openSqlite3Database(mmPath, &db);\n    if (rc != SQLITE_OK)\n    {\n        sqlite3_close(db);\n        return false;\n    }\n    \n    std::string sql = \"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\";\n\n    sqlite3_stmt* stmt = NULL;\n    rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL);\n    if (rc != SQLITE_OK)\n    {\n        std::string error = sqlite3_errmsg(db);\n        sqlite3_close(db);\n        return false;\n    }\n    \n    SessionHashCompare comp;\n    while (sqlite3_step(stmt) == SQLITE_ROW)\n    {\n        const unsigned char* pName = sqlite3_column_text(stmt, 0);\n        if (pName == NULL)\n        {\n            continue;\n        }\n        std::string name = reinterpret_cast<const char*>(pName);\n        // \"^Chat_([0-9a-f]{32})$\"\n        if (startsWith(name, \"Chat_\"))\n        {\n            std::string chatId = name.substr(5);\n\n            std::vector<Session>::iterator it = std::lower_bound(sessions.begin(), sessions.end(), chatId, comp);\n            if (it != sessions.end() && it->getHash() == chatId)\n            {\n                int recordCount = 0;\n                std::string sql2 = \"SELECT COUNT(*) AS rc FROM \" + name;\n                sqlite3_stmt* stmt2 = NULL;\n                rc = sqlite3_prepare_v2(db, sql2.c_str(), (int)(sql2.size()), &stmt2, NULL);\n                if (rc == SQLITE_OK)\n                {\n                    it->setDbFile(mmPath);\n                    \n                    if (sqlite3_step(stmt2) == SQLITE_ROW)\n                    {\n                        recordCount = sqlite3_column_int(stmt2, 0);\n                        it->setRecordCount(recordCount);\n                    }\n                    \n                    sqlite3_finalize(stmt2);\n                    \n                    /*\n                    uint32_t lastCreateTime = 0;\n                    std::string sql3 = \"SELECT MAX(CreateTime) AS lct FROM \" + name;\n                    sqlite3_stmt* stmt3 = NULL;\n                    rc = sqlite3_prepare_v2(db, sql3.c_str(), (int)(sql3.size()), &stmt3, NULL);\n                    if (rc == SQLITE_OK)\n                    {\n                        if (sqlite3_step(stmt3) == SQLITE_ROW)\n                        {\n                            lastCreateTime = sqlite3_column_int(stmt3, 0);\n                        }\n                        sqlite3_finalize(stmt3);\n                    }\n\n                    it = deletedSessions.emplace(deletedSessions.cend(), \"\", chatId, &user);\n                    it->setDbFile(mmPath);\n                    it->setLastMessageTime(lastCreateTime);\n                    it->setRecordCount(recordCount);\n                    */\n                }\n                \n                sql2 = \"SELECT CreateTime,Des,Message,Type FROM \" + name + \" ORDER BY CreateTime DESC LIMIT 1\";\n                rc = sqlite3_prepare_v2(db, sql2.c_str(), (int)(sql2.size()), &stmt2, NULL);\n                if (rc == SQLITE_OK)\n                {\n                    if (sqlite3_step(stmt2) == SQLITE_ROW)\n                    {\n                        sqlite3_int64 lastCreateTime = sqlite3_column_int64(stmt2, 0);\n                        int des = sqlite3_column_int(stmt2, 1);\n                        const unsigned char *pMsg = sqlite3_column_text(stmt2, 2);\n                        int type = sqlite3_column_int(stmt2, 3);\n                        \n                        it->setLastMessageTime((unsigned int)lastCreateTime);\n                        if (NULL != pMsg)\n                        {\n                            std::string msg = (const char *)pMsg;\n                            if (it->isChatroom())\n                            {\n                                if (des != 0)\n                                {\n                                    std::string::size_type enter = msg.find(\":\\n\");\n                                    if (enter != std::string::npos && enter + 2 < msg.size())\n                                    {\n                                        std::string senderId = msg.substr(0, enter);\n                                        \n                                        it->setLastMessageUsrName(senderId, m_friends);\n                                        if (type == 1)\n                                        {\n                                            msg = msg.substr(enter + 2);\n                                            it->setLastMessage(msg);\n                                        }\n                                        else\n                                        {\n                                            // it->setLastMessage(\"\");\n                                        }\n                                    }\n                                }\n                                else\n                                {\n                                    // Me\n                                    \n                                    it->setLastMessageUsrName(user.getUsrName(), user.getDisplayName());\n                                    if (type == 1)\n                                    {\n                                        it->setLastMessage(msg);\n                                    }\n                                    else\n                                    {\n                                        // it->setLastMessage(\"-\");\n                                    }\n                                }\n                            }\n                            else\n                            {\n                                if (des != 0)\n                                {\n                                    it->setLastMessageUsrName(it->getUsrName(), m_friends);\n                                }\n                                else\n                                {\n                                    // Me\n                                    it->setLastMessageUsrName(user.getUsrName(), user.getDisplayName());\n                                }\n                                if (type == 1)\n                                {\n                                    it->setLastMessage(msg);\n                                }\n                                else\n                                {\n                                    // it->setLastMessage(\"-\");\n                                }\n                            }\n                        }\n                    }\n\n                    sqlite3_finalize(stmt2);\n                    \n                }\n            }\n            else\n            {\n                // ASSERT (false)\n            }\n            \n        }\n    }\n    \n    sqlite3_finalize(stmt);\n    sqlite3_close(db);\n    \n    return true;\n}\n\nbool SessionsParser::parseCellData(const std::string& userRoot, Session& session)\n{\n    std::string fileName = session.getExtFileName();\n    if (startsWith(fileName, DIR_SEP) || startsWith(fileName, ALT_DIR_SEP))\n    {\n        fileName = fileName.substr(1);\n    }\n    std::string cellDataPath = combinePath(userRoot, fileName);\n    fileName = m_iTunesDb->findRealPath(cellDataPath);\n    \n    if (fileName.empty())\n    {\n        return false;\n    }\n\n    RawMessage msg;\n    if (!msg.mergeFile(fileName))\n    {\n        return false;\n    }\n\n    std::string value;\n    int value2 = 0;\n    if (msg.parse(\"1.1.1\", value))\n    {\n        if (session.isUsrNameEmpty() && (session.isHashEmpty() || md5(value) == session.getHash()))\n        {\n            session.setUsrName(value);\n        }\n    }\n    if (session.isMemberIdsEmpty() && msg.parse(\"1.3\", value) && !value.empty())\n    {\n        session.setMemberIds(value);\n    }\n    if (msg.parse(\"1.1.6\", value))\n    {\n        session.setDisplayName(value);\n    }\n    if (msg.parse(\"1.1.4\", value))\n    {\n        if (session.isDisplayNameEmpty())\n        {\n            session.setDisplayName(value);\n        }\n    }\n    if (msg.parse(\"1.1.14\", value))\n    {\n        if (startsWith(value, \"http://\") || startsWith(value, \"https://\"))\n        {\n            session.setPortrait(value);\n        }\n    }\n    if (msg.parse(\"1.5\", value))\n    {\n        parseMembers(value, session);\n    }\n    if (msg.parse(\"2.7\", value2))\n    {\n        // session.setLastMessageTime(static_cast<unsigned int>(value2));\n    }\n    if (msg.parse(\"2.2\", value2))\n    {\n        if (session.getRecordCount() == 0)\n        {\n            session.setRecordCount(value2);\n        }\n    }\n    \n    std::string lastMsg;\n    std::string lastMsgUser;\n    bool result1 = msg.parse(\"2.11\", lastMsg);\n    bool result2 = msg.parse(\"2.10\", lastMsgUser);\n    if (!result2 || lastMsgUser.empty())\n    {\n        result2 = msg.parse(\"2.8\", lastMsgUser);\n    }\n    if (result1 || result2)\n    {\n#ifndef NDEBUG\n        if (startsWith(lastMsg, \"17390608691\"))\n        {\n            msg.parse(\"2.9\", value);\n            msg.parse(\"2.10\", value);\n        }\n#endif\n        \n        // session.setLastMessage(lastMsg, lastMsgUser, friends);\n    }\n    // 9: \"wxid_tub3kj534ntk12\"\n    //  10: \"wxid_dsbf267m3a9o11\"\n    // if (msg.parse(\"2.9\", lastMsgUser))\n    {\n        // session.setLastMessageUsrName(lastMsgUser, friends);\n    }\n    \n    if (session.isDisplayNameEmpty())\n    {\n        SessionCellDataFilter filter(cellDataPath, m_cellDataVersion);\n        ITunesFileVector items = m_iTunesDb->filter(filter);\n\n        unsigned int lastModifiedTime = 0;\n        for (ITunesFilesConstIterator it = items.cbegin(); it != items.cend(); ++it)\n        {\n            fileName = m_iTunesDb->getRealPath(*it);\n            if (fileName.empty())\n            {\n                continue;\n            }\n            \n            RawMessage msg2;\n            if (!msg2.mergeFile(fileName))\n            {\n                continue;\n            }\n            std::string displayName;\n            std::string msgTime;\n            if (msg2.parse(\"10\", value))\n            {\n                msgTime = value;\n            }\n            \n            if (msg2.parse(\"7\", value))\n            {\n                displayName = value;\n            }\n            \n            unsigned int modifiedTime = 0;\n            if (items.size() > 1)\n            {\n                modifiedTime = ITunesDb::parseModifiedTime((*it)->blob);\n            }\n            if (session.isDisplayNameEmpty() || (!displayName.empty() && modifiedTime > lastModifiedTime))\n            {\n                session.setDisplayName(displayName);\n                lastModifiedTime = modifiedTime;\n            }\n        }\n    }\n\n    return true;\n}\n\nbool SessionsParser::parseSessionsInGroupApp(const std::string& userRoot, std::vector<Session>& sessions)\n{\n    std::map<std::string, Session*> sessionMap;\n    for (std::vector<Session>::iterator it = sessions.begin(); it != sessions.end(); ++it)\n    {\n        if (!it->isUsrNameEmpty())\n        {\n            sessionMap.insert(sessionMap.cend(), std::make_pair<>(it->getUsrName(), &(*it)));\n        }\n    }\n    \n    SessionHashCompare comp;\n    std::sort(sessions.begin(), sessions.end(), comp);\n    \n    std::string sessionDataArchivePath = m_iTunesDbShare->findRealPath(combinePath(userRoot, \"session\", \"sessionData.archive\"));\n    if (!sessionDataArchivePath.empty())\n    {\n        std::vector<unsigned char> contents;\n        if (readFile(sessionDataArchivePath, contents))\n        {\n            RawMessage msg;\n            if (msg.merge(reinterpret_cast<const char *>(&contents[0]), static_cast<int>(contents.size())))\n            {\n                std::string value;\n                if (msg.parse(\"1\", value))\n                {\n                    uint32_t blockLen = 0;\n                    const char * data = value.c_str();\n                    const char * data1 = NULL;\n                    while ((data1 = calcVarint32Ptr(data, value.c_str() + value.size(), &blockLen)) != NULL)\n                    {\n                        RawMessage msg2;\n                        if (msg2.merge(data1, static_cast<int>(blockLen)))\n                        {\n                            std::string usrName;\n                            if (msg2.parse(\"1\", usrName))\n                            {\n                                Session* session = NULL;\n                                std::map<std::string, Session*>::iterator it = sessionMap.find(usrName);\n                                if (it != sessionMap.end())\n                                {\n                                    session = it->second;\n                                }\n                                else\n                                {\n                                    std::string uidHash = md5(usrName);\n                                    std::vector<Session>::iterator it = std::lower_bound(sessions.begin(), sessions.end(), uidHash, comp);\n                                    if (it != sessions.end() && it->getHash() == uidHash)\n                                    {\n                                        session = &(*it);\n                                        if (it->isUsrNameEmpty())\n                                        {\n                                            it->setUsrName(usrName);\n                                            sessionMap.insert(sessionMap.cend(), std::make_pair<>(usrName, session));\n                                        }\n                                    }\n                                }\n                                \n                                if (NULL != session && session->isDisplayNameEmpty())\n                                {\n                                    std::string nameCand1;\n                                    std::string nameCand2;\n                                    if (msg2.parse(\"2\", nameCand1))\n                                    {\n                                        // nameCand1 = value2;\n                                    }\n                                    if (!msg2.parse(\"3\", nameCand2))\n                                    {\n                                        nameCand2 = \"\";\n                                    }\n                                    session->setDisplayName(nameCand2.empty() ? nameCand1 : nameCand2);\n                                }\n                            }\n                        }\n            \n                        data = data1 + blockLen;\n                    }\n                }\n            }\n        }\n    }\n    \n    std::string extraSessionDataArchivePath = m_iTunesDbShare->findRealPath(combinePath(userRoot, \"session\", \"extraSessionExtraSessionData.archive\"));\n    if (!extraSessionDataArchivePath.empty())\n    {\n        std::vector<unsigned char> contents;\n        if (readFile(extraSessionDataArchivePath, contents))\n        {\n            RawMessage msg;\n            if (msg.merge(reinterpret_cast<const char *>(&contents[0]), static_cast<int>(contents.size())))\n            {\n                std::string value;\n                if (msg.parse(\"1\", value))\n                {\n                    uint32_t blockLen = 0;\n                    const char * data = value.c_str();\n                    const char * data1 = NULL;\n                    while ((data1 = calcVarint32Ptr(data, value.c_str() + value.size(), &blockLen)) != NULL)\n                    {\n                        RawMessage msg2;\n                        if (msg2.merge(data1, static_cast<int>(blockLen)))\n                        {\n                            std::string value2;\n                            if (msg2.parse(\"1\", value2))\n                            {\n                                Session* session = NULL;\n                                std::map<std::string, Session*>::iterator it = sessionMap.find(value2);\n                                if (it != sessionMap.end())\n                                {\n                                    session = it->second;\n                                }\n                                else\n                                {\n                                    std::string uidHash = md5(value2);\n                                    std::vector<Session>::iterator it = std::lower_bound(sessions.begin(), sessions.end(), uidHash, comp);\n                                    if (it != sessions.end() && it->getHash() == uidHash)\n                                    {\n                                        session = &(*it);\n                                        if (it->isUsrNameEmpty())\n                                        {\n                                            it->setUsrName(value2);\n                                            sessionMap.insert(sessionMap.cend(), std::make_pair<>(value2, session));\n                                        }\n                                    }\n                                }\n                                \n                                if (NULL != session && session->isDisplayNameEmpty())\n                                {\n                                    std::string name;\n                                    if (msg2.parse(\"2\", value2))\n                                    {\n                                        name = value2;\n                                    }\n                                    if (!msg2.parse(\"3\", value2))\n                                    {\n                                        value2 = \"\";\n                                    }\n                                    session->setDisplayName(value2.empty() ? name : value2);\n                                }\n                            }\n                        }\n            \n                        data = data1 + blockLen;\n                    }\n                }\n            }\n        }\n    }\n    \n    // copy avatar\n    for (std::vector<Session>::iterator it = sessions.begin(); it != sessions.end(); ++it)\n    {\n        std::string avatarPath = m_iTunesDbShare->findRealPath(combinePath(userRoot, \"session\", \"headImg\", it->getHash() + \".pic\"));\n        if (!avatarPath.empty() && !Friend::isDefaultAvatar(avatarPath))\n        {\n            it->setPortrait(\"file://\" + avatarPath);\n        }\n    }\n\n    return true;\n}\n\nSessionParser::SessionParser(const ExportOption& options) : m_options(options)\n{\n}\n\nSessionParser::MessageEnumerator* SessionParser::buildMsgEnumerator(const Session& session, uint64_t minId)\n{\n    return new MessageEnumerator(session, m_options, minId);\n}\n\nuint32_t SessionParser::calcNumberOfMessages(const Session& session, uint64_t minId)\n{\n    sqlite3* db = NULL;\n    int rc = openSqlite3Database(session.getDbFile(), &db);\n    if (rc != SQLITE_OK)\n    {\n        if (NULL != db)\n        {\n            sqlite3_close(db);\n            db = NULL;\n        }\n        return 0;\n    }\n    \n    std::string sql = \"SELECT COUNT(MesLocalID) FROM Chat_\" + session.getHash() + \" WHERE MesLocalID>\" + std::to_string(minId);\n    \n    sqlite3_stmt* stmt = NULL;\n    rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL);\n    if (rc != SQLITE_OK)\n    {\n        sqlite3_close(db);\n        db = NULL;\n        return 0;\n    }\n    \n    uint32_t recordCount = 0;\n    if (sqlite3_step(stmt) == SQLITE_ROW)\n    {\n        recordCount = (uint32_t)sqlite3_column_int64(stmt, 0);\n    }\n    if (NULL != stmt) sqlite3_finalize(stmt);\n    if (NULL != db) sqlite3_close(db);\n    \n    return recordCount;\n}\n\nstruct MSG_ENUMERATOR_CONTEXT\n{\n    sqlite3* db;\n    sqlite3_stmt* stmt;\n    \n    MSG_ENUMERATOR_CONTEXT(sqlite3* d, sqlite3_stmt* s) : db(d), stmt(s)\n    {\n        \n    }\n    \n    ~MSG_ENUMERATOR_CONTEXT()\n    {\n        if (NULL != stmt) sqlite3_finalize(stmt);\n        if (NULL != db) sqlite3_close(db);\n    }\n};\n\nSessionParser::MessageEnumerator::MessageEnumerator(const Session& session, const ExportOption& options, int64_t minId)\n{\n    MSG_ENUMERATOR_CONTEXT* context = new MSG_ENUMERATOR_CONTEXT(NULL, NULL);\n    m_context = context;\n    \n    int rc = openSqlite3Database(session.getDbFile(), &(context->db));\n    if (rc != SQLITE_OK)\n    {\n        sqlite3_close(context->db);\n        context->db = NULL;\n        return;\n    }\n    \n    std::string sql = \"SELECT CreateTime,Des,MesLocalID,Message,MesSvrID,Status,TableVer,Type FROM Chat_\" + session.getHash();\n    if (minId > 0)\n    {\n        // Incremental Exporting\n        sql += \" WHERE MesLocalID>\" + std::to_string(minId);\n    }\n    sql += \" ORDER BY CreateTime\";\n    if (options.isDesc())\n    {\n        sql += \" DESC\";\n    }\n    \n    rc = sqlite3_prepare_v2(context->db, sql.c_str(), (int)(sql.size()), &(context->stmt), NULL);\n    if (rc != SQLITE_OK)\n    {\n        sqlite3_close(context->db);\n        context->db = NULL;\n        return;\n    }\n}\n\nSessionParser::MessageEnumerator::~MessageEnumerator()\n{\n    if (NULL != m_context)\n    {\n        MSG_ENUMERATOR_CONTEXT* context = reinterpret_cast<MSG_ENUMERATOR_CONTEXT *>(m_context);\n        if (NULL != context)\n        {\n            delete context;\n        }\n        m_context = NULL;\n    }\n}\n\nbool SessionParser::MessageEnumerator::isInvalid() const\n{\n    if (NULL != m_context)\n    {\n        const MSG_ENUMERATOR_CONTEXT* context = reinterpret_cast<const MSG_ENUMERATOR_CONTEXT *>(m_context);\n        if (NULL != context)\n        {\n            return NULL != context->db && NULL != context->stmt;\n        }\n    }\n    \n    return false;\n}\n\nbool SessionParser::MessageEnumerator::nextMessage(WXMSG& msg)\n{\n    if (NULL == m_context)\n    {\n        return NULL;\n    }\n        \n    MSG_ENUMERATOR_CONTEXT* context = reinterpret_cast<MSG_ENUMERATOR_CONTEXT *>(m_context);\n    if (NULL == context || NULL == context->db || NULL == context->stmt)\n    {\n        return false;\n    }\n    \n    if (sqlite3_step(context->stmt) == SQLITE_ROW)\n    {\n        msg.createTime = (unsigned int)sqlite3_column_int(context->stmt, 0);\n        msg.des = sqlite3_column_int(context->stmt, 1);\n        msg.msgIdValue = sqlite3_column_int64(context->stmt, 2);\n        msg.msgId = std::to_string(msg.msgIdValue);\n        const unsigned char* pMessage = sqlite3_column_text(context->stmt, 3);\n        if (pMessage != NULL)\n        {\n            msg.content = reinterpret_cast<const char*>(pMessage);\n        }\n        else\n        {\n            msg.content.clear();\n        }\n        msg.msgSvrId = (sqlite_uint64)sqlite3_column_int64(context->stmt, 4);\n        msg.status = sqlite3_column_int(context->stmt, 5);\n        msg.tableVersion = sqlite3_column_int(context->stmt, 6);\n        msg.type = sqlite3_column_int(context->stmt, 7);\n        \n        return true;\n    }\n    \n    return false;\n}\n"
  },
  {
    "path": "WechatExporter/core/WechatParser.h",
    "content": "//\n//  WechatParser.h\n//  WechatExporter\n//\n//  Created by Matthew on 2020/9/30.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#ifndef WechatParser_h\n#define WechatParser_h\n\n#include <stdio.h>\n#include <regex>\n#include <string>\n#include <vector>\n#include <atomic>\n#include <map>\n#include \"Utils.h\"\n#include \"Downloader.h\"\n#include \"WechatObjects.h\"\n#include \"ExportOption.h\"\n#include \"ITunesParser.h\"\n#include \"MessageParser.h\"\n#include \"Logger.h\"\n\ntemplate<class T>\nclass FilterBase\n{\nprotected:\n    std::string m_path;\n    std::string m_pattern;\n\npublic:\n    bool operator() (const ITunesFile* s1, const T& s2) const    // less\n    {\n        return !startsWith(s1->relativePath, m_path) && s1->relativePath < m_path;\n    }\n    bool operator() (const T& s2, const ITunesFile* s1) const    // greater\n    {\n        return !startsWith(s1->relativePath, m_path) && s1->relativePath > m_path;\n    }\n    bool operator==(const ITunesFile* s) const\n    {\n        return startsWith(s->relativePath, m_path) && (s->relativePath.find(m_pattern, m_path.size()) != std::string::npos);\n    }\n    std::string parse(const ITunesFile* s) const\n    {\n        if (*this == s)\n        {\n            return s->relativePath.substr(m_path.size());\n        }\n        return std::string(\"\");\n    }\n};\n\ntemplate<class T>\nclass RegexFilterBase\n{\nprotected:\n    std::string m_path;\n    std::regex m_pattern;\n\npublic:\n    bool operator() (const ITunesFile* s1, const T& s2) const    // less\n    {\n        return !startsWith(s1->relativePath, m_path) && s1->relativePath < m_path;\n    }\n    bool operator() (const T& s2, const ITunesFile* s1) const    // greater\n    {\n        return !startsWith(s1->relativePath, m_path) && s1->relativePath > m_path;\n    }\n    bool operator==(const ITunesFile* s) const\n    {\n        std::smatch sm;\n        return startsWith(s->relativePath, m_path) && std::regex_search(s->relativePath.begin() + m_path.size(), s->relativePath.end(), sm, m_pattern);\n    }\n    std::string parse(const ITunesFile* s) const\n    {\n        std::smatch sm;\n        if (std::regex_search(s->relativePath.begin() + m_path.size(), s->relativePath.end(), sm, m_pattern))\n        {\n            return sm[1];\n        }\n        return std::string(\"\");\n    }\n};\n\nclass MessageDbFilter : public RegexFilterBase<MessageDbFilter>\n{\npublic:\n    MessageDbFilter(const std::string& basePath) : RegexFilterBase()\n    {\n        std::string vpath = basePath;\n        std::replace(vpath.begin(), vpath.end(), '\\\\', '/');\n        if (!endsWith(vpath, \"/\"))\n        {\n            vpath += \"/\";\n        }\n        vpath += \"DB/\";\n        \n        m_path = vpath;\n        m_pattern = \"^(message_[0-9]{1,4}\\\\.sqlite)$\";\n    }\n};\n\nclass UserFolderFilter : public RegexFilterBase<UserFolderFilter>\n{\nprotected:\n    std::string m_suffix;\n    size_t m_pathLen;\n    size_t m_suffixLen;\npublic:\n    UserFolderFilter() : RegexFilterBase()\n    {\n        m_path = \"Documents/\";\n        // m_pattern = \"^([a-zA-Z0-9]{32})$\";\n        m_suffix = \"/DB/MM.sqlite\";\n        m_pathLen = m_path.length();\n        m_suffixLen = m_suffix.length();\n    }\n    \n    bool operator==(const ITunesFile* s) const\n    {\n        if (/*(s->relativePath.size() != (m_path.size() + 32 + 13)) || */!startsWith(s->relativePath, m_path)|| !endsWith(s->relativePath, m_suffix))\n        {\n            return false;\n        }\n        // if (s->relativePath.find('/', m_path.size(), 32) != std::string::npos)\n        {\n            // return false;\n        }\n        \n        return true;\n    }\n    \n    std::string parse(const ITunesFile* s) const\n    {\n        return s->relativePath.substr(m_pathLen, s->relativePath.length() - m_pathLen - m_suffixLen);\n        // return s->relativePath.substr(m_path.size()) : \"\";\n        // return s->relativePath.size() > 32 ? s->relativePath.substr(m_path.size()) : \"\";\n    }\n};\n\nclass WechatInfoParser\n{\nprivate:\n    ITunesDb *m_iTunesDb;\n    \npublic:\n    \n    WechatInfoParser(ITunesDb *iTunesDb);\n    \n    bool parse(WechatInfo& wechatInfo);\n    \nprotected:\n    bool parsePreferences(WechatInfo& wechatInfo);\n};\n\nclass SessionCellDataFilter : public FilterBase<SessionCellDataFilter>\n{\npublic:\n    SessionCellDataFilter(const std::string& cellDataBasePath, const std::string& cellDataVersion) : FilterBase()\n    {\n        std::string vpath = cellDataBasePath;\n        std::replace(vpath.begin(), vpath.end(), '\\\\', '/');\n        \n        m_path = vpath;\n        m_pattern = \"celldata\" + cellDataVersion;    // celldataV7\n    }\n};\n\nclass LoginInfo2Parser\n{\nprivate:\n    ITunesDb *m_iTunesDb;\n    std::string m_error;\n    Logger*     m_logger;\n    \npublic:\n    LoginInfo2Parser(ITunesDb *iTunesDb, Logger* logger);\n    \n    bool parse(std::vector<Friend>& users);\n    bool parse(const std::string& loginInfo2Path, std::vector<Friend>& users);\n    \n    std::string getError() const;\n    \nprivate:\n    int parseUser(const char* data, int length, std::vector<Friend>& users);\n    bool parseUserFromFolder(std::vector<Friend>& users);\n    bool parseMMSettingsFromMMKV(std::map<std::string, std::pair<std::string, std::string>>& mmsettingFiles);\n    \n    void debugLog(const std::string& log);\n};\n\nclass MMSettingInMMappedKVFilter : public FilterBase<MMSettingInMMappedKVFilter>\n{\nprotected:\n    std::string m_suffix;\n    \npublic:\n    MMSettingInMMappedKVFilter(const std::string& uid) : FilterBase()\n    {\n        m_pattern = \"Documents/MMappedKV/\";\n        m_path = m_pattern + \"mmsetting.archive.\" + uid;\n        m_suffix = \".crc\";\n    }\n    \n    MMSettingInMMappedKVFilter() : FilterBase()\n    {\n        m_pattern = \"Documents/MMappedKV/\";\n        m_path = m_pattern + \"mmsetting.archive.\";\n        m_suffix = \".crc\";\n    }\n    \n    std::string getPrefix() const\n    {\n        return \"mmsetting.archive.\";\n    }\n    \n    bool operator==(const ITunesFile* s) const\n    {\n        return startsWith(s->relativePath, m_path) && !endsWith(s->relativePath, m_suffix);\n    }\n    std::string parse(const ITunesFile* s) const\n    {\n        if (*this == s)\n        {\n            return s->relativePath.substr(m_pattern.size());\n        }\n        return std::string(\"\");\n    }\n};\n\nclass MMSettings\n{\nprotected:\n    std::string m_usrName;\n    std::string m_wxName;\n    std::string m_displayName;\n    std::string m_portrait;\n    std::string m_portraitHD;\npublic:\n    \n    std::string getUsrName() const;\n    std::string getWxName() const;      // WeiXin Hao\n    std::string getDisplayName() const;\n    std::string getPortrait() const;\n    std::string getPortraitHD() const;\n    \nprotected:\n    void clear();\n};\n\nclass MMKVParser : public MMSettings\n{\nprivate:\n    Logger*     m_logger;\n    \npublic:\n    MMKVParser(Logger* logger);\n    bool parse(const std::string& path, const std::string& crcPath);\n    \nprivate:\n    void debugLog(const std::string& log);\n};\n\nclass MMSettingParser : public MMSettings\n{\nprivate:\n    ITunesDb *m_iTunesDb;\n\npublic:\n    MMSettingParser(ITunesDb *iTunesDb);\n    bool parse(const std::string& usrNameHash);\n};\n\nclass FriendsParser\n{\npublic:\n    FriendsParser(bool detailedInfo = true);\n    bool parseWcdb(const std::string& mmPath, Friends& friends);\n#ifndef NDEBUG\n    void setOutputPath(const std::string& outputPath);\n#endif\n    \n    bool parseFriendTags(ITunesDb *iTunesDb, const std::string& uidHash, std::map<uint64_t, std::string>& tags);\n    \n    \nprivate:\n    bool parseRemark(const void *data, int length, Friend& f);\n    bool parseAvatar(const void *data, int length, Friend& f);\n    bool parseChatroom(const void *data, int length, Friend& f);\n    \nprivate:\n    bool m_detailedInfo;\n#ifndef NDEBUG\n    std::string m_outputPath;\n#endif\n};\n\nclass SessionsParser\n{\nprivate:\n    ITunesDb        *m_iTunesDb;\n    ITunesDb        *m_iTunesDbShare;\n    std::string     m_cellDataVersion;\n    const Friends&  m_friends;\n    bool            m_detailedInfo;\n    Logger*         m_logger;\n\npublic:\n    SessionsParser(ITunesDb *iTunesDb, ITunesDb *iTunesDbShare, const Friends& friends, const std::string& cellDataVersion, Logger* logger, bool detailedInfo = true);\n    \n    bool parse(const Friend& user, std::vector<Session>& sessions);\n\nprivate:\n    bool parseUniversalSessions(const Friend& user, const std::string& userRoot, std::vector<Session>& sessions);\n    bool parseCellData(const std::string& userRoot, Session& session);\n    bool parseMessageDbs(const Friend& user, const std::string& userRoot, std::vector<Session>& sessions);\n    bool parseMessageDb(const Friend& user, const std::string& mmPath, std::vector<Session>& sessions, std::vector<Session>& deletedSessions);\n    \n    bool parseSessionsInGroupApp(const std::string& userRoot, std::vector<Session>& sessions);\n    \n    bool parseDisplayNameFromMembers(const Friend& user, Session& session);\n    \n    void debugLog(const std::string& log);\n};\n\nclass SessionParser\n{\npublic:\n    class MessageEnumerator\n    {\n    protected:\n        MessageEnumerator(const Session& session, const ExportOption& options, int64_t minId);\n        \n        friend SessionParser;\n    public:\n        bool isInvalid() const;\n        bool nextMessage(WXMSG& msg);\n        \n        ~MessageEnumerator();\n    private:\n        \n        void* m_context;\n    };\n    \nprivate:\n    \n    ExportOption m_options;\n    \npublic:\n    SessionParser(const ExportOption& options);\n\n    MessageEnumerator* buildMsgEnumerator(const Session& session, uint64_t minId);\n    static uint32_t calcNumberOfMessages(const Session& session, uint64_t minId);\n};\n\n\n#endif /* WechatParser_h */\n"
  },
  {
    "path": "WechatExporter/core/WechatSource.h",
    "content": "//\n//  WechatDataSource.h\n//  WechatExporter\n//\n//  Created by Matthew on 2021/12/23.\n//  Copyright © 2021 Matthew. All rights reserved.\n//\n\n#ifndef WechatDataSource_h\n#define WechatDataSource_h\n\n#include \"ITunesParser.h\"\n#include \"IDeviceBackup.h\"\n\nclass WechatSource\n{\npublic:\n    WechatSource() : m_deviceInfo(NULL)\n    {\n    }\n    WechatSource(const BackupItem& backupItem) : m_backupItem(backupItem), m_deviceInfo(NULL)\n    {\n    }\n    WechatSource(const DeviceInfo& deviceInfo) : m_deviceInfo(NULL)\n    {\n        m_deviceInfo = new DeviceInfo(deviceInfo);\n    }\n    \n    ~WechatSource()\n    {\n        if (NULL != m_deviceInfo)\n        {\n            delete m_deviceInfo;\n        }\n    }\n    \n    bool operator==(const WechatSource& rhs)\n    {\n        if (isDevice() != rhs.isDevice())\n        {\n            return false;\n        }\n        \n        if (isDevice())\n        {\n            return m_deviceInfo->getUdid() == rhs.m_deviceInfo->getUdid();\n        }\n        else\n        {\n            return m_backupItem.getPath() == rhs.m_backupItem.getPath();\n        }\n        \n    }\n    \n    const BackupItem& getBackupItem() const\n    {\n        return m_backupItem;\n    }\n    \n    void setBackupItem(const BackupItem& backupItem)\n    {\n        m_backupItem = backupItem;\n    }\n    \n    const DeviceInfo* getDeviceInfo() const\n    {\n        return m_deviceInfo;\n    }\n    \n    DeviceInfo* getDeviceInfo()\n    {\n        return m_deviceInfo;\n    }\n    \n    bool isDevice() const\n    {\n        return NULL != m_deviceInfo;\n    }\n    \n    std::string getDisplayName() const\n    {\n        if (isDevice())\n        {\n            return m_deviceInfo->getName();\n        }\n        \n        return m_backupItem.toString();\n    }\nprivate:\n    \n    BackupItem m_backupItem;\n    DeviceInfo* m_deviceInfo;\n};\n\n#endif /* WechatDataSource_h */\n"
  },
  {
    "path": "WechatExporter/core/XmlParser.cpp",
    "content": "//\n//  XmlParser.cpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/11/12.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#include \"XmlParser.h\"\n\nstruct NodeValueHandler\n{\n    std::string& value;\n    \n    bool operator() (xmlNodeSetPtr xpathNodes)\n    {\n        // assert ((xpathNodes) && (xpathNodes->nodeNr > 0))\n        xmlNode *cur = xpathNodes->nodeTab[0];\n        xmlChar* sz = xmlNodeGetContent(cur);\n        if (sz != NULL)\n        {\n            value = reinterpret_cast<char *>(sz);\n            xmlFree(sz);\n            return true;\n        }\n        \n        return false;\n    }\n};\n\nstruct NodesValueHandler\n{\n    std::map<std::string, std::string>& values;\n    \n    bool operator() (xmlNodeSetPtr xpathNodes)\n    {\n        // assert ((xpathNodes) && (xpathNodes->nodeNr > 0))\n        for (int idx = 0; idx < xpathNodes->nodeNr; ++idx)\n        {\n            xmlNode *cur = xpathNodes->nodeTab[idx];\n            std::map<std::string, std::string>::iterator it = values.find(reinterpret_cast<const char *>(cur->name));\n            if (it != values.end())\n            {\n                xmlChar* sz = xmlNodeGetContent(cur);\n                if (sz != NULL)\n                {\n                    it->second = reinterpret_cast<char *>(sz);\n                    xmlFree(sz);\n                }\n                else\n                {\n                    it->second.clear();\n                }\n            }\n        }\n        \n        return true;\n    }\n};\n\nstruct AttributeHandler\n{\n    const std::string& name;\n    std::string& value;\n    \n    bool operator() (xmlNodeSetPtr xpathNodes)\n    {\n        // assert ((xpathNodes) && (xpathNodes->nodeNr > 0))\n        xmlNode *cur = xpathNodes->nodeTab[0];\n\n        xmlChar* attr = xmlGetProp(cur, reinterpret_cast<const xmlChar *>(name.c_str()));\n        if (NULL != attr)\n        {\n            value = reinterpret_cast<char *>(attr);\n            xmlFree(attr);\n            return true;\n        }\n        \n        return false;\n    }\n};\n\nstruct AttributesHandler\n{\n    std::map<std::string, std::string>& attributes;\n    \n    bool operator() (xmlNodeSetPtr xpathNodes)\n    {\n        // assert ((xpathNodes) && (xpathNodes->nodeNr > 0))\n        xmlNode *cur = xpathNodes->nodeTab[0];\n        for (std::map<std::string, std::string>::iterator it = attributes.begin(); it != attributes.end(); ++it)\n        {\n            xmlChar* attr = xmlGetProp(cur, reinterpret_cast<const xmlChar *>(it->first.c_str()));\n            if (NULL != attr)\n            {\n                it->second = reinterpret_cast<char *>(attr);\n                xmlFree(attr);\n            }\n            else\n            {\n                it->second.clear();\n            }\n        }\n        \n        return true;\n    }\n};\n\nbool XmlParser::getChildNodeContent(xmlNodePtr node, const std::string& childName, std::string& value)\n{\n    bool found = false;\n    xmlNodePtr childNode = xmlFirstElementChild(node);\n    while (NULL != childNode)\n    {\n        if (xmlStrcmp(childNode->name, BAD_CAST(childName.c_str())) == 0)\n        {\n            value = getNodeInnerText(childNode);\n            found = true;\n            break;\n        }\n        \n        childNode = childNode->next;\n    }\n\n    return found;\n}\n\nxmlNodePtr XmlParser::getChildNode(xmlNodePtr node, const std::string& childName)\n{\n    xmlNodePtr result = NULL;\n    xmlNodePtr childNode = xmlFirstElementChild(node);\n    while (NULL != childNode)\n    {\n        if (xmlStrcmp(childNode->name, BAD_CAST(childName.c_str())) == 0)\n        {\n            result = childNode;\n            break;\n        }\n        \n        childNode = childNode->next;\n    }\n\n    return result;\n}\n\nxmlNodePtr XmlParser::getNextNodeSibling(xmlNodePtr node)\n{\n    return xmlNextElementSibling(node);\n}\n\nbool XmlParser::getNodeAttributeValue(xmlNodePtr node, const std::string& attributeName, std::string& value)\n{\n    xmlChar* attr = xmlGetProp(node, BAD_CAST(attributeName.c_str()));\n    if (NULL != attr)\n    {\n        value = reinterpret_cast<char *>(attr);\n        xmlFree(attr);\n        return true;\n    }\n    return false;\n}\n\nXmlParser::XmlParser(const std::string& xml, bool noError/* = false*/) : m_doc(NULL), m_xpathCtx(NULL)\n{\n    int options = XML_PARSE_RECOVER;\n    if (noError)\n    {\n        options |= XML_PARSE_NOERROR;\n    }\n    \n    // xmlSetGenericErrorFunc(NULL, xmlGenericErrorImpl);\n    // xmlSetStructuredErrorFunc(NULL, xmlStructuredErrorImpl);\n    \n    m_doc = xmlReadMemory(xml.c_str(), static_cast<int>(xml.size()), NULL, NULL, options);\n    if (m_doc != NULL)\n    {\n        m_xpathCtx = xmlXPathNewContext(m_doc);\n    }\n}\n\nXmlParser::~XmlParser()\n{\n    if (m_xpathCtx) { xmlXPathFreeContext(m_xpathCtx); }\n    if (m_doc) { xmlFreeDoc(m_doc); }\n}\n\nxmlXPathObjectPtr XmlParser::evalXPathOnNode(xmlNodePtr node, const std::string& xpath)\n{\n    return xmlXPathNodeEval(node, BAD_CAST(xpath.c_str()), m_xpathCtx);\n}\n\nbool XmlParser::parseNodeValue(const std::string& xpath, std::string& value) const\n{\n    NodeValueHandler handler = {value};\n    return parseWithHandler(xpath, handler);\n}\n\nbool XmlParser::parseNodesValue(const std::string& xpath, std::map<std::string, std::string>& values) const\n{\n    NodesValueHandler handler = {values};\n    return parseWithHandler(xpath, handler);\n}\n\nbool XmlParser::parseChildNodesValue(xmlNodePtr parentNode, const std::string& xpath, std::map<std::string, std::string>& values) const\n{\n    XPathEnumerator enumerator(*this, parentNode, xpath);\n    if (enumerator.isInvalid())\n    {\n        return false;\n    }\n    \n    while (enumerator.hasNext())\n    {\n        xmlNodePtr cur = enumerator.nextNode();\n        \n        std::string name = reinterpret_cast<const char*>(cur->name);\n        std::map<std::string, std::string>::iterator it = values.find(name);\n        if (it != values.end())\n        {\n            it->second = getNodeInnerText(cur);\n        }\n    }\n    \n    return true;\n}\n\nbool XmlParser::parseAttributeValue(const std::string& xpath, const std::string& attributeName, std::string& value) const\n{\n    AttributeHandler handler = {attributeName, value};\n    return parseWithHandler(xpath, handler);\n}\n\nbool XmlParser::parseAttributesValue(const std::string& xpath, std::map<std::string, std::string>& attributes) const\n{\n    AttributesHandler handler = {attributes};\n    return parseWithHandler(xpath, handler);\n}\n"
  },
  {
    "path": "WechatExporter/core/XmlParser.h",
    "content": "//\n//  XmlParser.hpp\n//  WechatExporter\n//\n//  Created by Matthew on 2020/11/12.\n//  Copyright © 2020 Matthew. All rights reserved.\n//\n\n#ifndef XmlParser_h\n#define XmlParser_h\n\n#include <cstdio>\n#include <string>\n#include <map>\n#include <libxml/parser.h>\n#include <libxml/tree.h>\n#include <libxml/xpath.h>\n\nclass XmlParser;\n\nclass XmlParser\n{\npublic:\n    XmlParser(const std::string& xml, bool noError = false);\n    ~XmlParser();\n    bool parseNodeValue(const std::string& xpath, std::string& value) const;\n    bool parseNodesValue(const std::string& xpath, std::map<std::string, std::string>& values) const;  // e.g.: /path1/path2/*\n    bool parseChildNodesValue(xmlNodePtr parentNode, const std::string& xpath, std::map<std::string, std::string>& values) const;  // e.g.: /path1/path2/*\n    bool parseAttributeValue(const std::string& xpath, const std::string& attributeName, std::string& value) const;\n    bool parseAttributesValue(const std::string& xpath, std::map<std::string, std::string>& attributes) const;\n    \n    static xmlNodePtr getChildNode(xmlNodePtr node, const std::string& childName);\n    static xmlNodePtr getNextNodeSibling(xmlNodePtr node);\n    static std::string getNodeInnerText(xmlNodePtr node);\n    static std::string getNodeInnerXml(xmlNodePtr node);\n    static std::string getNodeOuterXml(xmlNodePtr node);\n    static bool getChildNodeContent(xmlNodePtr node, const std::string& childName, std::string& value);\n    static bool getNodeAttributeValue(xmlNodePtr node, const std::string& attributeName, std::string& value);\n    \n    class XPathEnumerator\n    {\n    public:\n        XPathEnumerator(const XmlParser& xmlParser, xmlNodePtr curNode, const std::string& xpath) : m_xPathObj(NULL), m_numberOfNodes(0), m_cursor(-1)\n        {\n            m_xPathObj = xmlXPathNodeEval(curNode, BAD_CAST(xpath.c_str()), xmlParser.m_xpathCtx);\n            if (NULL != m_xPathObj && NULL != m_xPathObj->nodesetval)\n            {\n                m_numberOfNodes = m_xPathObj->nodesetval->nodeNr;\n            }\n        }\n        \n        XPathEnumerator(const XmlParser& xmlParser, const std::string& xpath) : m_xPathObj(NULL), m_numberOfNodes(0), m_cursor(-1)\n        {\n            m_xPathObj = xmlXPathEvalExpression(BAD_CAST(xpath.c_str()), xmlParser.m_xpathCtx);\n            if (NULL != m_xPathObj && NULL != m_xPathObj->nodesetval)\n            {\n                m_numberOfNodes = m_xPathObj->nodesetval->nodeNr;\n            }\n        }\n        \n        bool isInvalid() const\n        {\n            return NULL == m_xPathObj;\n        }\n        \n        bool hasNext() const\n        {\n            return m_cursor < (m_numberOfNodes - 1);\n        }\n        \n        xmlNodePtr nextNode()\n        {\n            return m_xPathObj->nodesetval->nodeTab[++m_cursor];\n        }\n        \n        ~XPathEnumerator()\n        {\n            if (NULL != m_xPathObj)\n            {\n                xmlXPathFreeObject(m_xPathObj);\n            }\n        }\n        \n    private:\n        xmlXPathObjectPtr m_xPathObj;\n        int m_numberOfNodes;\n        mutable int m_cursor;\n    };\n    \npublic:\n    template <class TNodeHandler>\n    bool parseWithHandler(const std::string& xpath, TNodeHandler& handler) const;\n    xmlXPathObjectPtr evalXPathOnNode(xmlNodePtr node, const std::string& xpath);\n    \nprivate:\n    xmlDocPtr m_doc;\n    xmlXPathContextPtr m_xpathCtx;\n};\n\ninline std::string XmlParser::getNodeInnerText(xmlNodePtr node)\n{\n    if (NULL == node->children)\n    {\n        return \"\";\n    }\n    const char* content = reinterpret_cast<const char*>(XML_GET_CONTENT(node->children));\n    return NULL == content ? \"\" : std::string(content);\n}\n\ninline std::string XmlParser::getNodeInnerXml(xmlNodePtr node)\n{\n    xmlChar* content = xmlNodeGetContent(node);\n    // const char* szContent = reinterpret_cast<const char*>(content);\n    std::string xml = (NULL == content) ? \"\" : reinterpret_cast<const char*>(content);\n    xmlFree(content);\n    return xml;\n}\n\ninline std::string XmlParser::getNodeOuterXml(xmlNodePtr node)\n{\n    xmlBufferPtr buffer = xmlBufferCreate();\n#ifndef NDEBUG\n    int size = xmlNodeDump(buffer, node->doc, node, 0, 1);\n#else\n    int size = xmlNodeDump(buffer, node->doc, node, 0, 0);  // no format for release\n#endif\n    \n    // const char* content = reinterpret_cast<const char*>(XML_GET_CONTENT(node->children));\n    std::string xml;\n    if (size > 0 && NULL != buffer->content)\n    {\n        xml.assign(reinterpret_cast<const char*>(buffer->content), size);\n    }\n    xmlBufferFree(buffer);\n    return xml;\n}\n\ntemplate <class TNodeHandler>\nbool XmlParser::parseWithHandler(const std::string& xpath, TNodeHandler& handler) const\n{\n    bool result = false;\n    if (m_doc == NULL || m_xpathCtx == NULL)\n    {\n        return false;\n    }\n    \n    xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression(BAD_CAST(xpath.c_str()), m_xpathCtx);\n    if (xpathObj != NULL)\n    {\n        xmlNodeSetPtr xpathNodes = xpathObj->nodesetval;\n        if ((xpathNodes) && (xpathNodes->nodeNr > 0))\n        {\n            if (handler(xpathNodes))\n            {\n                result = true;\n            }\n        }\n        \n        xmlXPathFreeObject(xpathObj);\n    }\n\n    return result;\n}\n\n#endif /* XmlParser_h */\n"
  },
  {
    "path": "WechatExporter/core/endianness.h",
    "content": "#ifndef ENDIANNESS_H\n#define ENDIANNESS_H\n\n#ifndef __LITTLE_ENDIAN\n#define __LITTLE_ENDIAN 1234\n#endif\n\n#ifndef __BIG_ENDIAN\n#define __BIG_ENDIAN 4321\n#endif\n\n#ifndef __BYTE_ORDER\n#ifdef __LITTLE_ENDIAN__\n#define __BYTE_ORDER __LITTLE_ENDIAN\n#else\n#ifdef __BIG_ENDIAN__\n#define __BYTE_ORDER __BIG_ENDIAN\n#endif\n#endif\n#endif\n\n#ifndef be16toh\n#if __BYTE_ORDER == __BIG_ENDIAN\n#define be16toh(x) (x)\n#else\n#define be16toh(x) ((((x) & 0xFF00) >> 8) | (((x) & 0x00FF) << 8))\n#endif\n#endif\n\n#ifndef htobe16\n#define htobe16 be16toh\n#endif\n\n#ifndef le16toh\n#if __BYTE_ORDER == __BIG_ENDIAN\n#define le16toh(x) ((((x) & 0xFF00) >> 8) | (((x) & 0x00FF) << 8))\n#else\n#define le16toh(x) (x)\n#endif\n#endif\n\n#ifndef htole16\n#define htole16 le16toh\n#endif\n\n#ifndef __bswap_32\n#define __bswap_32(x) ((((x) & 0xFF000000) >> 24) \\\n                    | (((x) & 0x00FF0000) >> 8) \\\n                    | (((x) & 0x0000FF00) << 8) \\\n                    | (((x) & 0x000000FF) << 24))\n#endif\n\n#ifndef be32toh\n#if __BYTE_ORDER == __BIG_ENDIAN\n#define be32toh(x) (x)\n#else\n#define be32toh(x) __bswap_32(x)\n#endif\n#endif\n\n#ifndef htobe32\n#define htobe32 be32toh\n#endif\n\n#ifndef le32toh\n#if __BYTE_ORDER == __BIG_ENDIAN\n#define le32toh(x) __bswap_32(x)\n#else\n#define le32toh(x) (x)\n#endif\n#endif\n\n#ifndef htole32\n#define htole32 le32toh\n#endif\n\n#ifndef __bswap_64\n#define __bswap_64(x) ((((x) & 0xFF00000000000000ull) >> 56) \\\n                    | (((x) & 0x00FF000000000000ull) >> 40) \\\n                    | (((x) & 0x0000FF0000000000ull) >> 24) \\\n                    | (((x) & 0x000000FF00000000ull) >> 8) \\\n                    | (((x) & 0x00000000FF000000ull) << 8) \\\n                    | (((x) & 0x0000000000FF0000ull) << 24) \\\n                    | (((x) & 0x000000000000FF00ull) << 40) \\\n                    | (((x) & 0x00000000000000FFull) << 56))\n#endif\n\n#ifndef htobe64\n#if __BYTE_ORDER == __BIG_ENDIAN\n#define htobe64(x) (x)\n#else\n#define htobe64(x) __bswap_64(x)\n#endif\n#endif\n\n#ifndef be64toh\n#define be64toh htobe64\n#endif\n\n#ifndef le64toh\n#if __BYTE_ORDER == __LITTLE_ENDIAN\n#define le64toh(x) (x)\n#else\n#define le64toh(x) __bswap_64(x)\n#endif\n#endif\n\n#ifndef htole64\n#define htole64 le64toh\n#endif\n\n#endif /* ENDIANNESS_H */\n"
  },
  {
    "path": "WechatExporter/core/md5.c",
    "content": "/*\n * This code implements the MD5 message-digest algorithm.\n * The algorithm is due to Ron Rivest.  This code was\n * written by Colin Plumb in 1993, no copyright is claimed.\n * This code is in the public domain; do with it what you wish.\n *\n * Equivalent code is available from RSA Data Security, Inc.\n * This code has been tested against that, and is equivalent,\n * except that you don't need to include two pages of legalese\n * with every copy.\n *\n * To compute the message digest of a chunk of bytes, declare an\n * MD5Context structure, pass it to MD5Init, call MD5Update as\n * needed on buffers full of bytes, and then call MD5Final, which\n * will fill a supplied 16-byte array with the digest.\n */\n\n/* This code was modified in 1997 by Jim Kingdon of Cyclic Software to\n   not require an integer type which is exactly 32 bits.  This work\n   draws on the changes for the same purpose by Tatu Ylonen\n   <ylo@cs.hut.fi> as part of SSH, but since I didn't actually use\n   that code, there is no copyright issue.  I hereby disclaim\n   copyright in any changes I have made; this code remains in the\n   public domain.  */\n// #include <string.h>    /* for memcpy() */\n#ifdef HAVE_CONFIG_H\n#include \"config.h\"\n#endif\n\n#if HAVE_STRING_H || STDC_HEADERS\n#include <string.h>\t/* for memcpy() */\n#endif\n\n/* Add prototype support.  */\n#ifndef PROTO\n#if defined (USE_PROTOTYPES) ? USE_PROTOTYPES : defined (__STDC__)\n#define PROTO(ARGS) ARGS\n#else\n#define PROTO(ARGS) ()\n#endif\n#endif\n\n#include \"md5.h\"\n\n/* Little-endian byte-swapping routines.  Note that these do not\n   depend on the size of datatypes such as uint32, nor do they require\n   us to detect the endianness of the machine we are running on.  It\n   is possible they should be macros for speed, but I would be\n   surprised if they were a performance bottleneck for MD5.  */\n\nstatic uint32\ngetu32 (addr)\n     const unsigned char *addr;\n{\n\treturn (((((unsigned long)addr[3] << 8) | addr[2]) << 8)\n\t\t| addr[1]) << 8 | addr[0];\n}\n\nstatic void\nputu32 (data, addr)\n     uint32 data;\n     unsigned char *addr;\n{\n\taddr[0] = (unsigned char)data;\n\taddr[1] = (unsigned char)(data >> 8);\n\taddr[2] = (unsigned char)(data >> 16);\n\taddr[3] = (unsigned char)(data >> 24);\n}\n\n/*\n * Start MD5 accumulation.  Set bit count to 0 and buffer to mysterious\n * initialization constants.\n */\nvoid\nMD5Init(ctx)\n     struct MD5Context *ctx;\n{\n\tctx->buf[0] = 0x67452301;\n\tctx->buf[1] = 0xefcdab89;\n\tctx->buf[2] = 0x98badcfe;\n\tctx->buf[3] = 0x10325476;\n\n\tctx->bits[0] = 0;\n\tctx->bits[1] = 0;\n}\n\n/*\n * Update context to reflect the concatenation of another buffer full\n * of bytes.\n */\nvoid\nMD5Update(ctx, buf, len)\n     struct MD5Context *ctx;\n     unsigned char const *buf;\n     unsigned len;\n{\n\tuint32 t;\n\n\t/* Update bitcount */\n\n\tt = ctx->bits[0];\n\tif ((ctx->bits[0] = (t + ((uint32)len << 3)) & 0xffffffff) < t)\n\t\tctx->bits[1]++;\t/* Carry from low to high */\n\tctx->bits[1] += len >> 29;\n\n\tt = (t >> 3) & 0x3f;\t/* Bytes already in shsInfo->data */\n\n\t/* Handle any leading odd-sized chunks */\n\n\tif ( t ) {\n\t\tunsigned char *p = ctx->in + t;\n\n\t\tt = 64-t;\n\t\tif (len < t) {\n\t\t\tmemcpy(p, buf, len);\n\t\t\treturn;\n\t\t}\n\t\tmemcpy(p, buf, t);\n\t\tMD5Transform(ctx->buf, ctx->in);\n\t\tbuf += t;\n\t\tlen -= t;\n\t}\n\n\t/* Process data in 64-byte chunks */\n\n\twhile (len >= 64) {\n\t\tmemcpy(ctx->in, buf, 64);\n\t\tMD5Transform(ctx->buf, ctx->in);\n\t\tbuf += 64;\n\t\tlen -= 64;\n\t}\n\n\t/* Handle any remaining bytes of data. */\n\n\tmemcpy(ctx->in, buf, len);\n}\n\n/*\n * Final wrapup - pad to 64-byte boundary with the bit pattern \n * 1 0* (64-bit count of bits processed, MSB-first)\n */\nvoid\nMD5Final(digest, ctx)\n     unsigned char digest[16];\n     struct MD5Context *ctx;\n{\n\tunsigned count;\n\tunsigned char *p;\n\n\t/* Compute number of bytes mod 64 */\n\tcount = (ctx->bits[0] >> 3) & 0x3F;\n\n\t/* Set the first char of padding to 0x80.  This is safe since there is\n\t   always at least one byte free */\n\tp = ctx->in + count;\n\t*p++ = 0x80;\n\n\t/* Bytes of padding needed to make 64 bytes */\n\tcount = 64 - 1 - count;\n\n\t/* Pad out to 56 mod 64 */\n\tif (count < 8) {\n\t\t/* Two lots of padding:  Pad the first block to 64 bytes */\n\t\tmemset(p, 0, count);\n\t\tMD5Transform(ctx->buf, ctx->in);\n\n\t\t/* Now fill the next block with 56 bytes */\n\t\tmemset(ctx->in, 0, 56);\n\t} else {\n\t\t/* Pad block to 56 bytes */\n\t\tmemset(p, 0, count-8);\n\t}\n\n\t/* Append length in bits and transform */\n\tputu32(ctx->bits[0], ctx->in + 56);\n\tputu32(ctx->bits[1], ctx->in + 60);\n\n\tMD5Transform(ctx->buf, ctx->in);\n\tputu32(ctx->buf[0], digest);\n\tputu32(ctx->buf[1], digest + 4);\n\tputu32(ctx->buf[2], digest + 8);\n\tputu32(ctx->buf[3], digest + 12);\n\tmemset(ctx, 0, sizeof(ctx));\t/* In case it's sensitive */\n}\n\n#ifndef ASM_MD5\n\n/* The four core functions - F1 is optimized somewhat */\n\n/* #define F1(x, y, z) (x & y | ~x & z) */\n#define F1(x, y, z) (z ^ (x & (y ^ z)))\n#define F2(x, y, z) F1(z, x, y)\n#define F3(x, y, z) (x ^ y ^ z)\n#define F4(x, y, z) (y ^ (x | ~z))\n\n/* This is the central step in the MD5 algorithm. */\n#define MD5STEP(f, w, x, y, z, data, s) \\\n\t( w += f(x, y, z) + data, w &= 0xffffffff, w = w<<s | w>>(32-s), w += x )\n\n/*\n * The core of the MD5 algorithm, this alters an existing MD5 hash to\n * reflect the addition of 16 longwords of new data.  MD5Update blocks\n * the data and converts bytes into longwords for this routine.\n */\nvoid\nMD5Transform(buf, inraw)\n     uint32 buf[4];\n     const unsigned char inraw[64];\n{\n\tregister uint32 a, b, c, d;\n\tuint32 in[16];\n\tint i;\n\n\tfor (i = 0; i < 16; ++i)\n\t\tin[i] = getu32 (inraw + 4 * i);\n\n\ta = buf[0];\n\tb = buf[1];\n\tc = buf[2];\n\td = buf[3];\n\n\tMD5STEP(F1, a, b, c, d, in[ 0]+0xd76aa478,  7);\n\tMD5STEP(F1, d, a, b, c, in[ 1]+0xe8c7b756, 12);\n\tMD5STEP(F1, c, d, a, b, in[ 2]+0x242070db, 17);\n\tMD5STEP(F1, b, c, d, a, in[ 3]+0xc1bdceee, 22);\n\tMD5STEP(F1, a, b, c, d, in[ 4]+0xf57c0faf,  7);\n\tMD5STEP(F1, d, a, b, c, in[ 5]+0x4787c62a, 12);\n\tMD5STEP(F1, c, d, a, b, in[ 6]+0xa8304613, 17);\n\tMD5STEP(F1, b, c, d, a, in[ 7]+0xfd469501, 22);\n\tMD5STEP(F1, a, b, c, d, in[ 8]+0x698098d8,  7);\n\tMD5STEP(F1, d, a, b, c, in[ 9]+0x8b44f7af, 12);\n\tMD5STEP(F1, c, d, a, b, in[10]+0xffff5bb1, 17);\n\tMD5STEP(F1, b, c, d, a, in[11]+0x895cd7be, 22);\n\tMD5STEP(F1, a, b, c, d, in[12]+0x6b901122,  7);\n\tMD5STEP(F1, d, a, b, c, in[13]+0xfd987193, 12);\n\tMD5STEP(F1, c, d, a, b, in[14]+0xa679438e, 17);\n\tMD5STEP(F1, b, c, d, a, in[15]+0x49b40821, 22);\n\n\tMD5STEP(F2, a, b, c, d, in[ 1]+0xf61e2562,  5);\n\tMD5STEP(F2, d, a, b, c, in[ 6]+0xc040b340,  9);\n\tMD5STEP(F2, c, d, a, b, in[11]+0x265e5a51, 14);\n\tMD5STEP(F2, b, c, d, a, in[ 0]+0xe9b6c7aa, 20);\n\tMD5STEP(F2, a, b, c, d, in[ 5]+0xd62f105d,  5);\n\tMD5STEP(F2, d, a, b, c, in[10]+0x02441453,  9);\n\tMD5STEP(F2, c, d, a, b, in[15]+0xd8a1e681, 14);\n\tMD5STEP(F2, b, c, d, a, in[ 4]+0xe7d3fbc8, 20);\n\tMD5STEP(F2, a, b, c, d, in[ 9]+0x21e1cde6,  5);\n\tMD5STEP(F2, d, a, b, c, in[14]+0xc33707d6,  9);\n\tMD5STEP(F2, c, d, a, b, in[ 3]+0xf4d50d87, 14);\n\tMD5STEP(F2, b, c, d, a, in[ 8]+0x455a14ed, 20);\n\tMD5STEP(F2, a, b, c, d, in[13]+0xa9e3e905,  5);\n\tMD5STEP(F2, d, a, b, c, in[ 2]+0xfcefa3f8,  9);\n\tMD5STEP(F2, c, d, a, b, in[ 7]+0x676f02d9, 14);\n\tMD5STEP(F2, b, c, d, a, in[12]+0x8d2a4c8a, 20);\n\n\tMD5STEP(F3, a, b, c, d, in[ 5]+0xfffa3942,  4);\n\tMD5STEP(F3, d, a, b, c, in[ 8]+0x8771f681, 11);\n\tMD5STEP(F3, c, d, a, b, in[11]+0x6d9d6122, 16);\n\tMD5STEP(F3, b, c, d, a, in[14]+0xfde5380c, 23);\n\tMD5STEP(F3, a, b, c, d, in[ 1]+0xa4beea44,  4);\n\tMD5STEP(F3, d, a, b, c, in[ 4]+0x4bdecfa9, 11);\n\tMD5STEP(F3, c, d, a, b, in[ 7]+0xf6bb4b60, 16);\n\tMD5STEP(F3, b, c, d, a, in[10]+0xbebfbc70, 23);\n\tMD5STEP(F3, a, b, c, d, in[13]+0x289b7ec6,  4);\n\tMD5STEP(F3, d, a, b, c, in[ 0]+0xeaa127fa, 11);\n\tMD5STEP(F3, c, d, a, b, in[ 3]+0xd4ef3085, 16);\n\tMD5STEP(F3, b, c, d, a, in[ 6]+0x04881d05, 23);\n\tMD5STEP(F3, a, b, c, d, in[ 9]+0xd9d4d039,  4);\n\tMD5STEP(F3, d, a, b, c, in[12]+0xe6db99e5, 11);\n\tMD5STEP(F3, c, d, a, b, in[15]+0x1fa27cf8, 16);\n\tMD5STEP(F3, b, c, d, a, in[ 2]+0xc4ac5665, 23);\n\n\tMD5STEP(F4, a, b, c, d, in[ 0]+0xf4292244,  6);\n\tMD5STEP(F4, d, a, b, c, in[ 7]+0x432aff97, 10);\n\tMD5STEP(F4, c, d, a, b, in[14]+0xab9423a7, 15);\n\tMD5STEP(F4, b, c, d, a, in[ 5]+0xfc93a039, 21);\n\tMD5STEP(F4, a, b, c, d, in[12]+0x655b59c3,  6);\n\tMD5STEP(F4, d, a, b, c, in[ 3]+0x8f0ccc92, 10);\n\tMD5STEP(F4, c, d, a, b, in[10]+0xffeff47d, 15);\n\tMD5STEP(F4, b, c, d, a, in[ 1]+0x85845dd1, 21);\n\tMD5STEP(F4, a, b, c, d, in[ 8]+0x6fa87e4f,  6);\n\tMD5STEP(F4, d, a, b, c, in[15]+0xfe2ce6e0, 10);\n\tMD5STEP(F4, c, d, a, b, in[ 6]+0xa3014314, 15);\n\tMD5STEP(F4, b, c, d, a, in[13]+0x4e0811a1, 21);\n\tMD5STEP(F4, a, b, c, d, in[ 4]+0xf7537e82,  6);\n\tMD5STEP(F4, d, a, b, c, in[11]+0xbd3af235, 10);\n\tMD5STEP(F4, c, d, a, b, in[ 2]+0x2ad7d2bb, 15);\n\tMD5STEP(F4, b, c, d, a, in[ 9]+0xeb86d391, 21);\n\n\tbuf[0] += a;\n\tbuf[1] += b;\n\tbuf[2] += c;\n\tbuf[3] += d;\n}\n#endif\n\n#ifdef TEST\n/* Simple test program.  Can use it to manually run the tests from\n   RFC1321 for example.  */\n#include <stdio.h>\n\nint\nmain (int argc, char **argv)\n{\n\tstruct MD5Context context;\n\tunsigned char checksum[16];\n\tint i;\n\tint j;\n\n\tif (argc < 2)\n\t{\n\t\tfprintf (stderr, \"usage: %s string-to-hash\\n\", argv[0]);\n\t\texit (1);\n\t}\n\tfor (j = 1; j < argc; ++j)\n\t{\n\t\tprintf (\"MD5 (\\\"%s\\\") = \", argv[j]);\n\t\tMD5Init (&context);\n\t\tMD5Update (&context, argv[j], strlen (argv[j]));\n\t\tMD5Final (checksum, &context);\n\t\tfor (i = 0; i < 16; i++)\n\t\t{\n\t\t\tprintf (\"%02x\", (unsigned int) checksum[i]);\n\t\t}\n\t\tprintf (\"\\n\");\n\t}\n\treturn 0;\n}\n#endif /* TEST */\n"
  },
  {
    "path": "WechatExporter/core/md5.h",
    "content": "/* See md5.c for explanation and copyright information.  */\n// https://opensource.apple.com/source/cvs/cvs-19/cvs/lib/md5.h\n// https://opensource.apple.com/source/cvs/cvs-19/cvs/lib/md5.c\n\n#ifndef MD5_H\n#define MD5_H\n\n/* Unlike previous versions of this code, uint32 need not be exactly\n   32 bits, merely 32 bits or more.  Choosing a data type which is 32\n   bits instead of 64 is not important; speed is considerably more\n   important.  ANSI guarantees that \"unsigned long\" will be big enough,\n   and always using it seems to have few disadvantages.  */\ntypedef unsigned long uint32;\n\nstruct MD5Context {\n\tuint32 buf[4];\n\tuint32 bits[2];\n\tunsigned char in[64];\n};\n\n/*\nvoid MD5Init PROTO((struct MD5Context *context));\nvoid MD5Update PROTO((struct MD5Context *context, unsigned char const *buf, unsigned len));\nvoid MD5Final PROTO((unsigned char digest[16], struct MD5Context *context));\nvoid MD5Transform PROTO((uint32 buf[4], const unsigned char in[64]));\n*/\nvoid MD5Init (struct MD5Context *context);\nvoid MD5Update (struct MD5Context *context, unsigned char const *buf, unsigned len);\nvoid MD5Final (unsigned char digest[16], struct MD5Context *context);\nvoid MD5Transform (uint32 buf[4], const unsigned char in[64]);\n\n/*\n * This is needed to make RSAREF happy on some MS-DOS compilers.\n */\ntypedef struct MD5Context MD5_CTX;\n\n#endif /* !MD5_H */\n"
  },
  {
    "path": "WechatExporter/core/semaphore.h",
    "content": "#pragma once\n\n#include <mutex>\n#include <condition_variable>\n\nclass semaphore\n{\nprivate:\n\tstd::mutex mutex_;\n\tstd::condition_variable condition_;\n\tunsigned long count_ = 0; // Initialized as locked.\n\npublic:\n\tvoid notify() {\n\t\tstd::lock_guard<decltype(mutex_)> lock(mutex_);\n\t\t++count_;\n\t\tcondition_.notify_one();\n\t}\n\n\tvoid wait() {\n\t\tstd::unique_lock<decltype(mutex_)> lock(mutex_);\n\t\twhile (!count_) // Handle spurious wake-ups.\n\t\t\tcondition_.wait(lock);\n\t\t--count_;\n\t}\n\n\tbool try_wait() {\n\t\tstd::lock_guard<decltype(mutex_)> lock(mutex_);\n\t\tif (count_) {\n\t\t\t--count_;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n};\n\n\n"
  },
  {
    "path": "WechatExporter/en.lproj/Localizable.strings",
    "content": "/* \n  Localizable.strings\n  WechatExporter\n\n  Created by Matthew on 2021/3/10.\n  Copyright © 2021 Matthew. All rights reserved.\n*/\n\n\"btn-yes\" = \"Yes\";\n\"btn-no\" = \"NO\";\n\"btn-ok\" = \"OK\";\n\"btn-cancel\" = \"Cancel\";\n\"err-no-output-dir\" = \"Please choose a output directory.\";\n\"err-output-dir-doesnt-exist\" = \"Output directory doesn't exist.\";\n\"err-backup-dir-doesnt-exist\" = \"iTunes backup directory doesn't exist.\";\n\"err-no-backup-dir\" = \"Please choose an iTunes backup directory.\";\n\"err-encrypted-bkp-not-supported\" = \"Encrypted iTunes Backup is not supported.\";\n\"err-exp-is-running\" = \"Export is running.\";\n\"err-failed-to-parse-backup\" = \"Failed to parse iTunes Backup file.\";\n\"err-wrong-param\" = \"Wrong parameters.\";\n\"txt-all-wechat-users\" = \"All WeChat Accounts\";\n\"prompt-new-version-found\" = \"Found a new version: %@. Download it now?\";\n\"session-deleted\" = \"(Deleted)\";\n\"grant-full-disk-access\" = \"You should grant Full Disk Access to WechatExporter program as it reads data from iTunes Backup: Apple Menu › System Preferences › Security & Privacy › Privacy › Full Disk Access\";\n\"btn-grant-full-disk-access\" = \"Grant Full Disk Access\";\n\"not-text-msg\" = \"-\";\n\"show-logs\" = \"Show Logs\";\n\"hide-logs\" = \"Hide Logs\";\n\"alert-update-options\" = \"You may change exporting format and options in main menu:\";\n\"no-prev-exp-found\" = \"There is no previous exporting in output directory and [Incremental Exporting] will be ignored.\";\n\"prev-exp-found\" = \"The previous exporting at %@ found, will reuse previous options.\";\n\"invld-inc-exp-for-multi-users\" = \"Incremental Exporting will be invalid for multiple WeChat accounts as for wrong design. \\rPlease choose another directory for exporting.\";\n"
  },
  {
    "path": "WechatExporter/en.lproj/Main.strings",
    "content": "\n/* Class = \"NSButtonCell\"; title = \"...\"; ObjectID = \"1GT-FK-TVG\"; */\n\"1GT-FK-TVG.title\" = \"...\";\n\n/* Class = \"NSMenuItem\"; title = \"WechatExporter\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"WechatExporter\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit WechatExporter\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit WechatExporter\";\n\n/* Class = \"NSButtonCell\"; title = \"Check\"; ObjectID = \"58n-zQ-LqF\"; */\n\"58n-zQ-LqF.title\" = \"Check\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Table View Cell\"; ObjectID = \"5Mi-xr-dCf\"; */\n\"5Mi-xr-dCf.title\" = \"Table View Cell\";\n\n/* Class = \"NSMenuItem\"; title = \"Incremental Exporting\"; ObjectID = \"5c2-wM-XIf\"; */\n\"5c2-wM-XIf.title\" = \"Incremental Exporting\";\n\n/* Class = \"NSMenuItem\"; title = \"Including Subscriptions\"; ObjectID = \"GbO-lL-BLb\"; */\n\"GbO-lL-BLb.title\" = \"Including Subscriptions\";\n\n/* Class = \"NSMenuItem\"; title = \"About WechatExporter\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About WechatExporter\";\n\n/* Class = \"NSTableColumn\"; headerCell.title = \"Name\"; ObjectID = \"8FB-hv-fkB\"; */\n\"8FB-hv-fkB.headerCell.title\" = \"Name\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSTextFieldCell\"; title = \"iTunes Backup Directory:\"; ObjectID = \"BqF-Du-vMt\"; */\n\"BqF-Du-vMt.title\" = \"iTunes Backup Directory:\";\n\n/* Class = \"NSButtonCell\"; title = \"Cancel\"; ObjectID = \"CHb-bh-1kG\"; */\n\"CHb-bh-1kG.title\" = \"Cancel\";\n\n/* Class = \"NSMenuItem\"; title = \"From Newer To Earlier\"; ObjectID = \"eJH-AO-QUD\"; */\n\"eJH-AO-QUD.title\" = \"Sort by Time in Descending Order\";\n\n/* Class = \"NSMenuItem\"; title = \"Text\"; ObjectID = \"GeM-zb-UkW\"; */\n\"GeM-zb-UkW.title\" = \"Text\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Message Filter\"; ObjectID = \"HWn-ip-mii\"; */\n\"HWn-ip-mii.title\" = \"Show Message Filter\";\n\n/* Class = \"NSWindow\"; title = \"Wechat Exporter\"; ObjectID = \"IQv-IB-iLA\"; */\n\"IQv-IB-iLA.title\" = \"Wechat Exporter\";\n\n/* Class = \"NSButtonCell\"; title = \"...\"; ObjectID = \"LFI-6K-mnE\"; */\n\"LFI-6K-mnE.title\" = \"...\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Text Cell\"; ObjectID = \"O0i-cF-8Ix\"; */\n\"O0i-cF-8Ix.title\" = \"Text Cell\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide WechatExporter\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide WechatExporter\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Update Automatically\"; ObjectID = \"Pyu-qm-bT0\"; */\n\"Pyu-qm-bT0.title\" = \"Check Update Automatically\";\n\n/* Class = \"NSMenuItem\"; title = \"Output Detailed Logs\"; ObjectID = \"Pyu-qm-bT0\"; */\n\"Pyu-qm-bT0.title\" = \"Output Detailed Logs\";\n\n/* Class = \"NSMenuItem\"; title = \"Open the Folder After Exporting\"; ObjectID = \"TWh-AV-CCF\"; */\n\"TWh-AV-CCF.title\" = \"Open the Folder After Exporting\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Text Cell\"; ObjectID = \"Q3f-RG-97F\"; */\n\"Q3f-RG-97F.title\" = \"Text Cell\";\n\n/* Class = \"NSMenuItem\"; title = \"PDF\"; ObjectID = \"T2A-kG-daZ\"; */\n\"T2A-kG-daZ.title\" = \"PDF\";\n\n/* Class = \"NSMenuItem\"; title = \"File\"; ObjectID = \"UCE-Dj-bc1\"; */\n\"UCE-Dj-bc1.title\" = \"File\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenuItem\"; title = \"Load Messages Asynchronously(HTML Mode)\"; ObjectID = \"WOF-ft-ZWq\"; */\n\"WOF-ft-ZWq.title\" = \"Load Messages Asynchronously(HTML Mode)\";\n\n/* Class = \"NSButtonCell\"; title = \"Show Logs\"; ObjectID = \"WmO-MK-b5O\"; */\n\"WmO-MK-b5O.title\" = \"Show Logs\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Text Cell\"; ObjectID = \"X6v-WO-gkU\"; */\n\"X6v-WO-gkU.title\" = \"Text Cell\";\n\n/* Class = \"NSTableColumn\"; headerCell.title = \"Number of Msgs\"; ObjectID = \"bsY-Rr-JHi\"; */\n\"bsY-Rr-JHi.headerCell.title\" = \"Number of Msgs\";\n\n/* Class = \"NSMenuItem\"; title = \"Load Messages On Scrolling\"; ObjectID = \"c4Q-qP-ppM\"; */\n\"c4Q-qP-ppM.title\" = \"Load Messages On Scrolling\";\n\n/* Class = \"NSMenuItem\"; title = \"HTML\"; ObjectID = \"cWo-5k-XRR\"; */\n\"cWo-5k-XRR.title\" = \"HTML\";\n\n/* Class = \"NSButtonCell\"; title = \"Close\"; ObjectID = \"dkl-Nk-ye1\"; */\n\"dkl-Nk-ye1.title\" = \"Close\";\n\n/* Class = \"NSMenuItem\"; title = \"Options\"; ObjectID = \"fod-ax-X6A\"; */\n\"fod-ax-X6A.title\" = \"Options\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Table View Cell\"; ObjectID = \"fos-aD-v3q\"; */\n\"fos-aD-v3q.title\" = \"Table View Cell\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Text Cell\"; ObjectID = \"gtr-oV-Pno\"; */\n\"gtr-oV-Pno.title\" = \"Text Cell\";\n\n/* Class = \"NSTableColumn\"; headerCell.title = \"Last Message\"; ObjectID = \"hIY-8j-rpO\"; */\n\"hIY-8j-rpO.headerCell.title\" = \"Last Message\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"k5c-yf-BQl\"; */\n\"k5c-yf-BQl.title\" = \"Help\";\n\n/* Class = \"NSTableColumn\"; headerCell.title = \"WeChat Account\"; ObjectID = \"mqg-xB-lDI\"; */\n\"mqg-xB-lDI.headerCell.title\" = \"WeChat Account\";\n\n/* Class = \"NSMenu\"; title = \"Format\"; ObjectID = \"or1-Xl-Us0\"; */\n\"or1-Xl-Us0.title\" = \"Format\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"pOf-8G-Ji8\"; */\n\"pOf-8G-Ji8.title\" = \"Help\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Table View Cell\"; ObjectID = \"qJ3-t0-qJh\"; */\n\"qJ3-t0-qJh.title\" = \"Table View Cell\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Table View Cell\"; ObjectID = \"sWr-dF-dlL\"; */\n\"sWr-dF-dlL.title\" = \"Table View Cell\";\n\n/* Class = \"NSMenuItem\"; title = \"Save Avatar/Emoji in Chat Folder\"; ObjectID = \"tGU-e5-isp\"; */\n\"tGU-e5-isp.title\" = \"Save Avatar/Emoji in Chat Folder\";\n\n/* Class = \"NSMenu\"; title = \"Options\"; ObjectID = \"uG2-8c-wjP\"; */\n\"uG2-8c-wjP.title\" = \"Options\";\n\n/* Class = \"NSMenu\"; title = \"WechatExporter\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"WechatExporter\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Output Directory:\"; ObjectID = \"uh4-ix-NCq\"; */\n\"uh4-ix-NCq.title\" = \"Output Directory:\";\n\n/* Class = \"NSMenuItem\"; title = \"Format\"; ObjectID = \"v5F-yk-5ct\"; */\n\"v5F-yk-5ct.title\" = \"Format\";\n\n/* Class = \"NSMenuItem\"; title = \"WechatExporter Help\"; ObjectID = \"vp6-q8-ljb\"; */\n\"vp6-q8-ljb.title\" = \"WechatExporter Help\";\n\n/* Class = \"NSButtonCell\"; title = \"Export\"; ObjectID = \"wX7-rb-1cu\"; */\n\"wX7-rb-1cu.title\" = \"Export\";\n\n/* Class = \"NSMenu\"; title = \"File\"; ObjectID = \"yFn-8R-v2s\"; */\n\"yFn-8R-v2s.title\" = \"File\";\n"
  },
  {
    "path": "WechatExporter/main.m",
    "content": "//\r\n//  main.m\r\n//  WechatExporter\r\n//\r\n//  Created by Matthew on 2020/9/29.\r\n//  Copyright © 2020 Matthew. All rights reserved.\r\n//\r\n\r\n#import <Cocoa/Cocoa.h>\r\n\r\n\r\nint main(int argc, const char * argv[]) {\r\n    @autoreleasepool {\r\n        // Setup code that might create autoreleased objects goes here.\r\n    }\r\n    \r\n    return NSApplicationMain(argc, argv);\r\n}\r\n"
  },
  {
    "path": "WechatExporter/res/emoji/emoji.json",
    "content": "[\n    {\n        \"preTag\": \"[\",\n        \"postTag\": \"]\",\n        \"keys\": [\n            {\n                \"key\": \"666\",\n                \"file\": \"Awesome\"\n            },\n            {\n                \"key\": \"Aaagh!\",\n                \"file\": \"Aaagh!\"\n            },\n            {\n                \"key\": \"Angry\",\n                \"file\": \"Angry\"\n            },\n            {\n                \"key\": \"Awesome\",\n                \"file\": \"Awesome\"\n            },\n            {\n                \"key\": \"Awkward\",\n                \"file\": \"Awkward\"\n            },\n            {\n                \"key\": \"Bah！L\",\n                \"file\": \"Bah！L\"\n            },\n            {\n                \"key\": \"Bah！R\",\n                \"file\": \"Bah！R\"\n            },\n            {\n                \"key\": \"Basketball\",\n                \"file\": \"Basketball\"\n            },\n            {\n                \"key\": \"Beckon\",\n                \"file\": \"Beckon\"\n            },\n            {\n                \"key\": \"Beer\",\n                \"file\": \"Beer\"\n            },\n            {\n                \"key\": \"Blessing\",\n                \"file\": \"Blessing\"\n            },\n            {\n                \"key\": \"Blowkiss\",\n                \"file\": \"Blowkiss\"\n            },\n            {\n                \"key\": \"Blush\",\n                \"file\": \"Blush\"\n            },\n            {\n                \"key\": \"Bomb\",\n                \"file\": \"Bomb\"\n            },\n            {\n                \"key\": \"Boring\",\n                \"file\": \"Boring\"\n            },\n            {\n                \"key\": \"Broken\",\n                \"file\": \"Broken\"\n            },\n            {\n                \"key\": \"BrokenHeart\",\n                \"file\": \"BrokenHeart\"\n            },\n            {\n                \"key\": \"Bye\",\n                \"file\": \"Bye\"\n            },\n            {\n                \"key\": \"Cake\",\n                \"file\": \"Cake\"\n            },\n            {\n                \"key\": \"Candle\",\n                \"file\": \"Candle\"\n            },\n            {\n                \"key\": \"Chick\",\n                \"file\": \"Chick\"\n            },\n            {\n                \"key\": \"Chuckle\",\n                \"file\": \"Chuckle\"\n            },\n            {\n                \"key\": \"Clap\",\n                \"file\": \"Clap\"\n            },\n            {\n                \"key\": \"Cleaver\",\n                \"file\": \"Cleaver\"\n            },\n            {\n                \"key\": \"Coffee\",\n                \"file\": \"Coffee\"\n            },\n            {\n                \"key\": \"Commando\",\n                \"file\": \"Commando\"\n            },\n            {\n                \"key\": \"Concerned\",\n                \"file\": \"Concerned\"\n            },\n            {\n                \"key\": \"CoolGuy\",\n                \"file\": \"CoolGuy\"\n            },\n            {\n                \"key\": \"Cry\",\n                \"file\": \"Cry\"\n            },\n            {\n                \"key\": \"Dagger\",\n                \"file\": \"Dagger\"\n            },\n            {\n                \"key\": \"Determined\",\n                \"file\": \"Determined\"\n            },\n            {\n                \"key\": \"Dizzy\",\n                \"file\": \"Dizzy\"\n            },\n            {\n                \"key\": \"Doge\",\n                \"file\": \"Doge\"\n            },\n            {\n                \"key\": \"Dramatic\",\n                \"file\": \"Dramatic\"\n            },\n            {\n                \"key\": \"Drool\",\n                \"file\": \"Drool\"\n            },\n            {\n                \"key\": \"Drowsy\",\n                \"file\": \"Drowsy\"\n            },\n            {\n                \"key\": \"Duh\",\n                \"file\": \"Duh\"\n            },\n            {\n                \"key\": \"Emm\",\n                \"file\": \"Emm\"\n            },\n            {\n                \"key\": \"Emm\",\n                \"file\": \"Emm\"\n            },\n            {\n                \"key\": \"Facepalm\",\n                \"file\": \"Facepalm\"\n            },\n            {\n                \"key\": \"Fight\",\n                \"file\": \"Fight\"\n            },\n            {\n                \"key\": \"Firecracker\",\n                \"file\": \"Firecracker\"\n            },\n            {\n                \"key\": \"Fireworks\",\n                \"file\": \"Fireworks\"\n            },\n            {\n                \"key\": \"Fist\",\n                \"file\": \"Fist\"\n            },\n            {\n                \"key\": \"Flushed\",\n                \"file\": \"Flushed\"\n            },\n            {\n                \"key\": \"Frown\",\n                \"file\": \"Frown\"\n            },\n            {\n                \"key\": \"Gift\",\n                \"file\": \"Gift\"\n            },\n            {\n                \"key\": \"GoForIt\",\n                \"file\": \"GoForIt\"\n            },\n            {\n                \"key\": \"Grimace\",\n                \"file\": \"Grimace\"\n            },\n            {\n                \"key\": \"Grin\",\n                \"file\": \"Grin\"\n            },\n            {\n                \"key\": \"Hammer\",\n                \"file\": \"Hammer\"\n            },\n            {\n                \"key\": \"Happy\",\n                \"file\": \"Happy\"\n            },\n            {\n                \"key\": \"Heart\",\n                \"file\": \"Heart\"\n            },\n            {\n                \"key\": \"Hey\",\n                \"file\": \"Hey\"\n            },\n            {\n                \"key\": \"Hooray\",\n                \"file\": \"Hooray\"\n            },\n            {\n                \"key\": \"Hug\",\n                \"file\": \"Hug\"\n            },\n            {\n                \"key\": \"Hungry\",\n                \"file\": \"Hungry\"\n            },\n            {\n                \"key\": \"Hurt\",\n                \"file\": \"Hurt\"\n            },\n            {\n                \"key\": \"InLove\",\n                \"file\": \"InLove\"\n            },\n            {\n                \"key\": \"Joyful\",\n                \"file\": \"Joyful\"\n            },\n            {\n                \"key\": \"JumpRope\",\n                \"file\": \"JumpRope\"\n            },\n            {\n                \"key\": \"KeepFighting\",\n                \"file\": \"KeepFighting\"\n            },\n            {\n                \"key\": \"Kiss\",\n                \"file\": \"Kiss\"\n            },\n            {\n                \"key\": \"Kotow\",\n                \"file\": \"Kotow\"\n            },\n            {\n                \"key\": \"Ladybug\",\n                \"file\": \"Ladybug\"\n            },\n            {\n                \"key\": \"Laugh\",\n                \"file\": \"Laugh\"\n            },\n            {\n                \"key\": \"Let Down\",\n                \"file\": \"Let Down\"\n            },\n            {\n                \"key\": \"LetMeSee\",\n                \"file\": \"LetMeSee\"\n            },\n            {\n                \"key\": \"Lightning\",\n                \"file\": \"Lightning\"\n            },\n            {\n                \"key\": \"Lips\",\n                \"file\": \"Lips\"\n            },\n            {\n                \"key\": \"Lol\",\n                \"file\": \"Lol\"\n            },\n            {\n                \"key\": \"Meditate\",\n                \"file\": \"Meditate\"\n            },\n            {\n                \"key\": \"Moon\",\n                \"file\": \"Moon\"\n            },\n            {\n                \"key\": \"Moue\",\n                \"file\": \"Moue\"\n            },\n            {\n                \"key\": \"Moue\",\n                \"file\": \"Moue\"\n            },\n            {\n                \"key\": \"MyBad\",\n                \"file\": \"MyBad\"\n            },\n            {\n                \"key\": \"NO\",\n                \"file\": \"Nuh-uh\"\n            },\n            {\n                \"key\": \"NO\",\n                \"file\": \"Nuh-uh\"\n            },\n            {\n                \"key\": \"NoProb\",\n                \"file\": \"NoProb\"\n            },\n            {\n                \"key\": \"NosePick\",\n                \"file\": \"NosePick\"\n            },\n            {\n                \"key\": \"Nuh-uh\",\n                \"file\": \"Nuh-uh\"\n            },\n            {\n                \"key\": \"OK\",\n                \"file\": \"OK\"\n            },\n            {\n                \"key\": \"OK\",\n                \"file\": \"OK\"\n            },\n            {\n                \"key\": \"OK\",\n                \"file\": \"OK\"\n            },\n            {\n                \"key\": \"OMG\",\n                \"file\": \"OMG\"\n            },\n            {\n                \"key\": \"Onlooker\",\n                \"file\": \"Onlooker\"\n            },\n            {\n                \"key\": \"Packet\",\n                \"file\": \"Packet\"\n            },\n            {\n                \"key\": \"Packet\",\n                \"file\": \"Packet\"\n            },\n            {\n                \"key\": \"Panic\",\n                \"file\": \"Panic\"\n            },\n            {\n                \"key\": \"Party\",\n                \"file\": \"Party\"\n            },\n            {\n                \"key\": \"Peace\",\n                \"file\": \"Peace\"\n            },\n            {\n                \"key\": \"Pig\",\n                \"file\": \"Pig\"\n            },\n            {\n                \"key\": \"PingPong\",\n                \"file\": \"PingPong\"\n            },\n            {\n                \"key\": \"Pinky\",\n                \"file\": \"Pinky\"\n            },\n            {\n                \"key\": \"Pooh-pooh\",\n                \"file\": \"Pooh-pooh\"\n            },\n            {\n                \"key\": \"Poop\",\n                \"file\": \"Poop\"\n            },\n            {\n                \"key\": \"Puke\",\n                \"file\": \"Puke\"\n            },\n            {\n                \"key\": \"Pup\",\n                \"file\": \"Pup\"\n            },\n            {\n                \"key\": \"Respect\",\n                \"file\": \"Respect\"\n            },\n            {\n                \"key\": \"Rice\",\n                \"file\": \"Rice\"\n            },\n            {\n                \"key\": \"Rich\",\n                \"file\": \"Rich\"\n            },\n            {\n                \"key\": \"RockOn\",\n                \"file\": \"RockOn\"\n            },\n            {\n                \"key\": \"Rose\",\n                \"file\": \"Rose\"\n            },\n            {\n                \"key\": \"Ruthless\",\n                \"file\": \"Ruthless\"\n            },\n            {\n                \"key\": \"Salute\",\n                \"file\": \"Fight\"\n            },\n            {\n                \"key\": \"Scold\",\n                \"file\": \"Scold\"\n            },\n            {\n                \"key\": \"Scowl\",\n                \"file\": \"Scowl\"\n            },\n            {\n                \"key\": \"Scream\",\n                \"file\": \"Scream\"\n            },\n            {\n                \"key\": \"Shake\",\n                \"file\": \"Shake\"\n            },\n            {\n                \"key\": \"Shame\",\n                \"file\": \"Shame\"\n            },\n            {\n                \"key\": \"Shhh\",\n                \"file\": \"Shhh\"\n            },\n            {\n                \"key\": \"Shocked\",\n                \"file\": \"Shocked\"\n            },\n            {\n                \"key\": \"Shrunken\",\n                \"file\": \"Shrunken\"\n            },\n            {\n                \"key\": \"Shy\",\n                \"file\": \"Shy\"\n            },\n            {\n                \"key\": \"Sick\",\n                \"file\": \"Sick\"\n            },\n            {\n                \"key\": \"Sigh\",\n                \"file\": \"Sigh\"\n            },\n            {\n                \"key\": \"Silent\",\n                \"file\": \"Silent\"\n            },\n            {\n                \"key\": \"Skull\",\n                \"file\": \"Skull\"\n            },\n            {\n                \"key\": \"Sleep\",\n                \"file\": \"Sleep\"\n            },\n            {\n                \"key\": \"Slight\",\n                \"file\": \"Slight\"\n            },\n            {\n                \"key\": \"Sly\",\n                \"file\": \"Sly\"\n            },\n            {\n                \"key\": \"Smart\",\n                \"file\": \"Smart\"\n            },\n            {\n                \"key\": \"Smile\",\n                \"file\": \"Smile\"\n            },\n            {\n                \"key\": \"Smirk\",\n                \"file\": \"Smirk\"\n            },\n            {\n                \"key\": \"Smooch\",\n                \"file\": \"Smooch\"\n            },\n            {\n                \"key\": \"Smug\",\n                \"file\": \"Smug\"\n            },\n            {\n                \"key\": \"Sob\",\n                \"file\": \"Sob\"\n            },\n            {\n                \"key\": \"Soccer\",\n                \"file\": \"Soccer\"\n            },\n            {\n                \"key\": \"Speechless\",\n                \"file\": \"Speechless\"\n            },\n            {\n                \"key\": \"Sun\",\n                \"file\": \"Sun\"\n            },\n            {\n                \"key\": \"Surprise\",\n                \"file\": \"Surprise\"\n            },\n            {\n                \"key\": \"Surrender\",\n                \"file\": \"Surrender\"\n            },\n            {\n                \"key\": \"Sweat\",\n                \"file\": \"Sweat\"\n            },\n            {\n                \"key\": \"Sweats\",\n                \"file\": \"Sweats\"\n            },\n            {\n                \"key\": \"TaiChi L\",\n                \"file\": \"TaiChi L\"\n            },\n            {\n                \"key\": \"TaiChi R\",\n                \"file\": \"TaiChi R\"\n            },\n            {\n                \"key\": \"Tea\",\n                \"file\": \"Tea\"\n            },\n            {\n                \"key\": \"TearingUp\",\n                \"file\": \"TearingUp\"\n            },\n            {\n                \"key\": \"Terror\",\n                \"file\": \"Terror\"\n            },\n            {\n                \"key\": \"ThumbsDown\",\n                \"file\": \"ThumbsDown\"\n            },\n            {\n                \"key\": \"ThumbsUp\",\n                \"file\": \"ThumbsUp\"\n            },\n            {\n                \"key\": \"Toasted\",\n                \"file\": \"Toasted\"\n            },\n            {\n                \"key\": \"Tongue\",\n                \"file\": \"Tongue\"\n            },\n            {\n                \"key\": \"Tormented\",\n                \"file\": \"Tormented\"\n            },\n            {\n                \"key\": \"Tremble\",\n                \"file\": \"Tremble\"\n            },\n            {\n                \"key\": \"Trick\",\n                \"file\": \"Trick\"\n            },\n            {\n                \"key\": \"Twirl\",\n                \"file\": \"Twirl\"\n            },\n            {\n                \"key\": \"Waddle\",\n                \"file\": \"Waddle\"\n            },\n            {\n                \"key\": \"Watermelon\",\n                \"file\": \"Watermelon\"\n            },\n            {\n                \"key\": \"Wave\",\n                \"file\": \"Bye\"\n            },\n            {\n                \"key\": \"Whimper\",\n                \"file\": \"Whimper\"\n            },\n            {\n                \"key\": \"Wilt\",\n                \"file\": \"Wilt\"\n            },\n            {\n                \"key\": \"Worship\",\n                \"file\": \"Worship\"\n            },\n            {\n                \"key\": \"Wow\",\n                \"file\": \"Wow\"\n            },\n            {\n                \"key\": \"Wrath\",\n                \"file\": \"Wrath\"\n            },\n            {\n                \"key\": \"Yawn\",\n                \"file\": \"Yawn\"\n            },\n            {\n                \"key\": \"Yeah!\",\n                \"file\": \"Yeah!\"\n            },\n            {\n                \"key\": \"[囧]\",\n                \"file\": \"Blush\"\n            },\n            {\n                \"key\": \"一言難盡\",\n                \"file\": \"Emm\"\n            },\n            {\n                \"key\": \"乒乓\",\n                \"file\": \"PingPong\"\n            },\n            {\n                \"key\": \"乒乓\",\n                \"file\": \"PingPong\"\n            },\n            {\n                \"key\": \"乱舞\",\n                \"file\": \"Meditate\"\n            },\n            {\n                \"key\": \"亂舞\",\n                \"file\": \"Meditate\"\n            },\n            {\n                \"key\": \"亲亲\",\n                \"file\": \"Kiss\"\n            },\n            {\n                \"key\": \"便便\",\n                \"file\": \"Poop\"\n            },\n            {\n                \"key\": \"便便\",\n                \"file\": \"Poop\"\n            },\n            {\n                \"key\": \"偷笑\",\n                \"file\": \"Chuckle\"\n            },\n            {\n                \"key\": \"偷笑\",\n                \"file\": \"Chuckle\"\n            },\n            {\n                \"key\": \"傲慢\",\n                \"file\": \"Smug\"\n            },\n            {\n                \"key\": \"傲慢\",\n                \"file\": \"Smug\"\n            },\n            {\n                \"key\": \"再見\",\n                \"file\": \"Bye\"\n            },\n            {\n                \"key\": \"再见\",\n                \"file\": \"Bye\"\n            },\n            {\n                \"key\": \"冷汗\",\n                \"file\": \"Blush\"\n            },\n            {\n                \"key\": \"冷汗\",\n                \"file\": \"Blush\"\n            },\n            {\n                \"key\": \"凋谢\",\n                \"file\": \"Wilt\"\n            },\n            {\n                \"key\": \"刀\",\n                \"file\": \"Dagger\"\n            },\n            {\n                \"key\": \"刀\",\n                \"file\": \"Dagger\"\n            },\n            {\n                \"key\": \"加油\",\n                \"file\": \"GoForIt\"\n            },\n            {\n                \"key\": \"加油\",\n                \"file\": \"GoForIt\"\n            },\n            {\n                \"key\": \"加油加油\",\n                \"file\": \"KeepFighting\"\n            },\n            {\n                \"key\": \"加油！\",\n                \"file\": \"KeepFighting\"\n            },\n            {\n                \"key\": \"勝利\",\n                \"file\": \"Peace\"\n            },\n            {\n                \"key\": \"勾引\",\n                \"file\": \"Beckon\"\n            },\n            {\n                \"key\": \"勾引\",\n                \"file\": \"Beckon\"\n            },\n            {\n                \"key\": \"发呆\",\n                \"file\": \"Scowl\"\n            },\n            {\n                \"key\": \"发怒\",\n                \"file\": \"Angry\"\n            },\n            {\n                \"key\": \"发抖\",\n                \"file\": \"Tremble\"\n            },\n            {\n                \"key\": \"可怜\",\n                \"file\": \"Whimper\"\n            },\n            {\n                \"key\": \"可憐\",\n                \"file\": \"Whimper\"\n            },\n            {\n                \"key\": \"右哼哼\",\n                \"file\": \"Bah！R\"\n            },\n            {\n                \"key\": \"右哼哼\",\n                \"file\": \"Bah！R\"\n            },\n            {\n                \"key\": \"右太极\",\n                \"file\": \"TaiChi R\"\n            },\n            {\n                \"key\": \"右太極\",\n                \"file\": \"TaiChi R\"\n            },\n            {\n                \"key\": \"叹气\",\n                \"file\": \"Sigh\"\n            },\n            {\n                \"key\": \"吃瓜\",\n                \"file\": \"Onlooker\"\n            },\n            {\n                \"key\": \"吃西瓜\",\n                \"file\": \"Onlooker\"\n            },\n            {\n                \"key\": \"合十\",\n                \"file\": \"Worship\"\n            },\n            {\n                \"key\": \"合十\",\n                \"file\": \"Worship\"\n            },\n            {\n                \"key\": \"吐\",\n                \"file\": \"Puke\"\n            },\n            {\n                \"key\": \"吐\",\n                \"file\": \"Puke\"\n            },\n            {\n                \"key\": \"吐舌\",\n                \"file\": \"u1F61D\"\n            },\n            {\n                \"key\": \"吓\",\n                \"file\": \"Wrath\"\n            },\n            {\n                \"key\": \"吼嘿\",\n                \"file\": \"Hey\"\n            },\n            {\n                \"key\": \"呲牙\",\n                \"file\": \"Grin\"\n            },\n            {\n                \"key\": \"呲牙\",\n                \"file\": \"Grin\"\n            },\n            {\n                \"key\": \"咒罵\",\n                \"file\": \"Scold\"\n            },\n            {\n                \"key\": \"咒骂\",\n                \"file\": \"Scold\"\n            },\n            {\n                \"key\": \"咖啡\",\n                \"file\": \"Coffee\"\n            },\n            {\n                \"key\": \"咖啡\",\n                \"file\": \"Coffee\"\n            },\n            {\n                \"key\": \"哇\",\n                \"file\": \"Wow\"\n            },\n            {\n                \"key\": \"哈欠\",\n                \"file\": \"Yawn\"\n            },\n            {\n                \"key\": \"哈欠\",\n                \"file\": \"Yawn\"\n            },\n            {\n                \"key\": \"啤酒\",\n                \"file\": \"Beer\"\n            },\n            {\n                \"key\": \"啤酒\",\n                \"file\": \"Beer\"\n            },\n            {\n                \"key\": \"嘆息\",\n                \"file\": \"Sigh\"\n            },\n            {\n                \"key\": \"嘘\",\n                \"file\": \"Shhh\"\n            },\n            {\n                \"key\": \"嘴唇\",\n                \"file\": \"Lips\"\n            },\n            {\n                \"key\": \"嘴唇\",\n                \"file\": \"Lips\"\n            },\n            {\n                \"key\": \"嘿哈\",\n                \"file\": \"Hey\"\n            },\n            {\n                \"key\": \"噓\",\n                \"file\": \"Shhh\"\n            },\n            {\n                \"key\": \"噴火\",\n                \"file\": \"Aaagh!\"\n            },\n            {\n                \"key\": \"嚇\",\n                \"file\": \"Wrath\"\n            },\n            {\n                \"key\": \"回头\",\n                \"file\": \"Dramatic\"\n            },\n            {\n                \"key\": \"回頭\",\n                \"file\": \"Dramatic\"\n            },\n            {\n                \"key\": \"囧\",\n                \"file\": \"Blush\"\n            },\n            {\n                \"key\": \"困\",\n                \"file\": \"Drowsy\"\n            },\n            {\n                \"key\": \"坏笑\",\n                \"file\": \"Trick\"\n            },\n            {\n                \"key\": \"壞笑\",\n                \"file\": \"Trick\"\n            },\n            {\n                \"key\": \"大哭\",\n                \"file\": \"Cry\"\n            },\n            {\n                \"key\": \"大哭\",\n                \"file\": \"Cry\"\n            },\n            {\n                \"key\": \"大笑\",\n                \"file\": \"Laugh\"\n            },\n            {\n                \"key\": \"天啊\",\n                \"file\": \"OMG\"\n            },\n            {\n                \"key\": \"太阳\",\n                \"file\": \"Sun\"\n            },\n            {\n                \"key\": \"太陽\",\n                \"file\": \"Sun\"\n            },\n            {\n                \"key\": \"失敬失敬\",\n                \"file\": \"Respect\"\n            },\n            {\n                \"key\": \"失望\",\n                \"file\": \"Let Down\"\n            },\n            {\n                \"key\": \"失望\",\n                \"file\": \"Let Down\"\n            },\n            {\n                \"key\": \"奋斗\",\n                \"file\": \"Determined\"\n            },\n            {\n                \"key\": \"奮鬥\",\n                \"file\": \"Determined\"\n            },\n            {\n                \"key\": \"奸笑\",\n                \"file\": \"Smirk\"\n            },\n            {\n                \"key\": \"奸笑\",\n                \"file\": \"Smirk\"\n            },\n            {\n                \"key\": \"好的\",\n                \"file\": \"NoProb\"\n            },\n            {\n                \"key\": \"委屈\",\n                \"file\": \"Shrunken\"\n            },\n            {\n                \"key\": \"委屈\",\n                \"file\": \"Shrunken\"\n            },\n            {\n                \"key\": \"害羞\",\n                \"file\": \"Shy\"\n            },\n            {\n                \"key\": \"害羞\",\n                \"file\": \"Shy\"\n            },\n            {\n                \"key\": \"小狗\",\n                \"file\": \"Pup\"\n            },\n            {\n                \"key\": \"小狗\",\n                \"file\": \"Pup\"\n            },\n            {\n                \"key\": \"小雞\",\n                \"file\": \"Chick\"\n            },\n            {\n                \"key\": \"尴尬\",\n                \"file\": \"Awkward\"\n            },\n            {\n                \"key\": \"尷尬\",\n                \"file\": \"Awkward\"\n            },\n            {\n                \"key\": \"崩潰\",\n                \"file\": \"Broken\"\n            },\n            {\n                \"key\": \"左哼哼\",\n                \"file\": \"Bah！L\"\n            },\n            {\n                \"key\": \"左哼哼\",\n                \"file\": \"Bah！L\"\n            },\n            {\n                \"key\": \"左太极\",\n                \"file\": \"TaiChi L\"\n            },\n            {\n                \"key\": \"左太極\",\n                \"file\": \"TaiChi L\"\n            },\n            {\n                \"key\": \"差劲\",\n                \"file\": \"Pinky\"\n            },\n            {\n                \"key\": \"差勁\",\n                \"file\": \"Pinky\"\n            },\n            {\n                \"key\": \"庆祝\",\n                \"file\": \"Party\"\n            },\n            {\n                \"key\": \"弱\",\n                \"file\": \"ThumbsDown\"\n            },\n            {\n                \"key\": \"弱\",\n                \"file\": \"ThumbsDown\"\n            },\n            {\n                \"key\": \"強\",\n                \"file\": \"ThumbsUp\"\n            },\n            {\n                \"key\": \"强\",\n                \"file\": \"ThumbsUp\"\n            },\n            {\n                \"key\": \"强壮\",\n                \"file\": \"u1F4AA\"\n            },\n            {\n                \"key\": \"得意\",\n                \"file\": \"CoolGuy\"\n            },\n            {\n                \"key\": \"得意\",\n                \"file\": \"CoolGuy\"\n            },\n            {\n                \"key\": \"微笑\",\n                \"file\": \"Smile\"\n            },\n            {\n                \"key\": \"微笑\",\n                \"file\": \"Smile\"\n            },\n            {\n                \"key\": \"心碎\",\n                \"file\": \"BrokenHeart\"\n            },\n            {\n                \"key\": \"心碎\",\n                \"file\": \"BrokenHeart\"\n            },\n            {\n                \"key\": \"快哭了\",\n                \"file\": \"TearingUp\"\n            },\n            {\n                \"key\": \"快哭了\",\n                \"file\": \"TearingUp\"\n            },\n            {\n                \"key\": \"怄火\",\n                \"file\": \"Aaagh!\"\n            },\n            {\n                \"key\": \"恐惧\",\n                \"file\": \"Terror\"\n            },\n            {\n                \"key\": \"恐懼\",\n                \"file\": \"Terror\"\n            },\n            {\n                \"key\": \"悠閑\",\n                \"file\": \"Commando\"\n            },\n            {\n                \"key\": \"悠闲\",\n                \"file\": \"Commando\"\n            },\n            {\n                \"key\": \"惊恐\",\n                \"file\": \"Panic\"\n            },\n            {\n                \"key\": \"惊讶\",\n                \"file\": \"Surprise\"\n            },\n            {\n                \"key\": \"愉快\",\n                \"file\": \"Joyful\"\n            },\n            {\n                \"key\": \"愉快\",\n                \"file\": \"Joyful\"\n            },\n            {\n                \"key\": \"愛你\",\n                \"file\": \"RockOn\"\n            },\n            {\n                \"key\": \"愛心\",\n                \"file\": \"Heart\"\n            },\n            {\n                \"key\": \"愛情\",\n                \"file\": \"InLove\"\n            },\n            {\n                \"key\": \"慶祝\",\n                \"file\": \"Party\"\n            },\n            {\n                \"key\": \"憨笑\",\n                \"file\": \"Laugh\"\n            },\n            {\n                \"key\": \"打脸\",\n                \"file\": \"MyBad\"\n            },\n            {\n                \"key\": \"打臉\",\n                \"file\": \"MyBad\"\n            },\n            {\n                \"key\": \"抓狂\",\n                \"file\": \"Scream\"\n            },\n            {\n                \"key\": \"抓狂\",\n                \"file\": \"Scream\"\n            },\n            {\n                \"key\": \"投降\",\n                \"file\": \"Surrender\"\n            },\n            {\n                \"key\": \"投降\",\n                \"file\": \"Surrender\"\n            },\n            {\n                \"key\": \"抠鼻\",\n                \"file\": \"NosePick\"\n            },\n            {\n                \"key\": \"抱拳\",\n                \"file\": \"Fight\"\n            },\n            {\n                \"key\": \"抱拳\",\n                \"file\": \"Fight\"\n            },\n            {\n                \"key\": \"拥抱\",\n                \"file\": \"Hug\"\n            },\n            {\n                \"key\": \"拳头\",\n                \"file\": \"Fist\"\n            },\n            {\n                \"key\": \"拳頭\",\n                \"file\": \"Fist\"\n            },\n            {\n                \"key\": \"捂脸\",\n                \"file\": \"Facepalm\"\n            },\n            {\n                \"key\": \"掩面\",\n                \"file\": \"Facepalm\"\n            },\n            {\n                \"key\": \"握手\",\n                \"file\": \"Shake\"\n            },\n            {\n                \"key\": \"握手\",\n                \"file\": \"Shake\"\n            },\n            {\n                \"key\": \"摳鼻\",\n                \"file\": \"NosePick\"\n            },\n            {\n                \"key\": \"撇嘴\",\n                \"file\": \"Grimace\"\n            },\n            {\n                \"key\": \"撇嘴\",\n                \"file\": \"Grimace\"\n            },\n            {\n                \"key\": \"擁抱\",\n                \"file\": \"Hug\"\n            },\n            {\n                \"key\": \"擦汗\",\n                \"file\": \"Speechless\"\n            },\n            {\n                \"key\": \"擦汗\",\n                \"file\": \"Speechless\"\n            },\n            {\n                \"key\": \"敲打\",\n                \"file\": \"Hammer\"\n            },\n            {\n                \"key\": \"敲打\",\n                \"file\": \"Hammer\"\n            },\n            {\n                \"key\": \"无语\",\n                \"file\": \"Duh\"\n            },\n            {\n                \"key\": \"旺柴\",\n                \"file\": \"Doge\"\n            },\n            {\n                \"key\": \"旺柴\",\n                \"file\": \"Doge\"\n            },\n            {\n                \"key\": \"晕\",\n                \"file\": \"Dizzy\"\n            },\n            {\n                \"key\": \"暈\",\n                \"file\": \"Dizzy\"\n            },\n            {\n                \"key\": \"月亮\",\n                \"file\": \"Moon\"\n            },\n            {\n                \"key\": \"月亮\",\n                \"file\": \"Moon\"\n            },\n            {\n                \"key\": \"机智\",\n                \"file\": \"Smart\"\n            },\n            {\n                \"key\": \"枯萎\",\n                \"file\": \"Wilt\"\n            },\n            {\n                \"key\": \"機智\",\n                \"file\": \"Smart\"\n            },\n            {\n                \"key\": \"歐耶\",\n                \"file\": \"Yeah!\"\n            },\n            {\n                \"key\": \"汗\",\n                \"file\": \"Sweats\"\n            },\n            {\n                \"key\": \"流汗\",\n                \"file\": \"Sweat\"\n            },\n            {\n                \"key\": \"流汗\",\n                \"file\": \"Sweat\"\n            },\n            {\n                \"key\": \"流泪\",\n                \"file\": \"Sob\"\n            },\n            {\n                \"key\": \"流淚\",\n                \"file\": \"Sob\"\n            },\n            {\n                \"key\": \"激动\",\n                \"file\": \"Hooray\"\n            },\n            {\n                \"key\": \"激動\",\n                \"file\": \"Hooray\"\n            },\n            {\n                \"key\": \"炸弹\",\n                \"file\": \"Bomb\"\n            },\n            {\n                \"key\": \"炸彈\",\n                \"file\": \"Bomb\"\n            },\n            {\n                \"key\": \"烟花\",\n                \"file\": \"Fireworks\"\n            },\n            {\n                \"key\": \"無語\",\n                \"file\": \"Duh\"\n            },\n            {\n                \"key\": \"煙花\",\n                \"file\": \"Fireworks\"\n            },\n            {\n                \"key\": \"爆竹\",\n                \"file\": \"Firecracker\"\n            },\n            {\n                \"key\": \"爆竹\",\n                \"file\": \"Firecracker\"\n            },\n            {\n                \"key\": \"爱你\",\n                \"file\": \"RockOn\"\n            },\n            {\n                \"key\": \"爱心\",\n                \"file\": \"Heart\"\n            },\n            {\n                \"key\": \"爱情\",\n                \"file\": \"InLove\"\n            },\n            {\n                \"key\": \"猪头\",\n                \"file\": \"Pig\"\n            },\n            {\n                \"key\": \"献吻\",\n                \"file\": \"Smooch\"\n            },\n            {\n                \"key\": \"獻吻\",\n                \"file\": \"Smooch\"\n            },\n            {\n                \"key\": \"玫瑰\",\n                \"file\": \"Rose\"\n            },\n            {\n                \"key\": \"玫瑰\",\n                \"file\": \"Rose\"\n            },\n            {\n                \"key\": \"瓢虫\",\n                \"file\": \"Ladybug\"\n            },\n            {\n                \"key\": \"生病\",\n                \"file\": \"Sick\"\n            },\n            {\n                \"key\": \"生病\",\n                \"file\": \"Sick\"\n            },\n            {\n                \"key\": \"甲蟲\",\n                \"file\": \"Ladybug\"\n            },\n            {\n                \"key\": \"疑問\",\n                \"file\": \"Shocked\"\n            },\n            {\n                \"key\": \"疑问\",\n                \"file\": \"Shocked\"\n            },\n            {\n                \"key\": \"疯了\",\n                \"file\": \"Tormented\"\n            },\n            {\n                \"key\": \"瘋了\",\n                \"file\": \"Tormented\"\n            },\n            {\n                \"key\": \"發\",\n                \"file\": \"Rich\"\n            },\n            {\n                \"key\": \"發\",\n                \"file\": \"Rich\"\n            },\n            {\n                \"key\": \"發呆\",\n                \"file\": \"Scowl\"\n            },\n            {\n                \"key\": \"發怒\",\n                \"file\": \"Angry\"\n            },\n            {\n                \"key\": \"發抖\",\n                \"file\": \"Tremble\"\n            },\n            {\n                \"key\": \"白眼\",\n                \"file\": \"Slight\"\n            },\n            {\n                \"key\": \"白眼\",\n                \"file\": \"Slight\"\n            },\n            {\n                \"key\": \"皱眉\",\n                \"file\": \"Concerned\"\n            },\n            {\n                \"key\": \"皺眉\",\n                \"file\": \"Concerned\"\n            },\n            {\n                \"key\": \"睡\",\n                \"file\": \"Sleep\"\n            },\n            {\n                \"key\": \"睡\",\n                \"file\": \"Sleep\"\n            },\n            {\n                \"key\": \"破涕为笑\",\n                \"file\": \"Lol\"\n            },\n            {\n                \"key\": \"破涕為笑\",\n                \"file\": \"Lol\"\n            },\n            {\n                \"key\": \"磕头\",\n                \"file\": \"Kotow\"\n            },\n            {\n                \"key\": \"磕頭\",\n                \"file\": \"Kotow\"\n            },\n            {\n                \"key\": \"礼物\",\n                \"file\": \"Gift\"\n            },\n            {\n                \"key\": \"社会社会\",\n                \"file\": \"Respect\"\n            },\n            {\n                \"key\": \"福\",\n                \"file\": \"Blessing\"\n            },\n            {\n                \"key\": \"福\",\n                \"file\": \"Blessing\"\n            },\n            {\n                \"key\": \"禮物\",\n                \"file\": \"Gift\"\n            },\n            {\n                \"key\": \"笑脸\",\n                \"file\": \"Happy\"\n            },\n            {\n                \"key\": \"笑臉\",\n                \"file\": \"Happy\"\n            },\n            {\n                \"key\": \"篮球\",\n                \"file\": \"Basketball\"\n            },\n            {\n                \"key\": \"籃球\",\n                \"file\": \"Basketball\"\n            },\n            {\n                \"key\": \"糗大了\",\n                \"file\": \"Shame\"\n            },\n            {\n                \"key\": \"累\",\n                \"file\": \"Drowsy\"\n            },\n            {\n                \"key\": \"红包\",\n                \"file\": \"Packet\"\n            },\n            {\n                \"key\": \"羞辱\",\n                \"file\": \"Shame\"\n            },\n            {\n                \"key\": \"翻白眼\",\n                \"file\": \"Boring\"\n            },\n            {\n                \"key\": \"耶\",\n                \"file\": \"Yeah!\"\n            },\n            {\n                \"key\": \"胜利\",\n                \"file\": \"Peace\"\n            },\n            {\n                \"key\": \"脸红\",\n                \"file\": \"Flushed\"\n            },\n            {\n                \"key\": \"臉紅\",\n                \"file\": \"Flushed\"\n            },\n            {\n                \"key\": \"色\",\n                \"file\": \"Drool\"\n            },\n            {\n                \"key\": \"色\",\n                \"file\": \"Drool\"\n            },\n            {\n                \"key\": \"苦涩\",\n                \"file\": \"Hurt\"\n            },\n            {\n                \"key\": \"茶\",\n                \"file\": \"Tea\"\n            },\n            {\n                \"key\": \"茶\",\n                \"file\": \"Tea\"\n            },\n            {\n                \"key\": \"菜刀\",\n                \"file\": \"Cleaver\"\n            },\n            {\n                \"key\": \"菜刀\",\n                \"file\": \"Cleaver\"\n            },\n            {\n                \"key\": \"蛋糕\",\n                \"file\": \"Cake\"\n            },\n            {\n                \"key\": \"蛋糕\",\n                \"file\": \"Cake\"\n            },\n            {\n                \"key\": \"蜡烛\",\n                \"file\": \"Candle\"\n            },\n            {\n                \"key\": \"蠟燭\",\n                \"file\": \"Candle\"\n            },\n            {\n                \"key\": \"衰\",\n                \"file\": \"Toasted\"\n            },\n            {\n                \"key\": \"衰\",\n                \"file\": \"Toasted\"\n            },\n            {\n                \"key\": \"裂开\",\n                \"file\": \"Broken\"\n            },\n            {\n                \"key\": \"西瓜\",\n                \"file\": \"Watermelon\"\n            },\n            {\n                \"key\": \"西瓜\",\n                \"file\": \"Watermelon\"\n            },\n            {\n                \"key\": \"親親\",\n                \"file\": \"Kiss\"\n            },\n            {\n                \"key\": \"調皮\",\n                \"file\": \"Tongue\"\n            },\n            {\n                \"key\": \"讓我看看\",\n                \"file\": \"LetMeSee\"\n            },\n            {\n                \"key\": \"让我看看\",\n                \"file\": \"LetMeSee\"\n            },\n            {\n                \"key\": \"调皮\",\n                \"file\": \"Tongue\"\n            },\n            {\n                \"key\": \"豬頭\",\n                \"file\": \"Pig\"\n            },\n            {\n                \"key\": \"足球\",\n                \"file\": \"Soccer\"\n            },\n            {\n                \"key\": \"足球\",\n                \"file\": \"Soccer\"\n            },\n            {\n                \"key\": \"跳繩\",\n                \"file\": \"JumpRope\"\n            },\n            {\n                \"key\": \"跳绳\",\n                \"file\": \"JumpRope\"\n            },\n            {\n                \"key\": \"跳跳\",\n                \"file\": \"Waddle\"\n            },\n            {\n                \"key\": \"跳跳\",\n                \"file\": \"Waddle\"\n            },\n            {\n                \"key\": \"轉圈\",\n                \"file\": \"Twirl\"\n            },\n            {\n                \"key\": \"转圈\",\n                \"file\": \"Twirl\"\n            },\n            {\n                \"key\": \"鄙視\",\n                \"file\": \"Pooh-pooh\"\n            },\n            {\n                \"key\": \"鄙视\",\n                \"file\": \"Pooh-pooh\"\n            },\n            {\n                \"key\": \"酷\",\n                \"file\": \"Ruthless\"\n            },\n            {\n                \"key\": \"酷\",\n                \"file\": \"Ruthless\"\n            },\n            {\n                \"key\": \"閃電\",\n                \"file\": \"Lightning\"\n            },\n            {\n                \"key\": \"閉嘴\",\n                \"file\": \"Silent\"\n            },\n            {\n                \"key\": \"闪电\",\n                \"file\": \"Lightning\"\n            },\n            {\n                \"key\": \"闭嘴\",\n                \"file\": \"Silent\"\n            },\n            {\n                \"key\": \"阴险\",\n                \"file\": \"Sly\"\n            },\n            {\n                \"key\": \"陰險\",\n                \"file\": \"Sly\"\n            },\n            {\n                \"key\": \"难过\",\n                \"file\": \"Frown\"\n            },\n            {\n                \"key\": \"難受\",\n                \"file\": \"Hurt\"\n            },\n            {\n                \"key\": \"難過\",\n                \"file\": \"Frown\"\n            },\n            {\n                \"key\": \"飛吻\",\n                \"file\": \"Blowkiss\"\n            },\n            {\n                \"key\": \"飞吻\",\n                \"file\": \"Blowkiss\"\n            },\n            {\n                \"key\": \"飯\",\n                \"file\": \"Rice\"\n            },\n            {\n                \"key\": \"饑餓\",\n                \"file\": \"Hungry\"\n            },\n            {\n                \"key\": \"饥饿\",\n                \"file\": \"Hungry\"\n            },\n            {\n                \"key\": \"饭\",\n                \"file\": \"Rice\"\n            },\n            {\n                \"key\": \"驚恐\",\n                \"file\": \"Panic\"\n            },\n            {\n                \"key\": \"驚訝\",\n                \"file\": \"Surprise\"\n            },\n            {\n                \"key\": \"骷髅\",\n                \"file\": \"Skull\"\n            },\n            {\n                \"key\": \"骷髏頭\",\n                \"file\": \"Skull\"\n            },\n            {\n                \"key\": \"鬼魂\",\n                \"file\": \"u1F47B\"\n            },\n            {\n                \"key\": \"鸡\",\n                \"file\": \"Chick\"\n            },\n            {\n                \"key\": \"鼓掌\",\n                \"file\": \"Clap\"\n            },\n            {\n                \"key\": \"鼓掌\",\n                \"file\": \"Clap\"\n            }\n        ]\n    },\n    {\n        \"preTag\": \"/\",\n        \"postTag\": \"\",\n        \"keys\": [\n            {\n                \"key\": \"NO\",\n                \"file\": \"Nuh-uh\"\n            },\n            {\n                \"key\": \"OK\",\n                \"file\": \"OK\"\n            },\n            {\n                \"key\": \"乒乓\",\n                \"file\": \"PingPong\"\n            },\n            {\n                \"key\": \"亲亲\",\n                \"file\": \"Kiss\"\n            },\n            {\n                \"key\": \"便便\",\n                \"file\": \"Poop\"\n            },\n            {\n                \"key\": \"偷笑\",\n                \"file\": \"Chuckle\"\n            },\n            {\n                \"key\": \"傲慢\",\n                \"file\": \"Smug\"\n            },\n            {\n                \"key\": \"再见\",\n                \"file\": \"Bye\"\n            },\n            {\n                \"key\": \"冷汗\",\n                \"file\": \"Blush\"\n            },\n            {\n                \"key\": \"凋谢\",\n                \"file\": \"Wilt\"\n            },\n            {\n                \"key\": \"刀\",\n                \"file\": \"Dagger\"\n            },\n            {\n                \"key\": \"勾引\",\n                \"file\": \"Beckon\"\n            },\n            {\n                \"key\": \"发呆\",\n                \"file\": \"Scowl\"\n            },\n            {\n                \"key\": \"发怒\",\n                \"file\": \"Angry\"\n            },\n            {\n                \"key\": \"发抖\",\n                \"file\": \"Tremble\"\n            },\n            {\n                \"key\": \"可怜\",\n                \"file\": \"Whimper\"\n            },\n            {\n                \"key\": \"可爱\",\n                \"file\": \"Joyful\"\n            },\n            {\n                \"key\": \"右哼哼\",\n                \"file\": \"Bah！R\"\n            },\n            {\n                \"key\": \"右太极\",\n                \"file\": \"TaiChi R\"\n            },\n            {\n                \"key\": \"吐\",\n                \"file\": \"Puke\"\n            },\n            {\n                \"key\": \"吓\",\n                \"file\": \"Wrath\"\n            },\n            {\n                \"key\": \"呲牙\",\n                \"file\": \"Grin\"\n            },\n            {\n                \"key\": \"咒骂\",\n                \"file\": \"Scold\"\n            },\n            {\n                \"key\": \"咖啡\",\n                \"file\": \"Coffee\"\n            },\n            {\n                \"key\": \"哈欠\",\n                \"file\": \"Yawn\"\n            },\n            {\n                \"key\": \"啤酒\",\n                \"file\": \"Beer\"\n            },\n            {\n                \"key\": \"嘘\",\n                \"file\": \"Shhh\"\n            },\n            {\n                \"key\": \"回头\",\n                \"file\": \"Dramatic\"\n            },\n            {\n                \"key\": \"困\",\n                \"file\": \"Drowsy\"\n            },\n            {\n                \"key\": \"坏笑\",\n                \"file\": \"Trick\"\n            },\n            {\n                \"key\": \"大兵\",\n                \"file\": \"Commando\"\n            },\n            {\n                \"key\": \"大哭\",\n                \"file\": \"Cry\"\n            },\n            {\n                \"key\": \"太阳\",\n                \"file\": \"Sun\"\n            },\n            {\n                \"key\": \"奋斗\",\n                \"file\": \"Determined\"\n            },\n            {\n                \"key\": \"委屈\",\n                \"file\": \"Shrunken\"\n            },\n            {\n                \"key\": \"害羞\",\n                \"file\": \"Shy\"\n            },\n            {\n                \"key\": \"尴尬\",\n                \"file\": \"Awkward\"\n            },\n            {\n                \"key\": \"左哼哼\",\n                \"file\": \"Bah！L\"\n            },\n            {\n                \"key\": \"左太极\",\n                \"file\": \"TaiChi L\"\n            },\n            {\n                \"key\": \"差劲\",\n                \"file\": \"Pinky\"\n            },\n            {\n                \"key\": \"弱\",\n                \"file\": \"ThumbsDown\"\n            },\n            {\n                \"key\": \"强\",\n                \"file\": \"ThumbsUp\"\n            },\n            {\n                \"key\": \"得意\",\n                \"file\": \"CoolGuy\"\n            },\n            {\n                \"key\": \"微笑\",\n                \"file\": \"Smile\"\n            },\n            {\n                \"key\": \"心碎\",\n                \"file\": \"BrokenHeart\"\n            },\n            {\n                \"key\": \"快哭了\",\n                \"file\": \"TearingUp\"\n            },\n            {\n                \"key\": \"怄火\",\n                \"file\": \"Aaagh!\"\n            },\n            {\n                \"key\": \"惊恐\",\n                \"file\": \"Panic\"\n            },\n            {\n                \"key\": \"惊讶\",\n                \"file\": \"Surprise\"\n            },\n            {\n                \"key\": \"憨笑\",\n                \"file\": \"Laugh\"\n            },\n            {\n                \"key\": \"抓狂\",\n                \"file\": \"Scream\"\n            },\n            {\n                \"key\": \"折磨\",\n                \"file\": \"Tormented\"\n            },\n            {\n                \"key\": \"抠鼻\",\n                \"file\": \"NosePick\"\n            },\n            {\n                \"key\": \"抱拳\",\n                \"file\": \"Fight\"\n            },\n            {\n                \"key\": \"拥抱\",\n                \"file\": \"Hug\"\n            },\n            {\n                \"key\": \"拳头\",\n                \"file\": \"Fist\"\n            },\n            {\n                \"key\": \"挥手\",\n                \"file\": \"Surrender\"\n            },\n            {\n                \"key\": \"握手\",\n                \"file\": \"Shake\"\n            },\n            {\n                \"key\": \"撇嘴\",\n                \"file\": \"Grimace\"\n            },\n            {\n                \"key\": \"擦汗\",\n                \"file\": \"Speechless\"\n            },\n            {\n                \"key\": \"敲打\",\n                \"file\": \"Hammer\"\n            },\n            {\n                \"key\": \"晕\",\n                \"file\": \"Dizzy\"\n            },\n            {\n                \"key\": \"月亮\",\n                \"file\": \"Moon\"\n            },\n            {\n                \"key\": \"流汗\",\n                \"file\": \"Sweat\"\n            },\n            {\n                \"key\": \"流泪\",\n                \"file\": \"Sob\"\n            },\n            {\n                \"key\": \"激动\",\n                \"file\": \"Hooray\"\n            },\n            {\n                \"key\": \"炸弹\",\n                \"file\": \"Bomb\"\n            },\n            {\n                \"key\": \"爱你\",\n                \"file\": \"RockOn\"\n            },\n            {\n                \"key\": \"爱心\",\n                \"file\": \"Heart\"\n            },\n            {\n                \"key\": \"爱情\",\n                \"file\": \"InLove\"\n            },\n            {\n                \"key\": \"猪头\",\n                \"file\": \"Pig\"\n            },\n            {\n                \"key\": \"献吻\",\n                \"file\": \"Smooch\"\n            },\n            {\n                \"key\": \"玫瑰\",\n                \"file\": \"Rose\"\n            },\n            {\n                \"key\": \"瓢虫\",\n                \"file\": \"Ladybug\"\n            },\n            {\n                \"key\": \"疑问\",\n                \"file\": \"Shocked\"\n            },\n            {\n                \"key\": \"白眼\",\n                \"file\": \"Slight\"\n            },\n            {\n                \"key\": \"睡\",\n                \"file\": \"Sleep\"\n            },\n            {\n                \"key\": \"磕头\",\n                \"file\": \"Kotow\"\n            },\n            {\n                \"key\": \"示爱\",\n                \"file\": \"Lips\"\n            },\n            {\n                \"key\": \"礼物\",\n                \"file\": \"Gift\"\n            },\n            {\n                \"key\": \"篮球\",\n                \"file\": \"Basketball\"\n            },\n            {\n                \"key\": \"糗大了\",\n                \"file\": \"Shame\"\n            },\n            {\n                \"key\": \"胜利\",\n                \"file\": \"Peace\"\n            },\n            {\n                \"key\": \"色\",\n                \"file\": \"Drool\"\n            },\n            {\n                \"key\": \"菜刀\",\n                \"file\": \"Cleaver\"\n            },\n            {\n                \"key\": \"蛋糕\",\n                \"file\": \"Cake\"\n            },\n            {\n                \"key\": \"街舞\",\n                \"file\": \"Meditate\"\n            },\n            {\n                \"key\": \"衰\",\n                \"file\": \"Toasted\"\n            },\n            {\n                \"key\": \"西瓜\",\n                \"file\": \"Watermelon\"\n            },\n            {\n                \"key\": \"调皮\",\n                \"file\": \"Tongue\"\n            },\n            {\n                \"key\": \"足球\",\n                \"file\": \"Soccer\"\n            },\n            {\n                \"key\": \"跳绳\",\n                \"file\": \"JumpRope\"\n            },\n            {\n                \"key\": \"跳跳\",\n                \"file\": \"Waddle\"\n            },\n            {\n                \"key\": \"转圈\",\n                \"file\": \"Twirl\"\n            },\n            {\n                \"key\": \"鄙视\",\n                \"file\": \"Pooh-pooh\"\n            },\n            {\n                \"key\": \"酷\",\n                \"file\": \"Ruthless\"\n            },\n            {\n                \"key\": \"闪电\",\n                \"file\": \"Lightning\"\n            },\n            {\n                \"key\": \"闭嘴\",\n                \"file\": \"Silent\"\n            },\n            {\n                \"key\": \"阴险\",\n                \"file\": \"Sly\"\n            },\n            {\n                \"key\": \"难过\",\n                \"file\": \"Frown\"\n            },\n            {\n                \"key\": \"飞吻\",\n                \"file\": \"Blowkiss\"\n            },\n            {\n                \"key\": \"饥饿\",\n                \"file\": \"Hungry\"\n            },\n            {\n                \"key\": \"饭\",\n                \"file\": \"Rice\"\n            },\n            {\n                \"key\": \"骷髅\",\n                \"file\": \"Skull\"\n            },\n            {\n                \"key\": \"鼓掌\",\n                \"file\": \"Clap\"\n            }\n        ]\n    },\n    {\n        \"preTag\": \"/:\",\n        \"postTag\": \"\",\n        \"keys\": [\n            {\n                \"key\": \"!!!\",\n                \"file\": \"Skull\"\n            },\n            {\n                \"key\": \"#-0\",\n                \"file\": \"Hooray\"\n            },\n            {\n                \"key\": \"&-(\",\n                \"file\": \"Shame\"\n            },\n            {\n                \"key\": \"&>\",\n                \"file\": \"TaiChi R\"\n            },\n            {\n                \"key\": \",@!\",\n                \"file\": \"Toasted\"\n            },\n            {\n                \"key\": \",@-D\",\n                \"file\": \"Joyful\"\n            },\n            {\n                \"key\": \",@@\",\n                \"file\": \"Dizzy\"\n            },\n            {\n                \"key\": \",@P\",\n                \"file\": \"Chuckle\"\n            },\n            {\n                \"key\": \",@f\",\n                \"file\": \"Determined\"\n            },\n            {\n                \"key\": \",@o\",\n                \"file\": \"Smug\"\n            },\n            {\n                \"key\": \",@x\",\n                \"file\": \"Shhh\"\n            },\n            {\n                \"key\": \"--b\",\n                \"file\": \"Blush\"\n            },\n            {\n                \"key\": \"8*\",\n                \"file\": \"Whimper\"\n            },\n            {\n                \"key\": \"8-)\",\n                \"file\": \"CoolGuy\"\n            },\n            {\n                \"key\": \":!\",\n                \"file\": \"Panic\"\n            },\n            {\n                \"key\": \":\\\"(\",\n                \"file\": \"Cry\"\n            },\n            {\n                \"key\": \":\\\"|\",\n                \"file\": \"TearingUp\"\n            },\n            {\n                \"key\": \":$\",\n                \"file\": \"Shy\"\n            },\n            {\n                \"key\": \":(\",\n                \"file\": \"Frown\"\n            },\n            {\n                \"key\": \":)\",\n                \"file\": \"Smile\"\n            },\n            {\n                \"key\": \":*\",\n                \"file\": \"Kiss\"\n            },\n            {\n                \"key\": \":+\",\n                \"file\": \"Ruthless\"\n            },\n            {\n                \"key\": \":,@\",\n                \"file\": \"Commando\"\n            },\n            {\n                \"key\": \":-O\",\n                \"file\": \"Yawn\"\n            },\n            {\n                \"key\": \":-S\",\n                \"file\": \"Scold\"\n            },\n            {\n                \"key\": \":-|\",\n                \"file\": \"Awkward\"\n            },\n            {\n                \"key\": \":8\",\n                \"file\": \"Tormented\"\n            },\n            {\n                \"key\": \":<\",\n                \"file\": \"Sob\"\n            },\n            {\n                \"key\": \":>\",\n                \"file\": \"Laugh\"\n            },\n            {\n                \"key\": \":@\",\n                \"file\": \"Angry\"\n            },\n            {\n                \"key\": \":B\",\n                \"file\": \"Drool\"\n            },\n            {\n                \"key\": \":D\",\n                \"file\": \"Grin\"\n            },\n            {\n                \"key\": \":L\",\n                \"file\": \"Sweat\"\n            },\n            {\n                \"key\": \":O\",\n                \"file\": \"Surprise\"\n            },\n            {\n                \"key\": \":P\",\n                \"file\": \"Tongue\"\n            },\n            {\n                \"key\": \":Q\",\n                \"file\": \"Scream\"\n            },\n            {\n                \"key\": \":T\",\n                \"file\": \"Puke\"\n            },\n            {\n                \"key\": \":X\",\n                \"file\": \"Silent\"\n            },\n            {\n                \"key\": \":Z\",\n                \"file\": \"Sleep\"\n            },\n            {\n                \"key\": \":d\",\n                \"file\": \"Slight\"\n            },\n            {\n                \"key\": \":g\",\n                \"file\": \"Hungry\"\n            },\n            {\n                \"key\": \":|\",\n                \"file\": \"Scowl\"\n            },\n            {\n                \"key\": \":~\",\n                \"file\": \"Grimace\"\n            },\n            {\n                \"key\": \"<&\",\n                \"file\": \"TaiChi L\"\n            },\n            {\n                \"key\": \"<@\",\n                \"file\": \"Bah！L\"\n            },\n            {\n                \"key\": \"<L>\",\n                \"file\": \"Blowkiss\"\n            },\n            {\n                \"key\": \"<O>\",\n                \"file\": \"Aaagh!\"\n            },\n            {\n                \"key\": \"<W>\",\n                \"file\": \"Watermelon\"\n            },\n            {\n                \"key\": \">-|\",\n                \"file\": \"Pooh-pooh\"\n            },\n            {\n                \"key\": \"?\",\n                \"file\": \"Shocked\"\n            },\n            {\n                \"key\": \"@)\",\n                \"file\": \"Fight\"\n            },\n            {\n                \"key\": \"@>\",\n                \"file\": \"Bah！R\"\n            },\n            {\n                \"key\": \"@@\",\n                \"file\": \"Fist\"\n            },\n            {\n                \"key\": \"@x\",\n                \"file\": \"Wrath\"\n            },\n            {\n                \"key\": \"B-)\",\n                \"file\": \"Trick\"\n            },\n            {\n                \"key\": \"P-(\",\n                \"file\": \"Shrunken\"\n            },\n            {\n                \"key\": \"X-)\",\n                \"file\": \"Sly\"\n            },\n            {\n                \"key\": \"bad\",\n                \"file\": \"Pinky\"\n            },\n            {\n                \"key\": \"basketb\",\n                \"file\": \"Basketball\"\n            },\n            {\n                \"key\": \"beer\",\n                \"file\": \"Beer\"\n            },\n            {\n                \"key\": \"bome\",\n                \"file\": \"Bomb\"\n            },\n            {\n                \"key\": \"break\",\n                \"file\": \"BrokenHeart\"\n            },\n            {\n                \"key\": \"bye\",\n                \"file\": \"Bye\"\n            },\n            {\n                \"key\": \"cake\",\n                \"file\": \"Cake\"\n            },\n            {\n                \"key\": \"circle\",\n                \"file\": \"Twirl\"\n            },\n            {\n                \"key\": \"coffee\",\n                \"file\": \"Coffee\"\n            },\n            {\n                \"key\": \"dig\",\n                \"file\": \"NosePick\"\n            },\n            {\n                \"key\": \"eat\",\n                \"file\": \"Rice\"\n            },\n            {\n                \"key\": \"fade\",\n                \"file\": \"Wilt\"\n            },\n            {\n                \"key\": \"footb\",\n                \"file\": \"Soccer\"\n            },\n            {\n                \"key\": \"gift\",\n                \"file\": \"Gift\"\n            },\n            {\n                \"key\": \"handclap\",\n                \"file\": \"Clap\"\n            },\n            {\n                \"key\": \"heart\",\n                \"file\": \"Heart\"\n            },\n            {\n                \"key\": \"hiphot\",\n                \"file\": \"Meditate\"\n            },\n            {\n                \"key\": \"hug\",\n                \"file\": \"Hug\"\n            },\n            {\n                \"key\": \"jj\",\n                \"file\": \"Beckon\"\n            },\n            {\n                \"key\": \"jump\",\n                \"file\": \"Waddle\"\n            },\n            {\n                \"key\": \"kiss\",\n                \"file\": \"Smooch\"\n            },\n            {\n                \"key\": \"kn\",\n                \"file\": \"Dagger\"\n            },\n            {\n                \"key\": \"kotow\",\n                \"file\": \"Kotow\"\n            },\n            {\n                \"key\": \"ladybug\",\n                \"file\": \"Ladybug\"\n            },\n            {\n                \"key\": \"li\",\n                \"file\": \"Lightning\"\n            },\n            {\n                \"key\": \"love\",\n                \"file\": \"InLove\"\n            },\n            {\n                \"key\": \"lvu\",\n                \"file\": \"RockOn\"\n            },\n            {\n                \"key\": \"moon\",\n                \"file\": \"Moon\"\n            },\n            {\n                \"key\": \"no\",\n                \"file\": \"Nuh-uh\"\n            },\n            {\n                \"key\": \"oY\",\n                \"file\": \"Surrender\"\n            },\n            {\n                \"key\": \"ok\",\n                \"file\": \"OK\"\n            },\n            {\n                \"key\": \"oo\",\n                \"file\": \"PingPong\"\n            },\n            {\n                \"key\": \"pd\",\n                \"file\": \"Cleaver\"\n            },\n            {\n                \"key\": \"pig\",\n                \"file\": \"Pig\"\n            },\n            {\n                \"key\": \"rose\",\n                \"file\": \"Rose\"\n            },\n            {\n                \"key\": \"shake\",\n                \"file\": \"Tremble\"\n            },\n            {\n                \"key\": \"share\",\n                \"file\": \"Shake\"\n            },\n            {\n                \"key\": \"shit\",\n                \"file\": \"Poop\"\n            },\n            {\n                \"key\": \"showlove\",\n                \"file\": \"Lips\"\n            },\n            {\n                \"key\": \"skip\",\n                \"file\": \"JumpRope\"\n            },\n            {\n                \"key\": \"strong\",\n                \"file\": \"ThumbsUp\"\n            },\n            {\n                \"key\": \"sun\",\n                \"file\": \"Sun\"\n            },\n            {\n                \"key\": \"turn\",\n                \"file\": \"Dramatic\"\n            },\n            {\n                \"key\": \"v\",\n                \"file\": \"Peace\"\n            },\n            {\n                \"key\": \"weak\",\n                \"file\": \"ThumbsDown\"\n            },\n            {\n                \"key\": \"wipe\",\n                \"file\": \"Speechless\"\n            },\n            {\n                \"key\": \"xx\",\n                \"file\": \"Hammer\"\n            },\n            {\n                \"key\": \"|-)\",\n                \"file\": \"Drowsy\"\n            }\n        ]\n    }\n]"
  },
  {
    "path": "WechatExporter/res/en.txt",
    "content": "[\n\t{\n\t\t\"key\": \"Transfer_Subtype_1\",\n\t\t\"value\": \"Transfer\"\n\t},\n\t\n\t{\n\t\t\"key\": \"Transfer_Subtype_3\",\n\t\t\"value\": \"Accepted \"\n\t},\n\t{\n\t\t\"key\": \"Transfer_Subtype_5\",\n\t\t\"value\": \"Accepted \"\n\t},\n\t{\n\t\t\"key\": \"Transfer_Subtype_4\",\n\t\t\"value\": \"Rejected\"\n\t},\n\n\t{\n\t\t\"key\": \"Transfer_Subtype_8\",\n\t\t\"value\": \"Accepted\"\n\t},\n\t{\n\t\t\"key\": \"Transfer_Subtype_9\",\n\t\t\"value\": \"Rejected\"\n\t},\n\t{\n\t\t\"key\": \"Transfer_Subtype_10\",\n\t\t\"value\": \"Expired\"\n\t},\n\n\t{\n\t\t\"key\": \"Transfer_Subtype_1\",\n\t\t\"value\": \"Pay to %s\"\n\t},\n\t\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_3\",\n\t\t\"value\": \"From %s - Accepted \"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_5\",\n\t\t\"value\": \"From %s - Accepted \"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_4\",\n\t\t\"value\": \"From %s - Rejected\"\n\t},\n\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_8\",\n\t\t\"value\": \"Pay to %s - Accepted\"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_9\",\n\t\t\"value\": \"Pay to %s - Rejected\"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_10\",\n\t\t\"value\": \"Expired\"\n\t},\n\t{\n\t\t\"key\": \"\",\n\t\t\"value\": \"\"\n\t},\n\t{\n\t\t\"key\": \"\",\n\t\t\"value\": \"\"\n\t}\n]\n"
  },
  {
    "path": "WechatExporter/res/templates/audio.html",
    "content": "\t\t\t\t<div class=\"msg media %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<div class=\"voicebox\" style=\"width:%%AUDIOLENGTH%%px\" audio=\"%%AUDIOPATH%%\" onclick=\"javascript:playAudio(this, '%%MSGID%%');\">\n\t\t\t\t\t\t\t<div class=\"voiceimg\" id=\"voiceimg-%%MSGID%%\"></div>\n\t\t\t\t\t\t\t<div class=\"voicetime\">%%AUDIOTIME%%</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/card.html",
    "content": "\t\t\t\t<div class=\"msg media %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<div class=\"card-name\">\n\t\t\t\t\t\t\t<img src=\"%%CARDIMGPATH%%\" class=\"card-avatar\" /><span class=\"dont-break-out msg-text\">%%CARDNAME%%</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"card-type\">\n\t\t\t\t\t\t\t%%CARDTYPE%%\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/channels.html",
    "content": "\t\t\t\t<div class=\"msg chat %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<div class=\"triangle\"></div>\n\t\t\t\t\t\t<div><a href=\"%%CHANNELURL%%\"><img src=\"%%CHANNELTHUMBPATH%%\" class=\"channel-poster\" /></a></div>\n\t\t\t\t\t\t<div class=\"card-name channel-card\">\n\t\t\t\t\t\t\t<img src=\"%%CARDIMGPATH%%\" class=\"card-avatar\" /><span class=\"card-name msg-text\">%%CARDNAME%%</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<span class=\"channel-title msg-text\" title=\"%%MESSAGE%%\"><pre>%%MESSAGE%%</pre></span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"card-type\">\n\t\t\t\t\t\t\t%%CHANNELS%%\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/emoji.html",
    "content": "\t\t\t\t<div class=\"msg media %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<img src=\"%%EMOJIPATH%%\" alt=\"%%EMOJI_TITLE%%\" rawsrc=\"%%RAWEMOJIPATH%%\" style=\"max-width:100px;max-height:60px\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/filter.html",
    "content": "\t\t\t\t\t<input placeholder=\"%%ML:Press [Enter] key after inputting keywords%%\" id=\"filter-keyword\" class=\"filter-btn\" onkeypress=\"javascript:searchElements(event);\" />\n\t\t\t\t\t&nbsp; \n\t\t\t\t\t<a title=\"%%ML:Show Photos Only%%\" id=\"filter-image\" class=\"filter-btn\" href=\"javascript:showImageMsgs(event);\">%%ML:Photos%%</a>\n\t\t\t\t\t<a title=\"%%ML:Show Videos Only%%\" id=\"filter-video\" class=\"filter-btn\" href=\"javascript:showVideoMsgs();\">%%ML:Videos%%</a>\n\t\t\t\t\t<a title=\"%%ML:Show All Messages%%\" id=\"filter-none\" class=\"filter-btn\" href=\"javascript:showAllMsgs();\">%%ML:All%%</a>\n"
  },
  {
    "path": "WechatExporter/res/templates/frame.html",
    "content": "<!doctype html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover\">\n\t\t<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n\t\t<meta content=\"yes\" name=\"apple-touch-fullscreen\">\n\t\t<style type=text/css>\n\n\t\t\t/* data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== */\n\n\t\tbody\n\t\t{\n\t\t\tbackground-color: #c4c4c4;\n\t\t\tfont-family: \"Microsoft YaHei\", \"-apple-system\", \"Helvetica Neue\", \"Roboto\", \"Segoe UI\", sans-serif;\n\t\t\t/* -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,   Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\" */\n  \t\tfont-size: 10.5pt;\n      margin: 0px;\n\t\t}\n\t\t.main\n\t\t{\n\t\t\tmax-width:720px;\n\t\t\tmargin: 0 auto;\n\t\t\theight: 100%;\n\t\t\tpadding-top: 54px; /* header */\n\t\t\tpadding-bottom: 32px; /* footer */\n\t\t\tbackground-color: #ebebeb;\n\t\t}\n\t\t.header\n\t\t{\n\t\t\tposition: fixed;\n\t\t\ttop:0;\n\t\t\tmax-width:720px;\n\t\t\twidth: 100%;\n\t\t\theight: 48px;\n\t\t\tbackground-color: #f6f6f6;\n\t\t\tz-index: 2147483600;\n\t\t}\n\t\t.footer\n\t\t{\n\t\t\tposition: fixed;\n\t\t\tbottom: 0;\n\t\t\tmax-width:720px;\n\t\t\twidth: 100%;\n\t\t\theight: 32px;\n\t\t\tline-height: 32px;\n\t\t\tbackground-color: #f6f6f6;\n\t\t\tz-index: 2147483600;\n\t\t}\n\t\t.pager\n\t\t{\n\t\t\tmargin-left: 0.5em;\n\t\t\tmargin-right: 0.5em;\n\t\t}\n\t\t.sname\n\t\t{\n\t\t\tpadding-left: 12px;\n\t\t\tvertical-align: middle;\n\t\t\theight: 48px;\n\t\t\tline-height: 48px;\n\t\t\tfont-weight: bold;\n\t\t}\n\t\t.msgfilter\n\t\t{\n\t\t\tfloat:right;\n\t\t\theight: 48px;\n\t\t\tline-height: 48px;\n\t\t\tpadding-right: 8px;\n\t\t}\n\t\t.filter-btn\n\t\t{\n\t\t\tvertical-align: middle;\n\t\t}\n\t\tspan.filter-btn\n\t\t{\n\t\t\tcursor: hand;\n\t\t}\n\t\tspan.filter-btn:hover\n\t\t{\n\t\t\tcolor: blue;\n\t\t}\n\t\t\n\t\tdiv.msg\n\t\t{\n\t\t\tpage-break-inside: avoid;\n\t\t\tbreak-inside: avoid-page;\n\t\t}\n\n\t\t.msgs\n\t\t{\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t}\n\t\tspan.msg-text pre\n\t\t{\n\t\t\twhite-space: pre-wrap;       /* Since CSS 2.1 */\n\t\t\twhite-space: -moz-pre-wrap;  /* Mozilla, since 1999 */\n\t\t\twhite-space: -pre-wrap;      /* Opera 4-6 */\n\t\t\twhite-space: -o-pre-wrap;    /* Opera 7 */\n\t\t\tword-wrap: break-word;       /* Internet Explorer 5.5+ */\n\t\t\tmargin: 0px 0px;\n\t\t}\n\t  \t.chat.left, .media.left\n\t  \t{\n\t\t\tclear:both;\n\t\t\toverflow: auto;\n\t\t\tmargin:0 auto;\n\t\t\tpadding: 8px 8px 0px 8px;\n\t  \t}\n\t\t.chat.left div.avatar-box,\n\t\t.media.left div.avatar-box\n\t\t{\n\t\t\tfloat: left;\n\t\t}\n\t\t.chat.left div.avatar-box\n\t\t{\n\t\t\t/*padding-top: 6px;*/\n\t\t}\n\t\t.chat.left div.nt-box,\n\t\t.media.left div.nt-box\n\t\t{\n\t\t\tmargin: 0 50px 2px 50px;\n\t\t\tpadding: 0px;\n\t\t\tcolor: #848484;\n\t\t\tfont-size: 80%;\n\t\t\ttext-align: left;\n\t\t}\n\t\t.chat.left div.content-box\n\t\t{\n\t\t\tposition: relative;\n\t\t\tbackground-color: white;\n\t\t\t/*float: left;*/\n\t\t\tmargin: 0 50px 8px 50px;\n\t\t\tpadding: 8px 8px 8px 8px;\n\t\t\tborder-radius:4px;\n\t\t}\n\t\t.media.left div.content-box\n\t\t{\n\t\t\t/*float: left;*/\n\t\t\tmargin: 0 50px 8px 50px;\n\t\t\tpadding: 8px 8px 8px 8px;\n    \t}\n\t    div.content-box\n\t    {\n\t      width:fit-content;\n\t      width:-webkit-fit-content;\n\t      width:-moz-fit-content;\n\t    }\n\t\t.chat.left.channels .content-box, .chat.right.channels .content-box\n\t\t{\n\t\t\twidth:240px;\n\t\t}\n\t\t.refermsg\n\t\t{\n\t\t\tfont-size: 90%;\n\t\t}\n\t\t\n\t\t.chat.right, .media.right\n\t\t{\n\t\t\tclear:both;\n\t\t\toverflow: auto;\n\t\t\tmax-width: 670px;\n\t\t\tmargin: 0 auto;\n\t\t\tbackground-color: #ebebeb;\n\t\t\tpadding: 8px 8px 0px 58px;\n\t\t}\n\t\t.chat.right div.avatar-box,\n\t\t.media.right div.avatar-box\n\t\t{\n\t\t\tfloat: right;\n\t\t}\n\t\t.chat.right div.nt-box,\n\t\t.media.right div.nt-box\n\t\t{\n\t\t\tmargin: 0px 50px 2px 50px;\n\t\t\tpadding: 0px;\n\t\t\tcolor: #848484;\n\t\t\tfont-size: 80%;\n\t\t\ttext-align: right;\n\t\t}\n\t\t.chat.right div.content-box\n\t\t{\n      \t\tposition: relative;\n\t\t\tbackground-color: #b2e281;\n\t\t\tmargin: 0px 50px 8px auto;\n\t\t\tpadding: 8px 8px 8px 8px;\n\t\t\tborder-radius: 4px;\n\t\t}\n\t\t.media.right div.content-box\n\t\t{\n\t\t\tfloat: right;\n\t\t\tmargin: 0px 0px 8px 50px;\n\t\t\tpadding: 8px 8px 8px 8px;\n\t\t}\n\t\t\n\t\t.name.right\n\t\t{\n\t\t\tdisplay: none;\n\t\t}\n\t\timg.wxemoji\n\t\t{\n\t\t\tborder: 0;\n\t\t\twidth: 20px;\n\t\t    height: 20px;\n\t\t    display: inline;\n\t\t    display: -moz-inline-stack;\n\t\t    display: inline-block;\n\t\t    vertical-align: top;\n\t\t    /*zoom: 1;*/\n\t\t}\n\t\t\n\t\timg.avatar\n\t\t{\n\t\t\twidth: 40px;\n\t\t\theight: 40px;\n\t\t\tborder-radius: 4px;\n\t\t}\n\t\t\n\t\t.triangle\n\t\t{\n\t\t\theight: 0px;\n\t\t\twidth: 0px;\n\t\t\tborder-width: 6px;\n\t\t\tborder-style: solid;\n\t\t\tposition: relative;\n\t\t}\n\t\t.chat.left .triangle\n\t\t{\n\t\t\tposition: absolute;\n\t\t\tborder-color: transparent white transparent transparent;\n\t\t\tleft: -12px;\n\t\t\ttop: 6px;\n\t\t}\n\t\t.chat.right .triangle\n\t\t{\n\t\t\tposition: absolute;\n\t\t\tborder-color: transparent transparent transparent #b2e281;\n\t\t\tright: -12px;\n\t\t\ttop: 6px;\n\t\t}\n\t\t\n\t\t.chat-notice\n\t\t{\n\t\t\tclear: both;\n\t\t\tfont-size: 80%;\n\t\t\ttext-align: center;\n\t\t\tmargin-top: 15px;\n\t\t\tmargin-bottom: 15px;\n\t\t\tmargin:0 auto;\n\t\t\tpadding: 0px 8px;\n    \t}\n    \t.chat-notice .content-box\n\t\t{\n\t\t\twidth: 80%;\n\t\t\tmargin:0 auto;\n\t\t}\n\t\t.chat-notice.fmsgtag .content-box\n\t\t{\n\t\t\tbackground-color: #008080;\n\t\t\tborder-radius: 4px;\n\t\t\tfont-size: 100%;\n\t\t\tpadding: 5px 10px;\n\t\t\tcolor: white;\n\t\t}\n\t\taudio\n\t\t{\n\t\t\tmax-width: 220px;\n\t\t}\n\t\tvideo\n\t\t{\n\t\t\tmax-width: 240px;\n\t\t\tborder-radius: 4px;\n\t\t}\n\t\t.image\n\t\t{\n\t\t\twidth:90px;\n\t\t\tborder-radius:4px;\n\t\t\tmax-width: 360px;\n\t\t\tmax-height: 240px;\n\t\t\tmin-width: 120px;\n\t\t\tmin-height: 24px;\n\t\t}\n\t\timg\n\t\t{\n\t\t\timage-orientation: from-image;\n\t\t}\n\t\timg[src=\"\"]\n\t\t{\n\t\t\tdisplay: none;\n\t\t}\n\t\timg.app-icon\n\t\t{\n\t\t\twidth: 24px;\n\t\t\theight: 24px;\n\t\t\tvertical-align:middle;\n\t\t\tborder-radius: 2px;\n\t\t}\n\t\timg.transfer-icon\n\t\t{\n\t\t\twidth: 16px;\n\t\t\theight: 16px;\n\t\t\tvertical-align:middle;\n\t\t\tborder-radius: 2px;\n\t\t}\n\t\timg.channel-poster\n\t\t{\n\t\t\tobject-fit: cover;\n\t\t\twidth: 240px;\n\t\t\theight: 240px;\n\t\t}\n\t\t.raw-img .image:hover, .channel-poster:hover\n\t\t{\n\t\t\tcursor: -webkit-zoom-in;\n   \t\t\tcursor: zoom-in;\n\t\t  \t/* transform: scale(5); */\n\t\t}\n\t\timg.card-avatar\n\t\t{\n\t\t\twidth: 42px;\n\t\t\theight: 42px;\n\t\t\tvertical-align:middle;\n\t\t}\n\t\tdiv.card-name\n\t\t{\n\t\t\tdisplay: inline-flex;\n\t\t\talign-items:center;\n\t\t}\n\t\tdiv.card-name span\n\t\t{\n\t\t\tmargin-left: 8px;\n\t\t}\n\t\tspan.channel-title\n\t\t{\n\t\t\tmargin-left: 2px;\n\t\t}\n\t\tspan.app-name:empty\n\t\t{\n\t\t\tdisplay: none;\n\t\t}\n\t\tdiv.card-type\n\t\t{\n\t\t\tborder-top: 1px dotted #dedede;\n\t\t\tmargin: 10px 2px 2px 2px;\n\t\t\tpadding: 4 12px 8px 0px;\n\t\t\tline-height: 1.6;\n\t\t}\n\t\t.contact-card .card-avatar\n\t\t{\n\t\t\tborder-radius: 4px;\n\t\t}\n\t\t.channel-card .card-avatar\n\t\t{\n\t\t\tborder-radius: 21px; /*making circle*/\n\t\t}\n\t\tspan.channel-title\n\t\t{\n\t\t\toverflow: hidden;\n\t\t\ttext-overflow: ellipsis;\n\t\t\t-o-text-overflow: ellipsis;\n\t\t\twhite-space: nowrap;\n\t\t\t-webkit-line-clamp: 1;\n\t\t\t-webkit-box-orient: vertical;\n\t\t\tword-break: break-all;\n\t\t\twidth: 230px;\n\t\t\tdisplay:block;\n\t\t}\n\t\t.appinfo\n\t\t{\n\t\t\t/*padding-top: 12px;*/\n\t\t}\n\t\t.app-name\n\t\t{\n\t\t\tpadding-left: 4px;\n\t\t\tcolor: #999;\n\t\t\tfont-size: 80%;\n\t\t}\n\t\t\n\t\t.dont-break-out\n\t\t{\n\t\t\toverflow-wrap: break-word;\n\t\t\tword-wrap: break-word;\n\t\t\t\n\t\t\t-ms-word-break: break-all;\n\t\t\t/*word-break: break-all;*/\n\t\t\tword-break: break-word;\n\t\t\t-ms-hyphens: auto;\n\t\t\t-moz-hyphens: auto;\n\t\t\t-webkit-hyphens: auto;\n\t\t\thyphens: auto;\n\t\t}\n\n\t\t.img-popup\n\t\t{\n\t\t\tbackground: rgba(0,0,0,.4);\n\t\t\t\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tposition: fixed;\n\t\t\ttext-align: center;\n\t\t\ttop: 0;\n\t\t\tleft: 0;\n\t\t\tz-index: 2147483647;\t/* top-most */\n\t\t}\n\t\t.img-popup .img\n\t\t{\n\t\t\tcursor: pointer;\n\t\t\tdisplay: inline-block;\n\t\t\tvertical-align: middle;\n\t\t\tdisplay: inline-block;\n\t\t\tposition: absolute;\n\t\t\tbox-shadow: 10px 10px 60px #555;\n\t\t\tborder-radius: 8px;\n\t\t\tmax-width: 90%; \n\t\t\tmax-height: 90%;\n\t\t\tmargin: auto;\n\t\t\tleft: 0;\n\t\t\tright: 0;\n\t\t\ttop: 0;\n\t\t\tbottom: 0;\n\t\t}\n\n\t\t.voicebox\n\t\t{\n\t\t\tbackground-color: white;\n\t\t\tmax-width: 300px;\n\t\t\tmin-width: 48px;\n\t\t\theight: 30px;\n\t\t\tborder-radius: 4px;\n\t\t\tdisplay: inline-flex;\n\t\t\tborder: 1.5px solid #ccc;\n\t\t\tpadding: 2px;\n\t\t}\n\t\t.right .voicebox\n\t\t{\n\t\t\tbackground-color: #b2e281;\n\t\t\tflex-direction: row-reverse;\n\t\t}\n\t\t.voiceimg\n\t\t{\n\t\t\twidth: 25px;\n\t\t\theight: 26px;\n\t\t\tbackground-repeat: no-repeat;\n\t\t\tbackground-position: -50px 0;\n\t\t\tbackground-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAaCAMAAADIQlGKAAAAdVBMVEUAAAAAyWcHwWAHwWAHwWAFwWAHwWAHwV8GwWAHwV8HwWAGwWAHwWAHwWAHwWAGwmEJwGAKwF8LwGEHwWAIwWAIwWAFwGEHv2MAwWEAu2AHwV8HwWAIwWEGwWAGwl8GwmEIwmEGwV0IwmEHwWAIwWEGwGAHwWBdwbyCAAAAJnRSTlMABfxq+C3djVpG8cSwrXFUOBgWzIBiNCQQC7OShHpTTz8pILWpn7aeLVQAAADRSURBVDjL3dXJDoIwFEZh5klBQAUFcfa8/yO68xKb3NRYN/7bj5xFIdT72/nuUunoLFVB5qq1Syja705EoIbUPtWujxqURI11a03RKhBDaN1awahBws2z3oLCVyAlmnO+MR8UCOGswBauAquIysgIxLBVIIfuBXEAS6Mj0MCkQAe5UBhwMEICFwgVyKGeUVbujY5ABicF5LwstiTYKfD2HtXVMGiQsPjg86JTIIbKutUnqQYlQe9Zr/EVqOHh7v8V9Y5aG5ic3Rz3weE1tPd+sSe2ExTdqqGN0gAAAABJRU5ErkJggg==');\n\t\t}\n\t\t.voicetime\n\t\t{\n\t\t\theight: 26px;\n\t\t\tline-height: 26px;\n\t\t\tpadding-left: 4px;\n\t\t\tpadding-right: 8px;\n\t\t}\n\t\t.right .voiceimg\n\t\t{\n\t\t\t-moz-transform: scaleX(-1);\n    \t\t-o-transform: scaleX(-1);\n    \t\t-webkit-transform: scaleX(-1);\n    \t\ttransform: scaleX(-1);\n\t\t\t\t-ms-filter: \"FlipH\";\n    \t\tfilter: FlipH;\n\t\t}\n\n\t\t.voiceplaying\n\t\t{\n\t\t\tanimation:voiceplaying 1.5s steps(1, end) infinite;\n\t\t}\n\n\t\t@keyframes voiceplaying\n\t\t{\n\t\t\t0%{background-position: 0 0 }\n\t\t\t25%{ background-position:-25px 0;}\n\t\t\t75%{ background-position: -50px 0; }\n\t\t\t100%{ background-position: -50px 0; }\n\t\t}\n\t\t\n\t\t@media print\n\t\t{\n\t\t\tbody\n\t\t\t{\n\t\t\t\tbackground-color: white;\n\t\t\t}\n\t\t\t.header\n\t\t\t{\n\t\t\t\tposition: absolute;\n\t\t\t}\n\t\t\t.chat.left div.content-box\n\t\t\t{\n\t\t\t\tmargin: 0 50px 4px 50px;\n\t\t\t}\n\t\t\t.chat.right div.content-box\n\t\t\t{\n\t\t\t\tmargin: 0px 50px 4px auto;\n\t\t\t}\n\t\t}\n\t\t</style>\n\t\t<title>%%DISPLAYNAME%% - %%ML:WeChat Chat History%%</title>\n\t\t<script language=\"javascript\">\n\n\t\t\tfunction getQueryParam(param, url)\n\t\t\t{\n  \t\t\t\t// this is an expression to get query strings\n  \t\t\t\tvar regexp = new RegExp('[?&]' + param + '=([^&#]*)', 'i' );\n  \t\t\t\tvar qString = regexp.exec(url);\n  \t\t\t\treturn qString ? qString[1] : null;\n\t\t\t}\n\n\t\t\tfunction showImagePopup(e)\n\t\t\t{\n\t\t\t\tvar srcElement = e.target;\n\t\t\t\tvar imgUrl = srcElement.getAttribute(\"rawUrl\");\n\t\t\t\tif (imgUrl == null)\n\t\t\t\t{\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\n\t\t\t\tvar fragment = document.createDocumentFragment();\n\t\t\t\tvar div = document.createElement('div');\n\t\t\t\tdiv.className = \"img-popup\";\n\t\t\t\tdiv.onclick = function(event) {\n\t\t\t\t\tdocument.body.removeChild(div);\n\t\t\t\t};\n\t\t\t\tfragment.appendChild(div);\n\t\t\t\tvar img = document.createElement('img');\n\t\t\t\timg.className = \"img\";\n\t\t\t\timg.src = imgUrl;\n\t\t\t\tdiv.appendChild(img);\n\t\t\t\tdocument.body.appendChild(fragment);\n\t\t\t}\n\t\n\t\t\tfunction showElements(msgType)\n\t\t\t{\n\t\t\t\tvar kwElement = document.getElementById(\"filter-keyword\");\n\t\t\t\tif (null != kwElement) kwElement.value = \"\";\n\t\n\t\t\t\twindow.msgFilter = {'filterType': 'msgType', 'msgType': msgType};\n\t\n\t\t\t\tvar msgElements = document.querySelectorAll('div.msg');\n\t\t\t\tif (null == msgElements || msgElements.length == 0)\n\t\t\t\t{\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\tdocument.body.style.cursor = 'wait';\n\t\t\t\tvar visibleMsgs = 0;\n\t\t\t\tfor (idx = 0; idx < msgElements.length; idx++)\n\t\t\t\t{\n\t\t\t\t\tif (filterMsg(msgElements[idx]))\n\t\t\t\t\t{\n\t\t\t\t\t\tvisibleMsgs++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\tif (visibleMsgs < window.sizeOfMsgPage && (typeof window.wechatMsgsIndexes !== 'undefined') && (window.wechatMsgsIndexes.length > 0))\n\t\t\t\t{\n\t\t\t\t\twindow.numberOfMsgsToLoad = window.sizeOfMsgPage - visibleMsgs;\n\t\t\t\t\tloadMsgsForNextPage();\n\t\t\t\t}\n\t\t\t\tdocument.body.style.cursor = 'default';\n\t\t\t}\n\t\n\t\t\tfunction filterMsg(divMsg)\n\t\t\t{\n\t\t\t\tif ((typeof window.msgFilter === 'undefined') || (window.msgFilter == null) || (typeof window.msgFilter.filterType === 'undefined'))\n\t\t\t\t{\n\t\t\t\t\tdivMsg.style.display = \"block\";\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif (window.msgFilter.filterType == 'search')\n\t\t\t\t{\n\t\t\t\t\tif ((typeof window.msgFilter.keyword === 'undefined') || (window.msgFilter.keyword == null) || (window.msgFilter.keyword.length == 0))\n\t\t\t\t\t{\n\t\t\t\t\t\tdivMsg.style.display = \"block\";\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tvar elementClasses = ['span.dspname', 'span.msg-text'];\n\t\t\t\t\tvar matched = false;\n\t\t\t\t\tfor (idx1 = 0; idx1 < elementClasses.length; idx1++)\n\t\t\t\t\t{\n\t\t\t\t\t\tvar elements = divMsg.querySelectorAll(elementClasses[idx1]);\n\t\t\t\t\t\tif (null == elements || elements.length == 0) continue;\n\t\t\t\t\t\t\n\t\t\t\t\t\tfor (idx2 = 0; idx2 < elements.length; idx2++)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif (null != elements[idx2].innerText && elements[idx2].innerText.toLowerCase().indexOf(window.msgFilter.keyword) != -1)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tmatched = true;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (matched) break;\n\t\t\t\t\t}\n\t\t\t\t\tdivMsg.style.display = matched ? \"block\" : \"none\";\n\t\t\t\t\treturn matched;\n\t\t\t\t}\n\t\t\t\telse if (window.msgFilter.filterType == 'msgType')\n\t\t\t\t{\n\t\t\t\t\tvar matched = (typeof window.msgFilter.msgType === 'undefined') || (window.msgFilter.msgType == null) || (divMsg.getAttribute(\"msgType\") == window.msgFilter.msgType);\n\t\t\t\t\tdivMsg.style.display = matched ? \"block\" : \"none\";\n\t\t\t\t\treturn matched;\n\t\t\t\t}\n\t\n\t\t\t\tdivMsg.style.display = \"block\";\n\t\t\t\treturn true;\n\t\t\t}\n\t\n\t\t\tfunction searchElements(e)\n\t\t\t{\n\t\t\t\tvar keyCode = e.keyCode || e.which || 0;\n\t\t\t\tif(keyCode !== 13)\n\t\t\t\t{\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\n\t\t\t\tvar msgElements = document.querySelectorAll('div.msg');\n\t\t\t\tif (null == msgElements || msgElements.length == 0)\n\t\t\t\t{\n\t\t\t\t\treturn false;;\n\t\t\t\t}\n\t\n\t\t\t\tvar keyword = e.target.value == null ? \"\" : e.target.value.toLowerCase();\n\t\t\t\twindow.msgFilter = {'filterType': 'search', 'keyword': keyword};\n\t\t\t\tvar elementClasses = ['span.dspname', 'span.msg-text'];\n\t\t\t\tvar visibleMsgs = 0;\n\t\t\t\tdocument.body.style.cursor = 'wait';\n\t\t\t\tfor (idx = 0; idx < msgElements.length; idx++)\n\t\t\t\t{\n\t\t\t\t\tif (filterMsg(msgElements[idx]))\n\t\t\t\t\t{\n\t\t\t\t\t\tvisibleMsgs++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (visibleMsgs < window.sizeOfMsgPage && (typeof window.wechatMsgsIndexes !== 'undefined') && (window.wechatMsgsIndexes.length > 0))\n\t\t\t\t{\n\t\t\t\t\twindow.loadingMoreMsgs = true;\n\t\t\t\t\twindow.numberOfMsgsToLoad = window.sizeOfMsgPage - visibleMsgs;\n\t\t\t\t\tloadMsgsForNextPage();\n\t\t\t\t}\n\t\t\t\tdocument.body.style.cursor = 'default';\n\t\t\t\t\n\t\t\t\treturn false;\n\t\t\t}\n\t\n\t\t\tfunction showAllMsgs(e)\n\t\t\t{\n\t\t\t\tshowElements(null);\n\t\t\t}\n\t\t\tfunction showImageMsgs(e)\n\t\t\t{\n\t\t\t\tshowElements(\"image\");\n\t\t\t}\n\t\t\tfunction showVideoMsgs(e)\n\t\t\t{\n\t\t\t\tshowElements(\"video\");\n\t\t\t}\n\n\t\t\tfunction loadMsgsForNextPage()\n\t\t\t{\n\t\t\t\tif ((typeof window.wechatMsgsIndexes === 'undefined'))\n\t\t\t\t{\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\tif ((typeof window.moreWechatMsgs === 'undefined'))\n\t\t\t\t{\n\t\t\t\t\twindow.moreWechatMsgs = [];\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tvar visibleMsgs = 0;\n\t\t\t\tif (window.moreWechatMsgs.length > 0)\n\t\t\t\t{\n\t\t\t\t\tvar fragment = document.createDocumentFragment();\n\t\t\t\t\tvar div = document.createElement('div');\n\t\t\t\t\tfragment.appendChild(div);\n\t\t\t\t\t\n\t\t\t\t\twhile (window.moreWechatMsgs.length > 0)\n\t\t\t\t\t{\n\t\t\t\t\t\tdiv.innerHTML = window.moreWechatMsgs.shift();\n\t\t\t\t\t\tif (div.children.length > 0)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar msgDiv = div.children[0];\n\t\t\t\t\t\t\tfragment.appendChild(msgDiv);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (filterMsg(msgDiv))\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvisibleMsgs++;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\n\t\t\t\t\tfragment.removeChild(div);\n\t\t\t\t\tvar containerDiv = document.getElementById('msgs-div');\n\t\t\t\t\tif (null != containerDiv)\n\t\t\t\t\t{\n\t\t\t\t\t\tcontainerDiv.appendChild(fragment);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\twindow.numberOfMsgsToLoad -= visibleMsgs;\n\t\t\t\tif ((window.numberOfMsgsToLoad > 0) && window.wechatMsgsIndexes.length > 0)\n\t\t\t\t{\n\t\t\t\t\t// Load next page\n\t\t\t\t\tconsole.log(window.wechatMsgsIndexes);\n\t\t\t\t\tvar nextPage = window.wechatMsgsIndexes.shift();\n\n\t\t\t\t\tvar script   = document.createElement(\"script\");\n\t\t\t\t\tscript.type  = \"text/javascript\";\n\t\t\t\t\tscript.src   = \"%%DATA_PATH%%/msg-\" + nextPage + \".js\";\n\t\t\t\t\tdocument.body.appendChild(script);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\twindow.loadingMoreMsgs = false;\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tfunction checkScrollDirectionIsUp(e)\n\t\t\t{\n\t\t\t\tif (e.wheelDelta)\n\t\t\t\t{\n\t\t\t\t\treturn e.wheelDelta > 0;\n\t\t\t\t}\n\t\t\t\treturn e.deltaY < 0;\n\t\t\t}\n\n\t\t\tfunction playbackEnded(e)\n\t\t\t{\n\t\t\t\tvar playingMsgId = e.target.getAttribute(\"playingMsgId\");\n\t\t\t\tif (playingMsgId != null)\n\t\t\t\t{\n\t\t\t\t\tvar voiceDiv = document.getElementById(\"voiceimg-\" + playingMsgId);\n\t\t\t\t\tif (null != voiceDiv)\n\t\t\t\t\t{\n\t\t\t\t\t\tvoiceDiv.classList.remove(\"voiceplaying\");\n\t\t\t\t\t}\n\t\t\t\t\te.target.removeAttribute(\"playingMsgId\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction playAudio(src, msgId)\n\t\t\t{\n\t\t\t\t// Stop previous\n\t\t\t\tvar audioPlayer = document.getElementById(\"audioPlayer\");\n\t\t\t\tif (null == audioPlayer)\n\t\t\t\t{\n\t\t\t\t\tvar divFooter = document.getElementById(\"footer\");\n\t\t\t\t\t// Create audio player\n\t\t\t\t\taudioPlayer = document.createElement('audio');\n\t\t\t\t\taudioPlayer.id = \"audioPlayer\";\n\t\t\t\t\taudioPlayer.type = 'audio/mpeg';\n\t\t\t\t\taudioPlayer.hidden = true;\n\t\t\t\t\tif (null != divFooter)\n\t\t\t\t\t{\n\t\t\t\t\t\tdivFooter.appendChild(audioPlayer);\n\t\t\t\t\t}\n\n\t\t\t\t\taudioPlayer.onpause = playbackEnded;\n\t\t\t\t\taudioPlayer.onended = playbackEnded;\n\n\t\t\t\t\taudioPlayer.onaudioprocess = function(e)\n\t\t\t\t\t{\n\t\t\t\t\t\tconsole.log(\"onaudioprocess: \" + e.target.currentSrc);\n\t\t\t\t\t};\n\n\t\t\t\t\taudioPlayer.onloadeddata = function(e)\n\t\t\t\t\t{\n\t\t\t\t\t\tconsole.log(\"onloadeddata: \" + e.target.currentSrc);\n\t\t\t\t\t};\n\n\t\t\t\t\t\n\t\t\t\t}\n\n\t\t\t\taudioPlayer.pause();\n\t\t\t\t\n\t\t\t\tvar playingMsgId = audioPlayer.getAttribute(\"playingMsgId\");\n\t\t\t\tif (playingMsgId != null)\n\t\t\t\t{\n\t\t\t\t\tvar voiceDiv = document.getElementById(\"voiceimg-\" + playingMsgId);\n\t\t\t\t\tif (null != voiceDiv)\n\t\t\t\t\t{\n\t\t\t\t\t\tvoiceDiv.classList.remove(\"voiceplaying\");\n\t\t\t\t\t}\n\t\t\t\t\taudioPlayer.removeAttribute(\"playingMsgId\");\n\t\t\t\t}\n\n\t\t\t\tif (msgId != playingMsgId)\n\t\t\t\t{\n\t\t\t\t\tconsole.log(\"Will play \" + msgId);\n\t\t\t\t\t// Play new one\n\t\t\t\t\taudioPlayer.setAttribute(\"playingMsgId\", msgId);\n\t\t\t\t\tvar audioSrc = src.getAttribute(\"audio\");\n\t\t\t\t\tif (null != audioSrc)\n\t\t\t\t\t{\n\t\t\t\t\t\taudioPlayer.src = audioSrc;\n\t\t\t\t\t\taudioPlayer.load();\n\t\t\t\t\t\taudioPlayer.play();\n\t\t\t\t\t}\n\n\t\t\t\t\tvar voiceDiv = document.getElementById(\"voiceimg-\" + msgId);\n\t\t\t\t\tif (null != voiceDiv)\n\t\t\t\t\t{\n\t\t\t\t\t\tvoiceDiv.classList.add(\"voiceplaying\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tfunction playVideo(src, msgId)\n\t\t\t{\n\t\t\t\t// Stop previous\n\t\t\t\tvar videoPlayer = document.getElementById(\"videoPlayer\");\n\t\t\t\tif (null == videoPlayer)\n\t\t\t\t{\n\t\t\t\t\tvar divFooter = document.getElementById(\"footer\");\n\t\t\t\t\t// Create video player\n\t\t\t\t\tvideoPlayer = document.createElement('video');\n\t\t\t\t\tvideoPlayer.id = \"videoPlayer\";\n\t\t\t\t\tvideoPlayer.type = 'video/mpeg';\n\t\t\t\t\tvideoPlayer.hidden = true;\n\t\t\t\t\tif (null != divFooter)\n\t\t\t\t\t{\n\t\t\t\t\t\tdivFooter.appendChild(videoPlayer);\n\t\t\t\t\t}\n\n\t\t\t\t\tvideoPlayer.onpause = playbackEnded;\n\t\t\t\t\tvideoPlayer.onended = playbackEnded;\n\n\t\t\t\t\tvideoPlayer.onvideoprocess = function(e)\n\t\t\t\t\t{\n\t\t\t\t\t\tconsole.log(\"onvideoprocess: \" + e.target.currentSrc);\n\t\t\t\t\t};\n\n\t\t\t\t\tvideoPlayer.onloadeddata = function(e)\n\t\t\t\t\t{\n\t\t\t\t\t\tconsole.log(\"onloadeddata: \" + e.target.currentSrc);\n\t\t\t\t\t};\n\n\t\t\t\t\t\n\t\t\t\t}\n\n\t\t\t\tvideoPlayer.pause();\n\t\t\t\t\n\t\t\t\tvar playingMsgId = videoPlayer.getAttribute(\"playingMsgId\");\n\t\t\t\tif (playingMsgId != null)\n\t\t\t\t{\n\t\t\t\t\tvar voiceDiv = document.getElementById(\"voiceimg-\" + playingMsgId);\n\t\t\t\t\tif (null != voiceDiv)\n\t\t\t\t\t{\n\t\t\t\t\t\tvoiceDiv.classList.remove(\"voiceplaying\");\n\t\t\t\t\t}\n\t\t\t\t\tvideoPlayer.removeAttribute(\"playingMsgId\");\n\t\t\t\t}\n\n\t\t\t\tif (msgId != playingMsgId)\n\t\t\t\t{\n\t\t\t\t\tconsole.log(\"Will play \" + msgId);\n\t\t\t\t\t// Play new one\n\t\t\t\t\tvideoPlayer.setAttribute(\"playingMsgId\", msgId);\n\t\t\t\t\tvar videoSrc = src.getAttribute(\"video\");\n\t\t\t\t\tif (null != videoSrc)\n\t\t\t\t\t{\n\t\t\t\t\t\tvideoPlayer.src = videoSrc;\n\t\t\t\t\t\tvideoPlayer.load();\n\t\t\t\t\t\tvideoPlayer.play();\n\t\t\t\t\t}\n\n\t\t\t\t\tvar voiceDiv = document.getElementById(\"voiceimg-\" + msgId);\n\t\t\t\t\tif (null != voiceDiv)\n\t\t\t\t\t{\n\t\t\t\t\t\tvoiceDiv.classList.add(\"voiceplaying\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction initPageData(pageData) {\n  \t\t\tif (typeof pageData !== 'undefined') {\n  \t\t\t\twindow.pageData = pageData;\n  \t\t\t} else {\n  \t\t\t\twindow.pageData = [];\n  \t\t\t}\n  \t\t}\n\n\t\tfunction initWechatPage() {\n\t\t\t\n\t\t\twindow.pagerType = \"%%PAGER_TYPE%%\";\n\n\t\t\tinitPageData(%%PAGE_DATA%%);\t// array of {'numnberOfMsgs', 'year', 'month', 'page', }\n\n\t\t\tvar currentPage = getQueryParam('p', document.location.href);\n\t\t\tif (currentPage == null) {\n\t\t\t\tcurrentPage = 0;\n\t\t\t} else {\n\t\t\t\tcurrentPage = parseInt(currentPage) - 1;\n\t\t\t\tif (currentPage >= window.pageData.length) {\n\t\t\t\t\tcurrentPage = window.pageData.length - 1;\n\t\t\t\t}\n\t\t\t\tif (currentPage < 0) {\n\t\t\t\t\tcurrentPage = 0;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\twindow.loadingMoreMsgs = false;\n\t\t\twindow.sizeOfMsgPage = parseInt('%%SIZE_OF_PAGE%%') || 100;\n\t\t\tvar numberOfMsgs = parseInt('%%NUMBER_OF_MSGS%%') || 0;\n\t\t\tvar numberOfPages = parseInt('%%NUMBER_OF_PAGES%%') || 0;\n\t\t\tvar asyncLoadingType = \"%%ASYNC_LOADING_TYPE%%\";\n\n\t\t\tif (asyncLoadingType != \"pager\" && numberOfPages == 0)\n\t\t\t{\n\t\t\t\treturn;\n\t\t\t}\n\t\t\twindow.wechatMsgsIndexes = [];\n\t\t\twindow.numberOfMsgsToLoad = 0;\n\n\t\t\tif (asyncLoadingType == \"pager\")\n\t\t\t{\n\t\t\t\twindow.wechatMsgsIndexes.push(window.pageData[currentPage].page);\n\t\t\t\twindow.numberOfMsgsToLoad = window.pageData[currentPage].numnberOfMsgs;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfor (var idx = 1; idx <= numberOfPages; idx++)\n\t\t\t\t{\n\t\t\t\t\twindow.wechatMsgsIndexes.push(idx);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (asyncLoadingType == \"pager\")\n\t\t\t{\n\t\t\t\tloadMsgsForNextPage();\n\n\t\t\t\tconsole.log(\"currentPage=\" + currentPage );\n\n\t\t\t\tvar minIdx = Math.max(0, currentPage - 5);\n\t\t\t\tvar maxIdx = Math.min(window.pageData.length - 1, currentPage + 5);\n\n\t\t\t\tvar pagerContainer = document.getElementById('pager');\n\t\t\t\tvar prevYear = '';\n\n\t\t\t\t// show 10 pages\n\t\t\t\tfor (var idx = minIdx; idx <= maxIdx; idx++) {\n\t\t\t\t\tif (window.pagerType == \"1\") {\n\t\t\t\t\t\t// \n\t\t\t\t\t\tvar pageItem = window.pageData[idx];\n\t\t\t\t\t\tif (idx == currentPage) {\n\t\t\t\t\t\t\tvar spn = document.createElement('span');\n\t\t\t\t\t\t\tspn.className = \"pager\";\n\t\t\t\t\t\t\tspn.innerText = idx + 1;\n\t\t\t\t\t\t\tpagerContainer.appendChild(spn);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tvar lnk = document.createElement('a');\n\t\t\t\t\t\t\tlnk.className = \"pager\";\n\t\t\t\t\t\t\tlnk.innerText = idx + 1;\n\t\t\t\t\t\t\tif (0 == idx) {\n\t\t\t\t\t\t\t\tlnk.href = \"index.html\";\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlnk.href = \"index.html?p=\" + (idx + 1);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tpagerContainer.appendChild(lnk);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (window.pagerType == \"2\") {\t// Year\n\t\t\t\t\t\tvar pageItem = window.pageData[idx];\n\t\t\t\t\t\tif (idx == currentPage) {\n\t\t\t\t\t\t\tvar spn = document.createElement('span');\n\t\t\t\t\t\t\tspn.className = \"pager\";\n\t\t\t\t\t\t\tspn.innerText = pageItem.year;\n\t\t\t\t\t\t\tpagerContainer.appendChild(spn);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tvar lnk = document.createElement('a');\n\t\t\t\t\t\t\tlnk.className = \"pager\";\n\t\t\t\t\t\t\tlnk.innerText = pageItem.year;\n\t\t\t\t\t\t\tlnk.href = \"index.html?p=\" + pageItem.page;\n\t\t\t\t\t\t\tpagerContainer.appendChild(lnk);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t} else if (window.pagerType == \"3\") {\t// Month\n\t\t\t\t\t\tvar pageItem = window.pageData[idx];\n\t\t\t\t\t\tvar text = '';\n\t\t\t\t\t\tif (prevYear != pageItem.year) {\n\t\t\t\t\t\t\ttext = pageItem.year + \"-\" + pageItem.month;\n\t\t\t\t\t\t\tprevYear = pageItem.year;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ttext = pageItem.month;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (idx == currentPage) {\n\t\t\t\t\t\t\tvar spn = document.createElement('span');\n\t\t\t\t\t\t\tspn.className = \"pager\";\n\t\t\t\t\t\t\tspn.innerText = text;\n\t\t\t\t\t\t\tpagerContainer.appendChild(spn);\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tvar lnk = document.createElement('a');\n\t\t\t\t\t\t\tlnk.className = \"pager\";\n\t\t\t\t\t\t\tlnk.innerText = text;\n\t\t\t\t\t\t\tlnk.href = \"index.html?p=\" + pageItem.page;\n\t\t\t\t\t\t\tpagerContainer.appendChild(lnk);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (asyncLoadingType == \"onscroll\")\n\t\t\t{\n\t\t\t\twindow.addEventListener('scroll', function(e)\n\t\t\t\t{\n\t\t\t\t\tif (window.loadingMoreMsgs)\n\t\t\t\t\t{\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (window.wechatMsgsIndexes.length == 0)\n\t\t\t\t\t{\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!checkScrollDirectionIsUp(e))\n\t\t\t\t\t{\n\t\t\t\t\t\tvar containerDiv = document.querySelector(\"#msgs-div\");\n\t\t\t\t\t\tvar containerDivOffset = containerDiv.offsetTop + containerDiv.clientHeight;\n\n\t\t\t\t\t\tvar pageOffset = window.pageYOffset + window.innerHeight;\n\n\t\t\t\t\t\tif(pageOffset > containerDivOffset - 20)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\twindow.loadingMoreMsgs = true;\n\t\t\t\t\t\t\tif (window.numberOfMsgsToLoad <= 0) window.numberOfMsgsToLoad = window.sizeOfMsgPage;\n\t\t\t\t\t\t\tloadMsgsForNextPage();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, {passive: true});\n\n\t\t\t}\n\t\t}\n  \t\t\n\t\t(function() {\n\t\t  \twindow.addEventListener('load', function (e) {\n\t\t\t\tinitWechatPage();\n\t\t\t\t}, false);\n\t\t})();\n\n\t\t\n\n\t\t</script>\n\t</head>\n\t<body uid=\"%%USRNAME%%\" sid=\"%%SESSION_USRNAME%%\">\n\t\t<div class=\"main\">\n\t\t\t<div class=\"header\">\n\t\t\t\t<span class=\"sname\">%%DISPLAYNAME%%</span>\n\t\t\t\t<div class=\"msgfilter\">\n\t\t\t\t\t<!-- filter.html -->\n%%HEADER_FILTER%%\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"msgs\" id=\"msgs-div\">\n%%BODY%%\n\t\t\t</div>\n\t\t\t<br />\n\t\t\t<div id=\"footer\" class=\"footer\">\n\t\t\t\t<div id=\"pager\" class=\"pager\">\n\t\t\t\t\t\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n  \t</body>\n\n\t\t<script >\n\n\t\t\t!function(e){var t=function(u,D,f){\"use strict\";var k,H;if(function(){var e;var t={lazyClass:\"lazyload\",loadedClass:\"lazyloaded\",loadingClass:\"lazyloading\",preloadClass:\"lazypreload\",errorClass:\"lazyerror\",autosizesClass:\"lazyautosizes\",fastLoadedClass:\"ls-is-cached\",iframeLoadMode:0,srcAttr:\"data-src\",srcsetAttr:\"data-srcset\",sizesAttr:\"data-sizes\",minSize:40,customMedia:{},init:true,expFactor:1.5,hFac:.8,loadMode:2,loadHidden:true,ricTimeout:0,throttleDelay:125};H=u.lazySizesConfig||u.lazysizesConfig||{};for(e in t){if(!(e in H)){H[e]=t[e]}}}(),!D||!D.getElementsByClassName){return{init:function(){},cfg:H,noSupport:true}}var O=D.documentElement,i=u.HTMLPictureElement,P=\"addEventListener\",$=\"getAttribute\",q=u[P].bind(u),I=u.setTimeout,U=u.requestAnimationFrame||I,o=u.requestIdleCallback,j=/^picture$/i,r=[\"load\",\"error\",\"lazyincluded\",\"_lazyloaded\"],a={},G=Array.prototype.forEach,J=function(e,t){if(!a[t]){a[t]=new RegExp(\"(\\\\s|^)\"+t+\"(\\\\s|$)\")}return a[t].test(e[$](\"class\")||\"\")&&a[t]},K=function(e,t){if(!J(e,t)){e.setAttribute(\"class\",(e[$](\"class\")||\"\").trim()+\" \"+t)}},Q=function(e,t){var a;if(a=J(e,t)){e.setAttribute(\"class\",(e[$](\"class\")||\"\").replace(a,\" \"))}},V=function(t,a,e){var i=e?P:\"removeEventListener\";if(e){V(t,a)}r.forEach(function(e){t[i](e,a)})},X=function(e,t,a,i,r){var n=D.createEvent(\"Event\");if(!a){a={}}a.instance=k;n.initEvent(t,!i,!r);n.detail=a;e.dispatchEvent(n);return n},Y=function(e,t){var a;if(!i&&(a=u.picturefill||H.pf)){if(t&&t.src&&!e[$](\"srcset\")){e.setAttribute(\"srcset\",t.src)}a({reevaluate:true,elements:[e]})}else if(t&&t.src){e.src=t.src}},Z=function(e,t){return(getComputedStyle(e,null)||{})[t]},s=function(e,t,a){a=a||e.offsetWidth;while(a<H.minSize&&t&&!e._lazysizesWidth){a=t.offsetWidth;t=t.parentNode}return a},ee=function(){var a,i;var t=[];var r=[];var n=t;var s=function(){var e=n;n=t.length?r:t;a=true;i=false;while(e.length){e.shift()()}a=false};var e=function(e,t){if(a&&!t){e.apply(this,arguments)}else{n.push(e);if(!i){i=true;(D.hidden?I:U)(s)}}};e._lsFlush=s;return e}(),te=function(a,e){return e?function(){ee(a)}:function(){var e=this;var t=arguments;ee(function(){a.apply(e,t)})}},ae=function(e){var a;var i=0;var r=H.throttleDelay;var n=H.ricTimeout;var t=function(){a=false;i=f.now();e()};var s=o&&n>49?function(){o(t,{timeout:n});if(n!==H.ricTimeout){n=H.ricTimeout}}:te(function(){I(t)},true);return function(e){var t;if(e=e===true){n=33}if(a){return}a=true;t=r-(f.now()-i);if(t<0){t=0}if(e||t<9){s()}else{I(s,t)}}},ie=function(e){var t,a;var i=99;var r=function(){t=null;e()};var n=function(){var e=f.now()-a;if(e<i){I(n,i-e)}else{(o||r)(r)}};return function(){a=f.now();if(!t){t=I(n,i)}}},e=function(){var v,m,c,h,e;var y,z,g,p,C,b,A;var n=/^img$/i;var d=/^iframe$/i;var E=\"onscroll\"in u&&!/(gle|ing)bot/.test(navigator.userAgent);var _=0;var w=0;var M=0;var N=-1;var L=function(e){M--;if(!e||M<0||!e.target){M=0}};var x=function(e){if(A==null){A=Z(D.body,\"visibility\")==\"hidden\"}return A||!(Z(e.parentNode,\"visibility\")==\"hidden\"&&Z(e,\"visibility\")==\"hidden\")};var W=function(e,t){var a;var i=e;var r=x(e);g-=t;b+=t;p-=t;C+=t;while(r&&(i=i.offsetParent)&&i!=D.body&&i!=O){r=(Z(i,\"opacity\")||1)>0;if(r&&Z(i,\"overflow\")!=\"visible\"){a=i.getBoundingClientRect();r=C>a.left&&p<a.right&&b>a.top-1&&g<a.bottom+1}}return r};var t=function(){var e,t,a,i,r,n,s,o,l,u,f,c;var d=k.elements;if((h=H.loadMode)&&M<8&&(e=d.length)){t=0;N++;for(;t<e;t++){if(!d[t]||d[t]._lazyRace){continue}if(!E||k.prematureUnveil&&k.prematureUnveil(d[t])){R(d[t]);continue}if(!(o=d[t][$](\"data-expand\"))||!(n=o*1)){n=w}if(!u){u=!H.expand||H.expand<1?O.clientHeight>500&&O.clientWidth>500?500:370:H.expand;k._defEx=u;f=u*H.expFactor;c=H.hFac;A=null;if(w<f&&M<1&&N>2&&h>2&&!D.hidden){w=f;N=0}else if(h>1&&N>1&&M<6){w=u}else{w=_}}if(l!==n){y=innerWidth+n*c;z=innerHeight+n;s=n*-1;l=n}a=d[t].getBoundingClientRect();if((b=a.bottom)>=s&&(g=a.top)<=z&&(C=a.right)>=s*c&&(p=a.left)<=y&&(b||C||p||g)&&(H.loadHidden||x(d[t]))&&(m&&M<3&&!o&&(h<3||N<4)||W(d[t],n))){R(d[t]);r=true;if(M>9){break}}else if(!r&&m&&!i&&M<4&&N<4&&h>2&&(v[0]||H.preloadAfterLoad)&&(v[0]||!o&&(b||C||p||g||d[t][$](H.sizesAttr)!=\"auto\"))){i=v[0]||d[t]}}if(i&&!r){R(i)}}};var a=ae(t);var S=function(e){var t=e.target;if(t._lazyCache){delete t._lazyCache;return}L(e);K(t,H.loadedClass);Q(t,H.loadingClass);V(t,B);X(t,\"lazyloaded\")};var i=te(S);var B=function(e){i({target:e.target})};var T=function(e,t){var a=e.getAttribute(\"data-load-mode\")||H.iframeLoadMode;if(a==0){e.contentWindow.location.replace(t)}else if(a==1){e.src=t}};var F=function(e){var t;var a=e[$](H.srcsetAttr);if(t=H.customMedia[e[$](\"data-media\")||e[$](\"media\")]){e.setAttribute(\"media\",t)}if(a){e.setAttribute(\"srcset\",a)}};var s=te(function(t,e,a,i,r){var n,s,o,l,u,f;if(!(u=X(t,\"lazybeforeunveil\",e)).defaultPrevented){if(i){if(a){K(t,H.autosizesClass)}else{t.setAttribute(\"sizes\",i)}}s=t[$](H.srcsetAttr);n=t[$](H.srcAttr);if(r){o=t.parentNode;l=o&&j.test(o.nodeName||\"\")}f=e.firesLoad||\"src\"in t&&(s||n||l);u={target:t};K(t,H.loadingClass);if(f){clearTimeout(c);c=I(L,2500);V(t,B,true)}if(l){G.call(o.getElementsByTagName(\"source\"),F)}if(s){t.setAttribute(\"srcset\",s)}else if(n&&!l){if(d.test(t.nodeName)){T(t,n)}else{t.src=n}}if(r&&(s||l)){Y(t,{src:n})}}if(t._lazyRace){delete t._lazyRace}Q(t,H.lazyClass);ee(function(){var e=t.complete&&t.naturalWidth>1;if(!f||e){if(e){K(t,H.fastLoadedClass)}S(u);t._lazyCache=true;I(function(){if(\"_lazyCache\"in t){delete t._lazyCache}},9)}if(t.loading==\"lazy\"){M--}},true)});var R=function(e){if(e._lazyRace){return}var t;var a=n.test(e.nodeName);var i=a&&(e[$](H.sizesAttr)||e[$](\"sizes\"));var r=i==\"auto\";if((r||!m)&&a&&(e[$](\"src\")||e.srcset)&&!e.complete&&!J(e,H.errorClass)&&J(e,H.lazyClass)){return}t=X(e,\"lazyunveilread\").detail;if(r){re.updateElem(e,true,e.offsetWidth)}e._lazyRace=true;M++;s(e,t,r,i,a)};var r=ie(function(){H.loadMode=3;a()});var o=function(){if(H.loadMode==3){H.loadMode=2}r()};var l=function(){if(m){return}if(f.now()-e<999){I(l,999);return}m=true;H.loadMode=3;a();q(\"scroll\",o,true)};return{_:function(){e=f.now();k.elements=D.getElementsByClassName(H.lazyClass);v=D.getElementsByClassName(H.lazyClass+\" \"+H.preloadClass);q(\"scroll\",a,true);q(\"resize\",a,true);q(\"pageshow\",function(e){if(e.persisted){var t=D.querySelectorAll(\".\"+H.loadingClass);if(t.length&&t.forEach){U(function(){t.forEach(function(e){if(e.complete){R(e)}})})}}});if(u.MutationObserver){new MutationObserver(a).observe(O,{childList:true,subtree:true,attributes:true})}else{O[P](\"DOMNodeInserted\",a,true);O[P](\"DOMAttrModified\",a,true);setInterval(a,999)}q(\"hashchange\",a,true);[\"focus\",\"mouseover\",\"click\",\"load\",\"transitionend\",\"animationend\"].forEach(function(e){D[P](e,a,true)});if(/d$|^c/.test(D.readyState)){l()}else{q(\"load\",l);D[P](\"DOMContentLoaded\",a);I(l,2e4)}if(k.elements.length){t();ee._lsFlush()}else{a()}},checkElems:a,unveil:R,_aLSL:o}}(),re=function(){var a;var n=te(function(e,t,a,i){var r,n,s;e._lazysizesWidth=i;i+=\"px\";e.setAttribute(\"sizes\",i);if(j.test(t.nodeName||\"\")){r=t.getElementsByTagName(\"source\");for(n=0,s=r.length;n<s;n++){r[n].setAttribute(\"sizes\",i)}}if(!a.detail.dataAttr){Y(e,a.detail)}});var i=function(e,t,a){var i;var r=e.parentNode;if(r){a=s(e,r,a);i=X(e,\"lazybeforesizes\",{width:a,dataAttr:!!t});if(!i.defaultPrevented){a=i.detail.width;if(a&&a!==e._lazysizesWidth){n(e,r,i,a)}}}};var e=function(){var e;var t=a.length;if(t){e=0;for(;e<t;e++){i(a[e])}}};var t=ie(e);return{_:function(){a=D.getElementsByClassName(H.autosizesClass);q(\"resize\",t)},checkElems:t,updateElem:i}}(),t=function(){if(!t.i&&D.getElementsByClassName){t.i=true;re._();e._()}};return I(function(){H.init&&t()}),k={cfg:H,autoSizer:re,loader:e,init:t,uP:Y,aC:K,rC:Q,hC:J,fire:X,gW:s,rAF:ee}}(e,e.document,Date);e.lazySizes=t,\"object\"==typeof module&&module.exports&&(module.exports=t)}(\"undefined\"!=typeof window?window:{});\n\t\t</script>\n</html>\n"
  },
  {
    "path": "WechatExporter/res/templates/frame_filter.html",
    "content": "<!doctype html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover\">\n\t\t<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n\t\t<meta content=\"yes\" name=\"apple-touch-fullscreen\">\n\t\t<style type=text/css>\n\t\tbody\n\t\t{\n\t\t\tbackground-color: #c4c4c4;\n\t\t\tfont-family: \"-apple-system\", \"Helvetica Neue\", \"Roboto\", \"Segoe UI\", sans-serif;\n  \t\t\tfont-size: 10.5pt;\n      \t\tmargin: 0px;\n\t\t}\n\t\t.main\n\t\t{\n\t\t\tmax-width:720px;\n\t\t\tmargin: 0 auto;\n\t\t\theight: 100%;\n\t\t\tpadding-top: 54px; /* header */\n\t\t\tpadding-bottom: 20px; /* footer */\n\t\t\tbackground-color: #ebebeb;\n\t\t}\n\t\t.header\n\t\t{\n\t\t\tposition: fixed;\n\t\t\ttop:0;\n\t\t\tmax-width:720px;\n\t\t\twidth: 100%;\n\t\t\theight: 48px;\n\t\t\tbackground-color: #f6f6f6;\n\t\t\tz-index: 99999999;\n\t\t}\n\t\t.sname\n\t\t{\n\t\t\tpadding-left: 12px;\n\t\t\tvertical-align: middle;\n\t\t\theight: 48px;\n\t\t\tline-height: 48px;\n\t\t\tfont-weight: bold;\n\t\t}\n\t\t.msgfilter\n\t\t{\n\t\t\tfloat:right;\n\t\t\theight: 48px;\n\t\t\tline-height: 48px;\n\t\t\tpadding-right: 8px;\n\t\t}\n\t\t.filter-btn\n\t\t{\n\t\t\tvertical-align: middle;\n\t\t}\n\t\t.footer\n\t\t{\n\t\t\tposition: fixed;\n\t\t\tbottom: 0;\n\t\t\tmax-width:720px;\n\t\t\twidth: 100%;\n\t\t\theight: 20px;\n\t\t\tz-index: 99999999;\n\t\t}\n\t\t\n\t\t.msgs\n\t\t{\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t}\n\t  \t.chat-left, .media-left\n\t  \t{\n\t\t\tclear:both;\n\t\t\toverflow: auto;\n\t\t\tmargin:0 auto;\n\t\t\tpadding: 8px 8px;\n\t  \t}\n\t\t.chat-left div.avatar-box,\n\t\t.media-left div.avatar-box\n\t\t{\n\t\t\tfloat: left;\n\t\t}\n\t\t.chat-left div.avatar-box\n\t\t{\n\t\t\tpadding-top: 14px;\n\t\t}\n\t\t.chat-left div.nt-box,\n\t\t.media-left div.nt-box\n\t\t{\n\t\t\tmargin: 0 50px 2px 50px;\n\t\t\tpadding: 0px;\n\t\t\tcolor: #848484;\n\t\t\tfont-size: 80%;\n\t\t\ttext-align: left;\n\t\t}\n\t\t.chat-left div.content-box\n\t\t{\n\t\t\tposition: relative;\n\t\t\tbackground-color: white;\n\t\t\t/*float: left;*/\n\t\t\tmargin: 0 50px 10px 50px;\n\t\t\tpadding: 10px 10px 10px 10px;\n\t\t\tborder-radius:8px;\n\t\t}\n\t\t.media-left div.content-box\n\t\t{\n\t\t\t/*float: left;*/\n\t\t\tmargin: 0 50px 10px 50px;\n\t\t\tpadding: 10px 10px 10px 10px;\n    \t}\n\t    div.content-box\n\t    {\n\t      width:fit-content;\n\t      width:-webkit-fit-content;\n\t      width:-moz-fit-content;\n\t    }\n\t\t.chat-left.channels .content-box, .chat-right.channels .content-box\n\t\t{\n\t\t\twidth:240px;\n\t\t}\n\t\t.refermsg\n\t\t{\n\t\t\tfont-size: 90%;\n\t\t}\n\t\t\n\t\t.chat-right, .media-right\n\t\t{\n\t\t\tclear:both;\n\t\t\toverflow: auto;\n\t\t\tmax-width: 670px;\n\t\t\tmargin: 0 auto;\n\t\t\tbackground-color: #ebebeb;\n\t\t\tpadding: 8px 8px 8px 58px;\n\t\t}\n\t\t.chat-right div.avatar-box,\n\t\t.media-right div.avatar-box\n\t\t{\n\t\t\tfloat: right;\n\t\t}\n\t\t.chat-right div.nt-box,\n\t\t.media-right div.nt-box\n\t\t{\n\t\t\tmargin: 0px 50px 2px 50px;\n\t\t\tpadding: 0px;\n\t\t\tcolor: #848484;\n\t\t\tfont-size: 80%;\n\t\t\ttext-align: right;\n\t\t}\n\t\t.chat-right div.content-box\n\t\t{\n      \t\tposition: relative;\n\t\t\tbackground-color: #b2e281;\n\t\t\tmargin: 0px 50px 10px auto;\n\t\t\tpadding: 10px 10px 10px 10px;\n\t\t\tborder-radius: 8px;\n\t\t}\n\t\t.media-right div.content-box\n\t\t{\n\t\t\tfloat: right;\n\t\t\tmargin: 0px 0px 10px 50px;\n\t\t\tpadding: 10px 10px 10px 10px;\n\t\t}\n\t\t\n\t\t.name-right\n\t\t{\n\t\t\tdisplay: none;\n\t\t}\n\t\timg.avatar\n\t\t{\n\t\t\twidth: 40px;\n\t\t\theight: 40px;\n\t\t\tborder-radius: 4px;\n\t\t}\n\t\t\n\t\t.triangle\n\t\t{\n\t\t\theight: 0px;\n\t\t\twidth: 0px;\n\t\t\tborder-width: 6px;\n\t\t\tborder-style: solid;\n\t\t\tposition: relative;\n\t\t}\n\t\t.chat-left .triangle\n\t\t{\n\t\t\tposition: absolute;\n\t\t\tborder-color: transparent white transparent transparent;\n\t\t\tleft: -12px;\n\t\t\ttop: 12px;\n\t\t}\n\t\t.chat-right .triangle\n\t\t{\n\t\t\tposition: absolute;\n\t\t\tborder-color: transparent transparent transparent #b2e281;\n\t\t\tright: -12px;\n\t\t\ttop: 12px;\n\t\t}\n\t\t\n\t\t.chat-notice\n\t\t{\n\t\t\tclear: both;\n\t\t\tfont-size: 80%;\n\t\t\ttext-align: center;\n\t\t\tmargin-top: 15px;\n\t\t\tmargin-bottom: 15px;\n\t\t\tmargin:0 auto;\n\t\t\tpadding: 0px 8px;\n    \t}\n    \t.chat-notice .content-box\n\t\t{\n\t\t\twidth: 80%;\n\t\t\tmargin:0 auto;\n\t\t}\n\t\t.chat-notice.fmsgtag .content-box\n\t\t{\n\t\t\tbackground-color: #008080;\n\t\t\tborder-radius: 4px;\n\t\t\tfont-size: 100%;\n\t\t\tpadding: 5px 10px;\n\t\t\tcolor: white;\n\t\t}\n\t\taudio\n\t\t{\n\t\t\tmax-width: 220px;\n\t\t}\n\t\tvideo\n\t\t{\n\t\t\tmax-width: 240px;\n\t\t\tborder-radius: 8px;\n\t\t}\n\t\t.image\n\t\t{\n\t\t\twidth:90px;\n\t\t\t/*transition: all 2s;*/\n\t\t\tborder-radius:8px;\n\t\t\tmax-width: 360px;\n\t\t\tmax-height: 240px;\n\t\t\tmin-width: 120px;\n\t\t\tmin-height: 24px;\n\t\t}\n\t\timg\n\t\t{\n\t\t\timage-orientation: from-image;\n\t\t}\n\t\timg[src=\"\"]\n\t\t{\n\t\t\tdisplay: none;\n\t\t}\n\t\timg.app-icon\n\t\t{\n\t\t\twidth: 24px;\n\t\t\theight: 24px;\n\t\t\tvertical-align:middle;\n\t\t\tborder-radius: 2px;\n\t\t}\n\t\timg.channel-poster\n\t\t{\n\t\t\tobject-fit: cover;\n\t\t\twidth: 240px;\n\t\t\theight: 240px;\n\t\t}\n\t\t.image:hover, .channel-poster:hover\n\t\t{\n\t\t\tcursor: crosshair;\n\t\t  \t/* transform: scale(5); */\n\t\t}\n\t\t\n\t\timg.card-avatar\n\t\t{\n\t\t\twidth: 42px;\n\t\t\theight: 42px;\n\t\t\tvertical-align:middle;\n\t\t}\n\t\tdiv.card-name\n\t\t{\n\t\t\tdisplay: flex;\n\t\t\talign-items:center;\n\t\t}\n\t\tdiv.card-name span\n\t\t{\n\t\t\tmargin-left: 8px;\n\t\t}\n\t\tspan.channel-title\n\t\t{\n\t\t\tmargin-left: 2px;\n\t\t}\n\t\tdiv.card-type\n\t\t{\n\t\t\tborder-top: 1px dotted #dedede;\n\t\t\tmargin: 10px 2px 2px 2px;\n\t\t\tpadding: 4 12px 8px 0px;\n\t\t\tline-height: 1.6;\n\t\t}\n\t\t.contact-card .card-avatar\n\t\t{\n\t\t\tborder-radius: 4px;\n\t\t}\n\t\t.channel-card .card-avatar\n\t\t{\n\t\t\tborder-radius: 21px;\n\t\t}\n\t\tspan.channel-title\n\t\t{\n\t\t\toverflow: hidden;\n\t\t\ttext-overflow: ellipsis;\n\t\t\t-o-text-overflow: ellipsis;\n\t\t\twhite-space: nowrap;\n\t\t\t-webkit-line-clamp: 1;\n\t\t\t-webkit-box-orient: vertical;\n\t\t\tword-break: break-all;\n\t\t\twidth: 300px;\n\t\t\tdisplay:block;\n\t\t}\n\t\t.appinfo\n\t\t{\n\t\t\t/*padding-top: 12px;*/\n\t\t}\n\t\t.app-name\n\t\t{\n\t\t\tpadding-left: 4px;\n\t\t\tcolor: #999;\n\t\t\tfont-size: 80%;\n\t\t}\n\t\t\n\t\t.dont-break-out\n\t\t{\n\t\t\toverflow-wrap: break-word;\n\t\t\tword-wrap: break-word;\n\t\t\t\n\t\t\t-ms-word-break: break-all;\n\t\t\t/*word-break: break-all;*/\n\t\t\tword-break: break-word;\n\t\t\t-ms-hyphens: auto;\n\t\t\t-moz-hyphens: auto;\n\t\t\t-webkit-hyphens: auto;\n\t\t\thyphens: auto;\n\t\t}\n  \n\t\t</style>\n\t\t<title>%%DISPLAYNAME%% - 微信聊天记录</title>\n\t</head>\n\t<body uid=\"%%USRNAME%%\" sid=\"%%SESSION_USRNAME%%\">\n\t\t<div class=\"main\">\n\t\t\t<div class=\"header\">\n\t\t\t\t<span class=\"sname\">%%DISPLAYNAME%%</span>\n\t\t\t\t<div class=\"msgfilter\">\n\t\t\t\t\t<img width=\"24\" border=\"0\" alt=\"显示所有消息\" id=\"filter-none\" class=\"filter-btn\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAjVBMVEUAAABNyzpRwzJRwzJT0zNRwzJRxDJRwzFSxDZRxDJRwjJRwzJRwzFRwzFRxDJQwzJQxDFRwDJVyTBRwzJQwzFRwzJRwzJRxDJRwzFRwzFQwzFSxTFRwzJSwzJRwzJRwzFSxDJQwjJPwzNRwzJSwzJRwjFRwzJW0TVVzTRSxzNY1DZUyzRTyTNa2Dde4jquq4L8AAAAJnRSTlMABPn8BuSnyw/fIe3Dsn5WTBkK9LmsmpBvYzszvKKHdmhdKNNRP0nRLroAAAEmSURBVCjPbdLZcoMwDAVQWQIDYSf7vrSVvKX//3kNtKEp0zujF52xrbENP1FRFCmYRj1bE4oA2mZz3u7UX1Jw07kXEZud1Yso2LJzFgmNc2+zUSK4is2PcaBwWJDJ0lFaZKPhIowAc74vR6gEQ7xJAvFaO8zjcbNSkK14RiPLXfcydWGQCYkkSeHjpPW6jr7lKMjMaBawy4w8YuJ6kLoHCjHUznuP6I2RzSDFQ9BdU2vpVNmwrpDDvj+ozfycMa0ckgK0HeQsq2GENBM3By0YksTapLBoigH2vFglauWQveG+UEro8970N9MIEhHzo9BtXx9keUek3uh+ePaHUlokWOuZy25o/WpTxogxfV4ggknS2azb67idtJ/rb7N/voUCNdlnsvILSCAaBAI8PN8AAAAASUVORK5CYII=\" />\n\t\t\t\t\t&nbsp;\n\t\t\t\t\t<img width=\"24\" border=\"0\" alt=\"只显示照片\" id=\"filter-image\" class=\"filter-btn\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAkFBMVEX////8/v79//7///3J6f2+5v3j8/3H6f204f3M6v71/P6z4/7y+v3s+f3W7v275P2/5vu25P/3/P655v7L7Pz1+v7v+v245P624v6x4v6+6f275f/h8v235f3O7PzC6PzY8fz5/f7j9P7p9v3E6P295v/l8/7X8f6u4f3m9f3R7f285/3e8f7b8f3T7v7T7/lYv97kAAABWElEQVQoz13R2W7DIBBA0YFhibdiTGxwvcfZm7T//3cdO6mi9MrCEkcDD4Dc/MssBQlmiCIRvRLSN8bZHGRVZdk8z9mjOR6TxjQuhyEbat/3XeJt54Ov2/s58aaT8FkNdYs4m+bgnGs26qTvsvEL5HvGGO+t3Ub2UKuTQuHdCnLiDK/BxGmgiVExbV1OsD/fGDvK5IS894GA6/CAxAjtus+YY+boqCPXl6NZIBx222AVMo7X824kmNJ1or4hEwMDoBECOqpM18uHiWGRIpKAvo3tE7JhBgYIfAG8tCf1Bwpe4ZR+H5mOCb5SzgCRPwP4udDEAiUiAcBjIWiRIAf3ff94a8jYCjLUst21Su3WT7UVMLHCwfbsvXhflDl8db7RWm9f9eLKqwUab8/XqYzjsijipZJjKkF2jp5onAqK9tclLS45dMFSdZ0s0W8tbAz0IhIiisRbWve/BvAiZfLgy6oAAAAASUVORK5CYII=\" />\n\t\t\t\t\t&nbsp;\n\t\t\t\t\t<img width=\"24\" border=\"0\" alt=\"只显示视频\" id=\"filter-video\" class=\"filter-btn\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAZlBMVEW45f+65f/0+/214/7E6P215f+75f2z4v7L6/3T7v695v285v/y+v7G6f7B6P6+5v7J6v2x4v/1/P7P7f72+/32/Pv3/f6/5/7v+v7Z8P78/v6z5P+55fvW7/3e8v7i8/3q9v2r4P7/hAFWAAABUElEQVQoz1WQh47sIAxFjU0JkISWMOmZ/f+ffA6j2ac9oghdl4sBDjAuxtj9Ep05QIMAuGvf43+m+To4XOg3diEE/cE5HQq+DwBz46GEAKUUPAfvXdQ1gcZCXgSu+QDAt09rTyCzJN1iicOb4lU3OZCzVBTGxotYEWSowyaka0LM84R1keBNZyk+GVOQtchhiHEYljeR3bZB5gCyetunXRGR+jmR1mr7MiILc7D9Y6m5WeRWEhbJpcbZWTb3IFiI2R25yNY8/BGq1rllyOx/S5mTS53mEbgHeospGYZ+zmxKvfqPML/kdhXLFIu3MTFvcmg9hnRi38Bbg6ZX9/05KBLQxkR8akUsBLYr+fVBCPgO0QOgNaF5El98uhYCs9bREO1724aYJDdrwMO7rjw/Xo0Y47rdBOA1nEtGnPAhZ5xxseQVaAc7udeH8cERaSH+Ae9rFrU/VkySAAAAAElFTkSuQmCC\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"msgs\" id=\"msgs-div\">\n%%BODY%%\n\t\t\t</div>\n\t\t\t<div class=\"footer\">\n\t\n\t\t\t</div>\n\t\t</div>\n\n  </body>\n  %%LOADING_SCRIPTS%%\n\n  <script language=\"javascript\">\n\n    function showElements(msgType)\n    {\n      var msgElements = document.querySelectorAll('div.msg');\n      if (null == msgElements || msgElements.length == 0)\n      {\n        return;\n      }\n\n      for (idx = 0; idx < msgElements.length; idx++)\n      {\n        msgElements[idx].style.display = ((msgType == null) || msgElements[idx].getAttribute(\"msgtype\") == msgType) ? \"block\" : \"none\";\n      }\n    }\n\n    document.getElementById(\"filter-none\").addEventListener(\"click\", function() {\n      showElements(null);\n    });\n\n    document.getElementById(\"filter-image\").addEventListener(\"click\", function() {\n      showElements(\"image\");\n    });\n\n    document.getElementById(\"filter-video\").addEventListener(\"click\", function() {\n      showElements(\"video\");\n    });\n\n  </script>\n\n</html>\n"
  },
  {
    "path": "WechatExporter/res/templates/image.html",
    "content": "\t\t\t\t<div class=\"msg media %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<img rawUrl=\"%%IMGPATH%%\" data-src=\"%%IMGTHUMBPATH%%\" class=\"lazyload image img-%%ALIGNMENT%%_triangle\" onclick=\"javascript:showImagePopup(event);\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/listframe.html",
    "content": "<!doctype html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover\">\n\t\t<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n\t\t<meta content=\"yes\" name=\"apple-touch-fullscreen\">\n\t\t<style type=text/css>\n\t\t\tbody {\n\t\t\t\t  background-color: #ebebeb;\n\t\t\t\t  font-family: -apple-system;\n\t\t\t\t  font-family: \"-apple-system\", \"Helvetica Neue\", \"Roboto\", \"Segoe UI\", sans-serif;\n\t\t\t\t}\n            table {\n                font-size: 10pt;\n            }\n    \n\n\t\t</style>\n\t\t<title>%%ML:WeChat Chat History%% %%USERNAME%%</title>\n\t</head>\n\t<body>\n    <table width=\"400\" border=\"0\" style=\"border-collapse:separate; word-break:break-all; word-wrap:break-word;\" align=\"center\">\n    %%TBODY%%\n    </table>\n\t</body>\n</html>\n"
  },
  {
    "path": "WechatExporter/res/templates/listitem.html",
    "content": "    <tr height=\"60\">\n      <td width=\"100\" align=\"center\">\n        <img src=\"%%ITEMPICPATH%%\" style=\"float:left;max-width:60px;max-height:60px\" />\n      </td>\n      <td>\n        <a href=\"%%ITEMLINK%%\">%%ITEMTEXT%%</a>\n      </td>\n    </tr>\n"
  },
  {
    "path": "WechatExporter/res/templates/member.html",
    "content": "\t\t\t\t<div class=\"msg media %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<img rawUrl=\"%%IMGPATH%%\" src=\"%%IMGTHUMBPATH%%\" class=\"image img-%%ALIGNMENT%%_triangle\" onclick=\"javascript:showImagePopup(event);\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/members.html",
    "content": "\t\t\t\t<div class=\"msg media %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<img rawUrl=\"%%IMGPATH%%\" src=\"%%IMGTHUMBPATH%%\" class=\"image img-%%ALIGNMENT%%_triangle\" onclick=\"javascript:showImagePopup(event);\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/msg.html",
    "content": "\t\t\t\t<div class=\"msg chat %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\">\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<div class=\"triangle\"></div>\n\t\t\t\t\t\t<span class=\"dont-break-out msg-text\"><pre>%%MESSAGE%%</pre></span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/notice.html",
    "content": "\t\t\t\t<div class=\"msg chat-notice %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\">\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<span class=\"dont-break-out msg-text\"><pre>%%MESSAGE%%</pre></span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/plainshare.html",
    "content": "\t\t\t\t<div class=\"msg chat %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<div class=\"triangle\"></div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<a href=\"%%SHARINGURL%%\"><b><span class=\"dont-break-out msg-text\"><pre>%%SHARINGTITLE%%</pre></span></b></a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<span class=\"dont-break-out msg-text\"><pre>%%MESSAGE%%</pre></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"appinfo\">\n\t\t\t\t\t\t\t<img src=\"%%APPICONPATH%%\" class=\"app-icon\" /><span class=\"app-name\">%%APPNAME%%</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/refermsg.html",
    "content": "\t\t\t\t<div class=\"msg chat %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\">\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<div class=\"triangle\"></div>\n\t\t\t\t\t\t<span class=\"dont-break-out refermsg msg-text\"><pre>「%%REFERNAME%%：%%REFERMSG%%」</pre></span><br /><span>- - - - - - - - - - - - - - -</span><br /><span class=\"dont-break-out msg-text\"><pre>%%MESSAGE%%</pre></span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/scripts.html",
    "content": "    (function() {\n        var msgArray = %%JSON_DATA%%;\n        for (var idx = 0; idx < msgArray.length; idx++)\n        {\n          window.moreWechatMsgs.push(msgArray[idx]);\n        }\n\n        msgArray = null;\n        loadMsgsForNextPage();\n    })();\n\n\n"
  },
  {
    "path": "WechatExporter/res/templates/share.html",
    "content": "\t\t\t\t<div class=\"msg chat %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<div class=\"triangle\"></div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div style=\"display:table-cell;\">\n\t\t\t\t\t\t\t\t<img data-src=\"%%SHARINGIMGPATH%%\" style=\"max-width:60px;max-height:60px\" clas=\"lazyload\" />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div style=\"display:table-cell; vertical-align: middle; padding-left: 8px;\">\n\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t<a href=\"%%SHARINGURL%%\"><b>%%SHARINGTITLE%%</b></a>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t<span class=\"dont-break-out msg-text\"><pre>%%MESSAGE%%</pre></span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"appinfo\">\n\t\t\t\t\t\t\t<img src=\"%%APPICONPATH%%\" class=\"app-icon\" /><span class=\"app-name\">%%APPNAME%%</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/system.html",
    "content": "\t\t        <div class=\"msg chat-notice %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\"><span class=\"dont-break-out\">%%ML:System Message:%% %%MESSAGE%%</span></div>\n"
  },
  {
    "path": "WechatExporter/res/templates/thumb.html",
    "content": "\t\t\t\t<div class=\"msg media %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<img src=\"%%IMGTHUMBPATH%%\" class=\"image img-%%ALIGNMENT%%_triangle\" /><span class=\"dont-break-out msg-text\">%%MESSAGE%%</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/transfer.html",
    "content": "\t\t\t\t<div class=\"msg chat %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<div class=\"triangle\"></div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<span class=\"dont-break-out msg-text\"><pre>%%MESSAGE%%</pre></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"appinfo\">\n\t\t\t\t\t\t\t<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAARVBMVEXokT7////+/fzqnF745dvzzrnyyrP44tfxxKnokkD99vL89PD88u356eHtsIntroPsqn7rom3pmljpl1Lxwqjtso3sqXzLUHXSAAAAhElEQVRIx+3OSQ6DMBBE0a72AGYmJLn/UWNEsooE1RJskN/6l1RS3Eaabb3zmCz90EIX4UUFKj5/VMiedD+2yNSxfa1Y9fydTeT6d4d9/3dMgwCcPyjscChcPNBou9SNQonYeCec/neH5BRZMwir/t6heUBD4vtF0bzEYFrvmMxJirv4AJJMA1GI1RpRAAAAAElFTkSuQmCC\" class=\"transfer-icon\" /><span class=\"app-name\">%%ML:Weixin Transfer%%</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/video.html",
    "content": "\t\t\t\t<div class=\"msg media %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<video controls preload=\"none\" autoplay=\"false\" data-poster=\"%%THUMBPATH%%\" class=\"lazyload\"><source src=\"%%VIDEOPATH%%\" type=\"video/mp4\"><a href=\"%%VIDEOPATH%%\">%%ML:Play%%</a></video>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/videonew.html",
    "content": "\t\t\t\t<div class=\"msg media %%ALIGNMENT%% %%EXTRA_CLS%%\" msgid=\"%%MSGID%%\" msgtype=\"%%MSGTYPE%%\">\n\t\t\t\t\t<div class=\"avatar-box\">\n\t\t\t\t\t\t<img src=\"%%AVATAR%%\" class=\"avatar\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"nt-box\"><span class=\"dspname %%ALIGNMENT%%\" wxId=\"%%WXNAME%%\">%%NAME%%</span> %%TIME%%</div>\n\t\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t\t<video controls poster=\"%%THUMBPATH%%\"><source src=\"%%VIDEOPATH%%\" type=\"video/mp4\"><a href=\"%%VIDEOPATH%%\">%%ML:Play%%</a></video>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n"
  },
  {
    "path": "WechatExporter/res/templates/wxemoji.html",
    "content": "<img src=\"%%EMOJI_PATH%%\" title=\"%%EMOJI_TITLE%%\" rawemoji=\"%%EMOJI_RAW%%\" class=\"wxemoji\" />"
  },
  {
    "path": "WechatExporter/res/templates_txt/audio.html",
    "content": "%%NAME%% (%%TIME%%):%%MESSAGE%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/card.html",
    "content": "%%NAME%% (%%TIME%%):%%CARDTYPE%% %%CARDNAME%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/channels.html",
    "content": "%%NAME%% (%%TIME%%):%%MESSAGE%% - %%CHANNELS%% %%CARDNAME%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/emoji.html",
    "content": "%%NAME%% (%%TIME%%):%%ML:[Emoji]%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/frame.html",
    "content": "%%DISPLAYNAME%% - %%ML:WeChat Chat History%%\n\n%%BODY%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/image.html",
    "content": "%%NAME%% (%%TIME%%):%%ML:[Photo]%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/listframe.html",
    "content": "%%ML:WeChat Chat History%% %%USERNAME%%\n\n\n%%TBODY%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/listitem.html",
    "content": "%%ITEMLINK%%\n\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/msg.html",
    "content": "%%NAME%% (%%TIME%%):%%MESSAGE%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/notice.html",
    "content": "%%MESSAGE%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/plainshare.html",
    "content": "%%NAME%% (%%TIME%%):%%SHARINGTITLE%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/refermsg.html",
    "content": "「%%REFERNAME%%：%%REFERMSG%%」\n- - - - - - - - - - - - - - -\n%%MESSAGE%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/scripts.html",
    "content": ""
  },
  {
    "path": "WechatExporter/res/templates_txt/share.html",
    "content": "%%NAME%% (%%TIME%%):%%SHARINGTITLE%% - %%APPNAME%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/system.html",
    "content": "%%ML:System Message:%% %%MESSAGE%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/thumb.html",
    "content": "%%NAME%% (%%TIME%%):%%MESSAGE%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/transfer.html",
    "content": "%%NAME%% (%%TIME%%):%%ML:Weixin Transfer%% %%MESSAGE%%"
  },
  {
    "path": "WechatExporter/res/templates_txt/video.html",
    "content": "%%NAME%% (%%TIME%%):%%ML:[Video]%%\n"
  },
  {
    "path": "WechatExporter/res/templates_txt/wxemoji.html",
    "content": "[%%EMOJI_RAW%%]"
  },
  {
    "path": "WechatExporter/res/zh-Hans.txt",
    "content": "[\n\t{\n\t\t\"key\": \"[Video]\",\n\t\t\"value\": \"[视频]\"\n\t},\n\t{\n\t\t\"key\": \"(Video Missed)\",\n\t\t\"value\": \"（视频丢失）\"\n\t},\n\t{\n\t\t\"key\": \"[Emoji]\",\n\t\t\"value\": \"[表情]\"\n\t},\n\t{\n\t\t\"key\": \"[Audio]\",\n\t\t\"value\": \"[语音]\"\n\t},\n\t{\n\t\t\"key\": \"[Audio %s]\",\n\t\t\"value\": \"[语音 %s]\"\n\t},\n\t{\n\t\t\"key\": \"[Video/Audio Call]\",\n\t\t\"value\": \"[视频/语音通话]\"\n\t},\n\t{\n\t\t\"key\": \"[Photo]\",\n\t\t\"value\": \"[图片]\"\n\t},\n\t{\n\t\t\"key\": \"[Location]\",\n\t\t\"value\": \"[位置]\"\n\t},\n\t{\n\t\t\"key\": \"[Location] %s (%s,%s)\",\n\t\t\"value\": \"[位置] %s (%s,%s)\"\n\t},\n\t{\n\t\t\"key\": \"[Red Packet]\",\n\t\t\"value\": \"[红包]\"\n\t},\n\t{\n\t\t\"key\": \"[Transfer]\",\n\t\t\"value\": \"[转账]\"\n\t},\n\t{\n\t\t\"key\": \"[Real-time Location]\",\n\t\t\"value\": \"[实时位置共享]\"\n\t},\n\t{\n\t\t\"key\": \"[File]\",\n\t\t\"value\": \"[文件]\"\n\t},\n\t{\n\t\t\"key\": \"[Link]\",\n\t\t\"value\": \"[链接]\"\n\t},\n\t{\n\t\t\"key\": \"[Contact Card]\",\n\t\t\"value\": \"[个人名片]\"\n\t},\n\t{\n\t\t\"key\": \"[Channel Card]\",\n\t\t\"value\": \"[视频号名片]\"\n\t},\n\t{\n\t\t\"key\": \"[Contact Card] %s\",\n\t\t\"value\": \"[个人名片] %s\"\n\t},\n\t{\n\t\t\"key\": \"[Channel Card] %s\",\n\t\t\"value\": \"[视频号名片] %s\"\n\t},\n\t{\n\t\t\"key\": \"Channels\",\n\t\t\"value\": \"视频号\"\n\t},\n\t{\n\t\t\"key\": \"iTunes: %s\",\n\t\t\"value\": \"iTunes： %s\"\n\t},\n\t\n\t{\n\t\t\"key\": \"Failed to parse the backup data of iTunes in the directory: %s\",\n\t\t\"value\": \"解析iTunes备份目录失败： %s\"\n\t},\n\t{\n\t\t\"key\": \"Previous task has not completed.\",\n\t\t\"value\": \"前一个导出任务还未结束。\"\n\t},\n\t\n\t{\n\t\t\"key\": \"Can't access output directory: %s\",\n\t\t\"value\": \"不能访问输出目录： %s\"\n\t},\n\t{\n\t\t\"key\": \"Finding WeChat accounts...\",\n\t\t\"value\": \"查找微信登录账户...\"\n\t},\n\t{\n\t\t\"key\": \"Failed to find WeChat account.\",\n\t\t\"value\": \"查找微信登录账户失败。\"\n\t},\n\t{\n\t\t\"key\": \"%d WeChat account(s) found.\",\n\t\t\"value\": \"找到 %d个账号的消息记录。\"\n\t},\n\t{\n\t\t\"key\": \"Handling account: %s, WeChat Id: %s\",\n\t\t\"value\": \"开始处理微信账户: %s，微信号：%s\"\n\t},\n\t{\n\t\t\"key\": \"Reading account info.\",\n\t\t\"value\": \"读取账号信息\"\n\t},\n\t{\n\t\t\"key\": \"Reading chat info\",\n\t\t\"value\": \"读取对话信息\"\n\t},\n\t{\n\t\t\"key\": \"%d chats found.\",\n\t\t\"value\": \"找到%d条对话。\"\n\t},\n\t{\n\t\t\"key\": \"%d/%d: Handling the chat with %s\",\n\t\t\"value\": \"%d/%d: 处理与 %s 的对话\"\n\t},\n\t{\n\t\t\"key\": \"Skip subscription: %s\",\n\t\t\"value\": \"跳过订阅号：%s\"\n\t},\n\t{\n\t\t\"key\": \"Succeeded handling %d messages.\",\n\t\t\"value\": \"成功处理%d条消息\"\n\t},\n\t{\n\t\t\"key\": \"Completed in %s.\",\n\t\t\"value\": \"导出完成，耗时：%s\"\n\t},\n\t{\n\t\t\"key\": \"Cancelled in %s.\",\n\t\t\"value\": \"导出被取消，用时：%s\"\n\t},\n\t{\n\t\t\"key\": \"Waiting for images(%d) downloading.\",\n\t\t\"value\": \"等待%d张图片下载。\"\n\t},\n\t{\n\t\t\"key\": \"iTunes Version: %s, iOS Version: %s, WeChat Version: %s\",\n\t\t\"value\": \"iTunes版本：%s, iOS版本：%s, 微信版本：%s\"\n\t},\n\t{\n\t\t\"key\": \"iTunes Backup: %s\",\n\t\t\"value\": \"iTunes备份目录: %s\"\n\t},\n\t{\n\t\t\"key\": \"<< %s\",\n\t\t\"value\": \"<< %s\"\n\t},\n\t{\n\t\t\"key\": \"%s Ends >>\",\n\t\t\"value\": \"%s 结束 >>\"\n\t},\n\t{\n\t\t\"key\": \"[File: %s]\",\n\t\t\"value\": \"[文件: %s]\"\n\t},\n\t{\n\t\t\"key\": \"WeChat Chat History\",\n\t\t\"value\": \"微信聊天记录\"\n\t},\n\t{\n\t\t\"key\": \"Play\",\n\t\t\"value\": \"播放\"\n\t},\n\t{\n\t\t\"key\": \"Press [Enter] key after inputting keywords\",\n\t\t\"value\": \"输入关键词后按回车键\"\n\t},\n\t{\n\t\t\"key\": \"Show Photos Only\",\n\t\t\"value\": \"仅显示照片\"\n\t},\n\t{\n\t\t\"key\": \"Show Videos Only\",\n\t\t\"value\": \"仅显示视频\"\n\t},\n\t{\n\t\t\"key\": \"Show All Messages\",\n\t\t\"value\": \"显示所有消息\"\n\t},\n\t{\n\t\t\"key\": \"Photos\",\n\t\t\"value\": \"照片\"\n\t},\n\t{\n\t\t\"key\": \"Videos\",\n\t\t\"value\": \"视频\"\n\t},\n\t{\n\t\t\"key\": \"All\",\n\t\t\"value\": \"所有\"\n\t},\n\t{\n\t\t\"key\": \" (WeChat ID: %s)\",\n\t\t\"value\": \" （微信号：%s）\"\n\t},\n\t{\n\t\t\"key\": \"Weixin Transfer\",\n\t\t\"value\": \"微信转账\"\n\t},\n\n\t{\n\t\t\"key\": \"Transfer_Subtype_1\",\n\t\t\"value\": \"转账\"\n\t},\n\t{\n\t\t\"key\": \"Transfer_Subtype_3\",\n\t\t\"value\": \"已收款\"\n\t},\n\t{\n\t\t\"key\": \"Transfer_Subtype_5\",\n\t\t\"value\": \"已接收\"\n\t},\n\t{\n\t\t\"key\": \"Transfer_Subtype_4\",\n\t\t\"value\": \"已退还\"\n\t},\n\t\n\t{\n\t\t\"key\": \"Transfer_Subtype_8\",\n\t\t\"value\": \"已被接收\"\n\t},\n\t{\n\t\t\"key\": \"Transfer_Subtype_9\",\n\t\t\"value\": \"已被退还\"\n\t},\n\t{\n\t\t\"key\": \"Transfer_Subtype_10\",\n\t\t\"value\": \"已过期\"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_1\",\n\t\t\"value\": \"向%s转账\"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_3\",\n\t\t\"value\": \"来自%s的转账 - 已收款\"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_5\",\n\t\t\"value\": \"来自%s的转账 - 已收款\"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_4\",\n\t\t\"value\": \"来自%s的转账 - 已退还\"\n\t},\n\t\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_8\",\n\t\t\"value\": \"向%s转账 - 已被接收\"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_9\",\n\t\t\"value\": \"向%s转账 - 已被退还\"\n\t},\n\t{\n\t\t\"key\": \"Group_Transfer_Subtype_10\",\n\t\t\"value\": \"已过期\"\n\t},\n\t{\n\t\t\"key\": \"\",\n\t\t\"value\": \"\"\n\t},\n\t{\n\t\t\"key\": \"\",\n\t\t\"value\": \"\"\n\t}\n]\n"
  },
  {
    "path": "WechatExporter/zh-Hans.lproj/Localizable.strings",
    "content": "/* \n  Localizable.strings\n  WechatExporter\n\n  Created by Matthew on 2021/3/10.\n  Copyright © 2021 Matthew. All rights reserved.\n*/\n\n\"btn-yes\" = \"是\";\n\"btn-no\" = \"否\";\n\"btn-ok\" = \"确定\";\n\"btn-cancel\" = \"取消\";\n\"err-no-output-dir\" = \"请选择输出目录。\";\n\"err-output-dir-doesnt-exist\" = \"输出目录不存在。\";\n\"err-backup-dir-doesnt-exist\" = \"iTunes备份目录不存在。\";\n\"err-no-backup-dir\" = \"请选择iTunes备份目录。\";\n\"err-encrypted-bkp-not-supported\" = \"不支持加密的iTunes备份。\";\n\"err-exp-is-running\" = \"导出已经在执行。\";\n\"err-failed-to-parse-backup\" = \"解析iTunes Backup文件失败。\";\n\"err-wrong-param\" = \"参数错误。\";\n\"txt-all-wechat-users\" = \"所有微信账户\";\n\"prompt-new-version-found\" = \"发现新版本：%@，是否下载？\";\n\"session-deleted\" = \"(已删除)\";\n\"grant-full-disk-access\" = \"因为WechatExporter程序需要读取iTunes备份数据，请先授权完全磁盘访问权限：苹果菜单 › 系统偏好设置... › 安全心与隐私 › 隐私 › 完全磁盘访问权限\";\n\"btn-grant-full-disk-access\" = \"授权完全磁盘访问权限\";\n\"not-text-msg\" = \"-\";\n\"show-logs\" = \"显示日志\";\n\"hide-logs\" = \"隐藏日志\";\n\"alert-update-options\" = \"你可以在主菜单中修改导出格式和选项：\";\n\"no-prev-exp-found\" = \"输出目录下不存在前一次导出的信息，【增量导出】将被忽略。\";\n\"prev-exp-found\" = \"输出目录下发现了%s的导出数据，本次导出将采用“增量导出”模式，并复用前一次的设置。\";\n\"invld-inc-exp-for-multi-users\" = \"由于设计错误，对于多个微信账号的增量备份复原可能产生错误，请更换新的目录导出。\";\n"
  },
  {
    "path": "WechatExporter/zh-Hans.lproj/Main.strings",
    "content": "\n/* Class = \"NSButtonCell\"; title = \"...\"; ObjectID = \"1GT-FK-TVG\"; */\n\"1GT-FK-TVG.title\" = \"...\";\n\n/* Class = \"NSMenuItem\"; title = \"WechatExporter\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"微信聊天记录导出程序\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit WechatExporter\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"退出WechatExporter\";\n\n/* Class = \"NSButtonCell\"; title = \"Check\"; ObjectID = \"58n-zQ-LqF\"; */\n\"58n-zQ-LqF.title\" = \"Check\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Table View Cell\"; ObjectID = \"5Mi-xr-dCf\"; */\n\"5Mi-xr-dCf.title\" = \"Table View Cell\";\n\n/* Class = \"NSMenuItem\"; title = \"Incremental Exporting\"; ObjectID = \"5c2-wM-XIf\"; */\n\"5c2-wM-XIf.title\" = \"增量导出\";\n\n/* Class = \"NSMenuItem\"; title = \"Including Subscriptions\"; ObjectID = \"GbO-lL-BLb\"; */\n\"GbO-lL-BLb.title\" = \"保留公众号\";\n\n/* Class = \"NSMenuItem\"; title = \"About WechatExporter\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"关于WechatExporter\";\n\n/* Class = \"NSTableColumn\"; headerCell.title = \"Name\"; ObjectID = \"8FB-hv-fkB\"; */\n\"8FB-hv-fkB.headerCell.title\" = \"名称\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSTextFieldCell\"; title = \"iTunes Backup Directory:\"; ObjectID = \"BqF-Du-vMt\"; */\n\"BqF-Du-vMt.title\" = \"iTunes备份目录：\";\n\n/* Class = \"NSButtonCell\"; title = \"Cancel\"; ObjectID = \"CHb-bh-1kG\"; */\n\"CHb-bh-1kG.title\" = \"取消\";\n\n/* Class = \"NSMenuItem\"; title = \"From Earlier To Newer\"; ObjectID = \"eJH-AO-QUD\"; */\n\"eJH-AO-QUD.title\" = \"按消息时间倒序导出\";\n\n/* Class = \"NSMenuItem\"; title = \"Text\"; ObjectID = \"GeM-zb-UkW\"; */\n\"GeM-zb-UkW.title\" = \"文本模式\";\n\n/* Class = \"NSMenuItem\"; title = \"Show Message Filter\"; ObjectID = \"HWn-ip-mii\"; */\n\"HWn-ip-mii.title\" = \"显示聊天记录过滤\";\n\n/* Class = \"NSWindow\"; title = \"Wechat Exporter\"; ObjectID = \"IQv-IB-iLA\"; */\n\"IQv-IB-iLA.title\" = \"Wechat Exporter\";\n\n/* Class = \"NSButtonCell\"; title = \"...\"; ObjectID = \"LFI-6K-mnE\"; */\n\"LFI-6K-mnE.title\" = \"...\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Text Cell\"; ObjectID = \"O0i-cF-8Ix\"; */\n\"O0i-cF-8Ix.title\" = \"Text Cell\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide WechatExporter\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"隐藏WechatExporter\";\n\n/* Class = \"NSMenuItem\"; title = \"Check Update Automatically\"; ObjectID = \"Pyu-qm-bT0\"; */\n\"Pyu-qm-bT0.title\" = \"自动检查更新\";\n\n/* Class = \"NSMenuItem\"; title = \"Output Detailed Logs\"; ObjectID = \"Pyu-qm-bT0\"; */\n\"Pyu-qm-bT0.title\" = \"输出更多日志\";\n\n/* Class = \"NSMenuItem\"; title = \"Open the Folder After Exporting\"; ObjectID = \"TWh-AV-CCF\"; */\n\"TWh-AV-CCF.title\" = \"导出后打开目录\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Text Cell\"; ObjectID = \"Q3f-RG-97F\"; */\n\"Q3f-RG-97F.title\" = \"Text Cell\";\n\n/* Class = \"NSMenuItem\"; title = \"PDF\"; ObjectID = \"T2A-kG-daZ\"; */\n\"T2A-kG-daZ.title\" = \"PDF\";\n\n/* Class = \"NSMenuItem\"; title = \"File\"; ObjectID = \"UCE-Dj-bc1\"; */\n\"UCE-Dj-bc1.title\" = \"文件\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"隐藏其他\";\n\n/* Class = \"NSMenuItem\"; title = \"Load Messages Asynchronously(HTML Mode)\"; ObjectID = \"WOF-ft-ZWq\"; */\n\"WOF-ft-ZWq.title\" = \"异步加载聊天记录\";\n\n/* Class = \"NSButtonCell\"; title = \"Show Logs\"; ObjectID = \"WmO-MK-b5O\"; */\n\"WmO-MK-b5O.title\" = \"显示日志\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Text Cell\"; ObjectID = \"X6v-WO-gkU\"; */\n\"X6v-WO-gkU.title\" = \"Text Cell\";\n\n/* Class = \"NSTableColumn\"; headerCell.title = \"Number of Msgs\"; ObjectID = \"bsY-Rr-JHi\"; */\n\"bsY-Rr-JHi.headerCell.title\" = \"聊天记录数\";\n\n/* Class = \"NSMenuItem\"; title = \"Load Messages On Scrolling\"; ObjectID = \"c4Q-qP-ppM\"; */\n\"c4Q-qP-ppM.title\" = \"上滑滚动时加载更多聊天记录\";\n\n/* Class = \"NSMenuItem\"; title = \"HTML\"; ObjectID = \"cWo-5k-XRR\"; */\n\"cWo-5k-XRR.title\" = \"HTML\";\n\n/* Class = \"NSButtonCell\"; title = \"Close\"; ObjectID = \"dkl-Nk-ye1\"; */\n\"dkl-Nk-ye1.title\" = \"关闭\";\n\n/* Class = \"NSMenuItem\"; title = \"Options\"; ObjectID = \"fod-ax-X6A\"; */\n\"fod-ax-X6A.title\" = \"选项\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Table View Cell\"; ObjectID = \"fos-aD-v3q\"; */\n\"fos-aD-v3q.title\" = \"Table View Cell\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Text Cell\"; ObjectID = \"gtr-oV-Pno\"; */\n\"gtr-oV-Pno.title\" = \"Text Cell\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"k5c-yf-BQl\"; */\n\"k5c-yf-BQl.title\" = \"帮助\";\n\n/* Class = \"NSTableColumn\"; headerCell.title = \"Wechat Account\"; ObjectID = \"mqg-xB-lDI\"; */\n\"mqg-xB-lDI.headerCell.title\" = \"微信账号\";\n\n/* Class = \"NSMenu\"; title = \"Format\"; ObjectID = \"or1-Xl-Us0\"; */\n\"or1-Xl-Us0.title\" = \"格式\";\n\n/* Class = \"NSTableColumn\"; headerCell.title = \"Last Message\"; ObjectID = \"hIY-8j-rpO\"; */\n\"hIY-8j-rpO.headerCell.title\" = \"最新消息\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"pOf-8G-Ji8\"; */\n\"pOf-8G-Ji8.title\" = \"帮助\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Table View Cell\"; ObjectID = \"qJ3-t0-qJh\"; */\n\"qJ3-t0-qJh.title\" = \"Table View Cell\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Table View Cell\"; ObjectID = \"sWr-dF-dlL\"; */\n\"sWr-dF-dlL.title\" = \"Table View Cell\";\n\n/* Class = \"NSMenuItem\"; title = \"Save Avatar/Emoji in Chat Folder\"; ObjectID = \"tGU-e5-isp\"; */\n\"tGU-e5-isp.title\" = \"头像和表情存放到聊天记录子目录\";\n\n/* Class = \"NSMenu\"; title = \"Options\"; ObjectID = \"uG2-8c-wjP\"; */\n\"uG2-8c-wjP.title\" = \"选项\";\n\n/* Class = \"NSMenu\"; title = \"WechatExporter\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"WechatExporter\";\n\n/* Class = \"NSTextFieldCell\"; title = \"Output Directory:\"; ObjectID = \"uh4-ix-NCq\"; */\n\"uh4-ix-NCq.title\" = \"输出目录：\";\n\n/* Class = \"NSMenuItem\"; title = \"Format\"; ObjectID = \"v5F-yk-5ct\"; */\n\"v5F-yk-5ct.title\" = \"格式\";\n\n/* Class = \"NSMenuItem\"; title = \"WechatExporter Help\"; ObjectID = \"vp6-q8-ljb\"; */\n\"vp6-q8-ljb.title\" = \"WechatExporter帮助\";\n\n/* Class = \"NSButtonCell\"; title = \"Export\"; ObjectID = \"wX7-rb-1cu\"; */\n\"wX7-rb-1cu.title\" = \"导出\";\n\n/* Class = \"NSMenu\"; title = \"File\"; ObjectID = \"yFn-8R-v2s\"; */\n\"yFn-8R-v2s.title\" = \"文件\";\n"
  },
  {
    "path": "WechatExporter.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 50;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t341A5B2F253828F300914BE3 /* res in Resources */ = {isa = PBXBuildFile; fileRef = 341A5B2E253828F300914BE3 /* res */; };\n\t\t342B0349281125C7009FBD5E /* Template.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342B0347281125C7009FBD5E /* Template.cpp */; };\n\t\t342EDAFC25241D91006A295A /* WechatParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342EDAFA25241D91006A295A /* WechatParser.cpp */; };\n\t\t342EDB00252450EB006A295A /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 342EDAFF252450EB006A295A /* libcurl.tbd */; };\n\t\t342EDB0325245206006A295A /* Downloader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342EDB0125245206006A295A /* Downloader.cpp */; };\n\t\t342EDB062524700A006A295A /* Exporter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342EDB042524700A006A295A /* Exporter.cpp */; };\n\t\t342EDB0925247852006A295A /* Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342EDB0825247852006A295A /* Utils.cpp */; };\n\t\t342EDB0B252495BD006A295A /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 342EDB0A252495BD006A295A /* libxml2.tbd */; };\n\t\t343F6117252322D500FFE085 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 343F6116252322D500FFE085 /* AppDelegate.mm */; };\n\t\t343F611A252322D500FFE085 /* ViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 343F6119252322D500FFE085 /* ViewController.mm */; };\n\t\t343F611C252322D600FFE085 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 343F611B252322D600FFE085 /* Assets.xcassets */; };\n\t\t343F611F252322D600FFE085 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 343F611D252322D600FFE085 /* Main.storyboard */; };\n\t\t343F6122252322D600FFE085 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 343F6121252322D600FFE085 /* main.m */; };\n\t\t343F612D25234BD300FFE085 /* ITunesParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 343F612C25234BD300FFE085 /* ITunesParser.cpp */; };\n\t\t343F613025234BFC00FFE085 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 343F612F25234BFC00FFE085 /* libsqlite3.tbd */; };\n\t\t344AF3A827844A6D00ED7586 /* LICENSES in Resources */ = {isa = PBXBuildFile; fileRef = 344AF3A727844A6D00ED7586 /* LICENSES */; };\n\t\t344AF3AB2784551900ED7586 /* libmp3lame.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 344AF3AA2784551900ED7586 /* libmp3lame.0.dylib */; };\n\t\t344AF3AC2784551900ED7586 /* libmp3lame.0.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 344AF3AA2784551900ED7586 /* libmp3lame.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t345CE65825F8A456003DDD0F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 345CE65A25F8A456003DDD0F /* Localizable.strings */; };\n\t\t3471A77A25EB5A9A007D186B /* FileSystem.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34AB9A1325B8908D006D3617 /* FileSystem.cpp */; };\n\t\t347E601525C7E55100B33BAB /* SessionDataSource.mm in Sources */ = {isa = PBXBuildFile; fileRef = 347E601425C7E55100B33BAB /* SessionDataSource.mm */; };\n\t\t3489DE46262A843C00F51416 /* AsyncExecutor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3489DE44262A843C00F51416 /* AsyncExecutor.cpp */; };\n\t\t3489DE50262E74BF00F51416 /* AsyncTask.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3489DE4E262E74BE00F51416 /* AsyncTask.cpp */; };\n\t\t3489DE55262EB03000F51416 /* TaskManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3489DE53262EB03000F51416 /* TaskManager.cpp */; };\n\t\t3497342625F384D100CAC6CD /* Updater.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3497342425F384D100CAC6CD /* Updater.cpp */; };\n\t\t3497342B25F75D4300CAC6CD /* HttpHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3497342A25F75D4300CAC6CD /* HttpHelper.mm */; };\n\t\t3497891B26037783001D1F8F /* AppConfiguration.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3497891A26037783001D1F8F /* AppConfiguration.mm */; };\n\t\t349DAD2C255D3BB800BFE204 /* XmlParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 349DAD2A255D3BB800BFE204 /* XmlParser.cpp */; };\n\t\t349DAD32255E6A0700BFE204 /* Utils_thread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 349DAD31255E6A0600BFE204 /* Utils_thread.cpp */; };\n\t\t34A0336125E34B0300E06CC5 /* MessageParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34A0335F25E34B0300E06CC5 /* MessageParser.cpp */; };\n\t\t34C0E1CC277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1C6277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib */; };\n\t\t34C0E1CD277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1C6277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t34C0E1CE277FDAA800CD4ADE /* libplist-2.0.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1C7277FDAA800CD4ADE /* libplist-2.0.3.dylib */; };\n\t\t34C0E1CF277FDAA800CD4ADE /* libplist-2.0.3.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1C7277FDAA800CD4ADE /* libplist-2.0.3.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t34C0E1D0277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1C8277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib */; };\n\t\t34C0E1D1277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1C8277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t34C0E1D2277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1C9277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib */; };\n\t\t34C0E1D3277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1C9277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t34C0E1D4277FDAA800CD4ADE /* libcrypto.1.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1CA277FDAA800CD4ADE /* libcrypto.1.1.dylib */; };\n\t\t34C0E1D5277FDAA800CD4ADE /* libcrypto.1.1.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1CA277FDAA800CD4ADE /* libcrypto.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t34C0E1D6277FDAA800CD4ADE /* libssl.1.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1CB277FDAA800CD4ADE /* libssl.1.1.dylib */; };\n\t\t34C0E1D7277FDAA800CD4ADE /* libssl.1.1.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1CB277FDAA800CD4ADE /* libssl.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t34CC43BC275F001000ABC2BB /* IDeviceBackup.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34CC43BB275F001000ABC2BB /* IDeviceBackup.cpp */; };\n\t\t34E3E90A2531BD8E0093042D /* Utils_md5.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E9092531BD8E0093042D /* Utils_md5.cpp */; };\n\t\t34E3E90C2531BE200093042D /* Utils_protobuf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E90B2531BE1F0093042D /* Utils_protobuf.cpp */; };\n\t\t34E3E9242535555F0093042D /* RawMessage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E9232535555F0093042D /* RawMessage.cpp */; };\n\t\t34EC42B1272AEB6F0013570B /* ResManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34EC42B0272AEB6F0013570B /* ResManager.cpp */; };\n\t\t34ED31E825528A1800C42698 /* Utils_audio.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34ED31E725528A1800C42698 /* Utils_audio.cpp */; };\n\t\t34ED32082552A98600C42698 /* Utils_silk.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34ED32072552A98600C42698 /* Utils_silk.cpp */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t34C0E1D8277FDAA800CD4ADE /* Embed Libraries */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t\t34C0E1D1277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib in Embed Libraries */,\n\t\t\t\t34C0E1D3277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib in Embed Libraries */,\n\t\t\t\t34C0E1CD277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib in Embed Libraries */,\n\t\t\t\t34C0E1D5277FDAA800CD4ADE /* libcrypto.1.1.dylib in Embed Libraries */,\n\t\t\t\t34C0E1CF277FDAA800CD4ADE /* libplist-2.0.3.dylib in Embed Libraries */,\n\t\t\t\t344AF3AC2784551900ED7586 /* libmp3lame.0.dylib in Embed Libraries */,\n\t\t\t\t34C0E1D7277FDAA800CD4ADE /* libssl.1.1.dylib in Embed Libraries */,\n\t\t\t);\n\t\t\tname = \"Embed Libraries\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t3407BE1427748AE2008D0F9E /* WechatSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WechatSource.h; sourceTree = \"<group>\"; };\n\t\t34140A12254988C4003CE75A /* ExportNotifierImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExportNotifierImpl.h; sourceTree = \"<group>\"; };\n\t\t341A5B2E253828F300914BE3 /* res */ = {isa = PBXFileReference; lastKnownFileType = folder; path = res; sourceTree = \"<group>\"; };\n\t\t342B03462811255E009FBD5E /* Template.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Template.h; sourceTree = \"<group>\"; };\n\t\t342B0347281125C7009FBD5E /* Template.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Template.cpp; sourceTree = \"<group>\"; };\n\t\t342EDAFA25241D91006A295A /* WechatParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = WechatParser.cpp; sourceTree = \"<group>\"; };\n\t\t342EDAFB25241D91006A295A /* WechatParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WechatParser.h; sourceTree = \"<group>\"; };\n\t\t342EDAFD25241E64006A295A /* WechatObjects.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WechatObjects.h; sourceTree = \"<group>\"; };\n\t\t342EDAFE2524485C006A295A /* Utils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Utils.h; sourceTree = \"<group>\"; };\n\t\t342EDAFF252450EB006A295A /* libcurl.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libcurl.tbd; path = usr/lib/libcurl.tbd; sourceTree = SDKROOT; };\n\t\t342EDB0125245206006A295A /* Downloader.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Downloader.cpp; sourceTree = \"<group>\"; };\n\t\t342EDB0225245206006A295A /* Downloader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Downloader.h; sourceTree = \"<group>\"; };\n\t\t342EDB042524700A006A295A /* Exporter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Exporter.cpp; sourceTree = \"<group>\"; };\n\t\t342EDB052524700A006A295A /* Exporter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Exporter.h; sourceTree = \"<group>\"; };\n\t\t342EDB07252471D6006A295A /* Logger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Logger.h; sourceTree = \"<group>\"; };\n\t\t342EDB0825247852006A295A /* Utils.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Utils.cpp; sourceTree = \"<group>\"; };\n\t\t342EDB0A252495BD006A295A /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };\n\t\t343F6112252322D500FFE085 /* WechatExporter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WechatExporter.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t343F6115252322D500FFE085 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = \"<group>\"; };\n\t\t343F6116252322D500FFE085 /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = \"<group>\"; };\n\t\t343F6118252322D500FFE085 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = \"<group>\"; };\n\t\t343F6119252322D500FFE085 /* ViewController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ViewController.mm; sourceTree = \"<group>\"; };\n\t\t343F611B252322D600FFE085 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t343F611E252322D600FFE085 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t343F6120252322D600FFE085 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t343F6121252322D600FFE085 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = \"<group>\"; };\n\t\t343F6123252322D600FFE085 /* WechatExporter.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WechatExporter.entitlements; sourceTree = \"<group>\"; };\n\t\t343F612B25234BD300FFE085 /* ITunesParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITunesParser.h; sourceTree = \"<group>\"; };\n\t\t343F612C25234BD300FFE085 /* ITunesParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ITunesParser.cpp; sourceTree = \"<group>\"; };\n\t\t343F612F25234BFC00FFE085 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };\n\t\t344AF3A727844A6D00ED7586 /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = folder; name = LICENSES; path = WechatExporter/LICENSES; sourceTree = \"<group>\"; };\n\t\t344AF3AA2784551900ED7586 /* libmp3lame.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = libmp3lame.0.dylib; path = \"releases/windows-libs/x64/rel/bin/libmp3lame.0.dylib\"; sourceTree = \"<group>\"; };\n\t\t3455A16A27E5C528006B0797 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = \"<group>\"; };\n\t\t345C8D4D2543F5E30036368C /* ExportNotifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExportNotifier.h; sourceTree = \"<group>\"; };\n\t\t345C8D4E2543F5E30036368C /* semaphore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = semaphore.h; sourceTree = \"<group>\"; };\n\t\t345CE65925F8A456003DDD0F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = \"<group>\"; };\n\t\t345CE66125F8A717003DDD0F /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/Localizable.strings\"; sourceTree = \"<group>\"; };\n\t\t347A144E2685A77300E794ED /* MbdbReader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MbdbReader.h; sourceTree = \"<group>\"; };\n\t\t347BE8D12626B37D0004EBE4 /* PdfConverter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PdfConverter.h; sourceTree = \"<group>\"; };\n\t\t347E600D25C00A4100B33BAB /* MMKVReader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMKVReader.h; sourceTree = \"<group>\"; };\n\t\t347E601325C7E53500B33BAB /* SessionDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionDataSource.h; sourceTree = \"<group>\"; };\n\t\t347E601425C7E55100B33BAB /* SessionDataSource.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SessionDataSource.mm; sourceTree = \"<group>\"; };\n\t\t3481B1E2287A4FFA00E515E4 /* ExportOption.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExportOption.h; sourceTree = \"<group>\"; };\n\t\t3489DE44262A843C00F51416 /* AsyncExecutor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AsyncExecutor.cpp; sourceTree = \"<group>\"; };\n\t\t3489DE45262A843C00F51416 /* AsyncExecutor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncExecutor.h; sourceTree = \"<group>\"; };\n\t\t3489DE4E262E74BE00F51416 /* AsyncTask.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AsyncTask.cpp; sourceTree = \"<group>\"; };\n\t\t3489DE4F262E74BE00F51416 /* AsyncTask.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncTask.h; sourceTree = \"<group>\"; };\n\t\t3489DE53262EB03000F51416 /* TaskManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = TaskManager.cpp; sourceTree = \"<group>\"; };\n\t\t3489DE54262EB03000F51416 /* TaskManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TaskManager.h; sourceTree = \"<group>\"; };\n\t\t3489DE5A26316ECB00F51416 /* PdfConverterImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PdfConverterImpl.h; sourceTree = \"<group>\"; };\n\t\t348C373626A696B200FDBBDB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/Main.strings\"; sourceTree = \"<group>\"; };\n\t\t3497342425F384D100CAC6CD /* Updater.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Updater.cpp; sourceTree = \"<group>\"; };\n\t\t3497342525F384D100CAC6CD /* Updater.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Updater.h; sourceTree = \"<group>\"; };\n\t\t3497342A25F75D4300CAC6CD /* HttpHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = HttpHelper.mm; sourceTree = \"<group>\"; };\n\t\t3497342E25F75D5A00CAC6CD /* HttpHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HttpHelper.h; sourceTree = \"<group>\"; };\n\t\t3497891926037783001D1F8F /* AppConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppConfiguration.h; sourceTree = \"<group>\"; };\n\t\t3497891A26037783001D1F8F /* AppConfiguration.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AppConfiguration.mm; sourceTree = \"<group>\"; };\n\t\t349DAD2A255D3BB800BFE204 /* XmlParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = XmlParser.cpp; sourceTree = \"<group>\"; };\n\t\t349DAD2B255D3BB800BFE204 /* XmlParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XmlParser.h; sourceTree = \"<group>\"; };\n\t\t349DAD31255E6A0600BFE204 /* Utils_thread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_thread.cpp; sourceTree = \"<group>\"; };\n\t\t34A0335F25E34B0300E06CC5 /* MessageParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MessageParser.cpp; sourceTree = \"<group>\"; };\n\t\t34A0336025E34B0300E06CC5 /* MessageParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MessageParser.h; sourceTree = \"<group>\"; };\n\t\t34AB9A1225B89075006D3617 /* FileSystem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileSystem.h; sourceTree = \"<group>\"; };\n\t\t34AB9A1325B8908D006D3617 /* FileSystem.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = FileSystem.cpp; sourceTree = \"<group>\"; };\n\t\t34C0E1C6277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"libimobiledevice-1.0.6.dylib\"; path = \"releases/windows-libs/x64/rel/bin/libimobiledevice-1.0.6.dylib\"; sourceTree = \"<group>\"; };\n\t\t34C0E1C7277FDAA800CD4ADE /* libplist-2.0.3.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"libplist-2.0.3.dylib\"; path = \"releases/windows-libs/x64/rel/bin/libplist-2.0.3.dylib\"; sourceTree = \"<group>\"; };\n\t\t34C0E1C8277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"libusbmuxd-2.0.6.dylib\"; path = \"releases/windows-libs/x64/rel/bin/libusbmuxd-2.0.6.dylib\"; sourceTree = \"<group>\"; };\n\t\t34C0E1C9277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"libimobiledevice-glue-1.0.0.dylib\"; path = \"releases/windows-libs/x64/rel/bin/libimobiledevice-glue-1.0.0.dylib\"; sourceTree = \"<group>\"; };\n\t\t34C0E1CA277FDAA800CD4ADE /* libcrypto.1.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = libcrypto.1.1.dylib; path = \"releases/windows-libs/x64/rel/bin/libcrypto.1.1.dylib\"; sourceTree = \"<group>\"; };\n\t\t34C0E1CB277FDAA800CD4ADE /* libssl.1.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = libssl.1.1.dylib; path = \"releases/windows-libs/x64/rel/bin/libssl.1.1.dylib\"; sourceTree = \"<group>\"; };\n\t\t34CA9B0F269FE6FB00C530C2 /* ExportContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExportContext.h; sourceTree = \"<group>\"; };\n\t\t34CC43BA275EFFF400ABC2BB /* IDeviceBackup.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IDeviceBackup.h; sourceTree = \"<group>\"; };\n\t\t34CC43BB275F001000ABC2BB /* IDeviceBackup.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = IDeviceBackup.cpp; sourceTree = \"<group>\"; };\n\t\t34E3E9032525C2640093042D /* LoggerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoggerImpl.h; sourceTree = \"<group>\"; };\n\t\t34E3E9092531BD8E0093042D /* Utils_md5.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_md5.cpp; sourceTree = \"<group>\"; };\n\t\t34E3E90B2531BE1F0093042D /* Utils_protobuf.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_protobuf.cpp; sourceTree = \"<group>\"; };\n\t\t34E3E922253555470093042D /* RawMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RawMessage.h; sourceTree = \"<group>\"; };\n\t\t34E3E9232535555F0093042D /* RawMessage.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RawMessage.cpp; sourceTree = \"<group>\"; };\n\t\t34EC42AF272AEB6F0013570B /* ResManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResManager.h; sourceTree = \"<group>\"; };\n\t\t34EC42B0272AEB6F0013570B /* ResManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ResManager.cpp; sourceTree = \"<group>\"; };\n\t\t34ED31E725528A1800C42698 /* Utils_audio.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_audio.cpp; sourceTree = \"<group>\"; };\n\t\t34ED31EC25528CF500C42698 /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = System/Library/Frameworks/CoreAudio.framework; sourceTree = SDKROOT; };\n\t\t34ED31EF255294A000C42698 /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; };\n\t\t34ED31F1255294A500C42698 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };\n\t\t34ED31F3255294BB00C42698 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };\n\t\t34ED31F5255294D100C42698 /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; };\n\t\t34ED31F8255294E100C42698 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; };\n\t\t34ED31FB255294E500C42698 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };\n\t\t34ED31FE2552950100C42698 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };\n\t\t34ED32072552A98600C42698 /* Utils_silk.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_silk.cpp; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t343F610F252322D500FFE085 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t34C0E1D2277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib in Frameworks */,\n\t\t\t\t34C0E1D0277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib in Frameworks */,\n\t\t\t\t344AF3AB2784551900ED7586 /* libmp3lame.0.dylib in Frameworks */,\n\t\t\t\t342EDB00252450EB006A295A /* libcurl.tbd in Frameworks */,\n\t\t\t\t34C0E1D4277FDAA800CD4ADE /* libcrypto.1.1.dylib in Frameworks */,\n\t\t\t\t343F613025234BFC00FFE085 /* libsqlite3.tbd in Frameworks */,\n\t\t\t\t34C0E1D6277FDAA800CD4ADE /* libssl.1.1.dylib in Frameworks */,\n\t\t\t\t34C0E1CC277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib in Frameworks */,\n\t\t\t\t34C0E1CE277FDAA800CD4ADE /* libplist-2.0.3.dylib in Frameworks */,\n\t\t\t\t342EDB0B252495BD006A295A /* libxml2.tbd in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t343F6109252322D500FFE085 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t344AF3A727844A6D00ED7586 /* LICENSES */,\n\t\t\t\t343F6114252322D500FFE085 /* WechatExporter */,\n\t\t\t\t343F6113252322D500FFE085 /* Products */,\n\t\t\t\t343F612E25234BFC00FFE085 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t343F6113252322D500FFE085 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t343F6112252322D500FFE085 /* WechatExporter.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t343F6114252322D500FFE085 /* WechatExporter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3497891926037783001D1F8F /* AppConfiguration.h */,\n\t\t\t\t3497891A26037783001D1F8F /* AppConfiguration.mm */,\n\t\t\t\t343F6115252322D500FFE085 /* AppDelegate.h */,\n\t\t\t\t343F6116252322D500FFE085 /* AppDelegate.mm */,\n\t\t\t\t343F611B252322D600FFE085 /* Assets.xcassets */,\n\t\t\t\t343F612A25234BBE00FFE085 /* core */,\n\t\t\t\t34140A12254988C4003CE75A /* ExportNotifierImpl.h */,\n\t\t\t\t343F6120252322D600FFE085 /* Info.plist */,\n\t\t\t\t34E3E9032525C2640093042D /* LoggerImpl.h */,\n\t\t\t\t343F6121252322D600FFE085 /* main.m */,\n\t\t\t\t343F611D252322D600FFE085 /* Main.storyboard */,\n\t\t\t\t341A5B2E253828F300914BE3 /* res */,\n\t\t\t\t343F6118252322D500FFE085 /* ViewController.h */,\n\t\t\t\t343F6119252322D500FFE085 /* ViewController.mm */,\n\t\t\t\t343F6123252322D600FFE085 /* WechatExporter.entitlements */,\n\t\t\t\t347E601325C7E53500B33BAB /* SessionDataSource.h */,\n\t\t\t\t347E601425C7E55100B33BAB /* SessionDataSource.mm */,\n\t\t\t\t3497342A25F75D4300CAC6CD /* HttpHelper.mm */,\n\t\t\t\t3497342E25F75D5A00CAC6CD /* HttpHelper.h */,\n\t\t\t\t345CE65A25F8A456003DDD0F /* Localizable.strings */,\n\t\t\t\t3489DE5A26316ECB00F51416 /* PdfConverterImpl.h */,\n\t\t\t);\n\t\t\tpath = WechatExporter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t343F612A25234BBE00FFE085 /* core */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3489DE44262A843C00F51416 /* AsyncExecutor.cpp */,\n\t\t\t\t3489DE45262A843C00F51416 /* AsyncExecutor.h */,\n\t\t\t\t3489DE4E262E74BE00F51416 /* AsyncTask.cpp */,\n\t\t\t\t3489DE4F262E74BE00F51416 /* AsyncTask.h */,\n\t\t\t\t342EDB0125245206006A295A /* Downloader.cpp */,\n\t\t\t\t342EDB0225245206006A295A /* Downloader.h */,\n\t\t\t\t34CA9B0F269FE6FB00C530C2 /* ExportContext.h */,\n\t\t\t\t342EDB042524700A006A295A /* Exporter.cpp */,\n\t\t\t\t342EDB052524700A006A295A /* Exporter.h */,\n\t\t\t\t345C8D4D2543F5E30036368C /* ExportNotifier.h */,\n\t\t\t\t34AB9A1325B8908D006D3617 /* FileSystem.cpp */,\n\t\t\t\t34AB9A1225B89075006D3617 /* FileSystem.h */,\n\t\t\t\t34CC43BB275F001000ABC2BB /* IDeviceBackup.cpp */,\n\t\t\t\t34CC43BA275EFFF400ABC2BB /* IDeviceBackup.h */,\n\t\t\t\t343F612C25234BD300FFE085 /* ITunesParser.cpp */,\n\t\t\t\t343F612B25234BD300FFE085 /* ITunesParser.h */,\n\t\t\t\t342EDB07252471D6006A295A /* Logger.h */,\n\t\t\t\t347A144E2685A77300E794ED /* MbdbReader.h */,\n\t\t\t\t34A0335F25E34B0300E06CC5 /* MessageParser.cpp */,\n\t\t\t\t34A0336025E34B0300E06CC5 /* MessageParser.h */,\n\t\t\t\t347E600D25C00A4100B33BAB /* MMKVReader.h */,\n\t\t\t\t347BE8D12626B37D0004EBE4 /* PdfConverter.h */,\n\t\t\t\t34E3E9232535555F0093042D /* RawMessage.cpp */,\n\t\t\t\t34E3E922253555470093042D /* RawMessage.h */,\n\t\t\t\t34EC42B0272AEB6F0013570B /* ResManager.cpp */,\n\t\t\t\t34EC42AF272AEB6F0013570B /* ResManager.h */,\n\t\t\t\t345C8D4E2543F5E30036368C /* semaphore.h */,\n\t\t\t\t3489DE53262EB03000F51416 /* TaskManager.cpp */,\n\t\t\t\t3489DE54262EB03000F51416 /* TaskManager.h */,\n\t\t\t\t3497342425F384D100CAC6CD /* Updater.cpp */,\n\t\t\t\t3497342525F384D100CAC6CD /* Updater.h */,\n\t\t\t\t34ED31E725528A1800C42698 /* Utils_audio.cpp */,\n\t\t\t\t34E3E9092531BD8E0093042D /* Utils_md5.cpp */,\n\t\t\t\t34E3E90B2531BE1F0093042D /* Utils_protobuf.cpp */,\n\t\t\t\t34ED32072552A98600C42698 /* Utils_silk.cpp */,\n\t\t\t\t349DAD31255E6A0600BFE204 /* Utils_thread.cpp */,\n\t\t\t\t342EDB0825247852006A295A /* Utils.cpp */,\n\t\t\t\t342EDAFE2524485C006A295A /* Utils.h */,\n\t\t\t\t342EDAFD25241E64006A295A /* WechatObjects.h */,\n\t\t\t\t342EDAFA25241D91006A295A /* WechatParser.cpp */,\n\t\t\t\t342EDAFB25241D91006A295A /* WechatParser.h */,\n\t\t\t\t3407BE1427748AE2008D0F9E /* WechatSource.h */,\n\t\t\t\t349DAD2A255D3BB800BFE204 /* XmlParser.cpp */,\n\t\t\t\t349DAD2B255D3BB800BFE204 /* XmlParser.h */,\n\t\t\t\t342B03462811255E009FBD5E /* Template.h */,\n\t\t\t\t342B0347281125C7009FBD5E /* Template.cpp */,\n\t\t\t\t3481B1E2287A4FFA00E515E4 /* ExportOption.h */,\n\t\t\t);\n\t\t\tpath = core;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t343F612E25234BFC00FFE085 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t344AF3AA2784551900ED7586 /* libmp3lame.0.dylib */,\n\t\t\t\t34C0E1CA277FDAA800CD4ADE /* libcrypto.1.1.dylib */,\n\t\t\t\t34C0E1C6277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib */,\n\t\t\t\t34C0E1C9277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib */,\n\t\t\t\t34C0E1C7277FDAA800CD4ADE /* libplist-2.0.3.dylib */,\n\t\t\t\t34C0E1CB277FDAA800CD4ADE /* libssl.1.1.dylib */,\n\t\t\t\t34C0E1C8277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib */,\n\t\t\t\t34ED31FE2552950100C42698 /* CoreMedia.framework */,\n\t\t\t\t34ED31FB255294E500C42698 /* libz.tbd */,\n\t\t\t\t34ED31F8255294E100C42698 /* libiconv.tbd */,\n\t\t\t\t34ED31F5255294D100C42698 /* libbz2.tbd */,\n\t\t\t\t34ED31F3255294BB00C42698 /* Security.framework */,\n\t\t\t\t34ED31F1255294A500C42698 /* AudioToolbox.framework */,\n\t\t\t\t34ED31EF255294A000C42698 /* VideoToolbox.framework */,\n\t\t\t\t34ED31EC25528CF500C42698 /* CoreAudio.framework */,\n\t\t\t\t342EDB0A252495BD006A295A /* libxml2.tbd */,\n\t\t\t\t342EDAFF252450EB006A295A /* libcurl.tbd */,\n\t\t\t\t343F612F25234BFC00FFE085 /* libsqlite3.tbd */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t343F6111252322D500FFE085 /* WechatExporter */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 343F6126252322D600FFE085 /* Build configuration list for PBXNativeTarget \"WechatExporter\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t343F610E252322D500FFE085 /* Sources */,\n\t\t\t\t343F610F252322D500FFE085 /* Frameworks */,\n\t\t\t\t343F6110252322D500FFE085 /* Resources */,\n\t\t\t\t34C0E1D8277FDAA800CD4ADE /* Embed Libraries */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = WechatExporter;\n\t\t\tproductName = WechatExporter;\n\t\t\tproductReference = 343F6112252322D500FFE085 /* WechatExporter.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t343F610A252322D500FFE085 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 1200;\n\t\t\t\tORGANIZATIONNAME = Matthew;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t343F6111252322D500FFE085 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 11.3.1;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 343F610D252322D500FFE085 /* Build configuration list for PBXProject \"WechatExporter\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t\t\"zh-Hans\",\n\t\t\t);\n\t\t\tmainGroup = 343F6109252322D500FFE085;\n\t\t\tproductRefGroup = 343F6113252322D500FFE085 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t343F6111252322D500FFE085 /* WechatExporter */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t343F6110252322D500FFE085 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t341A5B2F253828F300914BE3 /* res in Resources */,\n\t\t\t\t345CE65825F8A456003DDD0F /* Localizable.strings in Resources */,\n\t\t\t\t343F611C252322D600FFE085 /* Assets.xcassets in Resources */,\n\t\t\t\t344AF3A827844A6D00ED7586 /* LICENSES in Resources */,\n\t\t\t\t343F611F252322D600FFE085 /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t343F610E252322D500FFE085 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t3497342625F384D100CAC6CD /* Updater.cpp in Sources */,\n\t\t\t\t34CC43BC275F001000ABC2BB /* IDeviceBackup.cpp in Sources */,\n\t\t\t\t3471A77A25EB5A9A007D186B /* FileSystem.cpp in Sources */,\n\t\t\t\t34E3E9242535555F0093042D /* RawMessage.cpp in Sources */,\n\t\t\t\t34E3E90C2531BE200093042D /* Utils_protobuf.cpp in Sources */,\n\t\t\t\t34A0336125E34B0300E06CC5 /* MessageParser.cpp in Sources */,\n\t\t\t\t343F611A252322D500FFE085 /* ViewController.mm in Sources */,\n\t\t\t\t3497891B26037783001D1F8F /* AppConfiguration.mm in Sources */,\n\t\t\t\t3489DE46262A843C00F51416 /* AsyncExecutor.cpp in Sources */,\n\t\t\t\t34ED31E825528A1800C42698 /* Utils_audio.cpp in Sources */,\n\t\t\t\t342EDB0325245206006A295A /* Downloader.cpp in Sources */,\n\t\t\t\t34E3E90A2531BD8E0093042D /* Utils_md5.cpp in Sources */,\n\t\t\t\t3489DE50262E74BF00F51416 /* AsyncTask.cpp in Sources */,\n\t\t\t\t342B0349281125C7009FBD5E /* Template.cpp in Sources */,\n\t\t\t\t343F6122252322D600FFE085 /* main.m in Sources */,\n\t\t\t\t342EDAFC25241D91006A295A /* WechatParser.cpp in Sources */,\n\t\t\t\t3497342B25F75D4300CAC6CD /* HttpHelper.mm in Sources */,\n\t\t\t\t34EC42B1272AEB6F0013570B /* ResManager.cpp in Sources */,\n\t\t\t\t342EDB0925247852006A295A /* Utils.cpp in Sources */,\n\t\t\t\t349DAD32255E6A0700BFE204 /* Utils_thread.cpp in Sources */,\n\t\t\t\t3489DE55262EB03000F51416 /* TaskManager.cpp in Sources */,\n\t\t\t\t343F6117252322D500FFE085 /* AppDelegate.mm in Sources */,\n\t\t\t\t349DAD2C255D3BB800BFE204 /* XmlParser.cpp in Sources */,\n\t\t\t\t342EDB062524700A006A295A /* Exporter.cpp in Sources */,\n\t\t\t\t347E601525C7E55100B33BAB /* SessionDataSource.mm in Sources */,\n\t\t\t\t34ED32082552A98600C42698 /* Utils_silk.cpp in Sources */,\n\t\t\t\t343F612D25234BD300FFE085 /* ITunesParser.cpp in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t343F611D252322D600FFE085 /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t343F611E252322D600FFE085 /* Base */,\n\t\t\t\t348C373626A696B200FDBBDB /* zh-Hans */,\n\t\t\t\t3455A16A27E5C528006B0797 /* en */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t345CE65A25F8A456003DDD0F /* Localizable.strings */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t345CE65925F8A456003DDD0F /* en */,\n\t\t\t\t345CE66125F8A717003DDD0F /* zh-Hans */,\n\t\t\t);\n\t\t\tname = Localizable.strings;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t343F6124252322D600FFE085 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tHEADER_SEARCH_PATHS = /usr/local/include/;\n\t\t\t\tLIBRARY_SEARCH_PATHS = /usr/local/lib;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.10;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t343F6125252322D600FFE085 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tHEADER_SEARCH_PATHS = /usr/local/include/;\n\t\t\t\tLIBRARY_SEARCH_PATHS = /usr/local/lib;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.10;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t343F6127252322D600FFE085 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = WechatExporter/WechatExporter.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 0;\n\t\t\t\tDEVELOPMENT_TEAM = L848BW5698;\n\t\t\t\tENABLE_HARDENED_RUNTIME = YES;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tHEADER_SEARCH_PATHS = (\n\t\t\t\t\t/usr/local/include/,\n\t\t\t\t\t\"${SDKROOT}/usr/include/libxml2/\",\n\t\t\t\t\t\"releases/windows-libs/include\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = WechatExporter/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tLIBRARY_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(PROJECT_DIR)/releases/windows-libs/x64/rel/bin\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.10;\n\t\t\t\tMARKETING_VERSION = 1.9.6;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"-L/usr/local/lib\",\n\t\t\t\t\t\"-lprotobuf\",\n\t\t\t\t\t\"-ljsoncpp\",\n\t\t\t\t\t\"-lSKP_SILK_SDK\",\n\t\t\t\t\t\"-lopencore-amrnb\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = org.wakin.WechatExporter;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t343F6128252322D600FFE085 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = WechatExporter/WechatExporter.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 0;\n\t\t\t\tDEVELOPMENT_TEAM = L848BW5698;\n\t\t\t\tENABLE_HARDENED_RUNTIME = YES;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = \"NDEBUG=1\";\n\t\t\t\tHEADER_SEARCH_PATHS = (\n\t\t\t\t\t/usr/local/include/,\n\t\t\t\t\t\"${SDKROOT}/usr/include/libxml2/\",\n\t\t\t\t\t\"releases/windows-libs/include\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = WechatExporter/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tLIBRARY_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(PROJECT_DIR)/releases/windows-libs/x64/rel/bin\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.10;\n\t\t\t\tMARKETING_VERSION = 1.9.6;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"-L/usr/local/lib\",\n\t\t\t\t\t\"-lprotobuf\",\n\t\t\t\t\t\"-ljsoncpp\",\n\t\t\t\t\t\"-lSKP_SILK_SDK\",\n\t\t\t\t\t\"-lopencore-amrnb\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = org.wakin.WechatExporter;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t343F610D252322D500FFE085 /* Build configuration list for PBXProject \"WechatExporter\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t343F6124252322D600FFE085 /* Debug */,\n\t\t\t\t343F6125252322D600FFE085 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t343F6126252322D600FFE085 /* Build configuration list for PBXNativeTarget \"WechatExporter\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t343F6127252322D600FFE085 /* Debug */,\n\t\t\t\t343F6128252322D600FFE085 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 343F610A252322D500FFE085 /* Project object */;\n}\n"
  },
  {
    "path": "WechatExporter.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<Workspace\r\n   version = \"1.0\">\r\n   <FileRef\r\n      location = \"self:WechatExporter.xcodeproj\">\r\n   </FileRef>\r\n</Workspace>\r\n"
  },
  {
    "path": "WechatExporter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n<plist version=\"1.0\">\r\n<dict>\r\n\t<key>IDEDidComputeMac32BitWarning</key>\r\n\t<true/>\r\n</dict>\r\n</plist>\r\n"
  },
  {
    "path": "WechatExporter.xcodeproj/xcshareddata/xcschemes/WechatExporter.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1200\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"343F6111252322D500FFE085\"\n               BuildableName = \"WechatExporter.app\"\n               BlueprintName = \"WechatExporter\"\n               ReferencedContainer = \"container:WechatExporter.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"343F6111252322D500FFE085\"\n            BuildableName = \"WechatExporter.app\"\n            BlueprintName = \"WechatExporter\"\n            ReferencedContainer = \"container:WechatExporter.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"343F6111252322D500FFE085\"\n            BuildableName = \"WechatExporter.app\"\n            BlueprintName = \"WechatExporter\"\n            ReferencedContainer = \"container:WechatExporter.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "WechatExporter.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:WechatExporter.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:WechatExporterCmd.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "WechatExporter.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "WechatExporterCmd/WechatExporter.cpp",
    "content": "//\n//  main.cpp\n//  WechatExporterCmd\n//\n//  Created by Matthew on 2022/3/4.\n//\n\n#include <iostream>\n#include <string>\n#include <vector>\n#include <thread>\n\n\n#ifdef _WIN32\n#elif defined(__APPLE__)\n#include <libgen.h>\n#include <mach-o/dyld.h>\n#include \"Utils.h\"\n#else\n#endif\n\n#include \"WechatExporterCmd.h\"\n\n\nstd::string getCurrentLanguageCode();\nstd::string getExecutablePath();\nstd::string getExecutableDir();\n\n\nclass LoggerImpl : public Logger\n{\npublic:\n    \n    void write(const std::string& log)\n    {\n        std::cout << log << std::endl;\n    }\n    \n    void debug(const std::string& log)\n    {\n        std::cout << \"DBG:: \" << log << std::endl;\n    }\n    \n};\n\nint main(int argc, const char * argv[]) {\n    \n    const char * fullPath = argv[0];\n    \n    const char *executableName = basename((char *)argv[0]);\n    int outputFormat = OUTPUT_FORMAT_HTML;\n    int asyncLoading = HTML_OPTION_ONSCROLL;\n    bool outputFilter = false;\n    std::string backupDir;\n    std::string outputDir;\n    std::string account;\n    std::vector<std::string> sessions;\n    \n    for (int idx = 1; idx < argc; idx++)\n    {\n        if (strcmp(\"--help\", argv[idx]) == 0)\n        {\n            printHelp(executableName);\n            return 0;\n        }\n        \n        const char* equals_pos = strchr(argv[idx], '=');\n        if (equals_pos == NULL)\n        {\n            continue;\n        }\n        \n        std::string name = std::string(argv[idx], equals_pos - argv[idx]);\n        if (name == \"--backup\")\n        {\n            backupDir = parseArgumentwithQuato(equals_pos + 1);\n        }\n        else if (name == \"--output\")\n        {\n            outputDir = parseArgumentwithQuato(equals_pos + 1);\n        }\n        else if (name == \"--account\")\n        {\n            account = parseArgumentwithQuato(equals_pos + 1);\n        }\n        else if (name == \"--session\")\n        {\n            sessions.push_back(parseArgumentwithQuato(equals_pos + 1));\n        }\n        else if (name == \"--asyncloading\")\n        {\n            if (strcmp(\"sync\", equals_pos + 1) == 0)\n            {\n                asyncLoading = HTML_OPTION_SYNC;\n            }\n            else if (strcmp(\"oninit\", equals_pos + 1) == 0)\n            {\n                asyncLoading = HTML_OPTION_ONINIT;\n            }\n        }\n        else if (name == \"--filter\")\n        {\n            if (strcmp(\"yes\", equals_pos + 1) == 0)\n            {\n                outputFilter = true;\n            }\n        }\n    }\n    \n    if (backupDir.empty() || !existsDirectory(backupDir))\n    {\n        std::cout << \"Please input valid iTunes backup directory.\" << std::endl;\n        return 1;\n    }\n    if (outputDir.empty() || !existsDirectory(outputDir))\n    {\n        std::cout << \"Please input valid output directory.\" << std::endl;\n        return 1;\n    }\n    if (account.empty())\n    {\n        std::cout << \"Please input account name.\" << std::endl;\n        return 1;\n    }\n    \n    std::string workDir(argv[0], 0, strlen(argv[0]) - strlen(executableName));\n    \n    workDir = getExecutableDir();\n    if (!endsWith(workDir, \"/\"))\n    {\n        workDir += \"/\";\n    }\n    \n    std::string languageCode = getCurrentLanguageCode();\n    LoggerImpl logger;\n    \n    return exportSessions(languageCode, &logger, workDir, backupDir, outputDir, account, sessions, outputFormat, asyncLoading, outputFilter);\n}\n\nstd::string getExecutablePath()\n{\n    char rawPathName[PATH_MAX];\n    char realPathName[PATH_MAX];\n    uint32_t rawPathSize = (uint32_t)sizeof(rawPathName);\n\n    if(!_NSGetExecutablePath(rawPathName, &rawPathSize)) {\n        realpath(rawPathName, realPathName);\n    }\n    return  std::string(realPathName);\n}\n\nstd::string getExecutableDir()\n{\n    std::string executablePath = getExecutablePath();\n    char *executablePathStr = new char[executablePath.length() + 1];\n    strcpy(executablePathStr, executablePath.c_str());\n    char* executableDir = dirname(executablePathStr);\n    delete [] executablePathStr;\n    return std::string(executableDir);\n}\n\nstd::string getCurrentLanguageCode()\n{\n    std::locale loc = std::locale(\"\");\n    \n    std::string name = loc.name();\n    return loc.name();\n    \n    // NSString *preferredLanguage = [[NSLocale preferredLanguages] objectAtIndex:0];\n    // if ([preferredLanguage hasPrefix:@\"zh-Hans\"])\n    {\n        // preferredLanguage = @\"zh-Hans\";\n    }\n    \n    // return preferredLanguage;\n}\n\n"
  },
  {
    "path": "WechatExporterCmd/WechatExporterCmd.h",
    "content": "//\n//  main.cpp\n//  WechatExporterCmd\n//\n//  Created by Matthew on 2022/3/4.\n//\n\n#include <iostream>\n#include <string>\n#include <vector>\n#include <thread>\n\n\n#ifdef _WIN32\n\n#elif defined(__APPLE__)\n#include \"Utils.h\"\n#include \"Exporter.h\"\n#include \"IDeviceBackup.h\"\n#include \"WechatSource.h\"\n#else\n#endif\n\n#ifndef WechatExporterCmd_h\n#define WechatExporterCmd_h\n\n#define OUTPUT_FORMAT_HTML      0\n#define OUTPUT_FORMAT_TEXT      1\n\n#define HTML_OPTION_SYNC        0\n#define HTML_OPTION_ONSCROLL    1\n#define HTML_OPTION_ONINIT      2\n\nclass Logger;\n\nvoid printHelp(const char *executableName)\n{\n    std::cout\n          <<\n          \"Usage: \" << executableName\n          << \" [OPTION]\\n\"\n             \"Export Wechat chat history based on the options given:\\n\"\n             \"  --backup=PATH       Specify the directory of iTunes Backup\\n\"\n             \"  --output=PATH       Specify the directory in that Wechat chat history will be exported.\\n\"\n             \"  --format=FORMAT     FORMAT may be one of 'html', 'text'. 'html' is default.\\n\"\n             \"  --account=ACCOUNT   Specify the WeChat account which will be exported.\\n\"\n             \"  --session=SESSION   Friend name or chat group name which will be exported. May be specified multiple times\\n\"\n             \"                      If no session is specified, all sessions of the account will be exported.\\n\"\n             \"  --asyncloading=[HTML LOADING OPTION]\\n\"\n             \"                      [HTML LOADING OPTION] may be one of 'sync', 'onscroll', 'oninit'. 'onscroll' is default.\\n\"\n             \"  --filter=FILTER     FILTER may be one of 'no', 'yes'. 'no' is default.\\n\"\n             \"  --help              Show this help.\\n\"\n          << std::endl;\n}\n\nstd::string parseArgumentwithQuato(const char *path)\n{\n    std::string parsedPath;\n    if (NULL == path)\n    {\n        return parsedPath;\n    }\n    \n    size_t start = 0;\n    size_t length = strlen(path);\n    if (length > 1 && path[length - 1] == '\"')\n    {\n        length--;\n    }\n    if (path[0] == '\"')\n    {\n        start = 1;\n        length--;\n    }\n    \n    parsedPath = std::string(path, start, length);\n    return parsedPath;\n}\n\nint exportSessions(const std::string& languageCode, Logger* logger, const std::string& workDir, const std::string& backupDir, const std::string& outputDir, const std::string& account, const std::vector<std::string>& sessions, int outputFormat, int asyncLoading, bool outputFilter)\n{\n    // const std::string& workDir, const std::string& backup, const std::string& output, Logger* logger, PdfConverter* pdfConverter\n    \n    Exporter exp(workDir, backupDir, outputDir, logger, NULL);\n    exp.setLanguageCode(languageCode);\n    ExportOption options;\n    \n    if (outputFormat == OUTPUT_FORMAT_TEXT)\n    {\n        options.setTextMode();\n        exp.setExtName(\"txt\");\n        exp.setTemplatesName(\"templates_txt\");\n    }\n    else\n    {\n        exp.setExtName(\"html\");\n        exp.setTemplatesName(\"templates\");\n        if (asyncLoading == HTML_OPTION_SYNC)\n        {\n            options.setTextMode();\n            // exp.setSyncLoading();\n        }\n        else\n        {\n            if (asyncLoading != HTML_OPTION_ONINIT)\n            {\n                options.setLoadingDataOnScroll();\n            }\n        }\n    }\n    if (outputFilter)\n    {\n        options.supportsFilter();\n    }\n    options.filterByName();\n    \n    exp.setOptions(options);\n    \n    std::map<std::string, std::map<std::string, void *>> usersAndSessions;\n    \n    std::map<std::string, void *> mapSessions;\n    \n    for (auto it = sessions.cbegin(); it != sessions.cend(); ++it)\n    {\n        mapSessions[*it] = NULL;\n    }\n    \n    usersAndSessions[account] = mapSessions;\n    \n    exp.filterUsersAndSessions(usersAndSessions);\n    \n    exp.run();\n    \n    exp.waitForComplition();\n    \n    return 0;\n}\n\n#endif // WechatExporterCmd_h\n"
  },
  {
    "path": "WechatExporterCmd.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 50;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t340E16BA2823B83600ECB4CD /* Template.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 340E16B82823B83600ECB4CD /* Template.cpp */; };\n\t\t3410714127D1AF0600CAC805 /* WechatExporter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410714027D1AF0600CAC805 /* WechatExporter.cpp */; };\n\t\t3410717E27D1AFD900CAC805 /* WechatParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410714B27D1AFD700CAC805 /* WechatParser.cpp */; };\n\t\t3410717F27D1AFD900CAC805 /* Utils_md5.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410714D27D1AFD700CAC805 /* Utils_md5.cpp */; };\n\t\t3410718027D1AFD900CAC805 /* Utils_protobuf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410714F27D1AFD700CAC805 /* Utils_protobuf.cpp */; };\n\t\t3410718127D1AFD900CAC805 /* Updater.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715027D1AFD700CAC805 /* Updater.cpp */; };\n\t\t3410718327D1AFD900CAC805 /* AsyncTask.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715227D1AFD800CAC805 /* AsyncTask.cpp */; };\n\t\t3410718427D1AFD900CAC805 /* RawMessage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715527D1AFD800CAC805 /* RawMessage.cpp */; };\n\t\t3410718627D1AFD900CAC805 /* MessageParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715727D1AFD800CAC805 /* MessageParser.cpp */; };\n\t\t3410718727D1AFD900CAC805 /* TaskManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715927D1AFD800CAC805 /* TaskManager.cpp */; };\n\t\t3410718827D1AFD900CAC805 /* XmlParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715F27D1AFD800CAC805 /* XmlParser.cpp */; };\n\t\t3410718927D1AFD900CAC805 /* AsyncExecutor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716127D1AFD800CAC805 /* AsyncExecutor.cpp */; };\n\t\t3410718A27D1AFD900CAC805 /* IDeviceBackup.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716827D1AFD800CAC805 /* IDeviceBackup.cpp */; };\n\t\t3410718B27D1AFD900CAC805 /* Downloader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716927D1AFD900CAC805 /* Downloader.cpp */; };\n\t\t3410718C27D1AFD900CAC805 /* Utils_audio.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716A27D1AFD900CAC805 /* Utils_audio.cpp */; };\n\t\t3410718D27D1AFD900CAC805 /* FileSystem.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716B27D1AFD900CAC805 /* FileSystem.cpp */; };\n\t\t3410718E27D1AFD900CAC805 /* Exporter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716E27D1AFD900CAC805 /* Exporter.cpp */; };\n\t\t3410718F27D1AFD900CAC805 /* Utils_silk.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716F27D1AFD900CAC805 /* Utils_silk.cpp */; };\n\t\t3410719027D1AFD900CAC805 /* Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410717127D1AFD900CAC805 /* Utils.cpp */; };\n\t\t3410719127D1AFD900CAC805 /* ResManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410717527D1AFD900CAC805 /* ResManager.cpp */; };\n\t\t3410719327D1AFD900CAC805 /* ITunesParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410717A27D1AFD900CAC805 /* ITunesParser.cpp */; };\n\t\t3410719427D1AFD900CAC805 /* Utils_thread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410717D27D1AFD900CAC805 /* Utils_thread.cpp */; };\n\t\t3410719927D1B04300CAC805 /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3410719727D1B01700CAC805 /* libcurl.tbd */; };\n\t\t3410719C27D1B05500CAC805 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3410719B27D1B04A00CAC805 /* libxml2.tbd */; };\n\t\t341071A527D1B18F00CAC805 /* libmp3lame.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3410719E27D1B18F00CAC805 /* libmp3lame.0.dylib */; };\n\t\t341071A627D1B18F00CAC805 /* libmp3lame.0.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 3410719E27D1B18F00CAC805 /* libmp3lame.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t341071A727D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3410719F27D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib */; };\n\t\t341071A827D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 3410719F27D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t341071A927D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A027D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib */; };\n\t\t341071AA27D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A027D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t341071AB27D1B19000CAC805 /* libplist-2.0.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A127D1B18F00CAC805 /* libplist-2.0.3.dylib */; };\n\t\t341071AC27D1B19000CAC805 /* libplist-2.0.3.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A127D1B18F00CAC805 /* libplist-2.0.3.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t341071AD27D1B19000CAC805 /* libssl.1.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A227D1B18F00CAC805 /* libssl.1.1.dylib */; };\n\t\t341071AE27D1B19000CAC805 /* libssl.1.1.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A227D1B18F00CAC805 /* libssl.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t341071AF27D1B19000CAC805 /* libimobiledevice-glue-1.0.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A327D1B18F00CAC805 /* libimobiledevice-glue-1.0.0.dylib */; };\n\t\t341071B027D1B19000CAC805 /* libimobiledevice-glue-1.0.0.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A327D1B18F00CAC805 /* libimobiledevice-glue-1.0.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t341071B127D1B19000CAC805 /* libcrypto.1.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A427D1B18F00CAC805 /* libcrypto.1.1.dylib */; };\n\t\t341071B227D1B19000CAC805 /* libcrypto.1.1.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A427D1B18F00CAC805 /* libcrypto.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n\t\t341071B627D1B1AE00CAC805 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071B527D1B1A300CAC805 /* libiconv.tbd */; };\n\t\t341071B727D1B1B600CAC805 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071B427D1B19600CAC805 /* libz.tbd */; };\n\t\t341071C427D1B5A000CAC805 /* res in CopyFiles */ = {isa = PBXBuildFile; fileRef = 341071C327D1B58800CAC805 /* res */; };\n\t\t341071C527D1B5A500CAC805 /* LICENSES in CopyFiles */ = {isa = PBXBuildFile; fileRef = 341071C227D1B56800CAC805 /* LICENSES */; };\n\t\t341071C727D1B5D500CAC805 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071C627D1B5CB00CAC805 /* libsqlite3.tbd */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t3410713B27D1AF0600CAC805 /* CopyFiles */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 12;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 16;\n\t\t\tfiles = (\n\t\t\t\t341071C527D1B5A500CAC805 /* LICENSES in CopyFiles */,\n\t\t\t\t341071C427D1B5A000CAC805 /* res in CopyFiles */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t341071B327D1B19000CAC805 /* Embed Libraries */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = Frameworks;\n\t\t\tdstSubfolderSpec = 16;\n\t\t\tfiles = (\n\t\t\t\t341071AA27D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib in Embed Libraries */,\n\t\t\t\t341071A827D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib in Embed Libraries */,\n\t\t\t\t341071B227D1B19000CAC805 /* libcrypto.1.1.dylib in Embed Libraries */,\n\t\t\t\t341071A627D1B18F00CAC805 /* libmp3lame.0.dylib in Embed Libraries */,\n\t\t\t\t341071B027D1B19000CAC805 /* libimobiledevice-glue-1.0.0.dylib in Embed Libraries */,\n\t\t\t\t341071AC27D1B19000CAC805 /* libplist-2.0.3.dylib in Embed Libraries */,\n\t\t\t\t341071AE27D1B19000CAC805 /* libssl.1.1.dylib in Embed Libraries */,\n\t\t\t);\n\t\t\tname = \"Embed Libraries\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t340E16B82823B83600ECB4CD /* Template.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Template.cpp; path = WechatExporter/core/Template.cpp; sourceTree = SOURCE_ROOT; };\n\t\t340E16B92823B83600ECB4CD /* Template.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Template.h; path = WechatExporter/core/Template.h; sourceTree = SOURCE_ROOT; };\n\t\t3410713D27D1AF0600CAC805 /* WechatExporterCmd */ = {isa = PBXFileReference; explicitFileType = \"compiled.mach-o.executable\"; includeInIndex = 0; path = WechatExporterCmd; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t3410714027D1AF0600CAC805 /* WechatExporter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = WechatExporter.cpp; sourceTree = \"<group>\"; };\n\t\t3410714B27D1AFD700CAC805 /* WechatParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = WechatParser.cpp; path = WechatExporter/core/WechatParser.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410714C27D1AFD700CAC805 /* WechatObjects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WechatObjects.h; path = WechatExporter/core/WechatObjects.h; sourceTree = SOURCE_ROOT; };\n\t\t3410714D27D1AFD700CAC805 /* Utils_md5.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_md5.cpp; path = WechatExporter/core/Utils_md5.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410714E27D1AFD700CAC805 /* ITunesParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ITunesParser.h; path = WechatExporter/core/ITunesParser.h; sourceTree = SOURCE_ROOT; };\n\t\t3410714F27D1AFD700CAC805 /* Utils_protobuf.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_protobuf.cpp; path = WechatExporter/core/Utils_protobuf.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410715027D1AFD700CAC805 /* Updater.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Updater.cpp; path = WechatExporter/core/Updater.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410715227D1AFD800CAC805 /* AsyncTask.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = AsyncTask.cpp; path = WechatExporter/core/AsyncTask.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410715327D1AFD800CAC805 /* WechatSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WechatSource.h; path = WechatExporter/core/WechatSource.h; sourceTree = SOURCE_ROOT; };\n\t\t3410715427D1AFD800CAC805 /* Downloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Downloader.h; path = WechatExporter/core/Downloader.h; sourceTree = SOURCE_ROOT; };\n\t\t3410715527D1AFD800CAC805 /* RawMessage.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = RawMessage.cpp; path = WechatExporter/core/RawMessage.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410715727D1AFD800CAC805 /* MessageParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = MessageParser.cpp; path = WechatExporter/core/MessageParser.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410715827D1AFD800CAC805 /* MbdbReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MbdbReader.h; path = WechatExporter/core/MbdbReader.h; sourceTree = SOURCE_ROOT; };\n\t\t3410715927D1AFD800CAC805 /* TaskManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = TaskManager.cpp; path = WechatExporter/core/TaskManager.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410715B27D1AFD800CAC805 /* FileSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FileSystem.h; path = WechatExporter/core/FileSystem.h; sourceTree = SOURCE_ROOT; };\n\t\t3410715C27D1AFD800CAC805 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = WechatExporter/core/Utils.h; sourceTree = SOURCE_ROOT; };\n\t\t3410715D27D1AFD800CAC805 /* MMKVReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MMKVReader.h; path = WechatExporter/core/MMKVReader.h; sourceTree = SOURCE_ROOT; };\n\t\t3410715F27D1AFD800CAC805 /* XmlParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = XmlParser.cpp; path = WechatExporter/core/XmlParser.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410716027D1AFD800CAC805 /* WechatParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WechatParser.h; path = WechatExporter/core/WechatParser.h; sourceTree = SOURCE_ROOT; };\n\t\t3410716127D1AFD800CAC805 /* AsyncExecutor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = AsyncExecutor.cpp; path = WechatExporter/core/AsyncExecutor.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410716227D1AFD800CAC805 /* Exporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Exporter.h; path = WechatExporter/core/Exporter.h; sourceTree = SOURCE_ROOT; };\n\t\t3410716327D1AFD800CAC805 /* MessageParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MessageParser.h; path = WechatExporter/core/MessageParser.h; sourceTree = SOURCE_ROOT; };\n\t\t3410716427D1AFD800CAC805 /* AsyncTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AsyncTask.h; path = WechatExporter/core/AsyncTask.h; sourceTree = SOURCE_ROOT; };\n\t\t3410716527D1AFD800CAC805 /* XmlParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = XmlParser.h; path = WechatExporter/core/XmlParser.h; sourceTree = SOURCE_ROOT; };\n\t\t3410716627D1AFD800CAC805 /* TaskManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TaskManager.h; path = WechatExporter/core/TaskManager.h; sourceTree = SOURCE_ROOT; };\n\t\t3410716827D1AFD800CAC805 /* IDeviceBackup.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = IDeviceBackup.cpp; path = WechatExporter/core/IDeviceBackup.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410716927D1AFD900CAC805 /* Downloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Downloader.cpp; path = WechatExporter/core/Downloader.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410716A27D1AFD900CAC805 /* Utils_audio.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_audio.cpp; path = WechatExporter/core/Utils_audio.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410716B27D1AFD900CAC805 /* FileSystem.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = FileSystem.cpp; path = WechatExporter/core/FileSystem.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410716C27D1AFD900CAC805 /* ExportContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ExportContext.h; path = WechatExporter/core/ExportContext.h; sourceTree = SOURCE_ROOT; };\n\t\t3410716D27D1AFD900CAC805 /* Logger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logger.h; path = WechatExporter/core/Logger.h; sourceTree = SOURCE_ROOT; };\n\t\t3410716E27D1AFD900CAC805 /* Exporter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Exporter.cpp; path = WechatExporter/core/Exporter.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410716F27D1AFD900CAC805 /* Utils_silk.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_silk.cpp; path = WechatExporter/core/Utils_silk.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410717027D1AFD900CAC805 /* ExportNotifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ExportNotifier.h; path = WechatExporter/core/ExportNotifier.h; sourceTree = SOURCE_ROOT; };\n\t\t3410717127D1AFD900CAC805 /* Utils.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils.cpp; path = WechatExporter/core/Utils.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410717227D1AFD900CAC805 /* ResManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResManager.h; path = WechatExporter/core/ResManager.h; sourceTree = SOURCE_ROOT; };\n\t\t3410717327D1AFD900CAC805 /* RawMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RawMessage.h; path = WechatExporter/core/RawMessage.h; sourceTree = SOURCE_ROOT; };\n\t\t3410717427D1AFD900CAC805 /* Updater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Updater.h; path = WechatExporter/core/Updater.h; sourceTree = SOURCE_ROOT; };\n\t\t3410717527D1AFD900CAC805 /* ResManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ResManager.cpp; path = WechatExporter/core/ResManager.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410717827D1AFD900CAC805 /* AsyncExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AsyncExecutor.h; path = WechatExporter/core/AsyncExecutor.h; sourceTree = SOURCE_ROOT; };\n\t\t3410717927D1AFD900CAC805 /* PdfConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PdfConverter.h; path = WechatExporter/core/PdfConverter.h; sourceTree = SOURCE_ROOT; };\n\t\t3410717A27D1AFD900CAC805 /* ITunesParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ITunesParser.cpp; path = WechatExporter/core/ITunesParser.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410717B27D1AFD900CAC805 /* IDeviceBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = IDeviceBackup.h; path = WechatExporter/core/IDeviceBackup.h; sourceTree = SOURCE_ROOT; };\n\t\t3410717C27D1AFD900CAC805 /* semaphore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = semaphore.h; path = WechatExporter/core/semaphore.h; sourceTree = SOURCE_ROOT; };\n\t\t3410717D27D1AFD900CAC805 /* Utils_thread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_thread.cpp; path = WechatExporter/core/Utils_thread.cpp; sourceTree = SOURCE_ROOT; };\n\t\t3410719727D1B01700CAC805 /* libcurl.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libcurl.tbd; path = usr/lib/libcurl.tbd; sourceTree = SDKROOT; };\n\t\t3410719B27D1B04A00CAC805 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };\n\t\t3410719E27D1B18F00CAC805 /* libmp3lame.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = libmp3lame.0.dylib; path = \"releases/windows-libs/x64/rel/cmd/libmp3lame.0.dylib\"; sourceTree = \"<group>\"; };\n\t\t3410719F27D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"libusbmuxd-2.0.6.dylib\"; path = \"releases/windows-libs/x64/rel/cmd/libusbmuxd-2.0.6.dylib\"; sourceTree = \"<group>\"; };\n\t\t341071A027D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"libimobiledevice-1.0.6.dylib\"; path = \"releases/windows-libs/x64/rel/cmd/libimobiledevice-1.0.6.dylib\"; sourceTree = \"<group>\"; };\n\t\t341071A127D1B18F00CAC805 /* libplist-2.0.3.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"libplist-2.0.3.dylib\"; path = \"releases/windows-libs/x64/rel/cmd/libplist-2.0.3.dylib\"; sourceTree = \"<group>\"; };\n\t\t341071A227D1B18F00CAC805 /* libssl.1.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = libssl.1.1.dylib; path = \"releases/windows-libs/x64/rel/cmd/libssl.1.1.dylib\"; sourceTree = \"<group>\"; };\n\t\t341071A327D1B18F00CAC805 /* libimobiledevice-glue-1.0.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"libimobiledevice-glue-1.0.0.dylib\"; path = \"releases/windows-libs/x64/rel/cmd/libimobiledevice-glue-1.0.0.dylib\"; sourceTree = \"<group>\"; };\n\t\t341071A427D1B18F00CAC805 /* libcrypto.1.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = libcrypto.1.1.dylib; path = \"releases/windows-libs/x64/rel/cmd/libcrypto.1.1.dylib\"; sourceTree = \"<group>\"; };\n\t\t341071B427D1B19600CAC805 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };\n\t\t341071B527D1B1A300CAC805 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; };\n\t\t341071C227D1B56800CAC805 /* LICENSES */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENSES; path = WechatExporter/LICENSES; sourceTree = SOURCE_ROOT; };\n\t\t341071C327D1B58800CAC805 /* res */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = res; path = WechatExporter/res; sourceTree = SOURCE_ROOT; };\n\t\t341071C627D1B5CB00CAC805 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };\n\t\t3455A17027E949BF006B0797 /* WechatExporterCmd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WechatExporterCmd.h; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t3410713A27D1AF0600CAC805 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t341071A727D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib in Frameworks */,\n\t\t\t\t341071B727D1B1B600CAC805 /* libz.tbd in Frameworks */,\n\t\t\t\t341071A927D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib in Frameworks */,\n\t\t\t\t341071A527D1B18F00CAC805 /* libmp3lame.0.dylib in Frameworks */,\n\t\t\t\t3410719927D1B04300CAC805 /* libcurl.tbd in Frameworks */,\n\t\t\t\t341071B627D1B1AE00CAC805 /* libiconv.tbd in Frameworks */,\n\t\t\t\t341071C727D1B5D500CAC805 /* libsqlite3.tbd in Frameworks */,\n\t\t\t\t341071AD27D1B19000CAC805 /* libssl.1.1.dylib in Frameworks */,\n\t\t\t\t341071AF27D1B19000CAC805 /* libimobiledevice-glue-1.0.0.dylib in Frameworks */,\n\t\t\t\t3410719C27D1B05500CAC805 /* libxml2.tbd in Frameworks */,\n\t\t\t\t341071AB27D1B19000CAC805 /* libplist-2.0.3.dylib in Frameworks */,\n\t\t\t\t341071B127D1B19000CAC805 /* libcrypto.1.1.dylib in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t3410713427D1AF0600CAC805 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3410713F27D1AF0600CAC805 /* WechatExporterCmd */,\n\t\t\t\t3410713E27D1AF0600CAC805 /* Products */,\n\t\t\t\t3410719627D1B01700CAC805 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3410713E27D1AF0600CAC805 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3410713D27D1AF0600CAC805 /* WechatExporterCmd */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3410713F27D1AF0600CAC805 /* WechatExporterCmd */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3455A17027E949BF006B0797 /* WechatExporterCmd.h */,\n\t\t\t\t341071C327D1B58800CAC805 /* res */,\n\t\t\t\t341071C227D1B56800CAC805 /* LICENSES */,\n\t\t\t\t3410714927D1AFC600CAC805 /* core */,\n\t\t\t\t3410714027D1AF0600CAC805 /* WechatExporter.cpp */,\n\t\t\t);\n\t\t\tpath = WechatExporterCmd;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3410714927D1AFC600CAC805 /* core */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t340E16B82823B83600ECB4CD /* Template.cpp */,\n\t\t\t\t340E16B92823B83600ECB4CD /* Template.h */,\n\t\t\t\t3410716127D1AFD800CAC805 /* AsyncExecutor.cpp */,\n\t\t\t\t3410717827D1AFD900CAC805 /* AsyncExecutor.h */,\n\t\t\t\t3410715227D1AFD800CAC805 /* AsyncTask.cpp */,\n\t\t\t\t3410716427D1AFD800CAC805 /* AsyncTask.h */,\n\t\t\t\t3410716927D1AFD900CAC805 /* Downloader.cpp */,\n\t\t\t\t3410715427D1AFD800CAC805 /* Downloader.h */,\n\t\t\t\t3410716C27D1AFD900CAC805 /* ExportContext.h */,\n\t\t\t\t3410716E27D1AFD900CAC805 /* Exporter.cpp */,\n\t\t\t\t3410716227D1AFD800CAC805 /* Exporter.h */,\n\t\t\t\t3410717027D1AFD900CAC805 /* ExportNotifier.h */,\n\t\t\t\t3410716B27D1AFD900CAC805 /* FileSystem.cpp */,\n\t\t\t\t3410715B27D1AFD800CAC805 /* FileSystem.h */,\n\t\t\t\t3410716827D1AFD800CAC805 /* IDeviceBackup.cpp */,\n\t\t\t\t3410717B27D1AFD900CAC805 /* IDeviceBackup.h */,\n\t\t\t\t3410717A27D1AFD900CAC805 /* ITunesParser.cpp */,\n\t\t\t\t3410714E27D1AFD700CAC805 /* ITunesParser.h */,\n\t\t\t\t3410716D27D1AFD900CAC805 /* Logger.h */,\n\t\t\t\t3410715827D1AFD800CAC805 /* MbdbReader.h */,\n\t\t\t\t3410715727D1AFD800CAC805 /* MessageParser.cpp */,\n\t\t\t\t3410716327D1AFD800CAC805 /* MessageParser.h */,\n\t\t\t\t3410715D27D1AFD800CAC805 /* MMKVReader.h */,\n\t\t\t\t3410717927D1AFD900CAC805 /* PdfConverter.h */,\n\t\t\t\t3410715527D1AFD800CAC805 /* RawMessage.cpp */,\n\t\t\t\t3410717327D1AFD900CAC805 /* RawMessage.h */,\n\t\t\t\t3410717527D1AFD900CAC805 /* ResManager.cpp */,\n\t\t\t\t3410717227D1AFD900CAC805 /* ResManager.h */,\n\t\t\t\t3410717C27D1AFD900CAC805 /* semaphore.h */,\n\t\t\t\t3410715927D1AFD800CAC805 /* TaskManager.cpp */,\n\t\t\t\t3410716627D1AFD800CAC805 /* TaskManager.h */,\n\t\t\t\t3410715027D1AFD700CAC805 /* Updater.cpp */,\n\t\t\t\t3410717427D1AFD900CAC805 /* Updater.h */,\n\t\t\t\t3410716A27D1AFD900CAC805 /* Utils_audio.cpp */,\n\t\t\t\t3410714D27D1AFD700CAC805 /* Utils_md5.cpp */,\n\t\t\t\t3410714F27D1AFD700CAC805 /* Utils_protobuf.cpp */,\n\t\t\t\t3410716F27D1AFD900CAC805 /* Utils_silk.cpp */,\n\t\t\t\t3410717D27D1AFD900CAC805 /* Utils_thread.cpp */,\n\t\t\t\t3410717127D1AFD900CAC805 /* Utils.cpp */,\n\t\t\t\t3410715C27D1AFD800CAC805 /* Utils.h */,\n\t\t\t\t3410714C27D1AFD700CAC805 /* WechatObjects.h */,\n\t\t\t\t3410714B27D1AFD700CAC805 /* WechatParser.cpp */,\n\t\t\t\t3410716027D1AFD800CAC805 /* WechatParser.h */,\n\t\t\t\t3410715327D1AFD800CAC805 /* WechatSource.h */,\n\t\t\t\t3410715F27D1AFD800CAC805 /* XmlParser.cpp */,\n\t\t\t\t3410716527D1AFD800CAC805 /* XmlParser.h */,\n\t\t\t);\n\t\t\tname = core;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3410719627D1B01700CAC805 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t341071C627D1B5CB00CAC805 /* libsqlite3.tbd */,\n\t\t\t\t341071B527D1B1A300CAC805 /* libiconv.tbd */,\n\t\t\t\t341071B427D1B19600CAC805 /* libz.tbd */,\n\t\t\t\t341071A427D1B18F00CAC805 /* libcrypto.1.1.dylib */,\n\t\t\t\t341071A027D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib */,\n\t\t\t\t341071A327D1B18F00CAC805 /* libimobiledevice-glue-1.0.0.dylib */,\n\t\t\t\t3410719E27D1B18F00CAC805 /* libmp3lame.0.dylib */,\n\t\t\t\t341071A127D1B18F00CAC805 /* libplist-2.0.3.dylib */,\n\t\t\t\t341071A227D1B18F00CAC805 /* libssl.1.1.dylib */,\n\t\t\t\t3410719F27D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib */,\n\t\t\t\t3410719B27D1B04A00CAC805 /* libxml2.tbd */,\n\t\t\t\t3410719727D1B01700CAC805 /* libcurl.tbd */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t3410713C27D1AF0600CAC805 /* WechatExporterCmd */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 3410714427D1AF0600CAC805 /* Build configuration list for PBXNativeTarget \"WechatExporterCmd\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t3410713927D1AF0600CAC805 /* Sources */,\n\t\t\t\t3410713A27D1AF0600CAC805 /* Frameworks */,\n\t\t\t\t3410713B27D1AF0600CAC805 /* CopyFiles */,\n\t\t\t\t341071B327D1B19000CAC805 /* Embed Libraries */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = WechatExporterCmd;\n\t\t\tproductName = WechatExporterCmd;\n\t\t\tproductReference = 3410713D27D1AF0600CAC805 /* WechatExporterCmd */;\n\t\t\tproductType = \"com.apple.product-type.tool\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t3410713527D1AF0600CAC805 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 1200;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t3410713C27D1AF0600CAC805 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 12.0.1;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 3410713827D1AF0600CAC805 /* Build configuration list for PBXProject \"WechatExporterCmd\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 3410713427D1AF0600CAC805;\n\t\t\tproductRefGroup = 3410713E27D1AF0600CAC805 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t3410713C27D1AF0600CAC805 /* WechatExporterCmd */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t3410713927D1AF0600CAC805 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t3410718E27D1AFD900CAC805 /* Exporter.cpp in Sources */,\n\t\t\t\t3410719427D1AFD900CAC805 /* Utils_thread.cpp in Sources */,\n\t\t\t\t3410718D27D1AFD900CAC805 /* FileSystem.cpp in Sources */,\n\t\t\t\t3410718627D1AFD900CAC805 /* MessageParser.cpp in Sources */,\n\t\t\t\t3410719027D1AFD900CAC805 /* Utils.cpp in Sources */,\n\t\t\t\t3410718727D1AFD900CAC805 /* TaskManager.cpp in Sources */,\n\t\t\t\t3410719127D1AFD900CAC805 /* ResManager.cpp in Sources */,\n\t\t\t\t3410718C27D1AFD900CAC805 /* Utils_audio.cpp in Sources */,\n\t\t\t\t3410718927D1AFD900CAC805 /* AsyncExecutor.cpp in Sources */,\n\t\t\t\t3410718327D1AFD900CAC805 /* AsyncTask.cpp in Sources */,\n\t\t\t\t3410718127D1AFD900CAC805 /* Updater.cpp in Sources */,\n\t\t\t\t3410718A27D1AFD900CAC805 /* IDeviceBackup.cpp in Sources */,\n\t\t\t\t3410718827D1AFD900CAC805 /* XmlParser.cpp in Sources */,\n\t\t\t\t3410718F27D1AFD900CAC805 /* Utils_silk.cpp in Sources */,\n\t\t\t\t3410718027D1AFD900CAC805 /* Utils_protobuf.cpp in Sources */,\n\t\t\t\t3410718B27D1AFD900CAC805 /* Downloader.cpp in Sources */,\n\t\t\t\t3410717E27D1AFD900CAC805 /* WechatParser.cpp in Sources */,\n\t\t\t\t3410717F27D1AFD900CAC805 /* Utils_md5.cpp in Sources */,\n\t\t\t\t3410718427D1AFD900CAC805 /* RawMessage.cpp in Sources */,\n\t\t\t\t340E16BA2823B83600ECB4CD /* Template.cpp in Sources */,\n\t\t\t\t3410719327D1AFD900CAC805 /* ITunesParser.cpp in Sources */,\n\t\t\t\t3410714127D1AF0600CAC805 /* WechatExporter.cpp in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin XCBuildConfiguration section */\n\t\t3410714227D1AF0600CAC805 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.10;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t3410714327D1AF0600CAC805 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.10;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t3410714527D1AF0600CAC805 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_TEAM = L848BW5698;\n\t\t\t\tENABLE_HARDENED_RUNTIME = YES;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tHEADER_SEARCH_PATHS = (\n\t\t\t\t\t/usr/local/include/,\n\t\t\t\t\t\"${SDKROOT}/usr/include/libxml2/\",\n\t\t\t\t\t\"releases/windows-libs/include\",\n\t\t\t\t);\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tLIBRARY_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(PROJECT_DIR)/releases/windows-libs/x64/rel/cmd\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.10;\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"-L/usr/local/lib\",\n\t\t\t\t\t\"-lprotobufd\",\n\t\t\t\t\t\"-ljsoncpp\",\n\t\t\t\t\t\"-lSKP_SILK_SDK\",\n\t\t\t\t\t\"-lopencore-amrnb\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t3410714627D1AF0600CAC805 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_TEAM = L848BW5698;\n\t\t\t\tENABLE_HARDENED_RUNTIME = YES;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = \"NDEBUG=1\";\n\t\t\t\tHEADER_SEARCH_PATHS = (\n\t\t\t\t\t/usr/local/include/,\n\t\t\t\t\t\"${SDKROOT}/usr/include/libxml2/\",\n\t\t\t\t\t\"releases/windows-libs/include\",\n\t\t\t\t);\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tLIBRARY_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(PROJECT_DIR)/releases/windows-libs/x64/rel/cmd\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.10;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"-L/usr/local/lib\",\n\t\t\t\t\t\"-lprotobuf\",\n\t\t\t\t\t\"-ljsoncpp\",\n\t\t\t\t\t\"-lSKP_SILK_SDK\",\n\t\t\t\t\t\"-lopencore-amrnb\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t3410713827D1AF0600CAC805 /* Build configuration list for PBXProject \"WechatExporterCmd\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t3410714227D1AF0600CAC805 /* Debug */,\n\t\t\t\t3410714327D1AF0600CAC805 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t3410714427D1AF0600CAC805 /* Build configuration list for PBXNativeTarget \"WechatExporterCmd\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t3410714527D1AF0600CAC805 /* Debug */,\n\t\t\t\t3410714627D1AF0600CAC805 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 3410713527D1AF0600CAC805 /* Project object */;\n}\n"
  },
  {
    "path": "WechatExporterCmd.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "WechatExporterCmd.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "WechatExporterCmd.xcodeproj/xcshareddata/xcschemes/WechatExporterCmd.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1200\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"3410713C27D1AF0600CAC805\"\n               BuildableName = \"WechatExporterCmd\"\n               BlueprintName = \"WechatExporterCmd\"\n               ReferencedContainer = \"container:WechatExporterCmd.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"3410713C27D1AF0600CAC805\"\n            BuildableName = \"WechatExporterCmd\"\n            BlueprintName = \"WechatExporterCmd\"\n            ReferencedContainer = \"container:WechatExporterCmd.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <CommandLineArguments>\n         <CommandLineArgument\n            argument = \"--output=&quot;/Users/matthew/Documents/WxExp/WechatHistory.cmd&quot;\"\n            isEnabled = \"YES\">\n         </CommandLineArgument>\n         <CommandLineArgument\n            argument = \"--account=&quot;Matthew&quot;\"\n            isEnabled = \"YES\">\n         </CommandLineArgument>\n         <CommandLineArgument\n            argument = \"--session=&quot;&#x65b0;&#x6c5f;&#x6e7e;&#x5c11;&#x513f;&#x6210;&#x957f;&#x7fa4;2&#x7fa4;&quot;\"\n            isEnabled = \"YES\">\n         </CommandLineArgument>\n         <CommandLineArgument\n            argument = \"--session=&quot;&#x1f353;&#x8349;&#x8393;&#x73ed;&#x1f353;&quot;\"\n            isEnabled = \"YES\">\n         </CommandLineArgument>\n         <CommandLineArgument\n            argument = \"--backup=&quot;/Users/matthew/Library/Application Support/MobileSync/Backup/11833774f1a5eed6ca84c0270417670f1483deae&quot;\"\n            isEnabled = \"YES\">\n         </CommandLineArgument>\n      </CommandLineArguments>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"3410713C27D1AF0600CAC805\"\n            BuildableName = \"WechatExporterCmd\"\n            BlueprintName = \"WechatExporterCmd\"\n            ReferencedContainer = \"container:WechatExporterCmd.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "docs/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>微信聊天记录导出程序 Wechat Exporter</title>\n    <meta charset=\"UTF-8\">\n    <meta name=\"description\" content=\"微信聊天记录导出程序 Wechat chat history exporting\">\n    <meta name=\"keywords\" content=\"微信,聊天记录,导出,Wechat,chat history,export, c++, c plusplus, windows, mac, macos, Visual Studio, Xcode, github\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n    <meta http-equiv=\"Pragma\" content=\"no-cache\" />\n    <meta http-equiv=\"Expires\" content=\"0\" />\n  </head>\n  <body>\n\n\n    <h1 id=\"wechatexporter\">微信聊天记录导出程序 Wechat Exporter (for Windows/MacOS)</h1>\n    <p>C++源码：<a href=\"https://github.com/BlueMatthew/WechatExporter\">https://github.com/BlueMatthew/WechatExporter</a></p>\n    <h2 id=\"操作步骤\">操作步骤</h2>\n    <ol style=\"list-style-type: decimal\">\n    <li><p>通过iTunes将手机备份到电脑上（建议备份前杀掉微信），Windows操作系统一般位于目录：C:\\用户[用户名]\\AppData\\Roaming\\Apple Computer\\MobileSync\\Backup\\。Android手机可以找一个iPad/iPhone设备，把聊天记录迁移到iPad/iPhone设备上，然后通过iTunes备份到电脑上。</p></li>\n    <li><p>下载本代码的执行文件：<a href=\"binaries/x64_win.zip\">Windows x64版本</a> 或者 <a href=\"binaries/x64_macos.zip\">MacOS x64版本</a>，然后解压压缩文件</p></li>\n    <li><p>执行解压出来的WechatExport.exe/WechatExporter (Windows下如果运行报缺少必须的dll文件，请安装<a href=\"https://aka.ms/vs/16/release/vc_redist.x64.exe\">Visual C++ 2017 redist</a>后再尝试运行)</p></li>\n    <li><p>按界面提示进行操作。<br />\n        <img src=\"screenshots/win.png\" alt=\"Windows界面截屏\" width=\"720\" />\n        <br />\n        <img src=\"screenshots/mac.png\" alt=\"MacOS界面截屏\" width=\"720\" /></p></li>\n    </ol>\n    <h2 id=\"模版修改\">模版修改</h2>\n    <p>解压目录下的res(MacOS版本位于Contents)子目录里存放了输出聊天记录的html页面模版，其中通过两个%包含起来的字符串，譬如，%%NAME%%，不要修改之外，其它页面内容和格式都可以自行调整。</p>\n    <h2 id=\"系统依赖\">系统依赖</h2>\n    <p>Windows版本：Windows 7+, <a href=\"https://aka.ms/vs/16/release/vc_redist.x64.exe\">Visual C++ 2017 redist</a> at <a href=\"https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\">The latest supported Visual C++ downloads</a><br />MacOS版本：MacOS 10.10(Yosemite)+</p>\n    <h2 id=\"程序编译\">程序编译</h2>\n    <p>程序依赖如下第三方库： <br />\n        - libxml2: http://www.xmlsoft.org/<br />\n        - libcurl: https://curl.se/libcurl/<br />\n        - libsqlite3: https://www.sqlite.org/index.html<br />\n        - libprotobuf: https://github.com/protocolbuffers/protobuf<br />\n        - libjsoncpp: https://github.com/open-source-parsers/jsoncpp<br />\n        - lame: http://lame.sourceforge.net/ <br />\n        - silk: https://github.com/collects/silk (也参考了： https://github.com/kn007/silk-v3-decoder)<br />\n        - libplist: https://github.com/libimobiledevice/libplist https://github.com/libimobiledevice-win32/libplist<br />\n        - libiconv(windows only): https://www.gnu.org/software/libiconv/<br />\n        - openssl(windows only)：https://github.com/openssl/openssl<br />\n        - WTL (windows only)：https://sourceforge.net/projects/wtl/</p>\n    <p>MacOS下，libxml2,libcurl,libsqlite3直接使用了Xcode自带的库，其它第三方库需自行编译。<br />libmp3lame需手动删除文件include/libmp3lame.sym中的行：lame_init_old</p>\n    <p>Windows环境下，silk自带Visual Studio工程文件，可以直接利用Visual Studio编译，其余除了libplist之外，都通过vcpkg可以编译。libplist在vcpkg中也存在，但是在编译x64-windows-static target的时候报了错，于是直接通过Visual Studio建了工程进行编译。可以直接下载<a href=\"binaries/x64-windows-static.zip\">预编译好的静态库文件</a></p>\n    <p>&nbsp;</p>\n    <p>&nbsp;</p>\n  </body>\n</html>"
  },
  {
    "path": "docs/update.conf",
    "content": "1.8.0.8\nhttps://src.wakin.org/github/wxexp/\nhttps://src.wakin.org/github/wxexp/"
  },
  {
    "path": "libplist.README.md",
    "content": "the libplist is for win32 from vcpkg as vcpkg install fails on x64-windows-static\nit should be from: https://github.com/libimobiledevice-win32/libplist\n\nFor Mac OS, please use the code at: https://github.com/libimobiledevice/libplist"
  },
  {
    "path": "release",
    "content": "releases"
  },
  {
    "path": "vcproject/AboutDlg.h",
    "content": "// aboutdlg.h : interface of the CAboutDlg class\n//\n/////////////////////////////////////////////////////////////////////////////\n\n#pragma once\n#include <winver.h>\n#include \"VersionDetector.h\"\n\nclass CAboutDlg : public CDialogImpl<CAboutDlg>\n{\npublic:\n\tenum { IDD = IDD_ABOUTBOX };\n\n\tBEGIN_MSG_MAP(CAboutDlg)\n\t\tMESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)\n\t\tCOMMAND_ID_HANDLER(IDOK, OnCloseCmd)\n\t\tCOMMAND_ID_HANDLER(IDCANCEL, OnCloseCmd)\n\tEND_MSG_MAP()\n\n// Handler prototypes (uncomment arguments if needed):\n//\tLRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\n//\tLRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n//\tLRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)\n\n\tLRESULT OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\n\t{\n\t\tm_homePageLinkCtrl.SubclassWindow(GetDlgItem(IDC_VERSION));\n\t\t// Replace to current version\n\t\t// If the version is set in aboud dlg resource directly,\n\t\t// The code below won't bring error.\n\t\t// CStatic lblVersion = GetDlgItem(IDC_VERSION);\n\t\tCString version;\n\t\tm_homePageLinkCtrl.GetWindowText(version);\n\t\tVersionDetector vd;\n\t\tCString newVersion = vd.GetProductVersion();\n\t\tnewVersion = TEXT(\"v\") + newVersion;\n\t\tversion.Replace(TEXT(\"v1.0\"), newVersion);\n\t\tm_homePageLinkCtrl.SetLabel(version);\n\n\t\tm_homePageLinkCtrl.SetHyperLink(TEXT(\"https://github.com/BlueMatthew/WechatExporter\"));\n\n\t\tCenterWindow(GetParent());\n\t\treturn TRUE;\n\t}\n\n\tLRESULT OnCloseCmd(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tEndDialog(wID);\n\t\treturn 0;\n\t}\n\nprotected:\n\tCHyperLink m_homePageLinkCtrl;\n};\n"
  },
  {
    "path": "vcproject/AppConfiguration.cpp",
    "content": "#include \"stdafx.h\"\r\n#include \"AppConfiguration.h\"\r\n#include \"PdfConverterImpl.h\"\r\n#include \"Core.h\"\r\n#include <Shlobj.h>\r\n#include \"Utils.h\"\r\n\r\n#define APP_ROOT_PATH TEXT(\"Software\\\\WechatExporter\")\r\n\r\nvoid AppConfiguration::SetDescOrder(BOOL descOrder)\r\n{\r\n\tSetDwordProperty(TEXT(\"DescOrder\"), descOrder);\r\n}\r\n\r\nBOOL AppConfiguration::GetDescOrder()\r\n{\r\n\tBOOL descOrder = FALSE;\r\n\tDWORD value = 0;\r\n\tif (GetDwordProperty(TEXT(\"DescOrder\"), value))\r\n\t{\r\n\t\tdescOrder = (value != 0) ? TRUE : FALSE;\r\n\t}\r\n\r\n\treturn descOrder;\r\n}\r\n\r\nvoid AppConfiguration::SetUsingRemoteEmoji(BOOL usingRemoteEmoji)\r\n{\r\n\tSetDwordProperty(TEXT(\"UsingRemoteEmoji\"), usingRemoteEmoji);\r\n}\r\n\r\nBOOL AppConfiguration::GetUsingRemoteEmoji()\r\n{\r\n\tDWORD value = 0;\r\n\tGetDwordProperty(TEXT(\"UsingRemoteEmoji\"), value);\r\n\treturn (value != 0) ? TRUE : FALSE;\r\n}\r\n\r\nvoid AppConfiguration::SetIncrementalExporting(BOOL incrementalExporting)\r\n{\r\n\tSetDwordProperty(TEXT(\"IncrementalExp\"), incrementalExporting);\r\n}\r\n\r\nBOOL AppConfiguration::GetIncrementalExporting()\r\n{\r\n\tDWORD value = 0;\t// default is 0\r\n\tGetDwordProperty(TEXT(\"IncrementalExp\"), value);\r\n\treturn (value != 0) ? TRUE : FALSE;\r\n}\r\n\r\nUINT AppConfiguration::GetOutputFormat()\r\n{\r\n\tUINT outputFormat = OUTPUT_FORMAT_HTML;\r\n\tDWORD dwValue = 0;\r\n\tif (GetDwordProperty(TEXT(\"OutputFormat\"), dwValue))\r\n\t{\r\n\t\tif (dwValue >= OUTPUT_FORMAT_HTML && dwValue < OUTPUT_FORMAT_LAST)\r\n\t\t{\r\n\t\t\toutputFormat = dwValue;\r\n\t\t}\r\n\t}\r\n\r\n\treturn outputFormat;\r\n}\r\n\r\nvoid AppConfiguration::SetOutputFormat(UINT outputFormat)\r\n{\r\n\tif (outputFormat < OUTPUT_FORMAT_HTML || outputFormat >= OUTPUT_FORMAT_LAST)\r\n\t{\r\n\t\toutputFormat = OUTPUT_FORMAT_HTML;\r\n\t}\r\n\tSetDwordProperty(TEXT(\"OutputFormat\"), outputFormat);\r\n}\r\n\r\nvoid AppConfiguration::SetSavingInSession(BOOL savingInSession)\r\n{\r\n\tSetDwordProperty(TEXT(\"SaveFilesInSF\"), savingInSession);\r\n}\r\n\r\nBOOL AppConfiguration::GetSavingInSession()\r\n{\r\n\tDWORD value = 1;\r\n\tif (GetDwordProperty(TEXT(\"SaveFilesInSF\"), value))\r\n\t{\r\n\t\treturn value != 0;\r\n\t}\r\n\treturn TRUE;\r\n}\r\n\r\nvoid AppConfiguration::SetLastOutputDir(LPCTSTR szOutputDir)\r\n{\r\n\tSetStringProperty(TEXT(\"OutputDir\"), szOutputDir);\r\n}\r\n\r\nCString AppConfiguration::GetLastOrDefaultOutputDir()\r\n{\r\n\tCString outputDir;\r\n\tif (GetStringProperty(TEXT(\"OutputDir\"), outputDir))\r\n\t{\r\n\t\treturn outputDir;\r\n\t}\r\n\r\n\treturn GetDefaultOutputDir();\r\n}\r\n\r\nCString AppConfiguration::GetDefaultOutputDir()\r\n{\r\n\tTCHAR szOutput[MAX_PATH] = { 0 };\r\n\tHRESULT hr = SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, szOutput);\r\n\treturn CString(SUCCEEDED(hr) ? szOutput : TEXT(\"\"));\r\n}\r\n\r\nvoid AppConfiguration::SetLastBackupDir(LPCTSTR szBackupDir)\r\n{\r\n\tSetStringProperty(TEXT(\"BackupDir\"), szBackupDir);\r\n}\r\n\r\nCString AppConfiguration::GetLastBackupDir()\r\n{\r\n\tCString backupDir;\r\n\tGetStringProperty(TEXT(\"BackupDir\"), backupDir);\r\n\treturn backupDir;\r\n}\r\n\r\nCString AppConfiguration::GetDefaultBackupDir(BOOL bCheckExistence/* = TRUE*/)\r\n{\r\n\tCString backupDir;\r\n\tTCHAR szPath[2][MAX_PATH] = { { 0 }, { 0 } };\r\n\r\n\t// Check iTunes Folder\r\n\tHRESULT hr = SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, szPath[0]);\r\n\t_tcscat(szPath[0], TEXT(\"\\\\Apple Computer\\\\MobileSync\\\\Backup\"));\r\n\r\n\t// iTunes App from MS Store\r\n\thr = SHGetFolderPath(NULL, CSIDL_PROFILE, NULL, SHGFP_TYPE_CURRENT, szPath[1]);\r\n\t_tcscat(szPath[1], TEXT(\"\\\\Apple\\\\MobileSync\\\\Backup\"));\r\n\r\n\tfor (int idx = 0; idx < 2; ++idx)\r\n\t{\r\n\t\tDWORD dwAttrib = ::GetFileAttributes(szPath[idx]);\r\n\t\tif (dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY))\r\n\t\t{\r\n\t\t\tbackupDir = szPath[idx];\r\n\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\tif (!bCheckExistence && backupDir.IsEmpty())\r\n\t{\r\n\t\tbackupDir = szPath[0];\r\n\t}\r\n\r\n\treturn backupDir;\r\n}\r\n\r\nDWORD AppConfiguration::GetLastCheckUpdateTime()\r\n{\r\n\tDWORD dwValue = 0;\r\n\tGetDwordProperty(TEXT(\"LastChkUpdateTime\"), dwValue);\r\n\treturn dwValue;\r\n}\r\n\r\nvoid AppConfiguration::SetLastCheckUpdateTime(DWORD lastCheckUpdateTime/* = 0*/)\r\n{\r\n\tif (0 == lastCheckUpdateTime)\r\n\t{\r\n\t\tlastCheckUpdateTime = (DWORD)getUnixTimeStamp();\r\n\t}\r\n\tSetDwordProperty(TEXT(\"LastChkUpdateTime\"), lastCheckUpdateTime);\r\n}\r\n\r\nvoid AppConfiguration::SetCheckingUpdateDisabled(BOOL disabled)\r\n{\r\n\tSetDwordProperty(TEXT(\"ChkUpdateDisabled\"), disabled);\r\n}\r\n\r\nBOOL AppConfiguration::GetCheckingUpdateDisabled()\r\n{\r\n\tDWORD dwValue = 0;\r\n\tGetDwordProperty(TEXT(\"ChkUpdateDisabled\"), dwValue);\r\n\treturn dwValue != 0;\r\n}\r\n\r\n\r\nvoid AppConfiguration::SetSyncLoading()\r\n{\r\n\tSetDwordProperty(TEXT(\"AsyncLoadingState\"), ASYNC_NONE);\r\n}\r\n\r\nBOOL AppConfiguration::GetSyncLoading()\r\n{\r\n\treturn GetDwordValue(TEXT(\"AsyncLoadingState\"), ASYNC_ONSCROLL) == ASYNC_NONE;\r\n}\r\n\r\nDWORD AppConfiguration::GetAsyncLoading()\r\n{\r\n\treturn GetDwordValue(TEXT(\"AsyncLoadingState\"), ASYNC_ONSCROLL);\r\n}\r\n\r\nvoid AppConfiguration::SetAsyncLoading(DWORD asyncLoading)\r\n{\r\n\tSetDwordProperty(TEXT(\"AsyncLoadingState\"), asyncLoading);\r\n}\r\n\r\nvoid AppConfiguration::SetLoadingDataOnScroll()\r\n{\r\n\tSetDwordProperty(TEXT(\"AsyncLoadingState\"), ASYNC_ONSCROLL);\r\n}\r\n\r\nBOOL AppConfiguration::GetLoadingDataOnScroll()\r\n{\r\n\treturn GetDwordValue(TEXT(\"AsyncLoadingState\"), ASYNC_ONSCROLL) == ASYNC_ONSCROLL;\r\n}\r\n\r\nvoid AppConfiguration::SetNormalPagination()\r\n{\r\n\tSetDwordProperty(TEXT(\"AsyncLoadingState\"), ASYNC_PAGER_NORMAL);\r\n}\r\n\r\nBOOL AppConfiguration::GetNormalPagination()\r\n{\r\n\treturn GetDwordValue(TEXT(\"AsyncLoadingState\"), ASYNC_ONSCROLL) == ASYNC_PAGER_NORMAL;\r\n}\r\n\r\nvoid AppConfiguration::SetPaginationOnYear()\r\n{\r\n\tSetDwordProperty(TEXT(\"AsyncLoadingState\"), ASYNC_PAGER_ON_YEAR);\r\n}\r\n\r\nBOOL AppConfiguration::GetPaginationOnYear()\r\n{\r\n\treturn GetDwordValue(TEXT(\"AsyncLoadingState\"), ASYNC_ONSCROLL) == ASYNC_PAGER_ON_YEAR;\r\n}\r\n\r\nvoid AppConfiguration::SetPaginationOnMonth()\r\n{\r\n\tSetDwordProperty(TEXT(\"AsyncLoadingState\"), ASYNC_PAGER_ON_MONTH);\r\n}\r\n\r\nBOOL AppConfiguration::GetPaginationOnMonth()\r\n{\r\n\treturn GetDwordValue(TEXT(\"AsyncLoadingState\"), ASYNC_ONSCROLL) == ASYNC_PAGER_ON_MONTH;\r\n}\r\n\r\nvoid AppConfiguration::SetSupportingFilter(BOOL supportingFilter)\r\n{\r\n\tSetDwordProperty(TEXT(\"Filter\"), supportingFilter);\r\n}\r\n\r\nBOOL AppConfiguration::GetSupportingFilter()\r\n{\r\n\tDWORD dwValue = 0;\t// FALSE\r\n\tGetDwordProperty(TEXT(\"Filter\"), dwValue);\r\n\treturn dwValue != 0;\r\n}\r\n\r\nvoid AppConfiguration::SetOutputDebugLogs(BOOL dbgLogs)\r\n{\r\n\tSetDwordProperty(TEXT(\"DebugLogs\"), dbgLogs);\r\n}\r\n\r\nBOOL AppConfiguration::OutputDebugLogs()\r\n{\r\n\tDWORD dwValue = 0;\t// FALSE\r\n\tGetDwordProperty(TEXT(\"DebugLogs\"), dwValue);\r\n\treturn dwValue != 0;\r\n}\r\n\r\nvoid AppConfiguration::SetIncludingSubscriptions(BOOL includingSubscriptions)\r\n{\r\n\tSetDwordProperty(TEXT(\"IncludingSubscriptions\"), includingSubscriptions);\r\n}\r\n\r\nBOOL AppConfiguration::IncludeSubscriptions()\r\n{\r\n\tDWORD dwValue = 0;\t// FALSE\r\n\tGetDwordProperty(TEXT(\"IncludingSubscriptions\"), dwValue);\r\n\treturn dwValue != 0;\r\n}\r\n\r\nvoid AppConfiguration::SetOpenningFolderAfterExp(BOOL openningFolderAfterExp)\r\n{\r\n\tSetDwordProperty(TEXT(\"OpenningFolderAfterExp\"), openningFolderAfterExp);\r\n}\r\n\r\nBOOL AppConfiguration::GetOpenningFolderAfterExp()\r\n{\r\n\tDWORD dwValue = 1;\t// TRUE\r\n\tGetDwordProperty(TEXT(\"OpenningFolderAfterExp\"), dwValue);\r\n\treturn dwValue != 0;\r\n}\r\n\r\nBOOL AppConfiguration::IsPdfSupported()\r\n{\r\n\tPdfConverterImpl converter(NULL);\r\n\r\n\treturn converter.isPdfSupported() ? TRUE : FALSE;\r\n}\r\n\r\n\r\nvoid AppConfiguration::upgrade()\r\n{\r\n\tCRegKey rk;\r\n\tif (rk.Create(HKEY_CURRENT_USER, APP_ROOT_PATH, REG_NONE, REG_OPTION_NON_VOLATILE, KEY_READ | KEY_WRITE) != ERROR_SUCCESS)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\tDWORD value = 0;\r\n\tHRESULT hr = rk.QueryDWORDValue(TEXT(\"AsyncLoading\"), value);\r\n\tif (ERROR_SUCCESS == hr)\r\n\t{\r\n\t\trk.DeleteValue(TEXT(\"AsyncLoading\"));\r\n\t\tif (value == 0)\r\n\t\t{\r\n\t\t\tSetSyncLoading();\r\n\t\t}\r\n\t}\r\n\r\n\thr = rk.QueryDWORDValue(TEXT(\"LoadingDataOnScroll\"), value);\r\n\tif (ERROR_SUCCESS == hr)\r\n\t{\r\n\t\trk.DeleteValue(TEXT(\"LoadingDataOnScroll\"));\r\n\t\tif (value == 1)\r\n\t\t{\r\n\t\t\tSetLoadingDataOnScroll();\r\n\t\t}\r\n\t}\r\n\r\n\trk.Close();\r\n}\r\n\r\nuint64_t AppConfiguration::BuildOptions()\r\n{\r\n\r\n\tExportOption options;\r\n\r\n\tif (GetOutputFormat() == OUTPUT_FORMAT_TEXT)\r\n\t{\r\n\t\toptions.setTextMode();\r\n\t}\r\n\r\n\tif (GetOutputFormat() == OUTPUT_FORMAT_PDF)\r\n\t{\r\n\t\toptions.setPdfMode();\r\n\t}\r\n\r\n\toptions.setOrder(!GetDescOrder());\r\n\r\n\t// getSavingInSession\r\n\r\n\tif (GetSyncLoading())\r\n\t{\r\n\t\toptions.setSyncLoading();\r\n\t}\r\n\telse\r\n\t{\r\n\t\toptions.setLoadingDataOnScroll(GetLoadingDataOnScroll());\r\n\t}\r\n\r\n\toptions.setIncrementalExporting(GetIncrementalExporting());\r\n\toptions.supportsFilter(GetSupportingFilter());\r\n\r\n\toptions.outputDebugLogs(OutputDebugLogs());\r\n\tif (IncludeSubscriptions())\r\n\t{\r\n\t\toptions.includesSubscription();\r\n\t}\r\n\r\n\treturn (uint64_t)options;\r\n}\r\n\r\nBOOL AppConfiguration::GetStringProperty(LPCTSTR name, CString& value)\r\n{\r\n\tCRegKey rk;\r\n\tif (rk.Open(HKEY_CURRENT_USER, APP_ROOT_PATH, KEY_READ) == ERROR_SUCCESS)\r\n\t{\r\n\t\tULONG chars = 0;\r\n\t\tHRESULT hr = rk.QueryStringValue(name, NULL, &chars);\r\n\t\tif (ERROR_SUCCESS == hr)\r\n\t\t{\r\n\t\t\thr = rk.QueryStringValue(name, value.GetBufferSetLength(chars), &chars);\r\n\t\t\tvalue.ReleaseBuffer();\r\n\t\t\treturn ERROR_SUCCESS == hr;\r\n\t\t}\r\n\t\t\r\n\t\trk.Close();\r\n\t}\r\n\r\n\treturn FALSE;\r\n}\r\n\r\nBOOL AppConfiguration::SetStringProperty(LPCTSTR name, LPCTSTR value)\r\n{\r\n\tCRegKey rk;\r\n\tif (rk.Create(HKEY_CURRENT_USER, APP_ROOT_PATH, REG_NONE, REG_OPTION_NON_VOLATILE, KEY_READ | KEY_WRITE) == ERROR_SUCCESS)\r\n\t{\r\n\t\tHRESULT hr = rk.SetStringValue(name, value);\r\n\t\trk.Close();\r\n\t\treturn ERROR_SUCCESS == hr;\r\n\t}\r\n\r\n\treturn FALSE;\r\n}\r\n\r\nBOOL AppConfiguration::GetDwordProperty(LPCTSTR name, DWORD& value)\r\n{\r\n\tCRegKey rk;\r\n\tif (rk.Open(HKEY_CURRENT_USER, APP_ROOT_PATH, KEY_READ) == ERROR_SUCCESS)\r\n\t{\r\n\t\tHRESULT hr = rk.QueryDWORDValue(name, value);\r\n\t\trk.Close();\r\n\t\treturn ERROR_SUCCESS == hr;\r\n\t}\r\n\r\n\treturn FALSE;\r\n}\r\n\r\nDWORD AppConfiguration::GetDwordValue(LPCTSTR name, DWORD defaultValue)\r\n{\r\n\tDWORD value = defaultValue;\r\n\tif (GetDwordProperty(name, value))\r\n\t{\r\n\t\treturn value;\r\n\t}\r\n\r\n\treturn defaultValue;\r\n}\r\n\r\nBOOL AppConfiguration::SetDwordProperty(LPCTSTR name, DWORD value)\r\n{\r\n\tCRegKey rk;\r\n\tif (rk.Create(HKEY_CURRENT_USER, APP_ROOT_PATH, REG_NONE, REG_OPTION_NON_VOLATILE, KEY_READ | KEY_WRITE) == ERROR_SUCCESS)\r\n\t{\r\n\t\tHRESULT hr = rk.SetDWORDValue(name, value);\r\n\t\trk.Close();\r\n\t\treturn ERROR_SUCCESS == hr;\r\n\t}\r\n\r\n\treturn FALSE;\r\n}\r\n"
  },
  {
    "path": "vcproject/AppConfiguration.h",
    "content": "#pragma once\r\n\r\n#include <cstdint>\r\n\r\n#define ASYNC_NONE              0\r\n#define ASYNC_ONSCROLL          1\r\n#define ASYNC_PAGER_NORMAL      2\r\n#define ASYNC_PAGER_ON_YEAR     3\r\n#define ASYNC_PAGER_ON_MONTH    4\r\n\r\nclass AppConfiguration\r\n{\r\npublic:\r\n\r\n\tenum { OUTPUT_FORMAT_HTML = 0, OUTPUT_FORMAT_TEXT, OUTPUT_FORMAT_PDF, OUTPUT_FORMAT_LAST };\r\n\r\n\tstatic void SetDescOrder(BOOL descOrder);\r\n\tstatic BOOL GetDescOrder();\r\n\tstatic UINT GetOutputFormat();\r\n\tstatic void SetOutputFormat(UINT outputFormat);\r\n\tstatic void SetSavingInSession(BOOL savingInSession);\r\n\tstatic BOOL GetSavingInSession();\r\n\r\n\tstatic void SetUsingRemoteEmoji(BOOL usingRemoteEmoji);\r\n\tstatic BOOL GetUsingRemoteEmoji();\r\n\r\n\tstatic void SetIncrementalExporting(BOOL incrementalExporting);\r\n\tstatic BOOL GetIncrementalExporting();\r\n\t\r\n\tstatic void SetLastOutputDir(LPCTSTR szOutputDir);\r\n\tstatic CString GetLastOrDefaultOutputDir();\r\n\tstatic CString GetDefaultOutputDir();\r\n\r\n\tstatic void SetLastBackupDir(LPCTSTR szBackupDir);\r\n\tstatic CString GetLastBackupDir();\r\n\tstatic CString GetDefaultBackupDir(BOOL bCheckExistence = TRUE);\r\n\r\n\tstatic DWORD GetLastCheckUpdateTime();\r\n\tstatic void SetLastCheckUpdateTime(DWORD lastCheckUpdateTime = 0);\r\n\r\n\tstatic void SetCheckingUpdateDisabled(BOOL disabled);\r\n\tstatic BOOL GetCheckingUpdateDisabled();\r\n\r\n\tstatic void SetLoadingDataOnScroll();\r\n\tstatic BOOL GetLoadingDataOnScroll();\r\n\r\n\tstatic void SetSyncLoading();\r\n\tstatic BOOL GetSyncLoading();\r\n\tstatic DWORD GetAsyncLoading();\r\n\tstatic void SetAsyncLoading(DWORD asyncLoading);\r\n\r\n\tstatic void SetNormalPagination();\r\n\tstatic BOOL GetNormalPagination();\r\n\tstatic void SetPaginationOnYear();\r\n\tstatic BOOL GetPaginationOnYear();\r\n\tstatic void SetPaginationOnMonth();\r\n\tstatic BOOL GetPaginationOnMonth();\r\n\r\n\tstatic void SetSupportingFilter(BOOL supportingFilter);\r\n\tstatic BOOL GetSupportingFilter();\r\n\r\n\tstatic void SetOutputDebugLogs(BOOL dbgLogs);\r\n\tstatic BOOL OutputDebugLogs();\r\n\r\n\tstatic void SetIncludingSubscriptions(BOOL includingSubscriptions);\r\n\tstatic BOOL IncludeSubscriptions();\r\n\r\n\tstatic void SetOpenningFolderAfterExp(BOOL openningFolderAfterExp);\r\n\tstatic BOOL GetOpenningFolderAfterExp();\r\n\r\n\tstatic BOOL IsPdfSupported();\r\n\r\n\tstatic void upgrade();\r\n\tstatic uint64_t BuildOptions();\r\n\r\nprotected:\r\n\tstatic BOOL GetStringProperty(LPCTSTR name, CString& value);\r\n\tstatic BOOL SetStringProperty(LPCTSTR name, LPCTSTR value);\r\n\r\n\tstatic BOOL GetDwordProperty(LPCTSTR name, DWORD& value);\r\n\tstatic BOOL SetDwordProperty(LPCTSTR name, DWORD value);\r\n\tstatic DWORD GetDwordValue(LPCTSTR name, DWORD defaultValue);\r\n\r\n\tstatic BOOL IsAppInstalled(LPCTSTR name, BOOL lmOrCU);\r\n\r\n};\r\n"
  },
  {
    "path": "vcproject/BackupDlg.h",
    "content": "// aboutdlg.h : interface of the CAboutDlg class\n//\n/////////////////////////////////////////////////////////////////////////////\n\n#pragma once\n\n#include <future>\n\nclass CBackupDlg : public CDialogImpl<CBackupDlg>\n{\npublic:\n\tenum { IDD = IDD_BACKUP_DLG };\n\n\tBEGIN_MSG_MAP(CBackupDlg)\n\t\tMESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)\n\t\tMESSAGE_HANDLER(WM_TIMER, OnTimer)\n\t\t// COMMAND_ID_HANDLER(IDOK, OnCloseCmd)\n\t\tMESSAGE_HANDLER(WM_DESTROY, OnDestroy)\n\t\tCOMMAND_ID_HANDLER(IDCANCEL, OnCloseCmd)\n\tEND_MSG_MAP()\n\n\n\tCBackupDlg(const DeviceInfo& deviceInfo, const CString& outputDir) : m_deviceInfo(deviceInfo), m_outputDir(outputDir), m_backup(NULL), m_eventId(0), m_result(false)\n\t{\n\n\t}\n\n// Handler prototypes (uncomment arguments if needed):\n//\tLRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\n//\tLRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n//\tLRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)\n\n\tLRESULT OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\n\t{\n\t\tCenterWindow(GetParent());\n\n\t\tCProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS);\n\t\t// progressCtrl.SetWindowText(TEXT(\"\"));\n\t\tprogressCtrl.ModifyStyle(PBS_MARQUEE, 0);\n\t\tprogressCtrl.SetRange32(1, 100);\n\t\tprogressCtrl.SetStep(1);\n\t\tprogressCtrl.SetPos(0);\n\n\t\tCW2A outputDir(CT2W((LPCTSTR)m_outputDir), CP_UTF8);\n\t\tm_backup = new IDeviceBackup(m_deviceInfo, (LPCSTR)outputDir);\n\n\t\tm_task = std::async(std::launch::async, &IDeviceBackup::backup, m_backup);\n\n\t\tm_eventId = SetTimer(1, 200);\n\t\treturn TRUE;\n\t}\n\n\tLRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\n\t{\n\t\tif (0 != m_eventId)\n\t\t{\n\t\t\tKillTimer(m_eventId);\n\t\t}\n\t\tCenterWindow(GetParent());\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnTimer(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\n\t{\n\t\tif (NULL != m_backup)\n\t\t{\n\t\t\tdouble progress = m_backup->getOverallProgress();\n\t\t\tint pos = (int)(progress * 100);\n\t\t\tif (pos > 100)\n\t\t\t{\n\t\t\t\tpos = 100;\n\t\t\t}\n\t\t\telse if (pos < 0)\n\t\t\t{\n\t\t\t\tpos = 0;\n\t\t\t}\n\t\t\t\n\t\t\tCProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS);\n\t\t\tif (pos != progressCtrl.GetPos())\n\t\t\t{\n\t\t\t\tprogressCtrl.SetPos(pos);\n\t\t\t}\n\t\t}\n\t\tstd::future_status status = m_task.wait_for(std::chrono::seconds(0));\n\t\tif (status == std::future_status::ready)\n\t\t{\n\t\t\tKillTimer(m_eventId);\n\t\t\tm_eventId = 0;\n\t\t\t\n\t\t\tm_result = m_task.get();\n\n\t\t\tEndDialog(IDOK);\n\t\t}\n\n\t\t\n\t\treturn 0;\n\t}\n\n\tLRESULT OnCloseCmd(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tEndDialog(wID);\n\t\treturn 0;\n\t}\n\n\tbool getBackupResult() const\n\t{\n\t\treturn m_result;\n\t}\n\nprotected:\n\t// CHyperLink m_homePageLinkCtrl;\n\tconst DeviceInfo&\tm_deviceInfo;\n\tCString m_outputDir;\n\tIDeviceBackup* m_backup;\n\tstd::future<bool> m_task;\n\n\tUINT m_eventId;\n\n\tbool m_result;\n};\n"
  },
  {
    "path": "vcproject/ColoredControls.h",
    "content": "#if !defined(AFX_COLOREDCONTROLS_H__20020226_0D16_1B29_088A_0080AD509054__INCLUDED_)\r\n#define AFX_COLOREDCONTROLS_H__20020226_0D16_1B29_088A_0080AD509054__INCLUDED_\r\n\r\n#pragma once\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// Colored Control - Windows Controls with colours\r\n//\r\n// Written by Bjarke Viksoe (bjarke@viksoe.dk)\r\n// Copyright (c) 2002 Bjarke Viksoe.\r\n//\r\n// This file include the following controls:\r\n//   CColoredDialog\r\n//   CColoredStaticCtrl\r\n//   CColoredEditCtrl\r\n//   CColoredButtonCtrl\r\n//   CColoredComboBoxCtrl\r\n//   CColoredTabCtrl\r\n//   CColoredListViewCtrl\r\n//   CColoredTreeViewCtrl\r\n//\r\n// Add the following macro to the parent's message map:\r\n//   REFLECT_NOTIFICATIONS()\r\n//\r\n// This code may be used in compiled form in any way you desire. This\r\n// source file may be redistributed by any means PROVIDING it is \r\n// not sold for profit without the authors written consent, and \r\n// providing that this notice and the authors name is included. \r\n//\r\n// This file is provided \"as is\" with no expressed or implied warranty.\r\n// The author accepts no liability if it causes any damage to you or your\r\n// computer whatsoever. It's free, so don't hassle me about it.\r\n//\r\n// Beware of bugs.\r\n//\r\n\r\n#ifndef __cplusplus\r\n  #error WTL requires C++ compilation (use a .cpp suffix)\r\n#endif\r\n\r\n#ifndef __ATLCTRLS_H__\r\n  #error ColoredControls.h requires atlctrls.h to be included first\r\n#endif\r\n\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// Dialog with new colour or bitmap background\r\n\r\n// To use this class: Derive from CColoredDialog<Base> and then\r\n// chain the message map CHAIN_MSG_MAP(CColoredDialog<Base>)...\r\ntemplate< class T >\r\nclass CColoredDialog\r\n{\r\npublic:\r\n   CBrush m_brBack;\r\n\r\n   // Operations\r\n\r\n   BOOL SetBitmap(UINT nRes)\r\n   {\r\n      // Load the bitmap and create a new brush\r\n      CBitmap bmBack;\r\n      bmBack.LoadBitmap(nRes);\r\n      if( !m_brBack.IsNull() ) m_brBack.DeleteObject();\r\n      m_brBack.CreatePatternBrush(bmBack);\r\n      // Repaint\r\n      T* pT = static_cast<T*>(this);\r\n      pT->Invalidate();\r\n   }\r\n   void SetColor(COLORREF clr)\r\n   {\r\n      // Create a new brush from the color\r\n      if( !m_brBack.IsNull() ) m_brBack.DeleteObject();\r\n      m_brBack.CreateSolidBrush(clr);\r\n      // Repaint\r\n      T* pT = static_cast<T*>(this);\r\n      pT->Invalidate();\r\n   }\r\n\r\n   // Message map and handlers\r\n\r\n   BEGIN_MSG_MAP(CColoredDialog)\r\n      MESSAGE_HANDLER(WM_CTLCOLORDLG, OnCtlColorDlg)\r\n   END_MSG_MAP()\r\n\r\n   LRESULT OnCtlColorDlg(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n   {\r\n      T* pT = static_cast<T*>(this);\r\n      if( m_brBack.IsNull() ) return pT->DefWindowProc();\r\n      return (LRESULT) (HBRUSH) m_brBack;\r\n   }\r\n};\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// CColoredStaticCtrl - A Static control with new colours\r\n\r\ntemplate< class T, class TBase = CStatic, class TWinTraits = CControlWinTraits >\r\nclass ATL_NO_VTABLE CColoredStaticImpl : \r\n   public CWindowImpl< T, TBase, TWinTraits >\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName())\r\n\r\n   // Color codes for Static control\r\n   COLORREF      m_clrBackground;\r\n   COLORREF      m_clrNormalText;\r\n   COLORREF      m_clrNormalBk;\r\n   COLORREF      m_clrDisabledText;\r\n   COLORREF      m_clrDisabledBk;\r\n   // User-defined colours needs a brush\r\n   CBrush        m_brBackground;\r\n   CBrush        m_brNormalBk;\r\n   CBrush        m_brDisabledBk;\r\n   // We use a brush-handle when no custom colours have been assigned\r\n   // because system-color brushes must not be destroyed...\r\n   CBrushHandle  m_hbrNormalBk;\r\n   CBrushHandle  m_hbrDisabledBk;\r\n\r\n   // Operations\r\n\r\n   BOOL SubclassWindow(HWND hWnd)\r\n   {\r\n      ATLASSERT(m_hWnd==NULL);\r\n      ATLASSERT(::IsWindow(hWnd));\r\n#ifdef _DEBUG\r\n      // Check class\r\n      TCHAR szBuffer[16];\r\n      if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) {\r\n         ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0);\r\n      }\r\n#endif\r\n      BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd);\r\n      if( bRet ) _Init();\r\n      return bRet;\r\n   }\r\n\r\n   void SetNormalColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT);\r\n      m_clrNormalText = clrText;\r\n      // Background\r\n      if( !m_brNormalBk.IsNull() ) m_brNormalBk.DeleteObject();\r\n      if( clrBack == CLR_INVALID ) {\r\n         m_clrNormalBk = ::GetSysColor(COLOR_BTNFACE);\r\n         m_hbrNormalBk = ::GetSysColorBrush(COLOR_BTNFACE);\r\n      }\r\n      else {\r\n         m_clrNormalBk = clrBack;\r\n         m_brNormalBk.CreateSolidBrush(clrBack);\r\n         m_hbrNormalBk = m_brNormalBk;\r\n      }\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetDisabledColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_GRAYTEXT);\r\n      m_clrDisabledText = clrText;\r\n      // Background\r\n      if( !m_brDisabledBk.IsNull() ) m_brDisabledBk.DeleteObject();\r\n      if( clrBack == CLR_INVALID ) {\r\n         m_clrDisabledBk = ::GetSysColor(COLOR_BTNFACE);\r\n         m_hbrDisabledBk = ::GetSysColorBrush(COLOR_BTNFACE);\r\n      }\r\n      else {\r\n         m_clrDisabledBk = clrBack;\r\n         m_brDisabledBk.CreateSolidBrush(clrBack);\r\n         m_hbrDisabledBk = m_brDisabledBk;\r\n      }\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetBkColor(COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Background\r\n      if( !m_brBackground.IsNull() ) m_brBackground.DeleteObject();\r\n      m_clrBackground = clrBack;\r\n      m_brBackground.CreateSolidBrush(clrBack);\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n\r\n   // Implementation\r\n\r\n   void _Init()\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      COLORREF clrNone = CLR_INVALID;\r\n      SetNormalColors( clrNone, clrNone );\r\n      SetDisabledColors( clrNone, clrNone );\r\n   }\r\n\r\n   // Message map and handlers\r\n\r\n   BEGIN_MSG_MAP(CColoredStaticImpl)\r\n      MESSAGE_HANDLER(WM_CREATE, OnCreate)\r\n      MESSAGE_HANDLER(OCM_CTLCOLORSTATIC, OnCtlColorStatic)\r\n      MESSAGE_HANDLER(OCM_CTLCOLORDLG, OnCtlColorDlg)\r\n      DEFAULT_REFLECTION_HANDLER()\r\n   END_MSG_MAP()\r\n\r\n   LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      LRESULT lRes = DefWindowProc(uMsg, wParam, lParam);\r\n      _Init();\r\n      return lRes;\r\n   }\r\n\r\n   LRESULT OnCtlColorStatic(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n   {\r\n      BOOL bEnabled = IsWindowEnabled();\r\n      CDCHandle dc( (HDC) wParam );\r\n      dc.SetBkMode(TRANSPARENT);\r\n      dc.SetTextColor( bEnabled ? m_clrNormalText : m_clrDisabledText );\r\n      dc.SetBkColor( bEnabled ? m_clrNormalBk : m_clrDisabledBk );\r\n      return (LRESULT) (HBRUSH) ( bEnabled ? m_hbrNormalBk : m_hbrDisabledBk );\r\n   }\r\n   LRESULT OnCtlColorDlg(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n   {\r\n      T* pT = static_cast<T*>(this);\r\n      if( m_brBackground.IsNull() ) return pT->DefWindowProc();\r\n      return (LRESULT) (HBRUSH) m_brBackground;\r\n   }\r\n};\r\n\r\nclass CColoredStaticCtrl : public CColoredStaticImpl<CColoredStaticCtrl>\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(_T(\"WTL_ColoredStatic\"), GetWndClassName())  \r\n};\r\n\r\nclass CColoredCheckboxCtrl : public CColoredStaticImpl<CColoredCheckboxCtrl, CButton>\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(_T(\"WTL_ColoredCheckbox\"), GetWndClassName())  \r\n};\r\n\r\nclass CColoredOptionCtrl : public CColoredStaticImpl<CColoredOptionCtrl, CButton>\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(_T(\"WTL_ColoredOption\"), GetWndClassName())  \r\n};\r\n\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// CColoredEditCtrl - An Edit control with new colours\r\n\r\ntemplate< class T, class TBase = CEdit, class TWinTraits = CControlWinTraits >\r\nclass ATL_NO_VTABLE CColoredEditImpl : \r\n   public CWindowImpl< T, TBase, TWinTraits >\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName())\r\n\r\n   // Color codes for Edit control\r\n   COLORREF      m_clrNormalText;\r\n   COLORREF      m_clrNormalBk;\r\n   COLORREF      m_clrDisabledText;\r\n   COLORREF      m_clrDisabledBk;\r\n   COLORREF      m_clrReadOnlyText;\r\n   COLORREF      m_clrReadOnlyBk;\r\n   // User-defined colours needs a brush\r\n   CBrush        m_brNormalBk;\r\n   CBrush        m_brDisabledBk;\r\n   CBrush        m_brReadOnlyBk;\r\n   // We use a brush-handle when no custom colours have been assigned\r\n   // because system-color brushes must not be destroyed...\r\n   CBrushHandle  m_hbrNormalBk;\r\n   CBrushHandle  m_hbrDisabledBk;\r\n   CBrushHandle  m_hbrReadOnlyBk;\r\n\r\n   // Operations\r\n\r\n   BOOL SubclassWindow(HWND hWnd)\r\n   {\r\n      ATLASSERT(m_hWnd==NULL);\r\n      ATLASSERT(::IsWindow(hWnd));\r\n#ifdef _DEBUG\r\n      // Check class\r\n      TCHAR szBuffer[16];\r\n      if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) {\r\n         ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0);\r\n      }\r\n#endif\r\n      BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd);\r\n      if( bRet ) _Init();\r\n      return bRet;\r\n   }\r\n\r\n   void SetNormalColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_WINDOWTEXT);\r\n      m_clrNormalText = clrText;\r\n      // Background\r\n      if( !m_brNormalBk.IsNull() ) m_brNormalBk.DeleteObject();\r\n      if( clrBack == CLR_INVALID ) {\r\n         m_clrNormalBk = ::GetSysColor(COLOR_WINDOW);\r\n         m_hbrNormalBk = ::GetSysColorBrush(COLOR_WINDOW);\r\n      }\r\n      else {\r\n         m_clrNormalBk = clrBack;\r\n         m_brNormalBk.CreateSolidBrush(clrBack);\r\n         m_hbrNormalBk = m_brNormalBk;\r\n      }\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetDisabledColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_GRAYTEXT);\r\n      m_clrDisabledText = clrText;\r\n      // Background\r\n      if( !m_brDisabledBk.IsNull() ) m_brDisabledBk.DeleteObject();\r\n      if( clrBack == CLR_INVALID ) {\r\n         m_clrDisabledBk = ::GetSysColor(COLOR_BTNFACE);\r\n         m_hbrDisabledBk = ::GetSysColorBrush(COLOR_BTNFACE);\r\n      }\r\n      else {\r\n         m_clrDisabledBk = clrBack;\r\n         m_brDisabledBk.CreateSolidBrush(clrBack);\r\n         m_hbrDisabledBk = m_brDisabledBk;\r\n      }\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetReadOnlyColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT);\r\n      m_clrReadOnlyText = clrText;\r\n      // Background\r\n      if( !m_brReadOnlyBk.IsNull() ) m_brReadOnlyBk.DeleteObject();\r\n      if( clrBack == CLR_INVALID ) {\r\n         m_clrReadOnlyBk = ::GetSysColor(COLOR_BTNFACE);\r\n         m_hbrReadOnlyBk = ::GetSysColorBrush(COLOR_BTNFACE);\r\n      }\r\n      else {\r\n         m_clrReadOnlyBk = clrBack;\r\n         m_brReadOnlyBk.CreateSolidBrush(clrBack);\r\n         m_hbrReadOnlyBk = m_brReadOnlyBk;\r\n      }\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n\r\n   // Implementation\r\n\r\n   void _Init()\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      COLORREF clrNone = CLR_INVALID;\r\n      SetNormalColors( clrNone, clrNone );\r\n      SetDisabledColors( clrNone, clrNone );\r\n      SetReadOnlyColors( clrNone, clrNone );\r\n   }\r\n\r\n   // Message map and handlers\r\n\r\n   BEGIN_MSG_MAP(CColoredEditImpl)\r\n      MESSAGE_HANDLER(WM_CREATE, OnCreate)\r\n      MESSAGE_HANDLER(OCM_CTLCOLOREDIT, OnCtlColorEdit)\r\n      MESSAGE_HANDLER(OCM_CTLCOLORMSGBOX, OnCtlColorEdit)\r\n      MESSAGE_HANDLER(OCM_CTLCOLORSTATIC, OnCtlColorStatic)\r\n      DEFAULT_REFLECTION_HANDLER()\r\n   END_MSG_MAP()\r\n\r\n   LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      LRESULT lRes = DefWindowProc(uMsg, wParam, lParam);\r\n      _Init();\r\n      return lRes;\r\n   }\r\n\r\n   LRESULT OnCtlColorEdit(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n   {\r\n      CDCHandle dc( (HDC) wParam );\r\n      dc.SetBkMode(TRANSPARENT);\r\n      dc.SetTextColor( m_clrNormalText );\r\n      dc.SetBkColor( m_clrNormalBk );\r\n      return (LRESULT) (HBRUSH) m_hbrNormalBk;\r\n   }\r\n\r\n   LRESULT OnCtlColorStatic(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n   {\r\n      // Microsoft Q130952 explains why we also need this\r\n      CDCHandle dc( (HDC) wParam );\r\n      dc.SetBkMode(TRANSPARENT);\r\n      if( IsWindowEnabled() ) {\r\n         ATLASSERT(GetStyle() & ES_READONLY);\r\n         dc.SetTextColor( m_clrReadOnlyText );\r\n         dc.SetBkColor( m_clrReadOnlyBk );\r\n         return (LRESULT) (HBRUSH) m_hbrReadOnlyBk;\r\n      }\r\n      else {\r\n         dc.SetTextColor( m_clrDisabledText );\r\n         dc.SetBkColor( m_clrDisabledBk );\r\n         return (LRESULT) (HBRUSH) m_hbrDisabledBk;\r\n      }\r\n   }\r\n};\r\n\r\nclass CColoredEditCtrl : public CColoredEditImpl<CColoredEditCtrl>\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(_T(\"WTL_ColoredEdit\"), GetWndClassName())  \r\n};\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// CColoredButtonCtrl - The Button control with custom colours\r\n\r\ntemplate< class T, class TBase = CButton, class TWinTraits = CControlWinTraits >\r\nclass ATL_NO_VTABLE CColoredButtonImpl : \r\n   public CWindowImpl< T, TBase, TWinTraits >\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName())\r\n\r\n   // Color codes for Button control\r\n   COLORREF      m_clrText;\r\n   COLORREF      m_clrBackUp;\r\n   COLORREF      m_clrBackDown;\r\n   COLORREF      m_clrBackDisabled;\r\n   // Image support\r\n   CImageList    m_ImageList;\r\n   UINT          m_nImage;\r\n   UINT          m_nSelImage;\r\n\r\n   // Operations\r\n\r\n   BOOL SubclassWindow(HWND hWnd)\r\n   {\r\n      ATLASSERT(m_hWnd==NULL);\r\n      ATLASSERT(::IsWindow(hWnd));\r\n#ifdef _DEBUG\r\n      // Check class\r\n      TCHAR szBuffer[20];\r\n      if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) {\r\n         ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0);\r\n      }\r\n#endif\r\n      BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd);\r\n      if( bRet ) _Init();\r\n      return bRet;\r\n   }\r\n\r\n   void SetTextColor(COLORREF clrText)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT);\r\n      m_clrText = clrText;\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetBkColor(COLORREF clrUp, COLORREF clrDown=-1)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      if( clrUp == CLR_INVALID ) clrUp = ::GetSysColor(COLOR_BTNFACE);\r\n      if( clrDown == CLR_INVALID ) clrDown = clrUp;\r\n      m_clrBackUp = clrUp;\r\n      m_clrBackDown = clrDown;\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetDisabledColor(COLORREF clrDisabled)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      if( clrDisabled == CLR_INVALID ) clrDisabled = ::GetSysColor(COLOR_BTNFACE);\r\n      m_clrBackDisabled = clrDisabled;\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetImageList(HIMAGELIST hImageList)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      m_ImageList = hImageList;\r\n   }\r\n   void SetImage(UINT nImage)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      ATLASSERT(!m_ImageList.IsNull());\r\n      m_nImage = nImage;\r\n   }\r\n   void SetSelImage(UINT nImage)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      ATLASSERT(!m_ImageList.IsNull());\r\n      m_nSelImage = nImage;\r\n   }\r\n\r\n   // Implementation\r\n\r\n   void _Init()\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n\r\n      // We need this style to prevent Windows from painting the button\r\n      ModifyStyle(0, BS_OWNERDRAW);\r\n      \r\n      COLORREF clrNone = CLR_INVALID;\r\n      SetTextColor( clrNone );\r\n      SetBkColor( clrNone, clrNone );\r\n      SetDisabledColor( clrNone );\r\n      m_nImage = m_nSelImage = (UINT)-1;\r\n   }\r\n\r\n   // Message map and handlers\r\n\r\n   BEGIN_MSG_MAP(CColoredButtonImpl)\r\n      MESSAGE_HANDLER(WM_CREATE, OnCreate)\r\n      MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)\r\n      MESSAGE_HANDLER(WM_LBUTTONDBLCLK, OnLButtonDblClick)\r\n      MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)\r\n   END_MSG_MAP()\r\n\r\n   LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      LRESULT lRes = DefWindowProc(uMsg, wParam, lParam);\r\n      _Init();\r\n      return lRes;\r\n   }\r\n   LRESULT OnEraseBackground(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n   {\r\n      return 1;   // no background needed\r\n   }\r\n   LRESULT OnLButtonDblClick(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      return DefWindowProc(WM_LBUTTONDOWN, wParam, lParam);\r\n   }   \r\n   LRESULT OnDrawItem(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      LPDRAWITEMSTRUCT lpDIS = (LPDRAWITEMSTRUCT) lParam;\r\n      ATLASSERT(lpDIS->CtlType==ODT_BUTTON);\r\n      CDCHandle dc = lpDIS->hDC;\r\n      RECT rc = lpDIS->rcItem;\r\n      DWORD dwStyle = GetStyle();\r\n      bool bSelected = (lpDIS->itemState & ODS_SELECTED) != 0;\r\n      bool bDisabled = (lpDIS->itemState & (ODS_DISABLED|ODS_GRAYED)) != 0;\r\n\r\n      COLORREF clrBack = bSelected ? m_clrBackDown : m_clrBackUp;\r\n      if( bDisabled ) clrBack = m_clrBackDisabled;\r\n      dc.FillSolidRect(&rc, clrBack);\r\n\r\n      // Draw edge\r\n      if( dwStyle & BS_FLAT ) {\r\n         dc.DrawEdge(&rc, bSelected ? BDR_SUNKENOUTER : BDR_RAISEDINNER, BF_RECT);\r\n      }\r\n      else {\r\n         dc.DrawEdge(&rc, bSelected ? EDGE_SUNKEN : EDGE_RAISED, BF_RECT);\r\n      }\r\n      ::InflateRect(&rc, -2 * ::GetSystemMetrics(SM_CXEDGE), -2 * ::GetSystemMetrics(SM_CYEDGE));\r\n\r\n      // Draw focus rectangle\r\n      if( lpDIS->itemState & ODS_FOCUS ) dc.DrawFocusRect(&rc);\r\n      ::InflateRect(&rc, -1, -1);\r\n\r\n      // Offset when button is selected\r\n      if( bSelected ) ::OffsetRect(&rc, 1, 1);\r\n\r\n      // Draw image\r\n      UINT nImage = m_nImage;\r\n      if( bSelected && m_nSelImage != -1 ) nImage = m_nSelImage;\r\n      if( !m_ImageList.IsNull() && nImage != -1 ) {\r\n         POINT pt = { rc.left, rc.top-1 };\r\n         SIZE sizeIcon;\r\n         m_ImageList.GetIconSize(sizeIcon);\r\n         if( bDisabled ) {\r\n            //m_ImageList.DrawEx(nImage, dc, pt.x, pt.y, sizeIcon.cx, sizeIcon.cy, CLR_NONE, ::GetSysColor(COLOR_GRAYTEXT), ILD_BLEND50 | ILD_TRANSPARENT);\r\n            HICON hIcon = m_ImageList.GetIcon(nImage);\r\n            dc.DrawState(pt, sizeIcon, hIcon, DST_ICON | DSS_DISABLED);\r\n            ::DestroyIcon(hIcon);\r\n         }\r\n         else {\r\n            m_ImageList.Draw(dc, nImage, pt, bDisabled ? ILD_BLEND25 : ILD_TRANSPARENT);\r\n         }\r\n         rc.left += sizeIcon.cx + 3;\r\n      }\r\n\r\n      // Draw text\r\n      UINT nLen = GetWindowTextLength();\r\n      LPTSTR pstr = (LPTSTR) _alloca( (nLen+1)*sizeof(TCHAR) );\r\n      GetWindowText(pstr, nLen+1);\r\n      UINT uFlags = 0;\r\n      if( dwStyle & BS_LEFT ) uFlags |= DT_LEFT;\r\n      else if( dwStyle & BS_RIGHT ) uFlags |= DT_RIGHT;\r\n      else if( dwStyle & BS_CENTER ) uFlags |= DT_CENTER;\r\n      else uFlags |= DT_CENTER;\r\n      if( dwStyle & BS_TOP ) uFlags |= DT_TOP;\r\n      else if( dwStyle & BS_BOTTOM ) uFlags |= DT_BOTTOM;\r\n      else if( dwStyle & BS_VCENTER ) uFlags |= DT_VCENTER;\r\n      else uFlags |= DT_VCENTER;\r\n      if( (dwStyle & BS_MULTILINE) == 0 ) uFlags |= DT_SINGLELINE;\r\n      dc.SetBkMode(TRANSPARENT);\r\n      dc.SetTextColor(bDisabled ? ::GetSysColor(COLOR_GRAYTEXT) : m_clrText);\r\n      dc.DrawText(pstr, nLen, &rc, uFlags);\r\n\r\n      return TRUE;\r\n   }\r\n};\r\n\r\nclass CColoredButtonCtrl : public CColoredButtonImpl<CColoredButtonCtrl>\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(_T(\"WTL_ColoredButton\"), GetWndClassName())  \r\n};\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// CColoredComboBoxCtrl - The ComboBox control with custom colours\r\n\r\ntemplate< class T, class TBase = CComboBox, class TWinTraits = CControlWinTraits >\r\nclass ATL_NO_VTABLE CColoredComboBoxImpl : \r\n   public CWindowImpl< T, TBase, TWinTraits >\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName())\r\n\r\n   // Color codes for ComboBox control\r\n   COLORREF      m_clrListText;\r\n   COLORREF      m_clrListBk;\r\n   COLORREF      m_clrSelectedListText;\r\n   COLORREF      m_clrSelectedListBk;\r\n   COLORREF      m_clrDisabledText;\r\n   COLORREF      m_clrDisabledBk;\r\n   COLORREF      m_clrEditText;\r\n   COLORREF      m_clrEditBk;\r\n   // User-defined colours needs a brush\r\n   CBrush        m_brListBk;\r\n   CBrush        m_brSelectedListBk;\r\n   CBrush        m_brDisabledBk;\r\n   CBrush        m_brEditBk;\r\n   // We use a brush-handle when no custom colours have been assigned\r\n   // because system-color brushes must not be destroyed...\r\n   CBrushHandle  m_hbrListBk;\r\n   CBrushHandle  m_hbrSelectedListBk;\r\n   CBrushHandle  m_hbrDisabledBk;\r\n   CBrushHandle  m_hbrEditBk;\r\n\r\n   // Operations\r\n\r\n   BOOL SubclassWindow(HWND hWnd)\r\n   {\r\n      ATLASSERT(m_hWnd==NULL);\r\n      ATLASSERT(::IsWindow(hWnd));\r\n#ifdef _DEBUG\r\n      // Check class\r\n      TCHAR szBuffer[16];\r\n      if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) {\r\n         ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0);\r\n      }\r\n#endif\r\n      BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd);\r\n      if( bRet ) _Init();\r\n      return bRet;\r\n   }\r\n\r\n   void SetListColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_WINDOWTEXT);\r\n      m_clrListText = clrText;\r\n      // Background\r\n      if( !m_brListBk.IsNull() ) m_brListBk.DeleteObject();\r\n      if( clrBack == CLR_INVALID ) {\r\n         m_clrListBk = ::GetSysColor(COLOR_WINDOW);\r\n         m_hbrListBk = ::GetSysColorBrush(COLOR_WINDOW);\r\n      }\r\n      else {\r\n         m_clrListBk = clrBack;\r\n         m_brListBk.CreateSolidBrush(clrBack);\r\n         m_hbrListBk = m_brListBk;\r\n      }\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetDisabledColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_GRAYTEXT);\r\n      m_clrDisabledText = clrText;\r\n      // Background\r\n      if( !m_brDisabledBk.IsNull() ) m_brDisabledBk.DeleteObject();\r\n      if( clrBack == CLR_INVALID ) {\r\n         m_clrDisabledBk = ::GetSysColor(COLOR_BTNFACE);\r\n         m_hbrDisabledBk = ::GetSysColorBrush(COLOR_BTNFACE);\r\n      }\r\n      else {\r\n         m_clrDisabledBk = clrBack;\r\n         m_brDisabledBk.CreateSolidBrush(clrBack);\r\n         m_hbrDisabledBk = m_brDisabledBk;\r\n      }\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetEditColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT);\r\n      m_clrEditText = clrText;\r\n      // Background\r\n      if( !m_brEditBk.IsNull() ) m_brEditBk.DeleteObject();\r\n      if( clrBack == CLR_INVALID ) {\r\n         m_clrEditBk = ::GetSysColor(COLOR_BTNFACE);\r\n         m_hbrEditBk = ::GetSysColorBrush(COLOR_BTNFACE);\r\n      }\r\n      else {\r\n         m_clrEditBk = clrBack;\r\n         m_brEditBk.CreateSolidBrush(clrBack);\r\n         m_hbrEditBk = m_brEditBk;\r\n      }\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n\r\n   // Implementation\r\n\r\n   void _Init()\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      COLORREF clrNone = CLR_INVALID;\r\n      SetListColors( clrNone, clrNone );\r\n      SetDisabledColors( clrNone, clrNone );\r\n      SetEditColors( clrNone, clrNone );\r\n   }\r\n\r\n   // Message map and handlers\r\n\r\n   BEGIN_MSG_MAP(CColoredComboBoxImpl)\r\n      MESSAGE_HANDLER(WM_CREATE, OnCreate)\r\n      MESSAGE_HANDLER(WM_CTLCOLOREDIT, OnCtlColorEdit)\r\n      MESSAGE_HANDLER(OCM_CTLCOLOREDIT, OnCtlColorEdit)\r\n      MESSAGE_HANDLER(WM_CTLCOLORMSGBOX, OnCtlColorEdit)\r\n      MESSAGE_HANDLER(WM_CTLCOLORLISTBOX, OnCtlColorListBox)\r\n      MESSAGE_HANDLER(OCM_CTLCOLORSTATIC, OnCtlColorStatic)\r\n      DEFAULT_REFLECTION_HANDLER()\r\n   END_MSG_MAP()\r\n\r\n   LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      LRESULT lRes = DefWindowProc(uMsg, wParam, lParam);\r\n      _Init();\r\n      return lRes;\r\n   }\r\n\r\n   LRESULT OnCtlColorEdit(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n   {\r\n      CDCHandle dc( (HDC) wParam );\r\n      dc.SetBkMode(TRANSPARENT);\r\n      dc.SetTextColor( m_clrEditText );\r\n      dc.SetBkColor( m_clrEditBk );\r\n      return (LRESULT) (HBRUSH) m_hbrEditBk;\r\n   }\r\n   LRESULT OnCtlColorListBox(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n   {\r\n      CDCHandle dc( (HDC) wParam );\r\n      dc.SetBkMode(TRANSPARENT);\r\n      dc.SetTextColor( m_clrListText );\r\n      dc.SetBkColor( m_clrListBk );\r\n      return (LRESULT) (HBRUSH) m_hbrListBk;\r\n   }\r\n   LRESULT OnCtlColorStatic(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n   {\r\n      CDCHandle dc( (HDC) wParam );\r\n      dc.SetBkMode(TRANSPARENT);\r\n      if( IsWindowEnabled() ) {\r\n         dc.SetTextColor( m_clrEditText );\r\n         dc.SetBkColor( m_clrEditBk );\r\n         return (LRESULT) (HBRUSH) m_hbrEditBk;\r\n      }\r\n      else {\r\n         dc.SetTextColor( m_clrDisabledText );\r\n         dc.SetBkColor( m_clrDisabledBk );\r\n         return (LRESULT) (HBRUSH) m_hbrDisabledBk;\r\n      }\r\n   }\r\n};\r\n\r\nclass CColoredComboBoxCtrl : public CColoredComboBoxImpl<CColoredComboBoxCtrl>\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(_T(\"WTL_ColoredComboBox\"), GetWndClassName())  \r\n};\r\n\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// CColoredTabCtrl - A Tab control with tab colours\r\n\r\ntemplate< class T, class TBase = CTabCtrl, class TWinTraits = CControlWinTraits >\r\nclass ATL_NO_VTABLE CColoredTabImpl : \r\n   public CWindowImpl< T, TBase, TWinTraits >\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName())\r\n\r\n   // Color codes for Tab control\r\n   COLORREF m_clrBackground;\r\n   COLORREF m_clrNormalText;\r\n   COLORREF m_clrNormalBk;\r\n   COLORREF m_clrDisabledText;\r\n   COLORREF m_clrDisabledBk;\r\n   COLORREF m_clrInactiveText;\r\n   COLORREF m_clrInactiveBk;\r\n   COLORREF m_clrHighlightText;\r\n   COLORREF m_clrHighlightBk;\r\n   HFONT    m_hInactiveFont;\r\n\r\n   // Operations\r\n\r\n   BOOL SubclassWindow(HWND hWnd)\r\n   {\r\n      ATLASSERT(m_hWnd==NULL);\r\n      ATLASSERT(::IsWindow(hWnd));\r\n#ifdef _DEBUG\r\n      // Check class\r\n      TCHAR szBuffer[20];\r\n      if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) {\r\n         ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0);\r\n      }\r\n#endif\r\n      BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd);\r\n      if( bRet ) _Init();\r\n      return bRet;\r\n   }\r\n\r\n   void SetBackgroundColor(COLORREF clrBack)\r\n   {\r\n      if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE);\r\n      m_clrBackground = clrBack;\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetNormalColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_WINDOWTEXT);\r\n      m_clrNormalText = clrText;\r\n      // Background\r\n      if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE);\r\n      m_clrNormalBk = clrBack;\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetDisabledColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_GRAYTEXT);\r\n      m_clrDisabledText = clrText;\r\n      // Background\r\n      if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE);\r\n      m_clrDisabledBk = clrBack;\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetInactiveColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT);\r\n      m_clrInactiveText = clrText;\r\n      // Background\r\n      if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE);\r\n      m_clrInactiveBk = clrBack;\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetHighlightColors(COLORREF clrText, COLORREF clrBack)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // Text \r\n#if(WINVER >= 0x0500)\r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_HOTLIGHT);\r\n#else\r\n      if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_HIGHLIGHT);\r\n#endif\r\n      m_clrHighlightText = clrText;\r\n      // Background\r\n      if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE);\r\n      m_clrHighlightBk = clrBack;\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n   void SetInactiveFont(HFONT hFont)\r\n   {\r\n      m_hInactiveFont = hFont;\r\n      // Repaint\r\n      Invalidate();\r\n   }\r\n\r\n   // Implementation\r\n\r\n   void _Init()\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      ATLASSERT((GetStyle() & (TCS_BUTTONS|TCS_VERTICAL))==0);\r\n\r\n      // We're drawing the text...\r\n      ModifyStyle(0, TCS_OWNERDRAWFIXED);\r\n\r\n      m_hInactiveFont = NULL;\r\n      COLORREF clrNone = CLR_INVALID;\r\n      SetBackgroundColor( clrNone );\r\n      SetNormalColors( clrNone, clrNone );\r\n      SetDisabledColors( clrNone, clrNone );\r\n      SetInactiveColors( clrNone, clrNone );\r\n      SetHighlightColors( clrNone, clrNone );\r\n   }\r\n\r\n   // Message map and handlers\r\n\r\n   BEGIN_MSG_MAP(CColoredTabImpl)\r\n      MESSAGE_HANDLER(WM_CREATE, OnCreate)\r\n      MESSAGE_HANDLER(WM_PAINT, OnPaint)\r\n      MESSAGE_HANDLER(WM_PRINTCLIENT, OnPaint)\r\n      MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)\r\n      DEFAULT_REFLECTION_HANDLER()\r\n   END_MSG_MAP()\r\n\r\n   LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      LRESULT lRes = DefWindowProc(uMsg, wParam, lParam);\r\n      _Init();\r\n      return lRes;\r\n   }\r\n\r\n   LRESULT OnDrawItem(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      LPDRAWITEMSTRUCT lpDIS = (LPDRAWITEMSTRUCT) lParam;\r\n      ATLASSERT(lpDIS->CtlType==ODT_TAB);\r\n      ATLASSERT(lpDIS->itemAction & ODA_DRAWENTIRE);\r\n      CDCHandle dc = lpDIS->hDC;\r\n      RECT rc = lpDIS->rcItem;\r\n\r\n      DWORD dwStyle = GetStyle();\r\n      HFONT hFont = GetFont();\r\n\r\n      TCHAR szText[128];\r\n      TCITEM itm = { 0 };\r\n      itm.mask = TCIF_TEXT | TCIF_IMAGE | TCIF_STATE;\r\n      itm.pszText = szText;\r\n      itm.cchTextMax = sizeof(szText)/sizeof(TCHAR);\r\n      itm.dwStateMask = (DWORD) -1;\r\n      GetItem(lpDIS->itemID, &itm);\r\n\r\n      COLORREF clrText;\r\n      COLORREF clrBack;\r\n      int cyOffset = 0;\r\n      if( lpDIS->itemState & ODS_DISABLED ) {\r\n         clrText = m_clrDisabledText;\r\n         clrBack = m_clrDisabledBk;\r\n      }\r\n      else if( itm.dwState & TCIS_HIGHLIGHTED ) {\r\n         clrText = m_clrHighlightText;\r\n         clrBack = m_clrHighlightBk;\r\n      }\r\n      else if( lpDIS->itemState & ODS_SELECTED ) {\r\n         clrText = m_clrNormalText;\r\n         clrBack = m_clrNormalBk;\r\n         cyOffset = 1;\r\n      }\r\n      else {\r\n         clrText = m_clrInactiveText;\r\n         clrBack = m_clrInactiveBk;\r\n         if( m_hInactiveFont != NULL ) hFont = m_hInactiveFont;\r\n      }\r\n\r\n      dc.FillSolidRect(&rc, clrBack);\r\n\r\n      if( itm.iImage != -1 ) {\r\n         CImageList iml = GetImageList();\r\n         int cxImage, cyImage;\r\n         iml.GetIconSize(cxImage, cyImage);\r\n         POINT pt = { rc.left + 5, rc.top + 1 + ((rc.bottom-rc.top)/2-(cyImage)/2) };\r\n         iml.Draw(dc, itm.iImage, pt, ILD_TRANSPARENT );\r\n         rc.left += cxImage;\r\n      }\r\n\r\n      HFONT hOldFont = dc.SelectFont(hFont);\r\n      dc.SetBkColor(clrBack),\r\n      dc.SetTextColor(clrText);\r\n      dc.SetBkMode(TRANSPARENT);\r\n      UINT dwFlags = DT_VCENTER | DT_SINGLELINE | DT_NOCLIP;\r\n      if( dwStyle & TCS_FORCELABELLEFT ) {\r\n         dwFlags |= DT_LEFT; \r\n         rc.left += 3;\r\n      }\r\n      else {\r\n         dwFlags |= DT_CENTER;\r\n      }\r\n      dc.DrawText(itm.pszText, -1, &rc, dwFlags);\r\n      dc.SelectFont(hOldFont);\r\n\r\n      return TRUE;\r\n   }\r\n\r\n   LRESULT OnPaint(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled)\r\n   {\r\n      T* pT = static_cast<T*>(this);\r\n      if( wParam != NULL ) {\r\n         RECT rc;\r\n         GetClientRect(&rc);\r\n         pT->DoPaint( (HDC) wParam );\r\n         // Let the tab-control paint itself\r\n         bHandled = FALSE;\r\n      }\r\n      else {\r\n         PAINTSTRUCT m_ps;\r\n         HDC hDC = ::BeginPaint(m_hWnd, &m_ps);\r\n         pT->DoPaint(hDC);\r\n         // Let the tab-control paint itself\r\n         DefWindowProc(WM_PRINTCLIENT, (WPARAM) hDC, PRF_CLIENT);\r\n         ::EndPaint(m_hWnd, &m_ps);\r\n      }\r\n      return 0;\r\n   }\r\n\r\n   void DoPaint(CDCHandle dc)\r\n   {\r\n      // Calculate the upper box, so we can paint a\r\n      // different background colour. We need to do the calculation\r\n      // because of a possible multi-line tab-control...\r\n      RECT rcWin;\r\n      GetWindowRect(&rcWin);\r\n      RECT rcClient = rcWin;\r\n      AdjustRect(FALSE, &rcClient);\r\n      RECT rcTop = { 0, 0, rcWin.right-rcWin.left, rcClient.top-rcWin.top };\r\n      dc.FillSolidRect(&rcTop, m_clrBackground);\r\n      // TODO: If you want to paint the tab client-area a different\r\n      //       colour, this is the place to do it...\r\n   }\r\n};\r\n\r\nclass CColoredTabCtrl : public CColoredTabImpl<CColoredTabCtrl>\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(_T(\"WTL_ColoredTab\"), GetWndClassName())  \r\n};\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// CColoredListViewCtrl - A ListView control with individual item colours\r\n\r\ntemplate< class T, class TBase = CListViewCtrl, class TWinTraits = CControlWinTraits >\r\nclass ATL_NO_VTABLE CColoredListViewImpl : \r\n   public CWindowImpl< T, TBase, TWinTraits >,\r\n   public CCustomDraw< T >\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName())\r\n\r\n   typedef struct {\r\n      COLORREF clrText;\r\n      COLORREF clrBackground;\r\n      HFONT    hFont;\r\n      LPARAM   lParam;\r\n   } LVCOLORPARAM;\r\n   int m_nColumns;\r\n\r\n   // Operations\r\n\r\n   BOOL SubclassWindow(HWND hWnd)\r\n   {\r\n      ATLASSERT(m_hWnd==NULL);\r\n      ATLASSERT(::IsWindow(hWnd));\r\n#ifdef _DEBUG\r\n      // Check class\r\n      TCHAR szBuffer[16];\r\n      if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) {\r\n         ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0);\r\n      }\r\n#endif\r\n      BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd);\r\n      if( bRet ) _Init();\r\n      return bRet;\r\n   }\r\n\r\n   int InsertItem(UINT nMask, int nItem, LPCTSTR lpszItem, UINT nState, UINT nStateMask, int nImage=-1, LPARAM lParam=0)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      // You must have initialized columns before calling this!\r\n      // And you are not allowed to add columns once the list is populated!\r\n      CHeaderCtrl ctrlHeader = GetHeader();\r\n      if( m_nColumns == 0 ) m_nColumns = ctrlHeader.GetItemCount();\r\n      ATLASSERT(m_nColumns > 0);\r\n      ATLASSERT(ctrlHeader.GetItemCount()==m_nColumns);\r\n      // Create a place-holder of property controls for each subitem...\r\n      LVCOLORPARAM* pParams;\r\n      ATLTRY( pParams = new LVCOLORPARAM[m_nColumns] );\r\n      ATLASSERT(pParams);\r\n      if( pParams == NULL ) return -1;\r\n      ::FillMemory(pParams, sizeof(LVCOLORPARAM) * m_nColumns, -1);\r\n      // Finally create the listview item itself...\r\n      if( nItem == -1 ) nItem = GetItemCount();\r\n      nMask |= LVIF_PARAM;\r\n      pParams[0].lParam = lParam;\r\n      return TBase::InsertItem(nMask, nItem, lpszItem, nState, nStateMask, nImage, (LPARAM) pParams);\r\n   }\r\n   int InsertItem(int nItem, LPCTSTR lpszItem, int nImage)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      return InsertItem(LVIF_TEXT|LVIF_IMAGE, nItem, lpszItem, 0, 0, nImage, 0);\r\n   }\r\n   DWORD_PTR GetItemData(int nItem) const\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      LVITEM lvi = { 0 };\r\n      lvi.iItem = nItem;\r\n      lvi.mask = LVIF_PARAM;\r\n      if( GetItem(&lvi) == -1 ) return 0;\r\n      LVCOLORPARAM* pParams = (LVCOLORPARAM*) lvi.lParam;\r\n      return (DWORD_PTR) pParams[0].lParam;\r\n   }\r\n   BOOL SetItemColors(int nItem, int nSubItem, COLORREF clrText, COLORREF clrBk)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      LVITEM lvi = { 0 };\r\n      lvi.iItem = nItem;\r\n      lvi.mask = LVIF_PARAM;\r\n      if( GetItem(&lvi) == -1 ) return FALSE;\r\n      LVCOLORPARAM* pParams = (LVCOLORPARAM*) lvi.lParam;\r\n      ATLASSERT(nSubItem>=0 && nSubItem<GetHeader().GetItemCount());\r\n      pParams[ nSubItem ].clrText = clrText;\r\n      pParams[ nSubItem ].clrBackground = clrBk;\r\n      // Repaint\r\n      RedrawItems(nItem, nItem);\r\n      return TRUE;\r\n   }\r\n\r\n   // Unsupported ListView methods\r\n\r\n   BOOL SetItemData(int /*nItem*/, DWORD_PTR /*dwData*/)\r\n   {\r\n      ATLASSERT(false); // not supported\r\n      return FALSE;\r\n   }\r\n   int AddItem(int /*nItem*/, int /*nSubItem*/, LPCTSTR /*strItem*/, int /*nImageIndex = -1*/)\r\n   {\r\n      ATLASSERT(false); // not supported\r\n      return -1;\r\n   }\r\n\r\n   // Implementation\r\n\r\n   void _Init()\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      m_nColumns = 0;\r\n   }\r\n\r\n   // Message map and handlers\r\n\r\n   BEGIN_MSG_MAP(CColoredListViewImpl)\r\n      MESSAGE_HANDLER(WM_CREATE, OnCreate)\r\n      REFLECTED_NOTIFY_CODE_HANDLER(LVN_DELETEITEM, OnDeleteItem)\r\n      CHAIN_MSG_MAP_ALT( CCustomDraw< T >, 1 )\r\n      // Because WTL 3.1 does not support subitem custom drawing.\r\n      // Should be forward-compatible with new versions...\r\n      REFLECTED_NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnNotifyCustomDraw)\r\n      DEFAULT_REFLECTION_HANDLER()\r\n   END_MSG_MAP()\r\n\r\n   LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      LRESULT lRes = DefWindowProc(uMsg, wParam, lParam);\r\n      _Init();\r\n      return lRes;\r\n   }\r\n   LRESULT OnDeleteItem(int /*idCtrl*/, LPNMHDR pnmh, BOOL& /*bHandled*/)\r\n   {\r\n      ATLASSERT(m_nColumns>0);\r\n      LPNMLISTVIEW pnmlv = (LPNMLISTVIEW) pnmh;\r\n      ATLASSERT(pnmlv->lParam);\r\n      LVCOLORPARAM* pParams = reinterpret_cast<LVCOLORPARAM*>(pnmlv->lParam);\r\n      delete [] pParams;\r\n      return 0;\r\n   }\r\n   LRESULT OnNotifyCustomDraw(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)\r\n   {\r\n      T* pT = static_cast<T*>(this);\r\n      pT->SetMsgHandled(FALSE);\r\n      LPNMCUSTOMDRAW lpNMCustomDraw = (LPNMCUSTOMDRAW) pnmh;\r\n      DWORD dwRet = 0;\r\n      switch( lpNMCustomDraw->dwDrawStage ) {\r\n      case CDDS_ITEMPREPAINT | CDDS_SUBITEM:\r\n         dwRet = pT->OnSubItemPrePaint(idCtrl, lpNMCustomDraw);\r\n         pT->SetMsgHandled(TRUE);\r\n         return dwRet;\r\n      }\r\n      bHandled = FALSE;\r\n      return dwRet;\r\n   }\r\n\r\n   // Custom painting\r\n\r\n   DWORD OnPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/)\r\n   {\r\n      return CDRF_NOTIFYITEMDRAW;   // We need per-item notifications\r\n   }\r\n   DWORD OnItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/)\r\n   {\r\n      return CDRF_NOTIFYSUBITEMDRAW; // We need per-subitem notifications\r\n   }\r\n   DWORD OnSubItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW lpNMCustomDraw)\r\n   {\r\n      LPNMLVCUSTOMDRAW lpNMLVCD = (LPNMLVCUSTOMDRAW) lpNMCustomDraw;\r\n      ATLASSERT(lpNMLVCD->nmcd.lItemlParam);\r\n      LVCOLORPARAM* pParams = reinterpret_cast<LVCOLORPARAM*>(lpNMLVCD->nmcd.lItemlParam);     \r\n      const LVCOLORPARAM& param = pParams[ lpNMLVCD->iSubItem ];\r\n      if( param.clrText != CLR_INVALID ) lpNMLVCD->clrText = param.clrText;\r\n      if( param.clrBackground != CLR_INVALID ) lpNMLVCD->clrTextBk = param.clrBackground;\r\n      return CDRF_NEWFONT; // We changed the colors\r\n   }\r\n};\r\n\r\nclass CColoredListViewCtrl : public CColoredListViewImpl<CColoredListViewCtrl>\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(_T(\"WTL_ColoredListView\"), GetWndClassName())  \r\n};\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// CColoredTreeViewCtrl - A TreeView with item colours\r\n\r\ntemplate< class T, class TBase = CTreeViewCtrl, class TWinTraits = CControlWinTraits >\r\nclass ATL_NO_VTABLE CColoredTreeViewImpl : \r\n   public CWindowImpl< T, TBase, TWinTraits >,\r\n   public CCustomDraw< T >\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName())\r\n\r\n   typedef struct tagTVCOLOR {\r\n      COLORREF clrText;\r\n      COLORREF clrBackground;\r\n      COLORREF clrSelText;\r\n      COLORREF clrSelBackground;\r\n      HFONT    hFont;\r\n   } TVCOLOR;\r\n   CSimpleMap< HTREEITEM, TVCOLOR > m_mapColors;\r\n\r\n   // Operations\r\n\r\n   BOOL SubclassWindow(HWND hWnd)\r\n   {\r\n      ATLASSERT(m_hWnd==NULL);\r\n      ATLASSERT(::IsWindow(hWnd));\r\n#ifdef _DEBUG\r\n      // Check class\r\n      TCHAR szBuffer[20];\r\n      if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) {\r\n         ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0);\r\n      }\r\n#endif\r\n      BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd);\r\n      if( bRet ) _Init();\r\n      return bRet;\r\n   }\r\n\r\n   // Use syntax:\r\n   //   CColoredTreeViewCtrl::TVCOLOR col;\r\n   //   col.clrText = RGB(120,120,120);\r\n   //   col.clrBackground = -1;\r\n   //   col.clrSelText = -1;\r\n   //   col.clrSelBackground = -1;\r\n   //   col.hFont = NULL;\r\n   //   ctrl.SetItemColors(hItem, &col);\r\n   //\r\n   void SetItemColors(HTREEITEM hItem, TVCOLOR* color)\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n      m_mapColors.Add(hItem, *color);\r\n   }\r\n\r\n   // Implementation\r\n\r\n   void _Init()\r\n   {\r\n      ATLASSERT(::IsWindow(m_hWnd));\r\n   }\r\n\r\n   // Message map and handlers\r\n\r\n   BEGIN_MSG_MAP(CColoredTreeViewImpl)\r\n      MESSAGE_HANDLER(WM_CREATE, OnCreate)\r\n      REFLECTED_NOTIFY_CODE_HANDLER(TVN_DELETEITEM, OnDeleteItem)\r\n      CHAIN_MSG_MAP_ALT( CCustomDraw< T >, 1 )\r\n      DEFAULT_REFLECTION_HANDLER()\r\n   END_MSG_MAP()\r\n\r\n   LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)\r\n   {\r\n      LRESULT lRes = DefWindowProc(uMsg, wParam, lParam);\r\n      _Init();\r\n      return lRes;\r\n   }\r\n   LRESULT OnDeleteItem(int /*idCtrl*/, LPNMHDR pnmh, BOOL& bHandled)\r\n   {\r\n      LPNMTREEVIEW pnmtv = (LPNMTREEVIEW) pnmh;\r\n      m_mapColors.Remove( pnmtv->itemOld.hItem );\r\n      bHandled = FALSE;\r\n      return 0;\r\n   }\r\n\r\n   // Custom painting\r\n\r\n   DWORD OnPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/)\r\n   {\r\n      return CDRF_NOTIFYITEMDRAW;   // We need per-item notifications\r\n   }\r\n   DWORD OnItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW lpNMCustomDraw)\r\n   {\r\n      LPNMTVCUSTOMDRAW lpNMTVCD = (LPNMTVCUSTOMDRAW) lpNMCustomDraw;\r\n      HTREEITEM hItem = (HTREEITEM) lpNMTVCD->nmcd.dwItemSpec;\r\n      int iIndex = m_mapColors.FindKey( hItem );\r\n      if( iIndex == -1 ) return CDRF_DODEFAULT;\r\n      // The TreeView allows us to change colours for\r\n      // the selected item also (text only).\r\n      const TVCOLOR& col = m_mapColors.GetValueAt(iIndex);\r\n      if( lpNMTVCD->nmcd.uItemState & CDIS_SELECTED ) {\r\n         if( col.clrSelText != CLR_INVALID ) lpNMTVCD->clrText = col.clrSelText;\r\n         // Selection background colour cannot be set with current\r\n         // versions of the TreeView control.\r\n         if( col.clrSelBackground != CLR_INVALID ) lpNMTVCD->clrTextBk = col.clrSelBackground;\r\n      }\r\n      else {\r\n         if( col.clrText != CLR_INVALID ) lpNMTVCD->clrText = col.clrText;\r\n         if( col.clrBackground != CLR_INVALID ) lpNMTVCD->clrTextBk = col.clrBackground;\r\n      }\r\n      if( col.hFont != NULL ) ::SelectObject(lpNMTVCD->nmcd.hdc, col.hFont);\r\n      return CDRF_NEWFONT; // We changed the colors/font\r\n   }\r\n};\r\n\r\nclass CColoredTreeViewCtrl : public CColoredTreeViewImpl<CColoredTreeViewCtrl>\r\n{\r\npublic:\r\n   DECLARE_WND_SUPERCLASS(_T(\"WTL_ColoredTreeView\"), GetWndClassName())  \r\n};\r\n\r\n\r\n#endif // !defined(AFX_COLOREDCONTROLS_H__20020226_0D16_1B29_088A_0080AD509054__INCLUDED_)\r\n\r\n"
  },
  {
    "path": "vcproject/Core.h",
    "content": "#pragma once\n\n#include \"..\\WechatExporter\\core\\FileSystem.h\"\n#include \"..\\WechatExporter\\core\\Logger.h\"\n#include \"..\\WechatExporter\\core\\PdfConverter.h\"\n#include \"..\\WechatExporter\\core\\ExportOption.h\"\n#include \"..\\WechatExporter\\core\\Exporter.h\"\n#include \"..\\WechatExporter\\core\\Updater.h\"\n#include \"..\\WechatExporter\\core\\IDeviceBackup.h\"\n#include \"..\\WechatExporter\\core\\WechatSource.h\"\n"
  },
  {
    "path": "vcproject/ExportNotifierImpl.h",
    "content": "#pragma once\r\n\r\n#include \"stdafx.h\"\r\n#include \"Core.h\"\r\n\r\nclass ExportNotifierImpl : public ExportNotifier\r\n{\r\nprotected:\r\n\tHWND m_hWnd;\r\n\r\npublic:\r\n\tstatic const UINT WM_START = WM_USER + 10;\r\n\tstatic const UINT WM_COMPLETE = WM_USER + 11;\r\n\tstatic const UINT WM_PROGRESS = WM_USER + 12;\r\n\tstatic const UINT WM_USR_SESS_START = WM_USER + 13;\r\n\tstatic const UINT WM_USR_SESS_COMPLETE = WM_USER + 14;\r\n\tstatic const UINT WM_SESSION_START = WM_USER + 16;\r\n\tstatic const UINT WM_SESSION_COMPLETE = WM_USER + 17;\r\n\tstatic const UINT WM_SESSION_PROGRESS = WM_USER + 18;\r\n\tstatic const UINT WM_TASKS_START = WM_USER + 19;\r\n\tstatic const UINT WM_TASKS_COMPLETE = WM_USER + 20;\r\n\tstatic const UINT WM_TASKS_PROGRESS = WM_USER + 21;\r\n\tstatic const UINT WM_EN_END = WM_TASKS_PROGRESS;\r\n\r\npublic:\r\n\tExportNotifierImpl(HWND hWnd) : m_hWnd(hWnd)\r\n\t{\r\n\t}\r\n\r\n\t~ExportNotifierImpl()\r\n\t{\r\n\t\tm_hWnd = NULL;\r\n\t}\r\n\r\n\tvoid onStart() const\r\n\t{\r\n\t\t::PostMessage(m_hWnd, WM_START, (WPARAM)0, (LPARAM)1/*cancellable*/);\r\n\t}\r\n\r\n\tvoid onProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const\r\n\t{\r\n\t\t::PostMessage(m_hWnd, WM_PROGRESS, (WPARAM)numberOfMessages, (LPARAM)numberOfTotalMessages);\r\n\t}\r\n\t\r\n\tvoid onComplete(bool cancelled) const\r\n\t{\r\n\t\t::PostMessage(m_hWnd, WM_COMPLETE, (WPARAM)0, (LPARAM)(cancelled ? 1 : 0));\r\n\t}\r\n\r\n\tvoid onUserSessionStart(const std::string& usrName, uint32_t numberOfSessions) const\r\n\t{\r\n\r\n\t}\r\n\r\n\tvoid onUserSessionComplete(const std::string& usrName) const\r\n\t{\r\n\r\n\t}\r\n\r\n\tvoid onSessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages) const\r\n\t{\r\n\t\t::PostMessage(m_hWnd, WM_SESSION_START, reinterpret_cast<WPARAM>(sessionData), (LPARAM)numberOfTotalMessages);\r\n\t}\r\n\r\n\tvoid onSessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const\r\n\t{\r\n\t\t::PostMessage(m_hWnd, WM_SESSION_PROGRESS, reinterpret_cast<WPARAM>(sessionData), (LPARAM)numberOfMessages);\r\n\t}\r\n\r\n\tvoid onSessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled) const\r\n\t{\r\n\t\t::PostMessage(m_hWnd, WM_SESSION_COMPLETE, reinterpret_cast<WPARAM>(sessionData), (LPARAM)(cancelled ? 1 : 0));\r\n\t}\r\n\r\n\tvoid onTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks) const\r\n\t{\r\n\t\t::PostMessage(m_hWnd, WM_TASKS_START, (WPARAM)numberOfTotalTasks, 0);\r\n\t}\r\n\r\n\tvoid onTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalMessages) const\r\n\t{\r\n\t\t::PostMessage(m_hWnd, WM_TASKS_PROGRESS, numberOfTotalMessages, (LPARAM)numberOfCompletedTasks);\r\n\t}\r\n\r\n\tvoid onTasksComplete(const std::string& usrName, bool cancelled) const\r\n\t{\r\n\t\t::PostMessage(m_hWnd, WM_TASKS_COMPLETE, 0, (LPARAM)(cancelled ? 1 : 0));\r\n\t}\r\n};\r\n\r\n"
  },
  {
    "path": "vcproject/ITunesDetector.h",
    "content": "#pragma once\r\n\r\n#include \"stdafx.h\"\r\n#include \"VersionDetector.h\"\r\n\r\nclass ITunesDetector\r\n{\r\npublic:\r\n\tITunesDetector() : m_installed(false)\r\n\t{\r\n\t\tdetectStandaloneITunes();\r\n\r\n\t\t// detectAppWithWmi();\r\n\r\n\t\tif (!m_installed)\r\n\t\t{\r\n\t\t\t// Check if there is iTunes installed in MS Store\r\n\t\t\t// detectITunesApp();\r\n\t\t}\r\n\r\n\t\tif (!m_installed)\r\n\t\t{\r\n\t\t\t// Check if there is iTunes installed in MS Store from registry\r\n\t\t\tdetectITunesAppFromRegistry2();\r\n\t\t}\r\n\r\n\t\tif (!m_installed)\r\n\t\t{\r\n\t\t\t// Check if there is iTunes installed in MS Store from registry\r\n\t\t\tCString rootKeyPath = TEXT(\"Software\\\\Classes\\\\Local Settings\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\AppModel\\\\Repository\\\\Packages\");\r\n\t\t\tdetectITunesAppFromRegistry(HKEY_CURRENT_USER, rootKeyPath);\r\n\t\t\tif (!m_installed)\r\n\t\t\t{\r\n\t\t\t\trootKeyPath = TEXT(\"SOFTWARE\\\\Classes\\\\Local Settings\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\AppModel\\\\PackageRepository\\\\Packages\");\r\n\t\t\t\tdetectITunesAppFromRegistry(HKEY_LOCAL_MACHINE, rootKeyPath);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t~ITunesDetector()\r\n\t{\r\n\t}\r\n\r\n\tbool isInstalled() const\r\n\t{\r\n\t\treturn m_installed;\r\n\t}\r\n\tCString getVersion() const\r\n\t{\r\n\t\treturn m_version;\r\n\t}\r\n\tCString getInstallPath() const\r\n\t{\r\n\t\treturn m_path;\r\n\t}\r\n\r\nprotected:\r\n\tvoid detectStandaloneITunes()\r\n\t{\r\n\t\tTCHAR value[MAX_PATH] = { 0 };\r\n\t\tCRegKey rkITunes;\r\n\t\tif (rkITunes.Open(HKEY_LOCAL_MACHINE, TEXT(\"SOFTWARE\\\\Apple Computer, Inc.\\\\iTunes\"), KEY_READ) == ERROR_SUCCESS)\r\n\t\t{\r\n\t\t\tULONG chars = MAX_PATH;\r\n\t\t\tif (rkITunes.QueryStringValue(TEXT(\"Version\"), value, &chars) == ERROR_SUCCESS)\r\n\t\t\t{\r\n\t\t\t\tm_installed = true;\r\n\t\t\t\tm_version = value;\r\n\t\t\t}\r\n\t\t\tif (rkITunes.QueryStringValue(TEXT(\"InstallDir\"), value, &chars) == ERROR_SUCCESS)\r\n\t\t\t{\r\n\t\t\t\tm_path = value;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\trkITunes.Close();\r\n\t\t}\t\r\n\t}\r\n\r\n\tvoid detectITunesAppFromRegistry(HKEY rootKey, const CString& rootKeyPath)\r\n\t{\r\n\t\t// Check if there is iTunes installed in MS Store\r\n\t\tCRegKey rkITunes;\r\n\t\t\r\n\t\tLRESULT res = rkITunes.Open(rootKey, rootKeyPath, KEY_READ);\r\n\t\tif (res == ERROR_SUCCESS)\r\n\t\t{\r\n\t\t\tDWORD dwIndex = 0;\r\n\t\t\tULONG chars = MAX_PATH;\r\n\t\t\tTCHAR subkeyName[MAX_PATH] = { 0 };\r\n\t\t\twhile ((res = rkITunes.EnumKey(dwIndex, subkeyName, &chars)) != ERROR_NO_MORE_ITEMS)\r\n\t\t\t{\r\n\t\t\t\tif (res == ERROR_SUCCESS)\r\n\t\t\t\t{\r\n\t\t\t\t\tCRegKey rk;\r\n\t\t\t\t\tres = rk.Open(rkITunes.m_hKey, subkeyName, KEY_READ);\r\n\t\t\t\t\tif (res == ERROR_SUCCESS)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tchars = MAX_PATH;\r\n\t\t\t\t\t\tTCHAR value[MAX_PATH] = { 0 };\r\n\t\t\t\t\t\tif (rk.QueryStringValue(TEXT(\"PackageID\"), value, &chars) == ERROR_SUCCESS)\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tif (_tcsstr(value, TEXT(\"AppleInc.iTunes_\")) != NULL)\r\n\t\t\t\t\t\t\t{\r\n#ifndef NDEBUG\r\n\t\t\t\t\t\t\t\tMessageBox(NULL, value, TEXT(\"Debug\"), MB_OK);\r\n#endif\r\n\t\t\t\t\t\t\t\tm_installed = true;\r\n\t\t\t\t\t\t\t\tm_version = parseVersionFromPackageId(value);\r\n\t\t\t\t\t\t\t\tchars = MAX_PATH;\r\n\t\t\t\t\t\t\t\tif (rk.QueryStringValue(TEXT(\"PackageRootFolder\"), value, &chars) == ERROR_SUCCESS)\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\tm_path = value;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\trk.Close();\r\n\t\t\t\t\t\tif (m_installed)\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tchars = MAX_PATH;\r\n\t\t\t\tdwIndex++;\r\n\t\t\t}\r\n\r\n\t\t\trkITunes.Close();\r\n\r\n#ifndef NDEBUG\r\n\t\t\t\r\n#endif\r\n\t\t}\r\n#ifndef NDEBUG\r\n\t\telse\r\n\t\t{\r\n\t\t\tMessageBox(NULL, TEXT(\"Failed to Open Registry Key: Software\\\\Classes\\\\Local Settings\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\AppModel\\\\Repository\\\\Packages\"), TEXT(\"Debug\"), MB_OK);\r\n\t\t}\r\n#endif\r\n\t}\r\n\r\n\tvoid detectITunesAppFromRegistry2()\r\n\t{\r\n\t\t// Check if there is iTunes installed in MS Store\r\n\t\tCRegKey rkITunes;\r\n\r\n\t\tCString rootKeyPath = TEXT(\"Software\\\\Classes\");\r\n\t\tLRESULT res = rkITunes.Open(HKEY_CURRENT_USER, rootKeyPath, KEY_READ);\r\n\t\tif (res == ERROR_SUCCESS)\r\n\t\t{\r\n\t\t\tDWORD dwIndex = 0;\r\n\t\t\tULONG chars = MAX_PATH;\r\n\t\t\tTCHAR subkeyName[MAX_PATH] = { 0 };\r\n\t\t\twhile ((res = rkITunes.EnumKey(dwIndex, subkeyName, &chars)) != ERROR_NO_MORE_ITEMS)\r\n\t\t\t{\r\n\t\t\t\tif (res == ERROR_SUCCESS)\r\n\t\t\t\t{\r\n\t\t\t\t\tif (regValueEqualsTo(rkITunes.m_hKey, subkeyName, NULL, TEXT(\"iTunes\")))\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tTCHAR shellPath[MAX_PATH] = { 0 };\r\n\t\t\t\t\t\t_tcscpy(shellPath, subkeyName);\r\n\t\t\t\t\t\t_tcscat(shellPath, TEXT(\"\\\\\"));\r\n\t\t\t\t\t\t_tcscat(shellPath, TEXT(\"Shell\\\\open\"));\r\n\t\t\t\t\t\tif (regValueEqualsTo(rkITunes.m_hKey, shellPath, TEXT(\"AppUserModelID\"), TEXT(\"AppleInc.iTunes_nzyj5cx40ttqa!iTunes\")))\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tchars = MAX_PATH;\r\n\t\t\t\t\t\t\tTCHAR value[MAX_PATH] = { 0 };\r\n\t\t\t\t\t\t\tif (queryRegValue(rkITunes.m_hKey, shellPath, TEXT(\"PackageId\"), value, chars))\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\tm_installed = true;\r\n\t\t\t\t\t\t\t\tm_version = parseVersionFromPackageId(value);\r\n\t\t\t\t\t\t\t\tm_path = TEXT(\"\");\r\n\t\t\t\t\t\t\t}\t\r\n\t\t\t\t\t\t}\t\r\n\t\t\t\t\t}\t\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (m_installed)\r\n\t\t\t\t{\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t\tchars = MAX_PATH;\r\n\t\t\t\tdwIndex++;\r\n\t\t\t}\r\n\r\n\t\t\trkITunes.Close();\r\n\r\n#ifndef NDEBUG\r\n\r\n#endif\r\n\t\t}\r\n#ifndef NDEBUG\r\n\t\telse\r\n\t\t{\r\n\t\t\tMessageBox(NULL, TEXT(\"Failed to Open Registry Key: Software\\\\Classes\\\\Local Settings\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\AppModel\\\\Repository\\\\Packages\"), TEXT(\"Debug\"), MB_OK);\r\n\t\t}\r\n#endif\r\n\t}\r\n\r\n\tbool regValueEqualsTo(HKEY rootKey, const CString& keyPath, LPCTSTR szValueName, LPCTSTR szExpectedValue)\r\n\t{\r\n\t\tbool result = false;\r\n\r\n\t\tULONG chars = MAX_PATH;\r\n\t\tTCHAR value[MAX_PATH] = { 0 };\r\n\t\t\r\n\t\tif (queryRegValue(rootKey, keyPath, szValueName, value, chars))\r\n\t\t{\r\n\t\t\tif (_tcsstr(value, szExpectedValue) != NULL)\r\n\t\t\t{\r\n\t\t\t\tresult = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\tbool queryRegValue(HKEY rootKey, const CString& keyPath, LPCTSTR szValueName, LPTSTR szValue, ULONG nChars)\r\n\t{\r\n\t\tbool result = false;\r\n\t\tCRegKey rk;\r\n\t\tLRESULT res = rk.Open(rootKey, keyPath, KEY_READ);\r\n\t\tif (res == ERROR_SUCCESS)\r\n\t\t{\r\n\r\n\t\t\tif (rk.QueryStringValue(szValueName, szValue, &nChars) == ERROR_SUCCESS)\r\n\t\t\t{\r\n\t\t\t\tresult = true;\r\n\t\t\t}\r\n\t\t\trk.Close();\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\tvoid detectITunesApp()\r\n\t{\r\n\t\t// Check if there is iTunes installed in MS Store\r\n\t\tHRESULT result = S_OK;\r\n\t\tTCHAR szPath[MAX_PATH] = { 0 };\r\n\t\tresult = SHGetFolderPath(NULL, CSIDL_PROGRAM_FILES, NULL, SHGFP_TYPE_CURRENT, szPath);\r\n\t\tif (FAILED(result))\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\t\t_tcscat(szPath, TEXT(\"\\\\WindowsApps\\\\\"));\r\n\r\n\t\tif (!PathFileExists(szPath))\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tWIN32_FIND_DATA FindFileData;\r\n\t\tHANDLE hFind = INVALID_HANDLE_VALUE;\r\n\r\n\t\tTCHAR szPath2[MAX_PATH] = { 0 };\r\n\t\t_tcscpy(szPath2, szPath);\r\n\t\t_tcscat(szPath2, TEXT(\"*.*\"));\r\n\r\n\t\thFind = FindFirstFile(szPath2, &FindFileData);\r\n\t\tif (hFind == INVALID_HANDLE_VALUE)\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tdo\r\n\t\t{\r\n\t\t\tif (_tcscmp(FindFileData.cFileName, TEXT(\".\")) == 0 || _tcscmp(FindFileData.cFileName, TEXT(\"..\")) == 0)\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\t\t\tif ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0)\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t// AppleInc.iTunes_12110.26.53016.0_x64__nzyj5cx40ttqa\r\n\t\t\tif (_tcsncmp(FindFileData.cFileName, TEXT(\"AppleInc.iTunes_\"), 16) == 0)\r\n\t\t\t{\r\n\t\t\t\tTCHAR szITunes[MAX_PATH] = { 0 };\r\n\t\t\t\t_tcscpy(szITunes, szPath);\r\n\t\t\t\t_tcscat(szITunes, FindFileData.cFileName);\r\n\r\n\t\t\t\tTCHAR szITunesExe[MAX_PATH] = { 0 };\r\n\t\t\t\t_tcscpy(szITunesExe, szITunes);\r\n\t\t\t\t_tcscat(szITunes, TEXT(\"\\\\iTunes.exe\"));\r\n\r\n\t\t\t\tif (!PathFileExists(szITunes))\r\n\t\t\t\t{\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tm_path = szITunes;\r\n\t\t\t\tVersionDetector versionDetector;\r\n\t\t\t\tm_version = versionDetector.GetProductVersion(szITunesExe);\r\n\t\t\t\tif (m_version.IsEmpty())\r\n\t\t\t\t{\r\n\t\t\t\t\tm_version = parseVersionFromPackageId(FindFileData.cFileName);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tm_installed = true;\r\n\t\t\t\tbreak;\r\n\t\t\t\t\r\n\t\t\t}\r\n\t\t\t// CW2A pszU8(CT2W(FindFileData.cFileName), CP_UTF8);\r\n\t\t\t// subDirectories.push_back((LPCSTR)pszU8);\r\n\t\t\t\r\n\t\t} while (::FindNextFile(hFind, &FindFileData));\r\n\t\tFindClose(hFind);\r\n\t}\r\n\r\n\tCString parseVersionFromPackageId(const CString packageId)\r\n\t{\r\n\t\tCString version = packageId;\r\n\t\t// AppleInc.iTunes_12110.26.53016.0_x64__nzyj5cx40ttqa\r\n\t\tint pos = version.Find('_');\r\n\t\tif (pos != -1)\r\n\t\t{\r\n\t\t\tversion = version.Mid(pos + 1);\r\n\t\t\tpos = version.Find('_');\r\n\t\t\tif (pos != -1)\r\n\t\t\t{\r\n\t\t\t\tversion = version.Left(pos);\r\n\t\t\t\treturn version;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn TEXT(\"\");\r\n\t}\r\n\r\nprivate:\r\n\tbool m_installed;\r\n\tCString m_path;\r\n\tCString m_version;\r\n\r\n};\r\n"
  },
  {
    "path": "vcproject/LogListBox.h",
    "content": "#if !defined(AFX_LOGLISTBOX_H_INCLUDED_)\r\n#define AFX_LOGLISTBOX_H_INCLUDED_\r\n\r\n#include <vector>\r\n#include <atlctrls.h>\r\n#pragma once\r\n\r\n#ifndef __cplusplus\r\n#error WTL requires C++ compilation (use a .cpp suffix)\r\n#endif\r\n\r\n#ifndef __ATLAPP_H__\r\n#error LogListBox.h requires atlapp.h to be included first\r\n#endif\r\n\r\n#ifndef __ATLCTRLS_H__\r\n#error LogListBox.h requires atlctrls.h to be included first\r\n#endif\r\n\r\ntemplate <class T, class TBase = CListBox, class TWinTraits = ATL::CControlWinTraits>\r\nclass ATL_NO_VTABLE CLogListBoxImpl : public ATL::CWindowImpl< T, TBase, TWinTraits >\r\n{\r\npublic:\r\n\t// DECLARE_WND_SUPERCLASS(_T(\"WTL_LogListBox\"), GetWndClassName())\r\n\tDECLARE_WND_SUPERCLASS2(NULL, T, TBase::GetWndClassName())\r\n\t// DECLARE_WND_SUPERCLASS2(NULL, CLogListBox, CListBox::GetWndClassName())\r\n\t// DECLARE_WND_SUPERCLASS(_T(\"WTL_LogListBox\"), GetWndClassName())\r\n\r\n\t// Message map\r\n\tBEGIN_MSG_MAP(CLogListBoxImpl)\r\n\t\tMESSAGE_HANDLER(WM_KEYDOWN, OnKeyDown)\r\n\tEND_MSG_MAP()\r\n\r\n\t// Message handlers\r\n\tLRESULT OnKeyDown(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tif ((::GetKeyState(VK_CONTROL) & 0x8000) != 0)\r\n\t\t{\r\n\t\t\tif (wParam == 'A' || wParam == 'a')\r\n\t\t\t{\r\n\t\t\t\tSetSel(-1);\r\n\t\t\t}\r\n\t\t\telse if (wParam == 'C' || wParam == 'c')\r\n\t\t\t{\r\n\t\t\t\tint selCount = GetSelCount();\r\n\t\t\t\tif (selCount > 0)\r\n\t\t\t\t{\r\n\t\t\t\t\tstd::vector<int> rgIndex(selCount, 0);\r\n\t\t\t\t\tGetSelItems(selCount, &rgIndex[0]);\r\n\t\t\t\t\tCString contents;\r\n\t\t\t\t\tCString line;\r\n\t\t\t\t\tfor (int idx = 0; idx < selCount; ++idx)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tGetText(rgIndex[idx], line);\r\n\t\t\t\t\t\tcontents.Append(line);\r\n\t\t\t\t\t\tcontents.Append(TEXT(\"\\r\\n\"));\r\n\t\t\t\t\t}\r\n\t\t\t\t\t\r\n\t\t\t\t\tint cch = contents.GetLength();\r\n\t\t\t\t\tif (cch > 0 && ::OpenClipboard(NULL))\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t::EmptyClipboard();\r\n\r\n\t\t\t\t\t\tHGLOBAL hglbCopy = ::GlobalAlloc(GMEM_MOVEABLE, (cch + 1) * sizeof(TCHAR));\r\n\t\t\t\t\t\tif (NULL != hglbCopy)\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t// Lock the handle and copy the text to the buffer. \r\n\t\t\t\t\t\t\tLPTSTR lptstrCopy = (LPTSTR)::GlobalLock(hglbCopy);\r\n\t\t\t\t\t\t\tLPTSTR lptstrBuffer = contents.LockBuffer();\r\n\t\t\t\t\t\t\tmemcpy(lptstrCopy, lptstrBuffer, cch * sizeof(TCHAR));\r\n\t\t\t\t\t\t\tcontents.UnlockBuffer();\r\n\t\t\t\t\t\t\tlptstrCopy[cch] = (TCHAR)0;    // null character \r\n\t\t\t\t\t\t\t::GlobalUnlock(hglbCopy);\r\n\t\t\t\t\t\t\t// Place the handle on the clipboard. \r\n\r\n#ifdef _UNICODE\r\n\t\t\t\t\t\t\t::SetClipboardData(CF_UNICODETEXT, hglbCopy);\r\n#else\r\n\t\t\t\t\t\t\t::SetClipboardData(CF_TEXT, hglbCopy);\r\n#endif\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t::CloseClipboard();\r\n\t\t\t\t\t\tif (NULL != hglbCopy) ::GlobalFree(hglbCopy);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn 0;\r\n\t}\r\n   \r\n};\r\n\r\n\r\n\r\nclass CLogListBox : public CLogListBoxImpl<CLogListBox>\r\n{\r\npublic:\r\n\tDECLARE_WND_SUPERCLASS(_T(\"WTL_LogListBox\"), GetWndClassName())\r\n\r\n};\r\n\r\n\r\n#endif // !defined(AFX_LOGLISTBOX_H_INCLUDED_)\r\n"
  },
  {
    "path": "vcproject/LoggerImpl.h",
    "content": "#pragma once\r\n\r\n#include \"stdafx.h\"\r\n#include \"Core.h\"\r\n#include <locale>\r\n#include <codecvt>\r\n\r\nclass LoggerImpl : public Logger\r\n{\r\nprotected:\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n\tCRITICAL_SECTION\tm_cs;\r\n\tTCHAR m_szLogFile[MAX_PATH];\r\n#endif\r\n\tHWND m_hWndLog;\r\npublic:\r\n\tLoggerImpl(HWND hWndLog) : m_hWndLog(hWndLog)\r\n\t{\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n\t\tInitializeCriticalSection(&m_cs);\r\n\t\tm_szLogFile[0] = 0;\r\n#endif\r\n\t}\r\n\r\n\t~LoggerImpl()\r\n\t{\r\n\t\tm_hWndLog = NULL;\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n\t\tDeleteCriticalSection(&m_cs);\r\n#endif\r\n\t}\r\n\r\n\tvoid setLogPath(LPCTSTR lpszLogPath)\r\n\t{\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n\t\tEnterCriticalSection(&m_cs);\r\n\t\t_tcscpy(m_szLogFile, lpszLogPath);\r\n\t\tPathAppend(m_szLogFile, TEXT(\"log.txt\"));\r\n\t\tHANDLE hFile = CreateFile(m_szLogFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);\r\n\t\tif (INVALID_HANDLE_VALUE != hFile)\r\n\t\t{\r\n\t\t\tCloseHandle(hFile);\r\n\t\t}\r\n\t\tLeaveCriticalSection(&m_cs);\r\n#endif\r\n\t}\r\n\r\n\tvoid outputLog(LPCTSTR pszLog)\r\n\t{\r\n\t\t::SendMessage(m_hWndLog, LB_ADDSTRING, 0, (LPARAM)pszLog);\r\n\t\tLRESULT count = ::SendMessage(m_hWndLog, LB_GETCOUNT, 0, 0L);\r\n\t\t::SendMessage(m_hWndLog, LB_SETTOPINDEX, count - 1, 0L);\r\n\t}\r\n\r\n\tvoid write(const std::string& log)\r\n\t{\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n\t\tstd::string timeString = getTimestampString(false, true) + \": \";\r\n#else\r\n\t\tstd::string timeString = getTimestampString() + \": \";\r\n#endif\r\n\t\tCA2T szTime(timeString.c_str());\r\n\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n\t\tEnterCriticalSection(&m_cs);\r\n\t\tif (_tcslen(m_szLogFile) > 0)\r\n\t\t{\r\n\t\t\tHANDLE hFile = ::CreateFile(m_szLogFile, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);\r\n\t\t\tif (INVALID_HANDLE_VALUE != hFile)\r\n\t\t\t{\r\n\t\t\t\t::SetFilePointer(hFile, 0, 0, FILE_END);\r\n\t\t\t\tDWORD dwBytesToWrite = static_cast<DWORD>(log.size());\r\n\t\t\t\tDWORD dwBytesWritten = 0;\r\n\t\t\t\tBOOL bErrorFlag = WriteFile(hFile, timeString.c_str(), timeString.size(), &dwBytesWritten, NULL);\r\n\t\t\t\tbErrorFlag = WriteFile(hFile, log.c_str(), dwBytesToWrite, &dwBytesWritten, NULL);\r\n\t\t\t\tif (bErrorFlag)\r\n\t\t\t\t{\r\n\t\t\t\t\tWriteFile(hFile, \"\\r\\n\", 2, &dwBytesWritten, NULL);\r\n\t\t\t\t}\r\n\t\t\t\tCloseHandle(hFile);\r\n\t\t\t}\r\n\t\t}\r\n\t\tLeaveCriticalSection(&m_cs);\r\n#endif\r\n\r\n\t\tCW2T pszT(CA2W(log.c_str(), CP_UTF8));\r\n\r\n\t\tstd::vector<TCHAR> szLog;\r\n\t\tszLog.resize(_tcslen(pszT) + _tcslen(szTime) + 1, 0);\r\n\r\n\t\t_tcscpy(&szLog[0], (LPCTSTR)szTime);\r\n\t\t_tcscat(&szLog[0], (LPCTSTR)pszT);\r\n\t\t\r\n\t\toutputLog(&szLog[0]);\r\n\t}\r\n\r\n\tvoid debug(const std::string& log)\r\n\t{\r\n// #if !defined(NDEBUG) || defined(DBG_PERF)\r\n\t\twrite(\"[DBG] \" + log);\r\n// #endif\r\n\t}\r\n};\r\n\r\n"
  },
  {
    "path": "vcproject/MainFrm.h",
    "content": "// MainFrm.h : interface of the CMainFrame class\n//\n/////////////////////////////////////////////////////////////////////////////\n\n#pragma once\n\nclass CMainFrame : \n\tpublic CFrameWindowImpl<CMainFrame>, \n\tpublic CUpdateUI<CMainFrame>,\n\tpublic CMessageFilter, public CIdleHandler\n{\npublic:\n\tDECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)\n\n\tBOOL m_pdfSupported;\n\tCView m_view;\n\n\tvirtual BOOL PreTranslateMessage(MSG* pMsg)\n\t{\n\t\tif(CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg))\n\t\t\treturn TRUE;\n\n\t\treturn m_view.PreTranslateMessage(pMsg);\n\t}\n\n\tvirtual BOOL OnIdle()\n\t{\n\t\treturn FALSE;\n\t}\n\n\tBEGIN_UPDATE_UI_MAP(CMainFrame)\n\tEND_UPDATE_UI_MAP()\n\n\tBEGIN_MSG_MAP(CMainFrame)\n\t\tMESSAGE_HANDLER(WM_CREATE, OnCreate)\n\t\tMESSAGE_HANDLER(WM_DESTROY, OnDestroy)\n\t\tMESSAGE_HANDLER(WM_INITMENU, OnInitMenu)\n\t\tCOMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)\n\t\tCOMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)\n\t\tCOMMAND_ID_HANDLER(ID_HELP_HOMEPAGE, OnHelpHomePage)\n\t\tCOMMAND_ID_HANDLER(ID_FILE_CHK_UPDATE, OnCheckUpdate)\n\t\tCOMMAND_ID_HANDLER(ID_FILE_EXP_ITUNES, OnExportITunes)\n\t\tCOMMAND_ID_HANDLER(ID_FILE_DBG_LOGS, OnOutputDbgLogs)\n\t\tCOMMAND_ID_HANDLER(ID_FILE_OPEN_FOLDER, OnOpenFolder)\n\t\tCOMMAND_RANGE_HANDLER(ID_FORMAT_HTML, ID_FORMAT_PDF, OnOutputFormat)\n\t\tCOMMAND_ID_HANDLER(ID_OPT_DESC_ORDER, OnDescOrder)\n\t\tCOMMAND_ID_HANDLER(ID_OPT_DL_EMOJI, OnDownloadingEmoji)\n\t\tCOMMAND_ID_HANDLER(ID_OPT_LM_ONSCROLL, OnAsyncLoading)\n\t\tCOMMAND_ID_HANDLER(ID_OPT_NORMALPAGINATION, OnAsyncLoading)\n\t\tCOMMAND_ID_HANDLER(ID_OPT_PAGINATION_YEAR, OnAsyncLoading)\n\t\tCOMMAND_ID_HANDLER(ID_OPT_PAGINATION_MONTH, OnAsyncLoading)\n\t\tCOMMAND_ID_HANDLER(ID_OPT_FILTER, OnFilter)\n\t\tCOMMAND_ID_HANDLER(ID_OPT_INCREMENTALEXP, OnIncrementalExporting)\n\t\tCOMMAND_ID_HANDLER(ID_OPT_SUBSCRIPTIONS, OnIncludeSubscriptions)\n\t\tCHAIN_MSG_MAP(CUpdateUI<CMainFrame>)\n\t\tCHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)\n\tEND_MSG_MAP()\n\n\t\n// Handler prototypes (uncomment arguments if needed):\n//\tLRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\n//\tLRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n//\tLRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)\n\n\tLRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\n\t{\n\t\tm_pdfSupported = AppConfiguration::IsPdfSupported();\n\t\tCenterWindow(m_hWnd);\n\t\tm_hWndClient = m_view.Create(m_hWnd);\n\n\t\t// register object for message filtering and idle updates\n\t\tCMessageLoop* pLoop = _Module.GetMessageLoop();\n\t\tATLASSERT(pLoop != NULL);\n\t\tpLoop->AddMessageFilter(this);\n\t\tpLoop->AddIdleHandler(this);\n\n\t\tAppConfiguration::upgrade();\n\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenuFile = menu.GetSubMenu(0);\n\t\tsubMenuFile.CheckMenuItem(ID_FILE_CHK_UPDATE, MF_BYCOMMAND | (AppConfiguration::GetCheckingUpdateDisabled() ? MF_UNCHECKED : MF_CHECKED));\n\t\tsubMenuFile.CheckMenuItem(ID_FILE_DBG_LOGS, MF_BYCOMMAND | (AppConfiguration::OutputDebugLogs() ? MF_CHECKED : MF_UNCHECKED));\n\t\tsubMenuFile.CheckMenuItem(ID_FILE_OPEN_FOLDER, MF_BYCOMMAND | (AppConfiguration::GetOpenningFolderAfterExp() ? MF_CHECKED : MF_UNCHECKED));\n\t\tAppConfiguration::upgrade();\n\n\t\tCMenuHandle subMenuFormat = menu.GetSubMenu(1);\n\t\tUINT outputFormat = AppConfiguration::GetOutputFormat();\n\t\tsubMenuFormat.CheckMenuRadioItem(ID_FORMAT_HTML, ID_FORMAT_PDF, ID_FORMAT_HTML + outputFormat, MF_BYCOMMAND | MFT_RADIOCHECK);\n\n\t\tCMenuHandle subMenuOptions = menu.GetSubMenu(2);\n\t\tsubMenuOptions.CheckMenuItem(ID_OPT_DESC_ORDER, MF_BYCOMMAND | (AppConfiguration::GetDescOrder() ? MF_CHECKED : MF_UNCHECKED));\n\t\t// subMenuOptions.CheckMenuItem(ID_OPT_SAVING_IN_SESSION, MF_BYCOMMAND | (AppConfiguration::GetSavingInSession() ? MF_CHECKED : MF_UNCHECKED));\n\t\tsubMenuOptions.CheckMenuItem(ID_OPT_DL_EMOJI, MF_BYCOMMAND | (AppConfiguration::GetUsingRemoteEmoji() ? MF_UNCHECKED : MF_CHECKED));\n\t\tsubMenuOptions.CheckMenuItem(ID_OPT_FILTER, MF_BYCOMMAND | (AppConfiguration::GetSupportingFilter() ? MF_CHECKED : MF_UNCHECKED));\n\n\t\tInitAsyncLoadingMenu(subMenuOptions, TRUE);\n\n\t\tsubMenuOptions.CheckMenuItem(ID_OPT_INCREMENTALEXP, MF_BYCOMMAND | (AppConfiguration::GetIncrementalExporting() ? MF_CHECKED : MF_UNCHECKED));\n\t\tsubMenuOptions.CheckMenuItem(ID_OPT_SUBSCRIPTIONS, MF_BYCOMMAND | (AppConfiguration::IncludeSubscriptions() ? MF_CHECKED : MF_UNCHECKED));\n\t\tsubMenuOptions.RemoveMenu(ID_OPT_SUBSCRIPTIONS, MF_BYCOMMAND);\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)\n\t{\n\t\t// unregister message filtering and idle updates\n\t\tCMessageLoop* pLoop = _Module.GetMessageLoop();\n\t\tATLASSERT(pLoop != NULL);\n\t\tpLoop->RemoveMessageFilter(this);\n\t\tpLoop->RemoveIdleHandler(this);\n\n\t\tbHandled = FALSE;\n\t\treturn 1;\n\t}\n\n\tLRESULT OnInitMenu(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)\n\t{\n\t\tBOOL enabled = m_view.IsViewIdle();\n\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenuFile = menu.GetSubMenu(0);\n\t\tsubMenuFile.EnableMenuItem(ID_FILE_EXP_ITUNES, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED));\n\t\tsubMenuFile.EnableMenuItem(ID_FILE_DBG_LOGS, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED));\n\t\tsubMenuFile.EnableMenuItem(ID_FILE_OPEN_FOLDER, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED));\n\n\t\tCMenuHandle subMenuFormat = menu.GetSubMenu(1);\n\t\tsubMenuFormat.EnableMenuItem(ID_FORMAT_HTML, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED));\n\t\tsubMenuFormat.EnableMenuItem(ID_FORMAT_TEXT, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED));\n\t\tsubMenuFormat.EnableMenuItem(ID_FORMAT_PDF, MF_BYCOMMAND | ((enabled && m_pdfSupported) ? MF_ENABLED : MF_DISABLED));\n\n\t\tCMenuHandle subMenuOptions = menu.GetSubMenu(2);\n\t\tsubMenuOptions.EnableMenuItem(ID_OPT_DESC_ORDER, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED));\n\t\t// subMenuOptions.EnableMenuItem(ID_OPT_SAVING_IN_SESSION, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED));\n\t\tsubMenuOptions.EnableMenuItem(ID_OPT_DL_EMOJI, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED));\n\t\tsubMenuOptions.EnableMenuItem(ID_OPT_FILTER, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED));\n\n\t\tInitAsyncLoadingMenu(subMenuOptions, enabled);\n\n\t\tsubMenuOptions.CheckMenuItem(ID_OPT_INCREMENTALEXP, MF_BYCOMMAND | (AppConfiguration::GetIncrementalExporting() ? MF_CHECKED : MF_UNCHECKED));\n\t\tsubMenuOptions.CheckMenuItem(ID_OPT_SUBSCRIPTIONS, MF_BYCOMMAND | (AppConfiguration::IncludeSubscriptions() ? MF_CHECKED : MF_UNCHECKED));\n\n\t\treturn 1;\n\t}\n\n\tvoid InitAsyncLoadingMenu(CMenuHandle& subMenuOptions, BOOL isIdle)\n\t{\n\t\tUINT outputFormat = AppConfiguration::GetOutputFormat();\n\t\tBOOL asyncLoadingEnabled = (outputFormat == AppConfiguration::OUTPUT_FORMAT_HTML);\n\t\tif (asyncLoadingEnabled)\n\t\t{\n\t\t\tasyncLoadingEnabled = isIdle;\n\t\t}\n\n\t\tDWORD asyncLoading = AppConfiguration::GetAsyncLoading();\n\t\tUINT ids[] = { ID_OPT_LM_ONSCROLL, ID_OPT_NORMALPAGINATION, ID_OPT_PAGINATION_YEAR, ID_OPT_PAGINATION_MONTH };\n\t\tUINT asyncLoadingStates[] = { ASYNC_ONSCROLL, ASYNC_PAGER_NORMAL, ASYNC_PAGER_ON_YEAR, ASYNC_PAGER_ON_MONTH };\n\n\t\tfor (int idx = 0; idx < (sizeof(ids) / sizeof(UINT)); ++idx)\n\t\t{\n\t\t\tsubMenuOptions.EnableMenuItem(ids[idx], MF_BYCOMMAND | (asyncLoadingEnabled ? MF_ENABLED : MF_DISABLED));\n\t\t\tsubMenuOptions.CheckMenuItem(ids[idx], MF_BYCOMMAND | ((asyncLoading == asyncLoadingStates[idx]) ? MF_CHECKED : MF_UNCHECKED));\n\t\t}\n\t}\n\n\tLRESULT OnFilter(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/)\n\t{\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenu = menu.GetSubMenu(2);\n\t\tUINT menuState = subMenu.GetMenuState(ID_OPT_FILTER, MF_BYCOMMAND);\n\t\tBOOL supportingFilter = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE;\n\t\tsubMenu.CheckMenuItem(ID_OPT_FILTER, MF_BYCOMMAND | (supportingFilter ? MF_UNCHECKED : MF_CHECKED));\n\t\tAppConfiguration::SetSupportingFilter(!supportingFilter);\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnDescOrder(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/)\n\t{\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenu = menu.GetSubMenu(2);\n\t\tUINT menuState = subMenu.GetMenuState(ID_OPT_DESC_ORDER, MF_BYCOMMAND);\n\t\tBOOL curDescOrder = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE;\n\t\tsubMenu.CheckMenuItem(ID_OPT_DESC_ORDER, MF_BYCOMMAND | (curDescOrder ? MF_UNCHECKED : MF_CHECKED));\n\t\tAppConfiguration::SetDescOrder(!curDescOrder);\n\t\t\n\t\treturn 0;\n\t}\n\n\tLRESULT OnDownloadingEmoji(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/)\n\t{\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenu = menu.GetSubMenu(2);\n\t\tUINT menuState = subMenu.GetMenuState(ID_OPT_DL_EMOJI, MF_BYCOMMAND);\n\t\tBOOL curState = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE;\n\t\tsubMenu.CheckMenuItem(ID_OPT_DL_EMOJI, MF_BYCOMMAND | (curState ? MF_UNCHECKED : MF_CHECKED));\n\t\tAppConfiguration::SetUsingRemoteEmoji(curState);\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnOutputFormat(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/)\n\t{\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenuFormat = menu.GetSubMenu(1);\n\t\tsubMenuFormat.CheckMenuRadioItem(ID_FORMAT_HTML, ID_FORMAT_PDF, wID, MF_BYCOMMAND | MFT_RADIOCHECK);\n\t\tUINT outputFormat = wID - ID_FORMAT_HTML;\n\t\tAppConfiguration::SetOutputFormat(outputFormat);\n\n\t\t// subMenuFormat.EnableMenuItem(ID_OPT_ASYNC_LOADING, MF_BYCOMMAND | ((outputFormat != AppConfiguration::OUTPUT_FORMAT_HTML) ? MF_DISABLED : MF_ENABLED));\n\t\t\n\t\treturn 0;\n\t}\n\n\tLRESULT OnIncrementalExporting(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenu = menu.GetSubMenu(2);\n\t\tUINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND);\n\t\tBOOL incrementalExporting = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE;\n\t\tsubMenu.CheckMenuItem(wID, MF_BYCOMMAND | (incrementalExporting ? MF_UNCHECKED : MF_CHECKED));\n\t\tAppConfiguration::SetIncrementalExporting(!incrementalExporting);\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnIncludeSubscriptions(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenu = menu.GetSubMenu(2);\n\t\tUINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND);\n\t\tBOOL stateValue = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE;\n\t\tsubMenu.CheckMenuItem(wID, MF_BYCOMMAND | (stateValue ? MF_UNCHECKED : MF_CHECKED));\n\t\tAppConfiguration::SetIncludingSubscriptions(!stateValue);\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnAsyncLoading(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/)\n\t{\n\t\tUINT ids[] = { ID_OPT_LM_ONSCROLL, ID_OPT_NORMALPAGINATION, ID_OPT_PAGINATION_YEAR, ID_OPT_PAGINATION_MONTH};\n\t\tUINT asyncLoading[] = { ASYNC_ONSCROLL, ASYNC_PAGER_NORMAL, ASYNC_PAGER_ON_YEAR, ASYNC_PAGER_ON_MONTH };\n\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenu = menu.GetSubMenu(2);\n\t\tUINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND);\n\t\tBOOL asyncState = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE;\n\n\t\tint selectedIdx = 0;\n\t\tfor (int idx = 0; idx < sizeof(ids) / sizeof(UINT); ++idx)\n\t\t{\n\t\t\tif (ids[idx] != wID)\n\t\t\t{\n\t\t\t\tsubMenu.CheckMenuItem(ids[idx], MF_BYCOMMAND | MF_UNCHECKED);\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tselectedIdx = idx;\n\t\t\t\tsubMenu.CheckMenuItem(ids[idx], MF_BYCOMMAND | (asyncState ? MF_UNCHECKED : MF_CHECKED));\n\t\t\t}\n\t\t}\n\n\t\tif (asyncState)\n\t\t{\n\t\t\tAppConfiguration::SetSyncLoading();\n\t\t}\n\t\telse\n\t\t{\n\t\t\tAppConfiguration::SetAsyncLoading(asyncLoading[selectedIdx]);\n\t\t}\n\t\t\n\t\treturn 0;\n\t}\n\n\tLRESULT OnCheckUpdate(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenu = menu.GetSubMenu(0);\n\t\tUINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND);\n\t\tBOOL stateValue = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE;\n\t\tsubMenu.CheckMenuItem(wID, MF_BYCOMMAND | (stateValue ? MF_UNCHECKED : MF_CHECKED));\n\t\tAppConfiguration::SetCheckingUpdateDisabled(stateValue);\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnExportITunes(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tm_view.ExportITunesBackup();\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnOpenFolder(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenu = menu.GetSubMenu(0);\n\t\tUINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND);\n\t\tBOOL stateValue = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE;\n\t\tsubMenu.CheckMenuItem(wID, MF_BYCOMMAND | (stateValue ? MF_UNCHECKED : MF_CHECKED));\n\t\tAppConfiguration::SetOpenningFolderAfterExp(!stateValue);\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnOutputDbgLogs(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tCMenuHandle menu = GetMenu();\n\t\tCMenuHandle subMenu = menu.GetSubMenu(0);\n\t\tUINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND);\n\t\tBOOL stateValue = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE;\n\t\tsubMenu.CheckMenuItem(wID, MF_BYCOMMAND | (stateValue ? MF_UNCHECKED : MF_CHECKED));\n\t\tAppConfiguration::SetOutputDebugLogs(!stateValue);\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnHelpHomePage(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tDWORD_PTR dwRet = (DWORD_PTR)::ShellExecute(0, _T(\"open\"), TEXT(\"https://github.com/BlueMatthew/WechatExporter\"), 0, 0, SW_SHOWNORMAL);\n\n\t\treturn 0;\n\t}\n\n\tLRESULT OnAppAbout(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tCAboutDlg dlg;\n\t\tdlg.DoModal();\n\t\treturn 0;\n\t}\n\n\tLRESULT OnFileExit(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\n\t{\n\t\tPostMessage(WM_CLOSE);\n\t\treturn 0;\n\t}\n};\n"
  },
  {
    "path": "vcproject/PdfConverterImpl.h",
    "content": "#pragma once\r\n\r\n#include \"stdafx.h\"\r\n#include \"Core.h\"\r\n#include <atlstr.h>\r\n\r\nclass PdfConverterImpl : public PdfConverter\r\n{\r\npublic:\r\n\tPdfConverterImpl(LPCTSTR outputDir) : m_pdfSupported(false)\r\n\t{\r\n\t\tif (isChromeInstalled(m_assemblyPath))\r\n\t\t{\r\n\t\t\tm_pdfSupported = true;\r\n\t\t\tm_param = TEXT(\"--headless --disable-extensions --disable-gpu --print-to-pdf=\\\"%%DEST%%\\\" --print-to-pdf-no-header \\\"file://%%SRC%%\\\"\");\r\n\t\t}\r\n\t\telse if (isEdgeInstalled(m_assemblyPath))\r\n\t\t{\r\n\t\t\tm_pdfSupported = true;\r\n\t\t\tm_param = TEXT(\"--headless --disable-extensions --disable-gpu --print-to-pdf=\\\"%%DEST%%\\\" --print-to-pdf-no-header \\\"file://%%SRC%%\\\"\");\r\n\t\t}\r\n\r\n\t\tif (NULL != outputDir)\r\n\t\t{\r\n\t\t\tinitShellFile(outputDir);\r\n\t\t}\r\n\t}\r\n\t\r\n\tbool isPdfSupported() const\r\n\t{\r\n\t\treturn m_pdfSupported;\r\n\t}\r\n\r\n\t~PdfConverterImpl()\r\n\t{\r\n\t}\r\n\r\n\tvoid executeCommand()\r\n\t{\r\n\t\tif (!::PathFileExists(m_shellPath))\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tconst char *pauseCmd = \"pause\\r\\n\";\r\n\t\tappendFile(m_shellPath, reinterpret_cast<const unsigned char *>(pauseCmd), strlen(pauseCmd));\r\n\r\n\t\tShellExecute(NULL, TEXT(\"open\"), m_shellPath, NULL, NULL, SW_SHOW);\r\n\t}\r\n\r\n\tbool makeUserDirectory(const std::string& dirName)\r\n\t{\r\n\t\tCW2T pszDir(CA2W(dirName.c_str(), CP_UTF8));\r\n\r\n\t\tTCHAR pdfOutputDir[MAX_PATH] = { 0 };\r\n\t\tPathCombine(pdfOutputDir, m_output, TEXT(\"pdf\"));\r\n\r\n\t\tTCHAR userOutputDir[MAX_PATH] = { 0 };\r\n\t\tPathCombine(userOutputDir, pdfOutputDir, pszDir);\r\n\t\tCString command = TEXT(\"\\r\\nIF NOT EXIST \\\"\");\r\n\t\tcommand += userOutputDir;\r\n\t\tcommand += TEXT(\"\\\" MKDIR \\\"\");\r\n\t\tcommand += userOutputDir;\r\n\t\tcommand += TEXT(\"\\\"\\r\\n\");\r\n\r\n\t\tCW2A pszU(CT2W(command), TARGET_CODE_PAGE);\r\n\r\n\t\tappendFile(m_shellPath, reinterpret_cast<const unsigned char *>((LPCSTR)pszU), strlen(pszU));\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\tbool convert(const std::string& htmlPath, const std::string& pdfPath)\r\n\t{\r\n\t\tif (!m_pdfSupported)\r\n\t\t{\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tCW2T pszHtmlPath(CA2W(htmlPath.c_str(), CP_UTF8));\r\n\t\tTCHAR url[MAX_PATH * 4] = { 0 };\r\n\t\tDWORD cchUrl = MAX_PATH * 4; // max posible buffer size\r\n\t\t\r\n\t\tHRESULT res = UrlCreateFromPath(pszHtmlPath, url, &cchUrl, NULL);\r\n\t\tif (FAILED(res))\r\n\t\t{\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tCW2T pszPdfPath(CA2W(pdfPath.c_str(), CP_UTF8));\r\n\r\n\t\tCString command(TEXT(\"\\\"\"));\r\n\t\tcommand += m_assemblyPath;\r\n\t\tcommand += TEXT(\"\\\" \");\r\n\t\tcommand += TEXT(\"--headless --disable-extensions --disable-gpu --print-to-pdf-no-header --print-to-pdf=\\\"\");\r\n\t\tcommand += (LPCTSTR)pszPdfPath;\r\n\t\tcommand += TEXT(\"\\\" \");\r\n\t\tcommand += url;\r\n\t\tcommand += TEXT(\"\\r\\n\");\r\n\r\n\t\tCT2A content(command, TARGET_CODE_PAGE);\r\n\t\t\r\n\t\tappendFile((LPCTSTR)m_shellPath, reinterpret_cast<const unsigned char *>((LPCSTR)content), strlen(content));\r\n\t\t// appendFile((LPCTSTR)m_shellPath, reinterpret_cast<const unsigned char *>((LPCTSTR)command), _tcslen(command) * sizeof(TCHAR));\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\t\r\n\tbool convert2(const std::string& htmlPath, const std::string& pdfPath)\r\n\t{\r\n\t\tCString param = m_param;\r\n\t\tCW2T pszSrc(CA2W(htmlPath.c_str(), CP_UTF8));\r\n\t\tCW2T pszDest(CA2W(pdfPath.c_str(), CP_UTF8));\r\n\r\n\t\tparam.Replace(TEXT(\"%%SRC%%\"), pszSrc);\r\n\t\tparam.Replace(TEXT(\"%%DEST%%\"), pszDest);\r\n\r\n\t\tSHELLEXECUTEINFO ShExecInfo = { 0 };\r\n\t\tShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO);\r\n\t\tShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS;\r\n\t\tShExecInfo.hwnd = NULL;\r\n\t\tShExecInfo.lpVerb = TEXT(\"open\");\r\n\t\tShExecInfo.lpFile = (LPCTSTR)m_assemblyPath;\r\n\t\tShExecInfo.lpParameters = (LPCTSTR)param;\r\n\t\tShExecInfo.lpDirectory = NULL;\r\n\t\tShExecInfo.nShow = SW_HIDE;\r\n\t\tShExecInfo.hInstApp = NULL;\r\n\t\tShellExecuteEx(&ShExecInfo);\r\n\t\tWaitForSingleObject(ShExecInfo.hProcess, INFINITE);\r\n\t\tCloseHandle(ShExecInfo.hProcess);\r\n\r\n\t\t// ShellExecute(NULL, TEXT(\"open\"), m_assemblyPath, param, NULL, SW_HIDE);\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\nprotected:\r\n\tbool isChromeInstalled(CString& assemblyPath)\r\n\t{\r\n\t\treturn isAppInstalled(TEXT(\"chrome.exe\"), true, assemblyPath) || isAppInstalled(TEXT(\"chrome.exe\"), false, assemblyPath);\r\n\t}\r\n\r\n\tbool isEdgeInstalled(CString& assemblyPath)\r\n\t{\r\n\t\treturn isAppInstalled(TEXT(\"msedge.exe\"), true, assemblyPath) || isAppInstalled(TEXT(\"msedge.exe\"), false, assemblyPath);\r\n\t}\r\n\r\n\tbool isAppInstalled(LPCTSTR name, bool lmOrCU, CString& assemblyPath)\r\n\t{\r\n\t\tBOOL installed = false;\r\n\t\tCRegKey rk;\r\n\t\tCString path = lmOrCU ? TEXT(\"SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\\") : TEXT(\"SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\\");\r\n\t\tpath += name;\r\n\t\tif (rk.Open(lmOrCU ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, (LPCTSTR)path, KEY_READ) == ERROR_SUCCESS)\r\n\t\t{\r\n\t\t\tULONG chars = 0;\r\n\t\t\tHRESULT hr = rk.QueryStringValue(NULL, NULL, &chars);\r\n\t\t\tif (SUCCEEDED(hr))\r\n\t\t\t{\r\n\t\t\t\tCString appPath;\r\n\t\t\t\thr = rk.QueryStringValue(NULL, appPath.GetBufferSetLength(chars), &chars);\r\n\t\t\t\tappPath.ReleaseBuffer();\r\n\r\n\t\t\t\tif (PathFileExists(appPath))\r\n\t\t\t\t{\r\n\t\t\t\t\tassemblyPath = appPath;\r\n\t\t\t\t\tinstalled = true;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\trk.Close();\r\n\t\t}\r\n\r\n\t\treturn installed;\r\n\t}\r\n\r\n\tvoid initShellFile(LPCTSTR outputDir)\r\n\t{\r\n\t\tm_output = outputDir;\r\n\r\n\t\tTCHAR shellFile[MAX_PATH] = { 0 };\r\n\t\tPathCombine(shellFile, outputDir, TEXT(\"pdf.bat\"));\r\n\t\tm_shellPath = shellFile;\r\n\t\t::DeleteFile(shellFile);\r\n\r\n\t\tCString command;\r\n\t\tif (TARGET_CODE_PAGE == CP_UTF8)\r\n\t\t{\r\n\t\t\tcommand += TEXT(\"CHCP 65001\\r\\n\");\r\n\t\t}\r\n\t\tTCHAR pdfPath[MAX_PATH] = { 0 };\r\n\t\tPathCombine(pdfPath, outputDir, TEXT(\"pdf\"));\r\n\r\n\t\tcommand += TEXT(\"IF NOT EXIST \\\"\");\r\n\t\tcommand += pdfPath;\r\n\t\tcommand += TEXT(\"\\\" MKDIR \\\"\");\r\n\t\tcommand += pdfPath;\r\n\t\tcommand += TEXT(\"\\\"\\r\\n\");\r\n\r\n\t\tCW2A pszU(CT2W(command), TARGET_CODE_PAGE);\r\n\r\n\t\tappendFile((LPCTSTR)m_shellPath, reinterpret_cast<const unsigned char *>((LPCSTR)pszU), strlen(pszU));\r\n\t}\r\n\r\n\tbool appendFile(LPCTSTR path, const unsigned char* data, size_t dataLength)\r\n\t{\r\n\t\tHANDLE hFile = ::CreateFile(path, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);\r\n\t\tif (hFile == INVALID_HANDLE_VALUE)\r\n\t\t{\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\t::SetFilePointer(hFile, 0, 0, FILE_END);\r\n\t\tDWORD dwBytesToWrite = static_cast<DWORD>(dataLength);\r\n\t\tDWORD dwBytesWritten = 0;\r\n\t\tBOOL bErrorFlag = WriteFile(hFile, data, dwBytesToWrite, &dwBytesWritten, NULL);\r\n\t\t::CloseHandle(hFile);\r\n\t\treturn (TRUE == bErrorFlag);\r\n\r\n\t}\r\n\r\nprivate:\r\n\tbool m_pdfSupported;\r\n\tCString m_assemblyPath;\r\n\tCString m_output;\r\n\tCString m_shellPath;\r\n\tCString m_param;\r\n\r\n\t// const UINT TARGET_CODE_PAGE = CP_ACP;\r\n\tconst UINT TARGET_CODE_PAGE = CP_UTF8;\r\n};\r\n"
  },
  {
    "path": "vcproject/ProgressListViewCtrl.h",
    "content": "#ifndef _PROGRESSLISTVIEWCTRL_H\r\n#define _PROGRESSLISTVIEWCTRL_H\r\n\r\nclass CProgressListViewCtrl : public CWindowImpl<CProgressListViewCtrl, CListViewCtrl>,\r\n\tpublic CCustomDraw<CProgressListViewCtrl>\r\n{\r\npublic:\r\n\r\n\tBEGIN_MSG_MAP(CProgressListViewCtrl)\r\n\t\tMESSAGE_HANDLER(WM_CREATE, OnCreate)\r\n\t\t// REFLECTED_NOTIFY_CODE_HANDLER(LVN_DELETEITEM, OnDeleteItem)\r\n\t\tCHAIN_MSG_MAP_ALT(CCustomDraw<CProgressListViewCtrl>, 1)\r\n\t\t// Because WTL 3.1 does not support subitem custom drawing.\r\n\t\t// Should be forward-compatible with new versions...\r\n\t\t// REFLECTED_NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnNotifyCustomDraw)\r\n\t\tDEFAULT_REFLECTION_HANDLER()\r\n\tEND_MSG_MAP()\r\n\r\n\tCProgressListViewCtrl() : CWindowImpl()\r\n\t{\r\n\t\tm_itemForProgressBar = -1;\r\n\t\tm_subItemForProgressBar = -1;\r\n\t\tm_progressUpper = 0;\r\n\t\tm_progressPos = 0;\r\n\t}\r\n\r\n\tBOOL SubclassWindow(HWND hWnd)\r\n\t{\r\n\t\treturn CWindowImpl<CProgressListViewCtrl, CListViewCtrl>::SubclassWindow(hWnd) ? _Init() : FALSE;\r\n\t}\r\n\r\n\tLRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)\r\n\t{\r\n\t\tbHandled = FALSE;\r\n\t\treturn _Init() ? 0 : -1;\r\n\t}\r\n\r\n\tLRESULT OnNotifyCustomDraw(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)\r\n\t{\r\n\t\tSetMsgHandled(FALSE);\r\n\t\tLPNMCUSTOMDRAW lpNMCustomDraw = (LPNMCUSTOMDRAW)pnmh;\r\n\t\tDWORD dwRet = CDRF_DODEFAULT;\r\n\t\tswitch (lpNMCustomDraw->dwDrawStage) {\r\n\t\tcase CDDS_ITEMPREPAINT | CDDS_SUBITEM:\r\n\t\t\tdwRet = OnSubItemPrePaint(idCtrl, lpNMCustomDraw);\r\n\t\t\tSetMsgHandled(TRUE);\r\n\t\t\treturn dwRet;\r\n\t\t}\r\n\t\tbHandled = FALSE;\r\n\t\treturn dwRet;\r\n\t}\r\n\r\n\tDWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW /*lpNMCustomDraw*/)\r\n\t{\r\n\t\treturn CDRF_NOTIFYITEMDRAW;\r\n\t}\r\n\r\n\tDWORD OnItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/)\r\n\t{\r\n\t\treturn CDRF_NOTIFYSUBITEMDRAW; // We need per-subitem notifications\r\n\t}\r\n\r\n\tDWORD OnSubItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW lpNMCustomDraw)\r\n\t{\r\n\t\tNMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>(lpNMCustomDraw);\r\n\t\tif (pLVCD->nmcd.dwItemSpec == m_itemForProgressBar && pLVCD->iSubItem == m_subItemForProgressBar)\r\n\t\t{\r\n\t\t\tCDCHandle dcPaint(pLVCD->nmcd.hdc);\r\n\t\t\tint nContextState = dcPaint.SaveDC();\r\n\r\n\t\t\tint State = ListView_GetItemState(m_hWnd, pLVCD->nmcd.dwItemSpec, LVIS_CUT | LVIS_SELECTED | LVIS_FOCUSED);\r\n\t\t\tCString text;\r\n\t\t\tGetItemText((int)pLVCD->nmcd.dwItemSpec, pLVCD->iSubItem, text);\r\n\r\n\t\t\tint colorIndex = (State & LVIS_SELECTED) ? 1 : 0;\r\n\r\n\t\t\t//3D border spacer (not exactly what we need either)\r\n\t\t\tint nSpacerW = GetSystemMetrics(SM_CXEDGE);\r\n\t\t\t//Get horizontal DPI setting\r\n\t\t\tint dpiX = dcPaint.GetDeviceCaps(LOGPIXELSX);\r\n\t\t\tint nSpacePx = (int)(nSpacerW * 2.5 * dpiX / 96.0);\r\n\r\n\t\t\tRECT rc = pLVCD->nmcd.rc;\r\n\t\t\t// ATLTRACE(TEXT(\"SubItem=%d Bounds: left=%d right=%d\\r\\n\"), pLVCD->iSubItem, rc.left, rc.right);\r\n\t\t\trc.left += 2;\r\n\t\t\trc.right -= 2;\r\n\t\t\trc.top += 1;\r\n\t\t\trc.bottom -= 2;\r\n\r\n\t\t\tRECT rcText = rc;\r\n\t\t\trcText.left += nSpacePx;\r\n\t\t\trcText.right -= nSpacePx;\r\n\r\n\t\t\tdcPaint.FillSolidRect(&pLVCD->nmcd.rc, m_clrBkgnd[colorIndex]);\r\n\t\t\tdcPaint.SetTextColor(m_clrText[colorIndex]);\r\n\t\t\tdcPaint.DrawText(text, -1, &rcText, DT_VCENTER | DT_SINGLELINE);\r\n\r\n\t\t\trc.right = rc.left + (LONG)(m_percent * (rc.right - rc.left));\r\n\t\t\tif (rcText.right > rc.right)\r\n\t\t\t{\r\n\t\t\t\trcText.right = rc.right;\r\n\t\t\t}\r\n\r\n\t\t\tdcPaint.FillSolidRect(&rc, m_clrPercent[colorIndex]);\r\n\t\t\tdcPaint.SetTextColor(m_clrPercentText[colorIndex]);\r\n\t\t\tdcPaint.DrawText(text, -1, &rcText, DT_VCENTER | DT_SINGLELINE);\r\n\r\n\t\t\tdcPaint.RestoreDC(nContextState);\r\n\r\n\t\t\treturn CDRF_SKIPDEFAULT;\r\n\t\t}\r\n\t\telse if (pLVCD->nmcd.dwItemSpec != m_itemForProgressBar && pLVCD->iSubItem == m_subItemForProgressBar)\r\n\t\t{\r\n\t\t\tpLVCD->clrText = RGB(0x0, 0xFF, 0x0);\r\n\t\t}\r\n\r\n\t\treturn CDRF_DODEFAULT;\r\n\t}\r\n\r\n\tvoid SetProgressBarInfo(int item, int subItem, int progressUpper, int progressPos = 0)\r\n\t{\r\n\t\tint nOrgItem = m_itemForProgressBar;\r\n\t\tint nOrgSubItem = m_subItemForProgressBar;\r\n\t\tm_itemForProgressBar = item;\r\n\t\tm_subItemForProgressBar = subItem;\r\n\t\tm_progressUpper = progressUpper;\r\n\r\n\t\tif (nOrgItem != -1 && nOrgSubItem != -1)\r\n\t\t{\r\n\t\t\tRECT rc;\r\n\t\t\tGetSubItemRect(nOrgItem, nOrgSubItem, LVIR_BOUNDS, &rc);\r\n\t\t\tInvalidateRect(&rc);\r\n\t\t}\r\n\t\t\r\n\t\tSetProgressPos(progressPos);\r\n\t}\r\n\r\n\tvoid SetProgressPos(int progressPos)\r\n\t{\r\n\t\tm_progressPos = progressPos;\r\n\t\tm_percent = (m_progressUpper > 0) ? (((float)progressPos) / ((float)m_progressUpper)) : 0.0f;\r\n\r\n\t\tRECT rc;\r\n\t\tGetSubItemRect(m_itemForProgressBar, m_subItemForProgressBar, LVIR_BOUNDS, &rc);\r\n\t\tInvalidateRect(&rc);\r\n\t}\r\n\r\n\tvoid ClearProgressBar()\r\n\t{\r\n\t\tint nOrgItem = m_itemForProgressBar;\r\n\t\tint nOrgSubItem = m_subItemForProgressBar;\r\n\t\tm_itemForProgressBar = -1;\r\n\t\t// m_subItemForProgressBar = -1;\r\n\t\tm_progressUpper = 0;\r\n\r\n\t\tif (nOrgItem != -1 && nOrgSubItem != -1)\r\n\t\t{\r\n\t\t\tRECT rc;\r\n\t\t\tGetSubItemRect(nOrgItem, nOrgSubItem, LVIR_BOUNDS, &rc);\r\n\t\t\tInvalidateRect(&rc);\r\n\t\t}\r\n\t}\r\n\r\nprotected:\r\n\r\n\tBOOL _Init()\r\n\t{\r\n\t\tm_clrText[0] = ::GetSysColor(COLOR_WINDOWTEXT);\r\n\t\tm_clrBkgnd[0] = ::GetSysColor(COLOR_WINDOW);\r\n\t\tm_clrPercentText[0] = ::GetSysColor(COLOR_HIGHLIGHTTEXT);\r\n\t\tm_clrPercent[0] = ::GetSysColor(COLOR_HIGHLIGHT);\r\n\r\n\t\tm_clrText[1] = ::GetSysColor(COLOR_HIGHLIGHTTEXT);\r\n\t\tm_clrBkgnd[1] = ::GetSysColor(COLOR_HIGHLIGHT);\r\n\t\tm_clrPercentText[1] = ::GetSysColor(COLOR_HOTLIGHT);\r\n\t\tm_clrPercent[1] = ::GetSysColor(COLOR_WINDOW);\r\n\r\n\t\treturn TRUE;\r\n\t}\r\n\r\n\tint m_itemForProgressBar;\r\n\tint m_subItemForProgressBar;\r\n\tint m_progressUpper;\r\n\tint m_progressPos;\r\n\tfloat m_percent;\r\n\r\n\tCOLORREF m_clrText[2];\r\n\tCOLORREF m_clrBkgnd[2];\r\n\tCOLORREF m_clrPercent[2];\r\n\tCOLORREF m_clrPercentText[2];\r\n\r\n};\r\n\r\n#endif // _PROGRESSLISTVIEWCTRL_H"
  },
  {
    "path": "vcproject/TextProgressBarCtrl.h",
    "content": "#pragma once\r\n\r\n#include <atlctrls.h>\r\n#include <atlmisc.h>\r\n#include <atltheme.h>\r\n\r\ntemplate<typename T, typename TBase = CProgressBarCtrl, typename TWinTraits = CControlWinTraits>\r\nclass ATL_NO_VTABLE CTextProgressBarCtrlT : public CWindowImpl<T, TBase, TWinTraits>, public CThemeImpl<T>\r\n{\r\npublic:\r\n\tCTextProgressBarCtrlT()\r\n\t{\r\n\t\tSetThemeClassList(L\"PROGRESS\");\r\n\t}\r\n\r\n\tBOOL SubclassWindow(HWND hWnd)\r\n\t{\r\n\t\tBOOL result = CWindowImpl<T, TBase, TWinTraits>::SubclassWindow(hWnd);\r\n\t\tif (result)\r\n\t\t{\r\n\t\t\tif (GetThemeClassList() != NULL)\r\n\t\t\t\tOpenThemeData();\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\tBEGIN_MSG_MAP(CTextProgressBarCtrl)\r\n\t\tCHAIN_MSG_MAP(CThemeImpl<CTextProgressBarCtrl>)\r\n\t\tMESSAGE_HANDLER(WM_PAINT, OnPaint)\r\n\t\tMESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)\r\n\t\tDEFAULT_REFLECTION_HANDLER()\r\n\tEND_MSG_MAP()\r\n\r\n\t// message handlers\r\n\r\n\tLRESULT OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tRECT rc;\r\n\t\tGetClientRect(&rc);\r\n\r\n\t\tCPaintDC dc(m_hWnd);\r\n\r\n\t\tif (rc.right > rc.left && rc.bottom > rc.top)\r\n\t\t{\r\n\t\t\tCMemoryDC dcMem(dc.m_hDC, rc);\r\n\r\n\t\t\tif (IsAppThemed())\r\n\t\t\t\tDrawThemedProgressBar(dcMem, rc);\r\n\t\t\telse\r\n\t\t\t\tDrawClassicProgressBar(dcMem, rc);\r\n\t\t\t\r\n\t\t\tDrawText(dcMem, rc);\r\n\t\t}\r\n\r\n\t\tbHandled = TRUE;\r\n\t\treturn 1;\r\n\t}\r\n\r\n\tLRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\treturn 1;    // we painted the background\r\n\t}\r\n\r\n\r\nprotected:\r\n\t\r\n\tvoid CalcFillRect(CRect& rc)\r\n\t{\r\n\t\tint nLower = 0, nUpper = 0;\r\n\t\tGetRange(nLower, nUpper);\r\n\t\tif (nLower >= nUpper) nLower = nUpper - 1;\r\n\t\tint nPos = GetPos();\r\n\r\n\t\tif (nPos < nLower) nPos = nLower;\r\n\t\tif (nPos > nUpper) nPos = nUpper;\r\n\r\n\t\tDWORD dwStyle = GetStyle();\r\n\t\tBOOL bVertical = (dwStyle & PBS_VERTICAL) == PBS_VERTICAL;\r\n\r\n\t\tif (bVertical)\r\n\t\t\trc.top = rc.bottom - (rc.bottom - rc.top) * (nPos - nLower) / (nUpper - nLower);\r\n\t\telse\r\n\t\t\trc.right = rc.left + (rc.right - rc.left) * (nPos - nLower) / (nUpper - nLower);\r\n\t}\r\n\r\n\tvoid DrawThemedProgressBar(CMemoryDC& dc, const CRect& rc)\r\n\t{\r\n\t\tDrawThemeBackground(dc, PP_BAR, 0, &rc, NULL);\r\n\r\n\t\tCRect rcFill = rc;\r\n\t\tCalcFillRect(rcFill);\r\n\t\tDrawThemeBackground(dc, PP_FILL, PBFS_NORMAL, &rcFill, NULL);\r\n\t}\r\n\r\n\tvoid DrawClassicProgressBar(CMemoryDC& dc, const CRect& rc)\r\n\t{\r\n\t\tCRect rcEdge = rc;\r\n\t\tdc.DrawEdge(&rcEdge, EDGE_ETCHED, BF_RECT);\r\n\r\n\t\tCRect rc2 = rc;\r\n\t\trc2.DeflateRect(1, 1, 1, 1);\r\n\t\tdc.FillSolidRect(&rc2, ::GetSysColor(COLOR_BTNFACE));\r\n\r\n\t\tCRect rcFill = rc2;\r\n\t\tif (rcFill.right > rcFill.left && rcFill.bottom > rcFill.top)\r\n\t\t{\r\n\t\t\tCalcFillRect(rcFill);\r\n\t\t\tdc.FillSolidRect(&rcFill, ::GetSysColor(COLOR_HIGHLIGHT));\r\n\t\t}\r\n\t}\r\n\r\n\t// draw text on the bar\r\n\tvoid DrawText(CMemoryDC& dc, const CRect& rc)\r\n\t{\r\n\t\tCString text;\r\n\t\tif (GetWindowText(text) > 0)\r\n\t\t{\r\n\t\t\tint oldBkMode = dc.SetBkMode(TRANSPARENT);\r\n\r\n\t\t\tHFONT hFont = GetFont();\r\n\t\t\tif (NULL == hFont)\r\n\t\t\t{\r\n\t\t\t\thFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);\r\n\t\t\t}\r\n\t\t\tHFONT oldFont = dc.SelectFont(hFont);\r\n\t\t\tCRect rcText = rc;\r\n\t\t\tdc.DrawText((LPCTSTR)text, -1, &rcText, DT_CENTER | DT_VCENTER | DT_SINGLELINE);\r\n\t\t\tdc.SelectFont(oldFont);\r\n\r\n\t\t\tdc.SetBkMode(oldBkMode);\r\n\t\t}\r\n\t}\r\n};\r\n\r\nclass CTextProgressBarCtrl : public CTextProgressBarCtrlT<CTextProgressBarCtrl>\r\n{\r\npublic:\r\n\tDECLARE_WND_SUPERCLASS(_T(\"WTL_progressbar\"), GetWndClassName())\r\n};\r\n"
  },
  {
    "path": "vcproject/ToolTipButton.h",
    "content": "#pragma once\n\ntemplate <class T, class TBase = CButton, class TWinTraits = ATL::CControlWinTraits>\nclass ATL_NO_VTABLE CToolTipButtonImpl : public ATL::CWindowImpl< T, TBase, TWinTraits >\n{\npublic:\n\tDECLARE_WND_SUPERCLASS2(NULL, T, TBase::GetWndClassName())\n\n\tenum\n\t{\n\t\t_nImageNormal = 0,\n\t\t_nImagePushed,\n\t\t_nImageFocusOrHover,\n\t\t_nImageDisabled,\n\n\t\t_nImageCount = 4,\n\t};\n\n\tenum\n\t{\n\t\tID_TIMER_FIRST = 1000,\n\t\tID_TIMER_REPEAT = 1001\n\t};\n\n\tCToolTipCtrl m_tip;\n\tLPTSTR m_lpstrToolTipText;\n\n\t// Constructor/Destructor\n\tCToolTipButtonImpl() :\n\t\tm_lpstrToolTipText(NULL)\n\t{\n\n\t}\n\n\t~CToolTipButtonImpl()\n\t{\n\t\tdelete[] m_lpstrToolTipText;\n\t}\n\n\t// overridden to provide proper initialization\n\tBOOL SubclassWindow(HWND hWnd)\n\t{\n\t\tBOOL bRet = ATL::CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd);\n\t\tif (bRet != FALSE)\n\t\t{\n\t\t\tT* pT = static_cast<T*>(this);\n\t\t\tpT->Init();\n\t\t}\n\n\t\treturn bRet;\n\t}\n\n\t// Attributes\n\n\tint GetToolTipTextLength() const\n\t{\n\t\treturn (m_lpstrToolTipText == NULL) ? -1 : lstrlen(m_lpstrToolTipText);\n\t}\n\n\tbool GetToolTipText(LPTSTR lpstrText, int nLength) const\n\t{\n\t\tATLASSERT(lpstrText != NULL);\n\t\tif (m_lpstrToolTipText == NULL)\n\t\t\treturn false;\n\n\t\terrno_t nRet = ATL::Checked::tcsncpy_s(lpstrText, nLength, m_lpstrToolTipText, _TRUNCATE);\n\n\t\treturn ((nRet == 0) || (nRet == STRUNCATE));\n\t}\n\n\tbool SetToolTipText(LPCTSTR lpstrText)\n\t{\n\t\tif (m_lpstrToolTipText != NULL)\n\t\t{\n\t\t\tdelete[] m_lpstrToolTipText;\n\t\t\tm_lpstrToolTipText = NULL;\n\t\t}\n\n\t\tif (lpstrText == NULL)\n\t\t{\n\t\t\tif (m_tip.IsWindow())\n\t\t\t\tm_tip.Activate(FALSE);\n\t\t\treturn true;\n\t\t}\n\n\t\tint cchLen = lstrlen(lpstrText) + 1;\n\t\tATLTRY(m_lpstrToolTipText = new TCHAR[cchLen]);\n\t\tif (m_lpstrToolTipText == NULL)\n\t\t\treturn false;\n\n\t\tATL::Checked::tcscpy_s(m_lpstrToolTipText, cchLen, lpstrText);\n\t\tif (m_tip.IsWindow())\n\t\t{\n\t\t\tm_tip.Activate(TRUE);\n\t\t\tm_tip.AddTool(this->m_hWnd, m_lpstrToolTipText);\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t// Overrideables\n\n\t// Message map and handlers\n\tBEGIN_MSG_MAP(CBitmapButtonImpl)\n\t\tMESSAGE_HANDLER(WM_CREATE, OnCreate)\n\t\tMESSAGE_HANDLER(WM_DESTROY, OnDestroy)\n\t\tMESSAGE_RANGE_HANDLER(WM_MOUSEFIRST, WM_MOUSELAST, OnMouseMessage)\n\t\t// MESSAGE_HANDLER(WM_MOUSEMOVE, OnMouseMove)\n\t\t// MESSAGE_HANDLER(WM_MOUSELEAVE, OnMouseLeave)\n\tEND_MSG_MAP()\n\n\tLRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)\n\t{\n\t\tT* pT = static_cast<T*>(this);\n\t\tpT->Init();\n\n\t\tbHandled = FALSE;\n\t\treturn 1;\n\t}\n\n\tLRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)\n\t{\n\t\tif (m_tip.IsWindow())\n\t\t{\n\t\t\tm_tip.DestroyWindow();\n\t\t\tm_tip.m_hWnd = NULL;\n\t\t}\n\t\tbHandled = FALSE;\n\t\treturn 1;\n\t}\n\n\tLRESULT OnMouseMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\n\t{\n\t\tMSG msg = { this->m_hWnd, uMsg, wParam, lParam };\n\t\tif (m_tip.IsWindow())\n\t\t\tm_tip.RelayEvent(&msg);\n\t\tbHandled = FALSE;\n\t\treturn 1;\n\t}\n\n\t// Implementation\n\tvoid Init()\n\t{\n\n\t\t// create a tool tip\n\t\tm_tip.Create(this->m_hWnd);\n\t\tATLASSERT(m_tip.IsWindow());\n\t\tif (m_tip.IsWindow() && (m_lpstrToolTipText != NULL))\n\t\t{\n\t\t\tm_tip.Activate(TRUE);\n\t\t\tm_tip.AddTool(this->m_hWnd, m_lpstrToolTipText);\n\t\t}\n\n\t}\n\n\tBOOL StartTrackMouseLeave()\n\t{\n\t\tTRACKMOUSEEVENT tme = {};\n\t\ttme.cbSize = sizeof(tme);\n\t\ttme.dwFlags = TME_LEAVE;\n\t\ttme.hwndTrack = this->m_hWnd;\n\t\treturn ::TrackMouseEvent(&tme);\n\t}\n};\n\nclass CToolTipButton : public CToolTipButtonImpl<CToolTipButton>\n{\npublic:\n\tDECLARE_WND_SUPERCLASS(_T(\"WTL_ToolTipButton\"), GetWndClassName())\n\n\tCToolTipButton() :\n\t\tCToolTipButtonImpl<CToolTipButton>()\n\t{ }\n};\n"
  },
  {
    "path": "vcproject/VersionDetector.h",
    "content": "#pragma once\r\n\r\n#include \"stdafx.h\"\r\n\r\nclass VersionDetector\r\n{\r\npublic:\r\n\tCString GetProductVersion()\r\n\t{\r\n\t\tTCHAR szPath[MAX_PATH] = { 0 };\r\n\r\n\t\tGetModuleFileName(NULL, szPath, MAX_PATH);\r\n\r\n\t\treturn GetProductVersion(szPath);\r\n\t}\r\n\r\n\tCString GetProductVersion(LPCTSTR szPath)\r\n\t{\r\n\t\tCString strResult;\r\n\r\n\t\tDWORD dwHandle;\r\n\t\tDWORD dwSize = GetFileVersionInfoSize(szPath, &dwHandle);\r\n\r\n\t\tif (dwSize > 0)\r\n\t\t{\r\n\t\t\tBYTE* pbBuf = new BYTE[dwSize];\r\n\t\t\tif (GetFileVersionInfo(szPath, dwHandle, dwSize, pbBuf))\r\n\t\t\t{\r\n\t\t\t\tUINT uiSize;\r\n\t\t\t\tBYTE* lpb;\r\n\t\t\t\tif (VerQueryValue(pbBuf,\r\n\t\t\t\t\tTEXT(\"\\\\VarFileInfo\\\\Translation\"),\r\n\t\t\t\t\t(void**)&lpb,\r\n\t\t\t\t\t&uiSize))\r\n\t\t\t\t{\r\n\t\t\t\t\tWORD* lpw = (WORD*)lpb;\r\n\t\t\t\t\tCString strQuery;\r\n\t\t\t\t\tstrQuery.Format(TEXT(\"\\\\StringFileInfo\\\\%04x%04x\\\\ProductVersion\"), lpw[0], lpw[1]);\r\n\t\t\t\t\tif (VerQueryValue(pbBuf,\r\n\t\t\t\t\t\tconst_cast<LPTSTR>((LPCTSTR)strQuery),\r\n\t\t\t\t\t\t(void**)&lpb,\r\n\t\t\t\t\t\t&uiSize) && uiSize > 0)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tstrResult = (LPCTSTR)lpb;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdelete[] pbBuf;\r\n\t\t}\r\n\r\n\t\treturn strResult;\r\n\t}\r\n};"
  },
  {
    "path": "vcproject/View.h",
    "content": "﻿// View.h : interface of the CView class\r\n//\r\n/////////////////////////////////////////////////////////////////////////////\r\n\r\n#pragma once\r\n\r\n#include <thread>\r\n#include <future>\r\n#include <chrono>\r\n#include \"LoggerImpl.h\"\r\n#include \"PdfConverterImpl.h\"\r\n#include \"ExportNotifierImpl.h\"\r\n// #include \"ColoredControls.h\"\r\n#include \"LogListBox.h\"\r\n#include \"ProgressListViewCtrl.h\"\r\n#include \"TextProgressBarCtrl.h\"\r\n#include \"AppConfiguration.h\"\r\n#include \"VersionDetector.h\"\r\n#include \"ViewHelper.h\"\r\n\r\n#define SAFE_DELETE(ptr) { delete ptr; ptr = NULL; }\r\n\r\nclass CView : public CDialogImpl<CView>, public CDialogResize<CView>\r\n{\r\nprivate:\r\n\r\n\tenum VIEW_STATE\r\n\t{\r\n\t\tVS_IDLE = 0,\r\n\t\tVS_LOADING,\r\n\t\tVS_EXPORTING\r\n\t};\r\n\r\n\tstatic const int SUBITEM_PROGRESS = 5;\r\n\tstatic const UINT_PTR EVENT_ID_PROGRESS = 1;\r\n\r\n\t// CColoredComboBoxCtrl\tm_cbmBoxBackups;\r\n\t// CColoredComboBoxCtrl\tm_cbmBoxUsers;\r\n\tCImageList\t\t\t\tm_sourceTypeIcons;\r\n\tCLogListBox\t\t\t\tm_logListBox;\r\n\tCSortListViewCtrl\t\tm_sessionsListCtrl;\r\n\tCProgressListViewCtrl\tm_progressListCtrl;\r\n\tCStatic\t\t\t\t\tm_progressTextCtrl;\r\n\tCTextProgressBarCtrl\tm_progressBarCtrl;\r\n\tCToolTipButton\t\t\tm_btnExpITunes;\r\n\t\r\n\tLoggerImpl*\t\t\tm_logger;\r\n\tPdfConverterImpl*\tm_pdfConverter;\r\n\tExportNotifierImpl* m_notifier;\r\n\tExporter*\t\t\tm_exporter;\r\n\r\n\tstd::vector<WechatSource *> m_wechatSources;\r\n\t// std::vector<DeviceInfo>\tm_devices;\r\n\tstd::vector<BackupItem> m_manifests;\r\n\tstd::vector<std::pair<Friend, std::vector<Session>>> m_usersAndSessions;\r\n\r\n\tint m_itemClicked;\r\n\r\n\tVIEW_STATE m_viewState;\r\n\tUINT_PTR m_eventIdProgress;\r\n\r\n\tclass CLoadingHandler\r\n\t{\r\n\tprotected:\r\n\t\tHWND m_hWnd;\r\n\t\tstd::future<bool> m_task;\r\n\t\tExporter m_exp;\r\n\t\tCWaitCursor m_waitCursor;\r\n\r\n\t\tbool runTask()\r\n\t\t{\r\n\t\t\tif (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED))\r\n\t\t\t{\r\n\t\t\t\tm_exp.setLanguageCode(\"zh-Hans\");\r\n\t\t\t}\r\n\r\n\t\t\tbool ret = m_exp.loadUsersAndSessions();\r\n\t\t\t::PostMessage(m_hWnd, WM_LOADDATA, (ret ? 1 : 0), reinterpret_cast<LPARAM>(this));\r\n\t\t\treturn ret;\r\n\t\t}\r\n\tpublic:\r\n\r\n\t\tCLoadingHandler(HWND hWnd, const std::string& resDir, const std::string& backupDir, Logger* logger) : m_hWnd(hWnd), m_exp(resDir, backupDir, \"\", logger, NULL)\r\n\t\t{\r\n\t\t\tExportOption options;\r\n\t\t\toptions.outputDebugLogs(AppConfiguration::OutputDebugLogs());\r\n\t\t\tm_exp.setOptions(options);\r\n\t\t}\r\n\r\n\t\t~CLoadingHandler()\r\n\t\t{\r\n\t\t}\r\n\r\n\t\tvoid startTask()\r\n\t\t{\r\n\t\t\tm_task = std::async(std::launch::async, &CLoadingHandler::runTask, this);\r\n\t\t}\r\n\r\n\t\tvoid waitForCompletion()\r\n\t\t{\r\n\t\t\tm_task.wait();\r\n\t\t}\r\n\r\n\t\tvoid getUsersAndSessions(std::vector<std::pair<Friend, std::vector<Session>>>& usersAndSessions)\r\n\t\t{\r\n\t\t\tm_exp.swapUsersAndSessions(usersAndSessions);\r\n\t\t}\r\n\r\n\t\tCString getVersions() const\r\n\t\t{\r\n\t\t\tCW2T pszV1(CA2W(m_exp.getITunesVersion().c_str(), CP_UTF8));\r\n\t\t\tCW2T pszV2(CA2W(m_exp.getIOSVersion().c_str(), CP_UTF8));\r\n\t\t\tCW2T pszV3(CA2W(m_exp.getWechatVersion().c_str(), CP_UTF8));\r\n\r\n\t\t\tCString versions;\r\n\t\t\tversions.Format(IDS_VERSIONS, (LPCTSTR)pszV1, (LPCTSTR)pszV2, (LPCTSTR)pszV3);\r\n\r\n\t\t\treturn versions;\r\n\t\t}\r\n\t};\r\n\r\n\tclass CUpdateHandler : public CIdleHandler\r\n\t{\r\n\tprotected:\r\n\t\tHWND m_hWnd;\r\n\t\tstd::future<bool> m_task;\r\n\t\tUpdater m_updater;\r\n\tpublic:\r\n\r\n\t\tCUpdateHandler(HWND hWnd, const std::string& currentVersion, const std::string& userAgent) : m_hWnd(hWnd), m_updater(currentVersion)\r\n\t\t{\r\n\t\t\tm_updater.setUserAgent(userAgent);\r\n\t\t}\r\n\r\n\t\t~CUpdateHandler()\r\n\t\t{\r\n\t\t}\r\n\r\n\t\tvoid startTask()\r\n\t\t{\r\n\t\t\tm_task = std::async(std::launch::async, &Updater::checkUpdate, &m_updater);\r\n\t\t}\r\n\r\n\t\tvirtual BOOL OnIdle()\r\n\t\t{\r\n\t\t\tstd::future_status status = m_task.wait_for(std::chrono::seconds(0));\r\n\t\t\tif (status == std::future_status::ready)\r\n\t\t\t{\r\n\t\t\t\t::PostMessage(m_hWnd, WM_CHKUPDATE, 1, reinterpret_cast<LPARAM>(this));\r\n\t\t\t\treturn FALSE;\r\n\t\t\t}\r\n\r\n\t\t\treturn TRUE;\r\n\t\t}\r\n\r\n\t\tbool hasNewVersion()\r\n\t\t{\r\n\t\t\treturn m_task.get();\r\n\t\t}\r\n\r\n\t\tCString getNewVersion() const\r\n\t\t{\r\n\t\t\tCW2T pszT(CA2W(m_updater.getNewVersion().c_str(), CP_UTF8));\r\n\t\t\treturn CString(pszT);\r\n\t\t}\r\n\r\n\t\tCString getUpdateUrl() const\r\n\t\t{\r\n\t\t\tCW2T pszT(CA2W(m_updater.getUpdateUrl().c_str(), CP_UTF8));\r\n\t\t\treturn CString(pszT);\r\n\t\t}\r\n\r\n\t};\r\n\r\npublic:\r\n\tenum { IDD = IDD_WECHATEXPORTER_FORM };\r\n\r\n\tstatic const UINT WM_START = ExportNotifierImpl::WM_START;\r\n\tstatic const UINT WM_COMPLETE = ExportNotifierImpl::WM_COMPLETE;\r\n\tstatic const UINT WM_PROGRESS = ExportNotifierImpl::WM_PROGRESS;\r\n\tstatic const UINT WM_USR_SESS_START = ExportNotifierImpl::WM_USR_SESS_START;\r\n\tstatic const UINT WM_USR_SESS_COMPLETE = ExportNotifierImpl::WM_USR_SESS_COMPLETE;\r\n\tstatic const UINT WM_SESSION_START = ExportNotifierImpl::WM_SESSION_START;\r\n\tstatic const UINT WM_SESSION_COMPLETE = ExportNotifierImpl::WM_SESSION_COMPLETE;\r\n\tstatic const UINT WM_SESSION_PROGRESS = ExportNotifierImpl::WM_SESSION_PROGRESS;\r\n\tstatic const UINT WM_TASKS_START = ExportNotifierImpl::WM_TASKS_START;\r\n\tstatic const UINT WM_TASKS_COMPLETE = ExportNotifierImpl::WM_TASKS_COMPLETE;\r\n\tstatic const UINT WM_TASKS_PROGRESS = ExportNotifierImpl::WM_TASKS_PROGRESS;\r\n\tstatic const UINT WM_MSG_START = ExportNotifierImpl::WM_EN_END;\r\n\tstatic const UINT WM_UPD_VIEWSTATE = WM_MSG_START + 1;\r\n\tstatic const UINT WM_LOADDATA = WM_MSG_START + 2;\r\n\tstatic const UINT WM_CHKUPDATE = WM_MSG_START + 3;\r\n\r\n\tCView() : CDialogImpl<CView>(), CDialogResize<CView>(), m_viewState(VS_IDLE), m_eventIdProgress(0)\r\n\t{\r\n\t}\r\n\r\n\tLRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tm_sourceTypeIcons.Create(16, 16, ILC_COLOR32 | ILC_MASK, 2, 2);\r\n\t\tint res = m_sourceTypeIcons.AddIcon(AtlLoadIcon(IDI_IPHONE));\r\n\t\tres = m_sourceTypeIcons.AddIcon(AtlLoadIcon(IDI_ITUNES));\r\n\r\n\t\tCComboBoxEx cmb = GetDlgItem(IDC_BACKUP);\r\n\t\tcmb.SetImageList(m_sourceTypeIcons);\r\n\r\n\t\tm_progressTextCtrl = GetDlgItem(IDC_PROGRESS_TEXT);\r\n\t\tm_logListBox.SubclassWindow(GetDlgItem(IDC_LOGS));\r\n\t\tm_progressBarCtrl.SubclassWindow(GetDlgItem(IDC_PROGRESS));\r\n\t\tm_btnExpITunes.SubclassWindow(GetDlgItem(IDC_EXP_ITNS));\r\n\t\t\r\n\r\n\t\t// m_cbmBoxBackups.SetEditColors(CLR_INVALID, ::GetSysColor(COLOR_WINDOWTEXT));\r\n\t\t// m_cbmBoxUsers.SetEditColors(CLR_INVALID, ::GetSysColor(COLOR_WINDOWTEXT));\r\n\t\t\r\n\t\tCString toolTip;\r\n\t\ttoolTip.LoadString(IDS_EXP_ITNS);\r\n\t\tm_btnExpITunes.SetToolTipText(toolTip);\r\n\r\n\t\tm_logger = NULL;\r\n\t\tm_pdfConverter = NULL;\r\n\t\tm_notifier = NULL;\r\n\t\tm_exporter = NULL;\r\n\r\n\t\tm_itemClicked = -2;\r\n\r\n\t\t// Init the CDialogResize code\r\n\t\tDlgResize_Init();\r\n\r\n\t\tInitializeSessionList();\r\n\t\tInitializeSessionProgressList();\r\n\r\n\t\tm_notifier = new ExportNotifierImpl(m_hWnd);\r\n\t\tm_logger = new LoggerImpl(GetDlgItem(IDC_LOGS));\r\n\r\n\t\tCString backupDir = AppConfiguration::GetDefaultBackupDir(FALSE);\r\n\t\tCString text;\r\n\t\ttext.Format(IDS_STATIC_BACKUP, (LPCTSTR)backupDir);\r\n\t\tCStatic label = GetDlgItem(IDC_STATIC_BACKUP);\r\n\t\tlabel.SetWindowText(text);\r\n\r\n\t\tSetDlgItemText(IDC_OUTPUT, AppConfiguration::GetLastOrDefaultOutputDir());\r\n\r\n\t\tstd::vector<WechatSource *> wechatSources;\r\n\t\tstd::vector<DeviceInfo> devices;\r\n\t\tIDeviceBackup::queryDevices(devices);\r\n\t\tfor (std::vector<DeviceInfo>::const_iterator it = devices.cbegin(); it != devices.cend(); ++it)\r\n\t\t{\r\n\t\t\tWechatSource* source = new WechatSource(*it);\r\n\t\t\twechatSources.push_back(source);\r\n\t\t}\r\n\r\n\t\tbackupDir = AppConfiguration::GetDefaultBackupDir();\r\n#ifndef NDEBUG\r\n\t\tCString lastBackupDir = AppConfiguration::GetLastBackupDir();\r\n#endif\r\n\t\tstd::vector<BackupItem> backupItems;\r\n\t\tif (!backupDir.IsEmpty())\r\n\t\t{\r\n\t\t\tCW2A backupDirU8(CT2W(backupDir), CP_UTF8);\r\n#ifndef USING_DECODED_ITUNESBACKUP\r\n\t\t\tstd::unique_ptr<ManifestParser> parser(new ManifestParser((LPCSTR)backupDirU8, false));\r\n#else\r\n\t\t\tstd::unique_ptr<ManifestParser> parser(new DecodedManifestParser((LPCSTR)backupDirU8, false));\r\n#endif\r\n\t\t\t// ManifestParser parser((LPCSTR)backupDirU8, false);\r\n\t\t\tparser->parse(backupItems);\r\n\t\t}\r\n#ifndef NDEBUG\r\n\t\tif (!lastBackupDir.IsEmpty() && lastBackupDir != backupDir)\r\n\t\t{\r\n\t\t\tCW2A backupDirU8(CT2W(lastBackupDir), CP_UTF8);\r\n\t\t\t// ManifestParser parser((LPCSTR)backupDirU8, false);\r\n#ifndef USING_DECODED_ITUNESBACKUP\r\n\t\t\tstd::unique_ptr<ManifestParser> parser(new ManifestParser((LPCSTR)backupDirU8, false));\r\n#else\r\n\t\t\tstd::unique_ptr<ManifestParser> parser(new DecodedManifestParser((LPCSTR)backupDirU8, false));\r\n#endif\r\n\t\t\tparser->parse(backupItems);\r\n\t\t}\r\n#endif\r\n\t\tfor (std::vector<BackupItem>::const_iterator it = backupItems.cbegin(); it != backupItems.cend(); ++it)\r\n\t\t{\r\n\t\t\tWechatSource* source = new WechatSource(*it);\r\n\t\t\twechatSources.push_back(source);\r\n\t\t}\r\n\r\n\t\tif (!wechatSources.empty())\r\n\t\t{\r\n\t\t\tUpdateBackups(wechatSources);\r\n\t\t}\r\n#if defined(NDEBUG) && !defined(DBG_PERF) \r\n\t\tif (!AppConfiguration::GetCheckingUpdateDisabled() && (getUnixTimeStamp() - AppConfiguration::GetLastCheckUpdateTime()) > 86400u)\r\n\t\t{\r\n\t\t\t::PostMessage(m_hWnd, WM_CHKUPDATE, 0, 0);\r\n\t\t}\r\n#endif\r\n\r\n\t\treturn TRUE;\r\n\t}\r\n\r\n\tvoid OnFinalMessage(HWND hWnd)\r\n\t{\r\n\t\tif (NULL != m_exporter)\r\n\t\t{\r\n\t\t\tm_exporter->cancel();\r\n\t\t\tm_exporter->waitForComplition();\r\n\t\t\tSAFE_DELETE(m_exporter);\r\n\t\t\tm_exporter = NULL;\r\n\t\t}\r\n\t\tSAFE_DELETE(m_notifier);\r\n\t\tSAFE_DELETE(m_pdfConverter);\r\n\t\tSAFE_DELETE(m_logger);\r\n\r\n\t\t// override to do something, if needed\r\n\t}\r\n\r\n\tBOOL PreTranslateMessage(MSG* pMsg)\r\n\t{\r\n\t\treturn CWindow::IsDialogMessage(pMsg);\r\n\t}\r\n\r\n\tBEGIN_MSG_MAP(CView)\r\n\t\tMESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)\r\n\t\tCHAIN_MSG_MAP(CDialogResize<CView>)\r\n\t\tMESSAGE_HANDLER(WM_TIMER, OnTimer)\r\n\t\tCOMMAND_HANDLER(IDC_BACKUP, CBN_SELCHANGE, OnBackupSelChange)\r\n\t\tCOMMAND_HANDLER(IDC_CHOOSE_BKP, BN_CLICKED, OnBnClickedChooseBkp)\r\n\t\tCOMMAND_HANDLER(IDC_CHOOSE_OUTPUT, BN_CLICKED, OnBnClickedChooseOutput)\r\n\t\tCOMMAND_HANDLER(IDC_USERS, CBN_SELCHANGE, OnUserSelChange)\r\n\t\tCOMMAND_HANDLER(IDC_SHOW_LOGS, BN_CLICKED, OnBnClickedShowLogs)\r\n\t\tCOMMAND_HANDLER(IDC_EXP_ITNS, BN_CLICKED, OnBnClickedExpITunes)\r\n\t\tCOMMAND_HANDLER(IDC_EXPORT, BN_CLICKED, OnBnClickedExport)\r\n\t\tCOMMAND_HANDLER(IDC_CANCEL, BN_CLICKED, OnBnClickedCancel)\r\n\t\tCOMMAND_HANDLER(IDC_CLOSE, BN_CLICKED, OnBnClickedClose)\r\n\t\tMESSAGE_HANDLER(WM_START, OnStart)\r\n\t\tMESSAGE_HANDLER(WM_COMPLETE, OnComplete)\r\n\t\tMESSAGE_HANDLER(WM_PROGRESS, OnProgress)\r\n\t\tMESSAGE_HANDLER(WM_USR_SESS_START, OnUserSessionStart)\r\n\t\tMESSAGE_HANDLER(WM_USR_SESS_COMPLETE, OnUserSessionComplete)\r\n\t\tMESSAGE_HANDLER(WM_SESSION_START, OnSessionStart)\r\n\t\tMESSAGE_HANDLER(WM_SESSION_COMPLETE, OnSessionComplete)\r\n\t\tMESSAGE_HANDLER(WM_SESSION_PROGRESS, OnSessionProgress)\r\n\t\tMESSAGE_HANDLER(WM_TASKS_START, OnTasksStart)\r\n\t\tMESSAGE_HANDLER(WM_TASKS_COMPLETE, OnTasksComplete)\r\n\t\tMESSAGE_HANDLER(WM_TASKS_PROGRESS, OnTasksProgress)\r\n\t\tMESSAGE_HANDLER(WM_UPD_VIEWSTATE, OnUpdateViewState)\r\n\t\tMESSAGE_HANDLER(WM_LOADDATA, OnLoadData)\r\n\t\tMESSAGE_HANDLER(WM_CHKUPDATE, OnCheckUpdate)\r\n\t\tNOTIFY_HANDLER(IDC_SESSIONS, LVN_ITEMCHANGED, OnListItemChanged)\r\n\t\tNOTIFY_CODE_HANDLER(HDN_ITEMSTATEICONCLICK, OnHeaderItemStateIconClick)\r\n\t\tNOTIFY_HANDLER(IDC_SESSIONS, NM_CLICK, OnListClick)\r\n\t\tREFLECT_NOTIFICATIONS()\r\n\tEND_MSG_MAP()\r\n\r\n\tBEGIN_DLGRESIZE_MAP(CView)\r\n\t\tDLGRESIZE_CONTROL(IDC_CHOOSE_BKP, DLSZ_MOVE_X)\r\n\t\tDLGRESIZE_CONTROL(IDC_BACKUP, DLSZ_SIZE_X)\r\n\t\tDLGRESIZE_CONTROL(IDC_CHOOSE_OUTPUT, DLSZ_MOVE_X)\r\n\t\tDLGRESIZE_CONTROL(IDC_OUTPUT, DLSZ_SIZE_X)\r\n\t\tDLGRESIZE_CONTROL(IDC_GRP_USR_CHAT, DLSZ_SIZE_X | DLSZ_SIZE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_VERSIONS, DLSZ_SIZE_X)\r\n\t\tDLGRESIZE_CONTROL(IDC_PROGRESS, DLSZ_SIZE_X)\r\n\t\t// DLGRESIZE_CONTROL(IDC_PROGRESS_TEXT, DLSZ_MOVE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_SESSIONS, DLSZ_SIZE_X | DLSZ_SIZE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_SESS_PROGRESS, DLSZ_SIZE_X | DLSZ_SIZE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_GRP_LOGS, DLSZ_SIZE_X | DLSZ_SIZE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_LOGS, DLSZ_SIZE_X | DLSZ_SIZE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_SHOW_LOGS, DLSZ_MOVE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_CANCEL, DLSZ_MOVE_X | DLSZ_MOVE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_EXP_ITNS, DLSZ_MOVE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_CLOSE, DLSZ_MOVE_X | DLSZ_MOVE_Y)\r\n\t\tDLGRESIZE_CONTROL(IDC_EXPORT, DLSZ_MOVE_X | DLSZ_MOVE_Y)\r\n\tEND_DLGRESIZE_MAP()\r\n\r\n\r\n// Handler prototypes (uncomment arguments if needed):\r\n//\tLRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n//\tLRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\r\n//\tLRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)\r\n\t\r\n\tLRESULT OnTimer(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tUINT_PTR nIDEvent = static_cast<UINT_PTR>(wParam);\r\n\t\tif (nIDEvent == m_eventIdProgress)\r\n\t\t{\r\n\t\t\tUpdateProgressBar(TRUE);\r\n\t\t}\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnUpdateViewState(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tm_viewState = static_cast<VIEW_STATE>(wParam);\r\n\t\tif (lParam != 0)\r\n\t\t{\r\n\t\t\tUpdateUI();\r\n\t\t}\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnLoadData(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tCLoadingHandler *handler = reinterpret_cast<CLoadingHandler *>(lParam);\r\n\t\tif (NULL != handler)\r\n\t\t{\r\n\t\t\thandler->waitForCompletion();\r\n\t\t\thandler->getUsersAndSessions(m_usersAndSessions);\r\n\r\n\t\t\tLoadUsers(handler->getVersions());\r\n\t\t\tPostMessage(WM_UPD_VIEWSTATE, VS_IDLE, 1);\r\n\t\t\t\r\n\t\t\tdelete handler;\r\n\t\t}\r\n\r\n\t\treturn 0;\r\n\t}\r\n\t\r\n\tLRESULT OnCheckUpdate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tif (0 == wParam)\r\n\t\t{\r\n\t\t\tVersionDetector vd;\r\n\t\t\tCString newVersion = vd.GetProductVersion();\r\n\t\t\tCW2A pszNV(CT2W((LPCTSTR)newVersion), CP_UTF8);\r\n\r\n\t\t\tchar userAgent[512] = { 0 };\r\n\t\t\tDWORD size = sizeof(userAgent);\r\n\t\t\tObtainUserAgentString(0, userAgent, &size);\r\n\t\t\tCW2A pszUA(CA2W(userAgent), CP_UTF8);\r\n\r\n\t\t\tCUpdateHandler *handler = new CUpdateHandler(m_hWnd, (LPCSTR)pszNV, (LPCSTR)pszUA);\r\n\t\t\t_Module.GetMessageLoop()->AddIdleHandler(handler);\r\n\t\t\thandler->startTask();\r\n\t\t}\r\n\t\telse if (1 == wParam)\r\n\t\t{\r\n\t\t\tCUpdateHandler *handler = reinterpret_cast<CUpdateHandler *>(lParam);\r\n\t\t\tif (NULL != handler)\r\n\t\t\t{\r\n\t\t\t\tif (_Module.GetMessageLoop()->RemoveIdleHandler(handler))\r\n\t\t\t\t{\r\n\t\t\t\t\tAppConfiguration::SetLastCheckUpdateTime();\r\n\r\n\t\t\t\t\tbool hasNewVersion = handler->hasNewVersion();\r\n\t\t\t\t\tCString newVersion = handler->getNewVersion();\r\n\t\t\t\t\tCString updateUrl = handler->getUpdateUrl();\r\n\t\t\t\t\tdelete handler;\r\n\r\n\t\t\t\t\tif (hasNewVersion)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tCString text;\r\n\t\t\t\t\t\ttext.Format(IDS_NEW_VERSION, (LPCTSTR)newVersion);\r\n\t\t\t\t\t\tCString caption;\r\n\t\t\t\t\t\tcaption.LoadStringW(IDR_MAINFRAME);\r\n\t\t\t\t\t\tUINT ret = MessageBoxTimeout(NULL, text, caption, MB_OKCANCEL, 0, 6000);\r\n\t\t\t\t\t\tif (ret == IDOK)\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tCT2A url(updateUrl);\r\n\t\t\t\t\t\t\t::ShellExecute(NULL, TEXT(\"open\"), (LPCTSTR)updateUrl, NULL, NULL, SW_SHOWNORMAL);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\t// If the handler is not in array, throw it away\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnBnClickedChooseBkp(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tCString text;\r\n\t\ttext.LoadString(IDS_SEL_BACKUP_DIR);\r\n\r\n\t\tCFolderDialog folder(NULL, text, BIF_RETURNONLYFSDIRS | BIF_USENEWUI | BIF_NONEWFOLDERBUTTON);\r\n\t\tif (IDOK == folder.DoModal())\r\n\t\t{\r\n\t\t\tCW2A backupDir(CT2W(folder.m_szFolderPath), CP_UTF8);\r\n\r\n\t\t\tstd::vector<WechatSource *> wechatSources;\r\n#ifndef USING_DECODED_ITUNESBACKUP\r\n\t\t\tstd::unique_ptr<ManifestParser> parser(new ManifestParser((LPCSTR)backupDir, false));\r\n#else\r\n\t\t\tstd::unique_ptr<ManifestParser> parser(new DecodedManifestParser((LPCSTR)backupDir, false));\r\n#endif\r\n\t\t\tstd::vector<BackupItem> backupItems;\r\n\t\t\tif (parser->parse(backupItems) && !backupItems.empty())\r\n\t\t\t{\r\n\t\t\t\tfor (std::vector<BackupItem>::const_iterator it = backupItems.cbegin(); it != backupItems.cend(); ++it)\r\n\t\t\t\t{\r\n\t\t\t\t\tWechatSource* source = new WechatSource(*it);\r\n\t\t\t\t\twechatSources.push_back(source);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tUpdateBackups(wechatSources);\r\n#ifndef NDEBUG\r\n\t\t\t\tAppConfiguration::SetLastBackupDir(folder.m_szFolderPath);\r\n#endif\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tm_logger->debug(parser->getLastError());\r\n\t\t\t\tMsgBox(m_hWnd, IDS_FAILED_TO_LOAD_BKP);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnBackupSelChange(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tCListBox lstboxLogs = GetDlgItem(IDC_LOGS);\r\n\t\tlstboxLogs.ResetContent();\r\n\r\n\t\tCComboBox cbmBox = GetDlgItem(IDC_USERS);\r\n\t\tcbmBox.ResetContent();\r\n\r\n\t\tCListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS);\r\n\t\t// listViewCtrl.SetRedraw(FALSE);\r\n\t\tlistViewCtrl.DeleteAllItems();\r\n\t\t// listViewCtrl.SetRedraw(TRUE);\r\n\r\n\t\tm_usersAndSessions.clear();\r\n\t\tcbmBox = GetDlgItem(IDC_BACKUP);\r\n\t\tif (cbmBox.GetCurSel() == -1)\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tconst WechatSource* wechatSource = m_wechatSources[cbmBox.GetCurSel()];\r\n\t\tif (wechatSource->isDevice())\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\t\tconst BackupItem& backupItem = wechatSource->getBackupItem();\r\n\t\tif (backupItem.isEncrypted())\r\n\t\t{\r\n\t\t\tMsgBox(m_hWnd, IDS_ENC_BKP_NOT_SUPPORTED);\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tTCHAR buffer[MAX_PATH] = { 0 };\r\n\t\tDWORD dwRet = GetCurrentDirectory(MAX_PATH, buffer);\r\n\t\tif (dwRet == 0)\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\t\t\r\n#ifndef NDEBUG\r\n\t\tm_logger->write(\"Start loading users and sessions.\");\r\n#endif\r\n\t\tSendMessage(WM_NEXTDLGCTL, 0, 0);\r\n\t\tCW2A resDir(CT2W(buffer), CP_UTF8);\r\n\r\n\t\tPostMessage(WM_UPD_VIEWSTATE, VS_LOADING, 1);\r\n\t\t\r\n\t\tstd::string backup = backupItem.getPath();\r\n\t\tCLoadingHandler *handler = new CLoadingHandler(m_hWnd, (LPCSTR)resDir, backup, m_logger);\r\n\t\t// _Module.GetMessageLoop()->AddIdleHandler(handler);\r\n\t\thandler->startTask();\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnUserSelChange(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tCListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS);\r\n\r\n\t\tCComboBox cbmBox = GetDlgItem(IDC_USERS);\r\n\t\tint curSel = cbmBox.GetCurSel();\r\n\t\tif (curSel == -1)\r\n\t\t{\r\n\t\t\tlistViewCtrl.DeleteAllItems();\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n#ifndef NDEBUG\r\n\t\tm_logger->debug(\"Display Sessions Start\");\r\n#endif\r\n\r\n\t\tBOOL allUsers = (curSel == 0);\r\n\t\tstd::string usrName;\r\n\t\tif (curSel > 0)\r\n\t\t{\r\n\t\t\tstd::vector<std::pair<Friend, std::vector<Session>>>::const_iterator it = m_usersAndSessions.cbegin() + curSel - 1;\r\n\t\t\tusrName = it->first.getUsrName();\r\n\t\t}\r\n\t\t\r\n\t\tlistViewCtrl.SetRedraw(FALSE);\r\n\t\tif (listViewCtrl.GetItemCount() > 0)\r\n\t\t{\r\n\t\t\tlistViewCtrl.DeleteAllItems();\r\n\t\t}\r\n\t\tLoadSessions(allUsers, usrName);\r\n\t\tlistViewCtrl.SetRedraw(TRUE);\r\n#ifndef NDEBUG\r\n\t\tm_logger->debug(\"Display Sessions End\");\r\n#endif\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnBnClickedChooseOutput(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tCString text;\r\n\t\ttext.LoadString(IDS_SEL_OUTPUT_DIR);\r\n\t\tCFolderDialog folder(NULL, text, BIF_RETURNONLYFSDIRS | BIF_USENEWUI);\r\n\t\t\r\n\t\tTCHAR outputDir[MAX_PATH] = { 0 };\r\n\t\tGetDlgItemText(IDC_OUTPUT, outputDir, MAX_PATH);\r\n\t\tif (_tcscmp(outputDir, TEXT(\"\")) == 0)\r\n\t\t{\r\n\t\t\tHRESULT result = SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, outputDir);\r\n\t\t}\r\n\t\tif (_tcscmp(outputDir, TEXT(\"\")) != 0)\r\n\t\t{\r\n\t\t\tfolder.SetInitialFolder(outputDir);\r\n\t\t}\r\n\t\t\r\n\t\tif (IDOK == folder.DoModal())\r\n\t\t{\r\n\t\t\tAppConfiguration::SetLastOutputDir(folder.m_szFolderPath);\r\n\t\t\tSetDlgItemText(IDC_OUTPUT, folder.m_szFolderPath);\r\n\t\t}\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnBnClickedCancel(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tif (MsgBox(m_hWnd, IDS_CANCEL_PROMPT, MB_YESNO) == IDNO)\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tif (NULL == m_exporter)\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tm_exporter->cancel();\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnBnClickedShowLogs(WORD /*wNotifyCode*/, WORD /*wID*/, HWND hWndCtl, BOOL& /*bHandled*/)\r\n\t{\t\r\n\t\tCButton btn = hWndCtl;\r\n\t\tBOOL showLogs = btn.GetCheck() == BST_CHECKED;\r\n\t\tCString text;\r\n\t\ttext.LoadString(showLogs ? IDS_HIDE_LOGS : IDS_SHOW_LOGS);\r\n\t\tbtn.SetWindowText(text);\r\n\r\n\t\tUpdateUI();\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnBnClickedClose(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\r\n\t{\r\n\t\t::PostMessage(GetParent(), WM_CLOSE, 0, 0);\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnHeaderItemStateIconClick(int idCtrl, LPNMHDR pnmh, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tCHeaderCtrl header = m_sessionsListCtrl.GetHeader();\r\n\t\tif (pnmh->hwndFrom == header.m_hWnd)\r\n\t\t{\r\n\t\t\tLPNMHEADER pnmHeader = (LPNMHEADER)pnmh;\r\n\t\t\tif (pnmHeader->pitem->mask & HDI_FORMAT && pnmHeader->pitem->fmt & HDF_CHECKBOX)\r\n\t\t\t{\r\n\t\t\t\tCheckAllItems(m_sessionsListCtrl, !(pnmHeader->pitem->fmt & HDF_CHECKED));\r\n\t\t\t\tSyncHeaderCheckbox();\r\n\t\t\t\treturn 1;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnListItemChanged(int idCtrl, LPNMHDR pnmh, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tLPNMLISTVIEW pnmlv = (LPNMLISTVIEW)pnmh;\r\n\r\n\t\tif (pnmlv->uChanged & LVIF_STATE)\r\n\t\t{\r\n\t\t\tif (pnmlv->iItem == m_itemClicked)\r\n\t\t\t{\r\n\t\t\t\tSyncHeaderCheckbox();\r\n\t\t\t\tm_itemClicked = -2;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t}\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnListClick(int /*idCtrl*/, LPNMHDR pnmh, BOOL& bHandled)\r\n\t{\r\n\t\tLPNMITEMACTIVATE pnmia = (LPNMITEMACTIVATE)pnmh;\r\n\t\tDWORD pos = GetMessagePos();\r\n\t\tPOINT pt = { LOWORD(pos), HIWORD(pos) };\r\n\t\tCListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS);\r\n\r\n\t\tlistViewCtrl.ScreenToClient(&pt);\r\n\t\tUINT flags = 0;\r\n\t\tint nItem = listViewCtrl.HitTest(pt, &flags);\r\n\t\tif (flags == LVHT_ONITEMSTATEICON)\r\n\t\t{\r\n\t\t\tm_itemClicked = nItem;\r\n\t\t\t// listViewCtrl.SetCheckState(nItem, !listViewCtrl.GetCheckState(nItem));\r\n\t\t\t// SetHeaderCheckbox();\r\n\t\t\t// bHandled = TRUE;\r\n\t\t}\r\n\t\t\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnBnClickedExpITunes(WORD /*wNotifyCode*/, WORD /*wID*/, HWND hWndCtl, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tCComboBox cbmBox = GetDlgItem(IDC_BACKUP);\r\n\t\tif (cbmBox.GetCurSel() == -1)\r\n\t\t{\r\n\t\t\tMsgBox(m_hWnd, IDS_SEL_BACKUP_DIR);\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tconst WechatSource* src = m_wechatSources[cbmBox.GetCurSel()];\r\n\t\tconst BackupItem& backupItem = src->getBackupItem();\r\n\t\tif (backupItem.isEncrypted())\r\n\t\t{\r\n\t\t\tMsgBox(m_hWnd, IDS_ENC_BKP_NOT_SUPPORTED);\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tstd::string backup = backupItem.getPath();\r\n\r\n\t\tTCHAR outputDir[MAX_PATH] = { 0 };\r\n\t\tGetDlgItemText(IDC_OUTPUT, outputDir, MAX_PATH);\r\n\t\tif (!::PathFileExists(outputDir))\r\n\t\t{\r\n\t\t\tMsgBox(m_hWnd, IDS_INVALID_OUTPUT_DIR);\r\n\t\t\treturn 0;\r\n\t\t}\r\n\t\t/*\r\n\t\telse\r\n\t\t{\r\n\t\t\tDWORD dwAttributes = GetFileAttributes(outputDir);\r\n\t\t\tif ((dwAttributes & FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY)\r\n\t\t\t{\r\n\t\t\t\tMsgBox(m_hWnd, IDS_READONLY_OUTPUT_DIR);\r\n\t\t\t\treturn 0;\r\n\t\t\t}\r\n\t\t}\r\n\t\t*/\r\n\t\tCW2A output(CT2W(outputDir), CP_UTF8);\r\n\r\n\t\tCWaitCursor waitCursor;\r\n\r\n\t\tSendMessage(WM_UPD_VIEWSTATE, VS_EXPORTING, 1);\r\n\r\n\t\t::EnableWindow(hWndCtl, FALSE);\r\n\r\n\t\tITunesDb iTunesDb(backup, \"\");\r\n\t\tstd::vector<std::string> domains;\r\n\t\tdomains.push_back(\"AppDomain-com.tencent.xin\");\r\n\t\tdomains.push_back(\"AppDomainGroup-group.com.tencent.xin\");\r\n\r\n\t\tDWORD idx = 0;\r\n\t\tstd::function<bool(const ITunesDb*, const ITunesFile*)> func = std::bind(&CView::onCopyFile, this, std::placeholders::_1, std::placeholders::_2, idx);\r\n\t\tbool ret = iTunesDb.copy((LPCSTR)output, backupItem.getBackupId(), domains, func);\r\n\t\t::EnableWindow(hWndCtl, TRUE);\r\n\r\n\t\tSendMessage(WM_UPD_VIEWSTATE, VS_IDLE, 1);\r\n\r\n\t\tif (ret)\r\n\t\t{\r\n\t\t\tPathAppend(outputDir, TEXT(\"Backup\"));\r\n\t\t\tOpenFolder(outputDir);\r\n\t\t}\r\n\r\n\t\tif (!ret)\r\n\t\t{\r\n#ifndef NDEBUG\r\n\t\t\tstd::string errMsg = iTunesDb.getLastError();\r\n\t\t\tCW2T lpszErrMsg(CA2W(errMsg.c_str(), CP_UTF8));\r\n\t\t\tMsgBox(m_hWnd, (LPCTSTR)lpszErrMsg);\r\n#endif\r\n\t\t}\r\n\t\treturn 0;\r\n\r\n\t}\r\n\r\n\tbool onCopyFile(const ITunesDb* iTunesDb, const ITunesFile* file, DWORD& idx)\r\n\t{\r\n\t\t++idx;\r\n\t\tif (idx % 32 != 0)\r\n\t\t{\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tMSG msg;\r\n\t\twhile (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))\r\n\t\t{\r\n\t\t\tif (msg.message == WM_QUIT)\r\n\t\t\t\treturn false;\r\n\t\t\tTranslateMessage(&msg);\r\n\t\t\tDispatchMessage(&msg);\r\n\t\t}\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\tvoid ExportITunesBackup()\r\n\t{\r\n\t\tPostMessage(WM_COMMAND, (BN_CLICKED << 16) | IDC_EXP_ITNS, (LPARAM)::GetDlgItem(m_hWnd, IDC_EXP_ITNS));\r\n\t}\r\n\r\n\tLRESULT OnBnClickedExport(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)\r\n\t{\r\n\t\tif (NULL != m_exporter)\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tCComboBox cbmBox = GetDlgItem(IDC_BACKUP);\r\n\t\tif (cbmBox.GetCurSel() == -1)\r\n\t\t{\r\n\t\t\tMsgBox(m_hWnd, IDS_SEL_BACKUP_DIR);\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tconst WechatSource* src = m_wechatSources[cbmBox.GetCurSel()];\r\n\t\tconst BackupItem& backupItem = src->getBackupItem();\r\n\t\tif (backupItem.isEncrypted())\r\n\t\t{\r\n\t\t\tMsgBox(m_hWnd, IDS_ENC_BKP_NOT_SUPPORTED);\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tstd::string backup = backupItem.getPath();\r\n\r\n\t\tTCHAR outputDir[MAX_PATH] = { 0 };\r\n\t\tGetDlgItemText(IDC_OUTPUT, outputDir, MAX_PATH);\r\n\t\tif (!::PathFileExists(outputDir))\r\n\t\t{\r\n\t\t\tMsgBox(m_hWnd, IDS_INVALID_OUTPUT_DIR);\r\n\t\t\treturn 0;\r\n\t\t}\r\n\t\tCW2A output(CT2W(outputDir), CP_UTF8);\r\n\r\n\t\tTCHAR curDir[MAX_PATH] = { 0 };\r\n\t\tDWORD dwRet = GetModuleFileName(NULL, curDir, MAX_PATH);\r\n\t\tif (dwRet == 0)\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\t\tif (!PathRemoveFileSpec(curDir))\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tif (AppConfiguration::GetIncrementalExporting())\r\n\t\t{\r\n\t\t\tuint64_t options = 0;\r\n\t\t\tstd::string exportTime;\r\n\t\t\tstd::string version;\r\n\t\t\tif (Exporter::hasPreviousExporting((LPCSTR)output, options, exportTime, version))\r\n\t\t\t{\r\n\t\t\t\tif (version.empty() && m_usersAndSessions.size() > 1)\r\n\t\t\t\t{\r\n\t\t\t\t\t// First version of implementation has design issue\r\n\t\t\t\t\tif (MsgBox(m_hWnd, IDS_INVLD_INCEXP_FOR_MULTI_USERS, MB_OK) == IDOK)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\treturn 0;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tCW2T exportTimeT(CA2W(exportTime.c_str(), CP_UTF8));\r\n\t\t\t\tCString text;\r\n\t\t\t\ttext.Format(IDS_PREV_EXP_FOUND, (LPCTSTR)exportTimeT);\r\n\t\t\t\tMessageBoxTimeout(m_hWnd, text, TEXT(\"\"), MB_OK, 0, 4000);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tCString text;\r\n\t\t\t\ttext.LoadString(IDS_NO_PREV_EXP);\r\n\t\t\t\tMessageBoxTimeout(m_hWnd, text, TEXT(\"\"), MB_OK, 0, 4000);\r\n\t\t\t}\r\n\t\t}\r\n\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n\t\tm_logger->setLogPath(outputDir);\r\n#endif\r\n\t\t// CButton btn = GetDlgItem(IDC_DESC_ORDER);\r\n\t\tExportOption options = AppConfiguration::BuildOptions();\r\n\t\t// bool descOrder = AppConfiguration::GetDescOrder();\r\n\t\t// bool saveFilesInSessionFolder = AppConfiguration::GetSavingInSession();\r\n\t\t// bool usingRemoteEmoji = AppConfiguration::GetUsingRemoteEmoji();\r\n\t\t// bool asyncLoading = AppConfiguration::GetAsyncLoading();\r\n\t\t// bool loadingDataOnScroll = AppConfiguration::GetLoadingDataOnScroll();\r\n\t\t// bool supportingFilter = AppConfiguration::GetSupportingFilter();\r\n\t\t// bool incrementalExp = AppConfiguration::GetIncrementalExporting();\r\n\t\tUINT outputFormat = AppConfiguration::GetOutputFormat();\r\n\r\n\t\tif (options.isPdfMode())\r\n\t\t{\r\n\t\t\toptions.setSyncLoading();\r\n\t\t\t// options.setLoadingDataOnScroll(false);\r\n\t\t\toptions.supportsFilter(false);\r\n\t\t}\r\n\r\n\t\tCListBox lstboxLogs = GetDlgItem(IDC_LOGS);\r\n\t\tlstboxLogs.ResetContent();\r\n\r\n\t\tCW2A resDir(CT2W(curDir), CP_UTF8);\r\n\r\n\t\tstd::map<std::string, std::map<std::string, void *>> usersAndSessions;\r\n\t\tint numberOfSessions = 0;\r\n\t\tint numberOfRecords = 0;\r\n\t\tGetCheckedSessionsAndCopyItems(usersAndSessions, numberOfSessions, numberOfRecords);\r\n\t\t\r\n\t\tCProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS);\r\n\t\tprogressCtrl.SetWindowText(TEXT(\"\"));\r\n\t\tprogressCtrl.ModifyStyle(PBS_MARQUEE, 0);\r\n\t\tprogressCtrl.SetRange32(1, ((numberOfRecords > 0) ? numberOfRecords : 1));\r\n\t\tprogressCtrl.SetStep(1);\r\n\t\tprogressCtrl.SetPos(0);\r\n\r\n\t\tUpdateProgressBarText(IDS_EXPORTING_MSGS, 0, true);\r\n\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n\t\tm_logger->debug(\"Record Count:\" + std::to_string(numberOfRecords));\r\n#endif\r\n\t\tif (NULL != m_pdfConverter)\r\n\t\t{\r\n\t\t\tdelete m_pdfConverter;\r\n\t\t\tm_pdfConverter = NULL;\r\n\t\t}\r\n\t\tif (outputFormat == AppConfiguration::OUTPUT_FORMAT_PDF)\r\n\t\t{\t\r\n\t\t\tm_pdfConverter = new PdfConverterImpl(outputDir);\r\n\t\t}\r\n\r\n\t\tm_exporter = new Exporter((LPCSTR)resDir, backup, (LPCSTR)output, m_logger, m_pdfConverter);\r\n\t\tif (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED))\r\n\t\t{\r\n\t\t\tm_exporter->setLanguageCode(\"zh-Hans\");\r\n\t\t}\r\n\t\tm_exporter->setNotifier(m_notifier);\r\n\t\t\r\n\t\t// m_exporter->setOrder(!descOrder);\r\n\t\t// m_exporter->useRemoteEmoji(usingRemoteEmoji);\r\n\t\t// m_exporter->setSyncLoading(!asyncLoading);\r\n\t\t// m_exporter->setLoadingDataOnScroll(loadingDataOnScroll);\r\n\t\t// m_exporter->supportsFilter(supportingFilter);\r\n\t\t// m_exporter->setIncrementalExporting(incrementalExp);\r\n\t\t// m_exporter->outputDebugLogs(AppConfiguration::OutputDebugLogs());\r\n\t\t// if (AppConfiguration::IncludeSubscriptions())\r\n\t\t{\r\n\t\t\t// m_exporter->includesSubscription();\r\n\t\t}\r\n\t\t// if (saveFilesInSessionFolder)\r\n\t\t{\r\n\t\t\t// m_exporter->saveFilesInSessionFolder();\r\n\t\t}\r\n\t\tif (options.isTextMode())\r\n\t\t{\r\n\t\t\tm_exporter->setExtName(\"txt\");\r\n\t\t\tm_exporter->setTemplatesName(\"templates_txt\");\r\n\t\t}\r\n\r\n\t\tm_exporter->setOptions(options);\r\n\t\t\r\n\t\tm_exporter->filterUsersAndSessions(usersAndSessions);\r\n\t\tif (m_exporter->run())\r\n\t\t{\r\n\t\t\tPostMessage(WM_UPD_VIEWSTATE, VS_EXPORTING, 1);\r\n\t\t}\r\n\r\n\t\treturn 0;\r\n\t}\r\n\t\r\n\tLRESULT OnStart(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tif (0 == m_eventIdProgress)\r\n\t\t{\r\n\t\t\tm_eventIdProgress = SetTimer(EVENT_ID_PROGRESS, 100, NULL);\r\n\t\t}\r\n\t\t\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnComplete(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tBOOL cancelled = (lParam != 0) ? TRUE : FALSE;\r\n\t\tif (NULL != m_pdfConverter)\r\n\t\t{\r\n\t\t\tif (!cancelled)\r\n\t\t\t{\r\n\t\t\t\tm_pdfConverter->executeCommand();\r\n\t\t\t}\r\n\t\t\tdelete m_pdfConverter;\r\n\t\t\tm_pdfConverter = NULL;\r\n\t\t}\r\n\r\n\t\tif (m_exporter)\r\n\t\t{\r\n\t\t\tm_exporter->waitForComplition();\r\n\t\t\tdelete m_exporter;\r\n\t\t\tm_exporter = NULL;\r\n\t\t}\r\n\r\n\t\tif (0 != m_eventIdProgress)\r\n\t\t{\r\n\t\t\tKillTimer(m_eventIdProgress);\r\n\t\t\tm_eventIdProgress = 0;\r\n\t\t}\r\n\r\n\t\tif (!cancelled && AppConfiguration::GetOpenningFolderAfterExp())\r\n\t\t{\r\n\t\t\tTCHAR outputDir[MAX_PATH] = { 0 };\r\n\t\t\tGetDlgItemText(IDC_OUTPUT, outputDir, MAX_PATH);\r\n\t\t\tOpenFolder( (LPCTSTR)outputDir);\r\n\t\t}\r\n\r\n\t\tPostMessage(WM_UPD_VIEWSTATE, VS_IDLE, 1);\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnProgress(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnUserSessionStart(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tif (m_eventIdProgress != 0)\r\n\t\t{\r\n\t\t\tKillTimer(m_eventIdProgress);\r\n\t\t\tm_eventIdProgress = 0;\r\n\t\t}\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnUserSessionComplete(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tif (0 == m_eventIdProgress)\r\n\t\t{\r\n\t\t\tm_eventIdProgress = SetTimer(EVENT_ID_PROGRESS, 100, NULL);\r\n\t\t}\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnSessionStart(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\t// wParam: nItem\r\n\t\t// lParam: NumberOfMessages\r\n\t\tint nItem = wParam;\r\n\t\tuint32_t numberOfMessages = static_cast<uint32_t>(lParam);\r\n\t\t\r\n\t\tm_progressListCtrl.EnsureVisible(nItem, FALSE);\r\n\t\tm_progressListCtrl.SetProgressBarInfo(nItem, SUBITEM_PROGRESS, numberOfMessages, 0);\r\n\t\t\r\n\t\tCString text;\r\n\t\ttext.Format(IDS_SESSION_PROGRESS, 0u, numberOfMessages);\r\n\t\tm_progressListCtrl.SetItemText(nItem, SUBITEM_PROGRESS, text);\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnSessionComplete(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tm_progressListCtrl.ClearProgressBar();\r\n\r\n\t\tint nItem = wParam;\r\n\t\tCString text;\r\n\t\ttext.LoadString((lParam != 0) ? IDS_SESSION_CANCELLED : IDS_SESSION_DONE);\r\n\t\tm_progressListCtrl.SetItemText(nItem, SUBITEM_PROGRESS, text);\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnSessionProgress(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tint nItem = wParam;\r\n\t\tuint32_t numberOfMessages = static_cast<uint32_t>(lParam);\r\n\r\n\t\tconst Session *session = reinterpret_cast<const Session *>(m_progressListCtrl.GetItemData(nItem));\r\n\t\t\r\n\t\tm_progressListCtrl.SetProgressPos(static_cast<int>(lParam));\r\n\t\tCString text;\r\n\t\ttext.Format(IDS_SESSION_PROGRESS, numberOfMessages, static_cast<uint32_t>(session->getRecordCount()));\r\n\t\tm_progressListCtrl.SetItemText(nItem, SUBITEM_PROGRESS, text);\r\n\r\n\t\tUpdateProgressBar();\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnTasksStart(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tCProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS);\r\n\t\tprogressCtrl.SetRange32(0, 100);\r\n\t\tprogressCtrl.SetPos(0);\r\n\r\n\t\tUpdateProgressBarText(IDS_DOWNLOADING_EMOJI, 0, true);\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnTasksComplete(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tCProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS);\r\n\t\t// progressCtrl.SetRange32(0, 100);\r\n\t\tprogressCtrl.SetPos(100);\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tLRESULT OnTasksProgress(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)\r\n\t{\r\n\t\tUINT totalNumberOfTasks = static_cast<UINT>(wParam - lParam);\r\n\t\tUINT numberOfTasks = static_cast<UINT>(wParam - lParam);\r\n\r\n#if !defined(NDEBUG) || defined(DBG_PERF)\r\n\t\tstd::string timeString = getTimestampString(false, true) + \": \";\r\n\t\tCA2T szTime(timeString.c_str());\r\n\r\n\t\tTCHAR szLog[256] = { 0 };\r\n\r\n\t\tHWND hWndLog = GetDlgItem(IDC_LOGS);\r\n\t\t_stprintf(szLog, TEXT(\"%s: Task Queue Size = %u\"), (LPCTSTR)szTime, numberOfTasks);\r\n\t\t::SendMessage(hWndLog, LB_ADDSTRING, 0, (LPARAM)szLog);\r\n\t\tLRESULT count = ::SendMessage(hWndLog, LB_GETCOUNT, 0, 0L);\r\n\t\t::SendMessage(hWndLog, LB_SETTOPINDEX, count - 1, 0L);\r\n#endif\r\n\r\n\t\tCProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS);\r\n\t\tint percent = (totalNumberOfTasks > 0 && numberOfTasks <= totalNumberOfTasks) ? (numberOfTasks * 100 / totalNumberOfTasks) : 100;\r\n\t\tprogressCtrl.SetPos(percent);\r\n\r\n\t\tUpdateProgressBarText(IDS_DOWNLOADING_EMOJI, lParam, true);\r\n\t\t//UpdateProgressBarText(IDS_EXPORTING_MSGS, percent);\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tBOOL IsViewIdle() const\r\n\t{\r\n\t\treturn m_viewState == VS_IDLE;\r\n\t}\r\n\r\nprotected:\r\n\r\n\tvoid UpdateUI()\r\n\t{\r\n\t\tCButton btn = GetDlgItem(IDC_SHOW_LOGS);\r\n\t\tBOOL showLogs = btn.GetCheck() == BST_CHECKED;\r\n\r\n\t\t::ShowWindow(GetDlgItem(IDC_GRP_LOGS), showLogs ? SW_SHOW : SW_HIDE);\r\n\t\t::ShowWindow(GetDlgItem(IDC_LOGS), showLogs ? SW_SHOW : SW_HIDE);\r\n\r\n\t\t::ShowWindow(GetDlgItem(IDC_GRP_USR_CHAT), showLogs ? SW_HIDE : SW_SHOW);\r\n\t\t::ShowWindow(GetDlgItem(IDC_USERS), showLogs ? SW_HIDE : SW_SHOW);\r\n\t\t::ShowWindow(GetDlgItem(IDC_VERSIONS), showLogs || m_viewState == VS_EXPORTING ? SW_HIDE : SW_SHOW);\r\n\t\t::ShowWindow(GetDlgItem(IDC_SESSIONS), showLogs || m_viewState == VS_EXPORTING ? SW_HIDE : SW_SHOW);\r\n\t\t::ShowWindow(GetDlgItem(IDC_SESS_PROGRESS), showLogs || m_viewState != VS_EXPORTING ? SW_HIDE : SW_SHOW);\r\n\r\n\t\t::ShowWindow(GetDlgItem(IDC_PROGRESS), showLogs || m_viewState != VS_EXPORTING ? SW_HIDE : SW_SHOW);\r\n\t\t// ::ShowWindow(GetDlgItem(IDC_PROGRESS_TEXT), showLogs || m_viewState != VS_EXPORTING ? SW_HIDE : SW_SHOW);\r\n\t\t\r\n\t\t::ShowWindow(GetDlgItem(IDC_CANCEL), m_viewState == VS_EXPORTING ? SW_SHOW : SW_HIDE);\r\n\t\t::ShowWindow(GetDlgItem(IDC_CLOSE), m_viewState != VS_EXPORTING ? SW_SHOW : SW_HIDE);\r\n\r\n\t\tUINT ids[] = { IDC_BACKUP, IDC_CHOOSE_BKP, IDC_CHOOSE_OUTPUT, IDC_USERS/*, IDC_EXP_ITNS*/, IDC_CLOSE, IDC_EXPORT };\r\n\t\tfor (int idx = 0; idx < sizeof(ids) / sizeof(UINT); ++idx)\r\n\t\t{\r\n\t\t\t::EnableWindow(GetDlgItem(ids[idx]), m_viewState == VS_IDLE);\r\n\t\t}\r\n\r\n\t\tm_btnExpITunes.EnableWindow(m_viewState == VS_IDLE);\r\n\r\n\t\tUINT state = (m_viewState == VS_IDLE) ? MF_ENABLED : (MF_DISABLED | MF_GRAYED);\r\n\t\t::EnableMenuItem(::GetSystemMenu(::GetParent(m_hWnd), FALSE), SC_CLOSE, MF_BYCOMMAND | state);\r\n\t}\r\n\r\n\tvoid UpdateProgressBarOnDownloadingEmoji(uint32_t restedNumberOfFiles, uint32_t totalNumberOfFiles)\r\n\t{\r\n\t\t\r\n\t}\r\n\r\n\tvoid UpdateProgressBar(BOOL increaseUpper = FALSE)\r\n\t{\r\n\t\tUpdateProgressBar(1, increaseUpper ? 1 : 0);\r\n\t}\r\n\r\n\tvoid UpdateProgressBar(int increasedPos, int increasedUpper)\r\n\t{\r\n\t\tCProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS);\r\n\t\tPBRANGE range;\r\n\t\tint pos = progressCtrl.GetPos();\r\n\t\tprogressCtrl.GetRange(&range);\r\n\t\tif ((increasedUpper > 0) || pos == range.iHigh)\r\n\t\t{\r\n\t\t\trange.iHigh += increasedUpper;\r\n\t\t\tprogressCtrl.SetRange32(range.iLow, range.iHigh);\r\n\t\t}\r\n\t\tif (increasedPos > 0)\r\n\t\t{\r\n\t\t\tprogressCtrl.OffsetPos(increasedPos);\r\n\t\t\tpos = progressCtrl.GetPos();\r\n\t\t}\r\n\r\n\t\tint percent = (range.iHigh > range.iLow) ? ((pos - range.iLow) * 100 / (range.iHigh - range.iLow)) : 0;\r\n\t\tif (percent >= 100)\r\n\t\t{\r\n\t\t\tpercent = (pos < range.iHigh) ? 99 : 100;\r\n\t\t}\r\n\r\n\t\tUpdateProgressBarText(IDS_EXPORTING_MSGS, percent);\r\n\t}\r\n\r\n\tvoid UpdateProgressBarText(UINT stringId, DWORD value, bool forceUpdate = false)\r\n\t{\r\n\t\tif (forceUpdate || value != m_progressTextCtrl.GetWindowLongPtr(GWLP_USERDATA))\r\n\t\t{\r\n\t\t\t// Avoid flashing\r\n\t\t\tCString text;\r\n\t\t\ttext.Format(stringId, value);\r\n\t\t\t// text.Format(TEXT(\"%d%%\"), percent);\r\n\t\t\tm_progressTextCtrl.SetWindowLongPtr(GWLP_USERDATA, value);\r\n\t\t\tm_progressTextCtrl.SetWindowText(text);\r\n\t\t\tm_progressBarCtrl.SetWindowText(text);\r\n\t\t}\r\n\t}\r\n\r\n\tvoid UpdateBackups(const std::vector<BackupItem>& backupItems, BOOL onLaunch = FALSE)\r\n\t{\r\n\t\tif (backupItems.empty())\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tint selectedIndex = -1;\r\n\t\t// Add default backup folder\r\n\t\tfor (std::vector<BackupItem>::const_iterator it = backupItems.cbegin(); it != backupItems.cend(); ++it)\r\n\t\t{\r\n\t\t\tstd::vector<BackupItem>::const_iterator it2 = std::find(m_manifests.cbegin(), m_manifests.cend(), *it);\r\n\t\t\tif (it2 != m_manifests.cend())\r\n\t\t\t{\r\n\t\t\t\tif (selectedIndex == -1)\r\n\t\t\t\t{\r\n\t\t\t\t\tselectedIndex = static_cast<int>(std::distance(m_manifests.cbegin(), it2));\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tm_manifests.push_back(*it);\r\n\t\t\t\tif (selectedIndex == -1)\r\n\t\t\t\t{\r\n\t\t\t\t\tselectedIndex = static_cast<int>(m_manifests.size() - 1);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Update\r\n\t\tint res = -1;\r\n\t\tCComboBoxEx cmb = GetDlgItem(IDC_BACKUP);\r\n\t\tcmb.SetRedraw(FALSE);\r\n\t\tcmb.ResetContent();\r\n\t\tfor (std::vector<BackupItem>::const_iterator it = m_manifests.cbegin(); it != m_manifests.cend(); ++it)\r\n\t\t{\r\n\t\t\tCOMBOBOXEXITEM item = { 0 };\r\n\t\t\titem.mask = CBEIF_IMAGE | CBEIF_TEXT | CBEIF_SELECTEDIMAGE;\r\n\t\t\t// item.mask = CBEIF_TEXT;\r\n\t\t\tCW2T itemText(CA2W(it->toString().c_str(), CP_UTF8));\r\n\t\t\titem.iItem = cmb.GetCount();\r\n\t\t\titem.iImage = item.iItem % 2;\r\n\t\t\titem.iSelectedImage = item.iImage;\r\n\t\t\titem.pszText = (LPTSTR)itemText;\r\n\t\t\titem.iIndent = 16;\r\n\t\t\tres = cmb.InsertItem(&item);\r\n\t\t}\r\n\t\tcmb.SetRedraw(TRUE);\r\n\t\tif (selectedIndex != -1 && selectedIndex < cmb.GetCount())\r\n\t\t{\r\n\t\t\tSetComboBoxCurSel(m_hWnd, cmb, selectedIndex);\r\n\t\t}\r\n\t}\r\n\r\n\tvoid UpdateBackups(const std::vector<WechatSource*>& sources, BOOL onLaunch = FALSE)\r\n\t{\r\n\t\tif (sources.empty())\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tint selectedIndex = -1;\r\n\t\t// Add default backup folder\r\n\t\tfor (std::vector<WechatSource*>::const_iterator it = sources.cbegin(); it != sources.cend(); ++it)\r\n\t\t{\r\n\t\t\tstd::vector<WechatSource*>::const_iterator it2 = std::find(m_wechatSources.cbegin(), m_wechatSources.cend(), *it);\r\n\t\t\tif (it2 != m_wechatSources.cend())\r\n\t\t\t{\r\n\t\t\t\tif (selectedIndex == -1)\r\n\t\t\t\t{\r\n\t\t\t\t\tselectedIndex = static_cast<int>(std::distance(m_wechatSources.cbegin(), it2));\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tm_wechatSources.push_back(*it);\r\n\t\t\t\tif (selectedIndex == -1)\r\n\t\t\t\t{\r\n\t\t\t\t\tselectedIndex = static_cast<int>(m_wechatSources.size() - 1);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Update\r\n\t\tint res = -1;\r\n\t\tCComboBoxEx cmb = GetDlgItem(IDC_BACKUP);\r\n\t\tcmb.SetRedraw(FALSE);\r\n\t\tcmb.ResetContent();\r\n\t\tfor (std::vector<WechatSource*>::const_iterator it = m_wechatSources.cbegin(); it != m_wechatSources.cend(); ++it)\r\n\t\t{\r\n\t\t\tCOMBOBOXEXITEM item = { 0 };\r\n\t\t\titem.mask = CBEIF_IMAGE | CBEIF_TEXT | CBEIF_SELECTEDIMAGE | CBEIF_INDENT;\r\n\t\t\t// item.mask = CBEIF_TEXT;\r\n\t\t\tCW2T itemText(CA2W((*it)->getDisplayName().c_str(), CP_UTF8));\r\n\t\t\titem.iItem = cmb.GetCount();\r\n\t\t\titem.iImage = (*it)->isDevice() ? 0 : 1;\r\n\t\t\titem.iSelectedImage = item.iImage;\r\n\t\t\titem.pszText = (LPTSTR)itemText;\r\n\t\t\t// item.iIndent = 4;\r\n\t\t\tres = cmb.InsertItem(&item);\r\n\t\t}\r\n\t\tcmb.SetRedraw(TRUE);\r\n\t\tif (selectedIndex != -1 && selectedIndex < cmb.GetCount())\r\n\t\t{\r\n\t\t\tSetComboBoxCurSel(m_hWnd, cmb, selectedIndex);\r\n\t\t}\r\n\t}\r\n\r\n\tvoid InitializeSessionList()\r\n\t{\r\n\t\tm_sessionsListCtrl.SubclassWindow(GetDlgItem(IDC_SESSIONS));\r\n\r\n\t\tCString strColumn1;\r\n\t\tCString strColumn2;\r\n\t\tCString strColumn3;\r\n\t\tCString strColumn4;\r\n\r\n\t\tstrColumn1.LoadString(IDS_SESSION_NAME);\r\n\t\tstrColumn2.LoadString(IDS_SESSION_COUNT);\r\n\t\tstrColumn3.LoadString(IDS_SESSION_LAST_MSG);\r\n\t\tstrColumn4.LoadString(IDS_SESSION_USER);\r\n\r\n\t\tDWORD dwStyle = m_sessionsListCtrl.GetExStyle();\r\n\t\tdwStyle |= LVS_EX_FULLROWSELECT | LVS_EX_LABELTIP | LVS_EX_GRIDLINES | LVS_EX_CHECKBOXES | LVS_EX_DOUBLEBUFFER;\r\n\t\tm_sessionsListCtrl.SetExtendedListViewStyle(dwStyle, dwStyle);\r\n\r\n\t\tLVCOLUMN lvc = { 0 };\r\n\t\tListView_InsertColumn(m_sessionsListCtrl, 0, &lvc);\r\n\t\tlvc.mask = LVCF_TEXT | LVCF_WIDTH;\r\n\t\tlvc.iSubItem++;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn1;\r\n\t\tlvc.cx = 256;\r\n\t\tListView_InsertColumn(m_sessionsListCtrl, 1, &lvc);\r\n\t\tlvc.iSubItem++;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn2;\r\n\t\tlvc.cx = 96;\r\n\t\tListView_InsertColumn(m_sessionsListCtrl, 2, &lvc);\r\n\t\tlvc.iSubItem++;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn3;\r\n\t\tlvc.cx = 320;\r\n\t\tListView_InsertColumn(m_sessionsListCtrl, 3, &lvc);\r\n\t\tlvc.iSubItem++;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn4;\r\n\t\tlvc.cx = 128;\r\n\t\tListView_InsertColumn(m_sessionsListCtrl, 4, &lvc);\r\n\r\n\t\t// Set column widths\r\n\t\tListView_SetColumnWidth(m_sessionsListCtrl, 0, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\tListView_SetColumnWidth(m_sessionsListCtrl, 2, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\t// ListView_SetColumnWidth(listViewCtrl, 1, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\t// ListView_SetColumnWidth(listViewCtrl, 2, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\t// ListView_SetColumnWidth(listViewCtrl, 3, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\tm_sessionsListCtrl.SetColumnSortType(0, LVCOLSORT_NONE);\r\n\t\tm_sessionsListCtrl.SetColumnSortType(2, LVCOLSORT_LONG);\r\n\t\tm_sessionsListCtrl.SetColumnSortType(3, LVCOLSORT_NONE);\r\n\t\tm_sessionsListCtrl.SetColumnSortType(4, LVCOLSORT_NONE);\r\n\r\n\t\tHWND header = ListView_GetHeader(m_sessionsListCtrl);\r\n\t\tDWORD dwHeaderStyle = ::GetWindowLong(header, GWL_STYLE);\r\n\t\tdwHeaderStyle |= HDS_CHECKBOXES;\r\n\t\t::SetWindowLong(header, GWL_STYLE, dwHeaderStyle);\r\n\r\n\t\tHDITEM hdi = { 0 };\r\n\t\thdi.mask = HDI_FORMAT;\r\n\t\tHeader_GetItem(header, 0, &hdi);\r\n\t\thdi.fmt |= HDF_CHECKBOX | HDF_FIXEDWIDTH;\r\n\t\tHeader_SetItem(header, 0, &hdi);\r\n\t}\r\n\r\n\tvoid InitializeSessionProgressList()\r\n\t{\r\n\t\tm_progressListCtrl.SubclassWindow(GetDlgItem(IDC_SESS_PROGRESS));\r\n\r\n\t\tCString strColumn0;\r\n\t\tCString strColumn1;\r\n\t\tCString strColumn2;\r\n\t\tCString strColumn3;\r\n\t\tCString strColumn4;\r\n\t\tCString strColumn5;\r\n\r\n\t\tstrColumn0.LoadString(IDS_SESSION_NUMBER);\r\n\t\tstrColumn1.LoadString(IDS_SESSION_NAME);\r\n\t\tstrColumn2.LoadString(IDS_SESSION_COUNT);\r\n\t\tstrColumn3.LoadString(IDS_SESSION_LAST_MSG);\r\n\t\tstrColumn4.LoadString(IDS_SESSION_USER);\r\n\t\tstrColumn5.LoadString(IDS_SESSION_STATUS);\r\n\r\n\t\tDWORD dwStyle = m_progressListCtrl.GetExStyle();\r\n\t\tdwStyle |= LVS_EX_FULLROWSELECT | LVS_EX_LABELTIP | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER;\r\n\t\tdwStyle &= ~LVS_EX_CHECKBOXES;\r\n\t\t\r\n\t\tm_progressListCtrl.SetExtendedListViewStyle(dwStyle, dwStyle);\r\n\r\n\t\tLVCOLUMN lvc = { 0 };\r\n\t\tlvc.mask = LVCF_TEXT | LVCF_WIDTH;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn0;\r\n\t\tlvc.cx = 32;\r\n\t\tListView_InsertColumn(m_progressListCtrl, 0, &lvc);\r\n\t\tlvc.mask = LVCF_TEXT | LVCF_WIDTH;\r\n\t\tlvc.iSubItem++;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn1;\r\n\t\tlvc.cx = 256;\r\n\t\tListView_InsertColumn(m_progressListCtrl, 1, &lvc);\r\n\t\tlvc.iSubItem++;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn2;\r\n\t\tlvc.cx = 96;\r\n\t\tListView_InsertColumn(m_progressListCtrl, 2, &lvc);\r\n\t\tlvc.iSubItem++;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn3;\r\n\t\tlvc.cx = 320;\r\n\t\tListView_InsertColumn(m_progressListCtrl, 3, &lvc);\r\n\t\tlvc.iSubItem++;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn4;\r\n\t\tlvc.cx = 128;\r\n\t\tListView_InsertColumn(m_progressListCtrl, 4, &lvc);\r\n\t\tlvc.iSubItem++;\r\n\t\tlvc.pszText = (LPTSTR)(LPCTSTR)strColumn5;\r\n\t\tlvc.cx = 192;\r\n\t\tListView_InsertColumn(m_progressListCtrl, 5, &lvc);\r\n\r\n\t\t// Set column widths\r\n\t\tListView_SetColumnWidth(m_progressListCtrl, 0, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\tListView_SetColumnWidth(m_progressListCtrl, 2, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\t// ListView_SetColumnWidth(m_progressListCtrl, 1, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\t// ListView_SetColumnWidth(m_progressListCtrl, 2, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\t// ListView_SetColumnWidth(m_progressListCtrl, 3, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\t\r\n\t}\r\n\r\n\tvoid GetCheckedSessionsAndCopyItems(std::map<std::string, std::map<std::string, void *>>& usersAndSessions, int& numberOfSessions, int& numberOfRecords)\r\n\t{\r\n\t\tnumberOfSessions = 0;\r\n\t\tnumberOfRecords = 0;\r\n\t\tm_progressListCtrl.SetRedraw(FALSE);\r\n\t\tif (m_progressListCtrl.GetItemCount() > 0)\r\n\t\t{\r\n\t\t\tm_progressListCtrl.DeleteAllItems();\r\n\t\t}\r\n\r\n\t\tTCHAR numberString[32] = { 0 };\r\n\t\tCListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS);\r\n\r\n\t\tfor (int column = 0; column < listViewCtrl.GetHeader().GetItemCount(); column++)\r\n\t\t{\r\n\t\t\tm_progressListCtrl.SetColumnWidth(column, listViewCtrl.GetColumnWidth(column));\r\n\t\t}\r\n\r\n\t\t// std::map<std::string, std::set<std::string>> usersAndSessions;\r\n\t\tfor (int nItem = 0; nItem < listViewCtrl.GetItemCount(); nItem++)\r\n\t\t{\r\n\t\t\tif (!listViewCtrl.GetCheckState(nItem))\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t++numberOfSessions;\r\n\t\t\t_itot(m_progressListCtrl.GetItemCount() + 1, numberString, 10);\r\n\t\t\tLVITEM lvItem = {};\r\n\t\t\tlvItem.mask = LVIF_TEXT | LVIF_PARAM;\r\n\t\t\tlvItem.iItem = m_progressListCtrl.GetItemCount();\r\n\t\t\tlvItem.iSubItem = 0;\r\n\t\t\tlvItem.pszText = numberString;\r\n\t\t\tlvItem.lParam = listViewCtrl.GetItemData(nItem);\r\n\t\t\tint newItem = m_progressListCtrl.InsertItem(&lvItem);\r\n\r\n\t\t\tCString text;\r\n\t\t\tfor (int nSubItem = 1; nSubItem < 5; nSubItem++)\r\n\t\t\t{\r\n\t\t\t\tlistViewCtrl.GetItemText(nItem, nSubItem, text);\r\n\t\t\t\tm_progressListCtrl.AddItem(newItem, nSubItem, text);\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tSession* session = reinterpret_cast<Session*>(listViewCtrl.GetItemData(nItem));\r\n\t\t\tif (NULL != session)\r\n\t\t\t{\r\n\t\t\t\tnumberOfRecords += session->getRecordCount();\r\n\t\t\t\t\r\n\t\t\t\tsession->setData(reinterpret_cast<void *>(newItem));\r\n\t\t\t\tstd::string usrName = session->getOwner()->getUsrName();\r\n\t\t\t\tstd::map<std::string, std::map<std::string, void *>>::iterator it = usersAndSessions.find(usrName);\r\n\t\t\t\tif (it == usersAndSessions.end())\r\n\t\t\t\t{\r\n\t\t\t\t\tit = usersAndSessions.insert(usersAndSessions.end(), std::pair<std::string, std::map<std::string, void *>>(usrName, std::map<std::string, void *>()));\r\n\t\t\t\t}\r\n\r\n\t\t\t\tit->second.insert(std::pair<std::string, void *>(session->getUsrName(), session->getData()));\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tListView_SetColumnWidth(m_progressListCtrl, 0, LVSCW_AUTOSIZE_USEHEADER);\r\n\t\tm_progressListCtrl.SetRedraw(TRUE);\r\n\t}\r\n\t\r\n\tvoid LoadUsers(const CString& versions)\r\n\t{\r\n\t\tCEdit edtVersions = GetDlgItem(IDC_VERSIONS);\r\n\t\tedtVersions.SetWindowText(versions);\r\n\r\n\t\tCComboBox cbmBox = GetDlgItem(IDC_USERS);\r\n\t\t\r\n\t\tif (!m_usersAndSessions.empty())\r\n\t\t{\r\n\t\t\tCString text;\r\n\t\t\ttext.LoadString(IDS_ALL_USERS);\r\n\t\t\tcbmBox.AddString(text);\r\n#ifndef NDEBUG\r\n\t\t\tstd::string log = std::to_string(m_usersAndSessions.size()) + \" users\";\r\n\t\t\tm_logger->debug(log);\r\n#endif\r\n\t\t}\r\n\t\tfor (std::vector<std::pair<Friend, std::vector<Session>>>::const_iterator it = m_usersAndSessions.cbegin(); it != m_usersAndSessions.cend(); ++it)\r\n\t\t{\r\n\t\t\tstd::string displayName = it->first.getDisplayName();\r\n\t\t\tCW2T pszDisplayName(CA2W(displayName.c_str(), CP_UTF8));\r\n\t\t\tcbmBox.AddString(pszDisplayName);\r\n\t\t}\r\n\t\tif (cbmBox.GetCount() > 0)\r\n\t\t{\r\n\t\t\tSetComboBoxCurSel(m_hWnd, cbmBox, 0);\r\n\t\t}\r\n\t}\r\n\r\n\tvoid LoadSessions(BOOL allUsers, const std::string& usrName)\r\n\t{\r\n\t\tm_sessionsListCtrl.SetSortColumn(-1);\r\n\t\tCListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS);\r\n\t\tCString strDeletedSession;\r\n\t\tstrDeletedSession.LoadStringW(RBN_DELETEDBAND);\r\n\r\n\t\tBOOL includesSubscriptions = AppConfiguration::IncludeSubscriptions();\r\n\t\tTCHAR recordCount[16] = { 0 };\r\n\t\tfor (std::vector<std::pair<Friend, std::vector<Session>>>::const_iterator it = m_usersAndSessions.cbegin(); it != m_usersAndSessions.cend(); ++it)\r\n\t\t{\r\n\t\t\tif (!allUsers)\r\n\t\t\t{\r\n\t\t\t\tif (it->first.getUsrName() != usrName)\r\n\t\t\t\t{\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tstd::string userDisplayName = it->first.getDisplayName();\r\n\t\t\tCW2T pszUserDisplayName(CA2W(userDisplayName.c_str(), CP_UTF8));\r\n\r\n\t\t\tfor (std::vector<Session>::const_iterator it2 = it->second.cbegin(); it2 != it->second.cend(); ++it2)\r\n\t\t\t{\r\n\t\t\t\tif (!includesSubscriptions && it2->isSubscription())\r\n\t\t\t\t{\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tstd::string displayName = it2->getDisplayName();\r\n\t\t\t\tif (displayName.empty())\r\n\t\t\t\t{\r\n\t\t\t\t\tdisplayName = it2->getUsrName();\r\n\t\t\t\t}\r\n\r\n\t\t\t\tCW2T pszDisplayName(CA2W(displayName.c_str(), CP_UTF8));\r\n\t\t\t\tLVITEM lvItem = {};\r\n\t\t\t\tlvItem.mask = LVIF_TEXT | LVIF_PARAM;\r\n\t\t\t\tlvItem.iItem = listViewCtrl.GetItemCount();\r\n\t\t\t\tlvItem.iSubItem = 0;\r\n\t\t\t\tlvItem.pszText = TEXT(\"\");\r\n\t\t\t\t// lvItem.state = INDEXTOSTATEIMAGEMASK(2);\r\n\t\t\t\t// lvItem.stateMask = LVIS_STATEIMAGEMASK;\r\n\t\t\t\tint idx = std::distance(it->second.cbegin(), it2);\r\n\t\t\t\tLPARAM lParam = reinterpret_cast<LPARAM>(&(*it2));\r\n\t\t\t\tlvItem.lParam = lParam;\r\n\t\t\t\tint nItem = listViewCtrl.InsertItem(&lvItem);\r\n\r\n\t\t\t\t_itot(it2->getRecordCount(), recordCount, 10);\r\n\t\t\t\tif (it2->isDeleted())\r\n\t\t\t\t{\r\n\t\t\t\t\tlistViewCtrl.AddItem(nItem, 1, pszDisplayName + strDeletedSession);\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tlistViewCtrl.AddItem(nItem, 1, pszDisplayName);\r\n\t\t\t\t}\r\n\t\t\t\tlistViewCtrl.AddItem(nItem, 2, recordCount);\r\n\r\n\t\t\t\tstd::string msg;\r\n\t\t\t\tif (it2->isTextMessage())\r\n\t\t\t\t{\r\n\t\t\t\t\tif (it2->hasLastMessageUserDisplayName())\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tmsg = it2->getLastMessageUserDisplayName() + \": \" + it2->getLastMessage();\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tmsg = it2->getLastMessage();\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tmsg = \"-\";\r\n\t\t\t\t}\r\n\r\n\t\t\t\tCW2T pszDisplayMsg(CA2W(msg.c_str(), CP_UTF8));\r\n\t\t\t\tlistViewCtrl.AddItem(nItem, 3, pszDisplayMsg);\r\n\r\n\t\t\t\tlistViewCtrl.AddItem(nItem, 4, pszUserDisplayName);\r\n\t\t\t\t// BOOL bRet = listViewCtrl.SetItem(&lvSubItem);\r\n\t\t\t\tlistViewCtrl.SetCheckState(nItem, TRUE);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tSetHeaderCheckState(listViewCtrl, TRUE);\r\n\t}\r\n\r\n\tvoid SyncHeaderCheckbox()\r\n\t{\r\n\t\t// Loop through all of our items.  If any of them are\r\n\t\t// unchecked, we'll want to uncheck the header checkbox.\r\n\t\tCListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS);\r\n\t\tBOOL fChecked = TRUE;\r\n\t\tfor (int nItem = 0; nItem < ListView_GetItemCount(listViewCtrl); nItem++)\r\n\t\t{\r\n\t\t\tif (!ListView_GetCheckState(listViewCtrl, nItem))\r\n\t\t\t{\r\n\t\t\t\tfChecked = FALSE;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tSetHeaderCheckState(listViewCtrl, fChecked);\r\n\t}\r\n\r\n\t\r\n\r\n};\r\n"
  },
  {
    "path": "vcproject/ViewHelper.cpp",
    "content": "#include \"stdafx.h\"\r\n#include <atlctrls.h>\r\n#include \"resource.h\"\r\n#include \"ViewHelper.h\"\r\n\r\n#include <memory>\r\n\r\nstruct LibraryDeleter\r\n{\r\n\ttypedef HMODULE pointer;\r\n\tvoid operator()(HMODULE h) { if (NULL != h) ::FreeLibrary(h); }\r\n};\r\n\r\n//Functions & other definitions required-->\r\ntypedef int(__stdcall *MSGBOXAAPI)(IN HWND hWnd,\r\n\tIN LPCSTR lpText, IN LPCSTR lpCaption,\r\n\tIN UINT uType, IN WORD wLanguageId, IN DWORD dwMilliseconds);\r\ntypedef int(__stdcall *MSGBOXWAPI)(IN HWND hWnd,\r\n\tIN LPCWSTR lpText, IN LPCWSTR lpCaption,\r\n\tIN UINT uType, IN WORD wLanguageId, IN DWORD dwMilliseconds);\r\n\r\nint MessageBoxTimeoutA(HWND hWnd, LPCSTR lpText,\r\n\tLPCSTR lpCaption, UINT uType, WORD wLanguageId,\r\n\tDWORD dwMilliseconds)\r\n{\r\n\tstd::unique_ptr<HMODULE, LibraryDeleter> hUser32(::LoadLibraryA(\"user32.dll\"));\r\n\tif (NULL != hUser32.get())\r\n\t{\r\n\t\tMSGBOXAAPI MsgBoxTOA = (MSGBOXAAPI)GetProcAddress(hUser32.get(), \"MessageBoxTimeoutA\");\r\n\t\tif (NULL != MsgBoxTOA)\r\n\t\t{\r\n\r\n\t\t\treturn MsgBoxTOA(hWnd, lpText, lpCaption, uType, wLanguageId, dwMilliseconds);\r\n\t\t}\r\n\t}\r\n\r\n\treturn MessageBoxA(hWnd, lpText, lpCaption, uType);\r\n}\r\n\r\nint MessageBoxTimeoutW(HWND hWnd, LPCWSTR lpText,\r\n\tLPCWSTR lpCaption, UINT uType, WORD wLanguageId, DWORD dwMilliseconds)\r\n{\r\n\tstd::unique_ptr<HMODULE, LibraryDeleter> hUser32(::LoadLibraryW(L\"user32.dll\"));\r\n\tif (NULL != hUser32.get())\r\n\t{\r\n\t\tMSGBOXWAPI MsgBoxTOW = (MSGBOXWAPI)GetProcAddress(hUser32.get(), \"MessageBoxTimeoutW\");\r\n\t\tif (NULL != MsgBoxTOW)\r\n\t\t{\r\n\t\t\t\r\n\t\t\treturn MsgBoxTOW(hWnd, lpText, lpCaption, uType, wLanguageId, dwMilliseconds);\r\n\t\t}\r\n\t}\r\n\r\n\treturn MessageBoxW(hWnd, lpText, lpCaption, uType);\r\n}\r\n\r\nint MsgBox(HWND hWnd, UINT uStrId, UINT uType/* = MB_OK*/)\r\n{\r\n\tCString text;\r\n\ttext.LoadString(uStrId);\r\n\treturn MsgBox(hWnd, text, uType);\r\n}\r\n\r\nint MsgBox(HWND hWnd, const CString& text, UINT uType/* = MB_OK*/)\r\n{\r\n\tCString caption;\r\n\tcaption.LoadString(IDR_MAINFRAME);\r\n\treturn ::MessageBox(hWnd, (LPCTSTR)text, (LPCTSTR)caption, uType);\r\n}\r\n\r\nvoid SetComboBoxCurSel(HWND hWnd, CComboBox &cbm, int nCurSel)\r\n{\r\n\tcbm.SetCurSel(nCurSel);\r\n\tint nID = cbm.GetDlgCtrlID();\r\n\t::PostMessage(hWnd, WM_COMMAND, MAKEWPARAM(nID, CBN_SELCHANGE), LPARAM(cbm.m_hWnd));\r\n}\r\n\r\nvoid CheckAllItems(CListViewCtrl& listViewCtrl, BOOL fChecked)\r\n{\r\n\tfor (int nItem = 0; nItem < ListView_GetItemCount(listViewCtrl); nItem++)\r\n\t{\r\n\t\tListView_SetCheckState(listViewCtrl, nItem, fChecked);\r\n\t}\r\n}\r\n\r\nvoid SetHeaderCheckState(CListViewCtrl& listViewCtrl, BOOL fChecked)\r\n{\r\n\t// We need to get the current format of the header\r\n\t// and set or remove the HDF_CHECKED flag\r\n\tHWND header = ListView_GetHeader(listViewCtrl);\r\n\tHDITEM hdi = { 0 };\r\n\thdi.mask = HDI_FORMAT;\r\n\tHeader_GetItem(header, 0, &hdi);\r\n\tif (fChecked) {\r\n\t\thdi.fmt |= HDF_CHECKED;\r\n\t}\r\n\telse {\r\n\t\thdi.fmt &= ~HDF_CHECKED;\r\n\t}\r\n\tHeader_SetItem(header, 0, &hdi);\r\n}\r\n\r\nBOOL SetHeaderCheckState(CListViewCtrl& listViewCtrl)\r\n{\r\n\tHWND header = ListView_GetHeader(listViewCtrl);\r\n\tHDITEM hdi = { 0 };\r\n\thdi.mask = HDI_FORMAT;\r\n\tHeader_GetItem(header, 0, &hdi);\r\n\treturn (hdi.fmt & HDF_CHECKED) == HDF_CHECKED ? TRUE : FALSE;\r\n}\r\n\r\n\r\n// Return value of GetCurrentExplorerFolders()\r\nstruct ExplorerFolderInfo\r\n{\r\n\tHWND hwnd = nullptr;  // window handle of explorer\r\n\tUniquePidlPtr pidl;   // PIDL that points to current folder\r\n};\r\n\r\n// Get information about all currently open explorer windows.\r\n// Throws std::system_error exception to report errors.\r\nstd::vector< ExplorerFolderInfo > GetCurrentExplorerFolders()\r\n{\r\n\tCComPtr< IShellWindows > pshWindows;\r\n\tThrowIfFailed(\r\n\t\tpshWindows.CoCreateInstance(CLSID_ShellWindows),\r\n\t\t\"Could not create instance of IShellWindows\");\r\n\r\n\tlong count = 0;\r\n\tThrowIfFailed(\r\n\t\tpshWindows->get_Count(&count),\r\n\t\t\"Could not get number of shell windows\");\r\n\r\n\tstd::vector< ExplorerFolderInfo > result;\r\n\tresult.reserve(count);\r\n\r\n\tfor (long i = 0; i < count; ++i)\r\n\t{\r\n\t\tExplorerFolderInfo info;\r\n\r\n\t\tCComVariant vi{ i };\r\n\t\tCComPtr< IDispatch > pDisp;\r\n\t\tThrowIfFailed(\r\n\t\t\tpshWindows->Item(vi, &pDisp),\r\n\t\t\t\"Could not get item from IShellWindows\");\r\n\r\n\t\tif (!pDisp)\r\n\t\t\t// Skip - this shell window was registered with a NULL IDispatch\r\n\t\t\tcontinue;\r\n\r\n\t\tCComQIPtr< IWebBrowserApp > pApp{ pDisp };\r\n\t\tif (!pApp)\r\n\t\t\t// This window doesn't implement IWebBrowserApp \r\n\t\t\tcontinue;\r\n\r\n\t\t// Get the window handle.\r\n\t\tpApp->get_HWND(reinterpret_cast<SHANDLE_PTR*>(&info.hwnd));\r\n\r\n\t\tCComQIPtr< IServiceProvider > psp{ pApp };\r\n\t\tif (!psp)\r\n\t\t\t// This window doesn't implement IServiceProvider\r\n\t\t\tcontinue;\r\n\r\n\t\tCComPtr< IShellBrowser > pBrowser;\r\n\t\tif (FAILED(psp->QueryService(SID_STopLevelBrowser, &pBrowser)))\r\n\t\t\t// This window doesn't provide IShellBrowser\r\n\t\t\tcontinue;\r\n\r\n\t\tCComPtr< IShellView > pShellView;\r\n\t\tif (FAILED(pBrowser->QueryActiveShellView(&pShellView)))\r\n\t\t\t// For some reason there is no active shell view\r\n\t\t\tcontinue;\r\n\r\n\t\tCComQIPtr< IFolderView > pFolderView{ pShellView };\r\n\t\tif (!pFolderView)\r\n\t\t\t// The shell view doesn't implement IFolderView\r\n\t\t\tcontinue;\r\n\r\n\t\t// Get the interface from which we can finally query the PIDL of\r\n\t\t// the current folder.\r\n\t\tCComPtr< IPersistFolder2 > pFolder;\r\n\t\tif (FAILED(pFolderView->GetFolder(IID_IPersistFolder2, (void**)&pFolder)))\r\n\t\t\tcontinue;\r\n\r\n\t\tLPITEMIDLIST pidl = nullptr;\r\n\t\tif (SUCCEEDED(pFolder->GetCurFolder(&pidl)))\r\n\t\t{\r\n\t\t\t// Take ownership of the PIDL via std::unique_ptr.\r\n\t\t\tinfo.pidl = UniquePidlPtr{ pidl };\r\n\t\t\tresult.push_back(std::move(info));\r\n\t\t}\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n\r\nBOOL OpenFolder(LPCTSTR szFolder)\r\n{\r\n\tCT2W wszFolder(szFolder);\r\n\ttry\r\n\t{\r\n\t\t// std::wcout << L\"Currently open explorer windows:\\n\";\r\n\t\tfor (const auto& info : GetCurrentExplorerFolders())\r\n\t\t{\r\n\t\t\tCComHeapPtr<wchar_t> pPath;\r\n\t\t\tif (SUCCEEDED(::SHGetNameFromIDList(info.pidl.get(), SIGDN_FILESYSPATH, &pPath)))\r\n\t\t\t{\r\n\t\t\t\tif (wcscmp(wszFolder, pPath) == 0)\r\n\t\t\t\t{\r\n\t\t\t\t\tSetForegroundWindow(info.hwnd);\r\n\t\t\t\t\treturn TRUE;\r\n\t\t\t\t}\r\n\t\t\t\t// std::wcout << L\"hwnd: 0x\" << std::hex << info.hwnd\r\n\t\t\t\t// \t<< L\", path: \" << static_cast<LPWSTR>(pPath) << L\"\\n\";\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tcatch (std::system_error& e)\r\n\t{\r\n\t\t// std::cout << \"ERROR: \" << e.what() << \"\\nError code: \" << e.code() << \"\\n\";\r\n\t}\r\n\r\n\tHINSTANCE inst = ::ShellExecute(NULL, TEXT(\"open\"), szFolder, NULL, NULL, SW_SHOWNORMAL);\r\n\treturn (INT_PTR)inst > 32;\r\n}\r\n"
  },
  {
    "path": "vcproject/ViewHelper.h",
    "content": "#pragma once\r\n\r\n#include <shlobj.h>\r\n#include <atlcomcli.h>  // for COM smart pointers\r\n#include <atlbase.h>    // for COM smart pointers\r\n#include <vector>\r\n#include <system_error>\r\n#include <memory>\r\n#include <iostream>\r\n\r\n#define MB_TIMEDOUT 32000\r\n\r\nint MessageBoxTimeoutA(IN HWND hWnd, IN LPCSTR lpText,\r\n\tIN LPCSTR lpCaption, IN UINT uType,\r\n\tIN WORD wLanguageId, IN DWORD dwMilliseconds);\r\nint MessageBoxTimeoutW(IN HWND hWnd, IN LPCWSTR lpText,\r\n\tIN LPCWSTR lpCaption, IN UINT uType,\r\n\tIN WORD wLanguageId, IN DWORD dwMilliseconds);\r\n\r\n#ifdef UNICODE\r\n#define MessageBoxTimeout MessageBoxTimeoutW\r\n#else\r\n#define MessageBoxTimeout MessageBoxTimeoutA\r\n#endif \r\nint MsgBox(HWND hWnd, UINT uStrId, UINT uType = MB_OK);\r\nint MsgBox(HWND hWnd, const CString& text, UINT uType = MB_OK);\r\nvoid SetComboBoxCurSel(HWND hWnd, CComboBox &cbm, int nCurSel);\r\nvoid CheckAllItems(CListViewCtrl& listViewCtrl, BOOL fChecked);\r\nvoid SetHeaderCheckState(CListViewCtrl& listViewCtrl, BOOL fChecked);\r\nBOOL SetHeaderCheckState(CListViewCtrl& listViewCtrl);\r\n\r\ntemplate< typename T >\r\nvoid ThrowIfFailed(HRESULT hr, T&& msg)\r\n{\r\n\tif (FAILED(hr))\r\n\t\tthrow std::system_error{ hr, std::system_category(), std::forward<T>(msg) };\r\n}\r\n\r\n// Deleter for a PIDL allocated by the shell.\r\nstruct CoTaskMemDeleter\r\n{\r\n\tvoid operator()(ITEMIDLIST* pidl) const { ::CoTaskMemFree(pidl); }\r\n};\r\n// A smart pointer for PIDLs.\r\nusing UniquePidlPtr = std::unique_ptr< ITEMIDLIST, CoTaskMemDeleter >;\r\nBOOL OpenFolder(LPCTSTR szFolder);\r\n"
  },
  {
    "path": "vcproject/WechatExporter.cpp",
    "content": "// WechatExporter.cpp : main source file for WechatExporter.exe\r\n//\r\n\r\n#include \"stdafx.h\"\r\n\r\n#include <atlframe.h>\r\n#include <atlctrls.h>\r\n#include <atlctrlx.h>\r\n#include <atldlgs.h>\r\n#include \"ToolTipButton.h\"\r\n\r\n#include \"resource.h\"\r\n\r\n#include \"Core.h\"\r\n\r\n#include \"BackupDlg.h\"\r\n#include \"View.h\"\r\n#include \"aboutdlg.h\"\r\n#include \"MainFrm.h\"\r\n\r\nCAppModule _Module;\r\n\r\nint Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)\r\n{\r\n\tCMessageLoop theLoop;\r\n\t_Module.AddMessageLoop(&theLoop);\r\n\r\n\tCMainFrame wndMain;\r\n\r\n\tif(wndMain.CreateEx() == NULL)\r\n\t{\r\n\t\tATLTRACE(_T(\"Main window creation failed!\\n\"));\r\n\t\treturn 0;\r\n\t}\r\n\r\n\twndMain.ShowWindow(nCmdShow);\r\n\r\n\tint nRet = theLoop.Run();\r\n\r\n\t_Module.RemoveMessageLoop();\r\n\treturn nRet;\r\n}\r\n\r\nint WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow)\r\n{\r\n\tHRESULT hRes = ::CoInitialize(NULL);\r\n\tATLASSERT(SUCCEEDED(hRes));\r\n\r\n\tif (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED))\r\n\t{\r\n\t\t::SetThreadUILanguage(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED));\r\n\t}\r\n\telse\r\n\t{\r\n\t\t::SetThreadUILanguage(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US));\r\n\t}\r\n\r\n\tAtlInitCommonControls(ICC_PROGRESS_CLASS | ICC_BAR_CLASSES | ICC_LISTVIEW_CLASSES | ICC_USEREX_CLASSES);\t// add flags to support other controls\r\n\r\n\thRes = _Module.Init(NULL, hInstance);\r\n\tATLASSERT(SUCCEEDED(hRes));\r\n\r\n\tTCHAR buffer[MAX_PATH] = { 0 };\r\n\tDWORD dwRet = GetModuleFileName(NULL, buffer, MAX_PATH);\r\n\tif (dwRet > 0)\r\n\t{\r\n\t\tif (PathRemoveFileSpec(buffer))\r\n\t\t{\r\n\t\t\tPathAppend(buffer, TEXT(\"Dlls\"));\r\n\t\t\tSetDllDirectory(buffer);\r\n\t\t}\r\n\t}\r\n#ifndef NDEBUG\r\n\telse\r\n\t{\r\n\t\tassert(false);\r\n\t}\r\n#endif\r\n\r\n\tint nRet = Run(lpstrCmdLine, nCmdShow);\r\n\r\n\t_Module.Term();\r\n\t::CoUninitialize();\r\n\r\n\treturn nRet;\r\n}\r\n"
  },
  {
    "path": "vcproject/WechatExporter.h",
    "content": "// WechatExporter.h\n"
  },
  {
    "path": "vcproject/WechatExporter.rc",
    "content": "// Microsoft Visual C++ generated resource script.\r\n//\r\n#include \"resource.h\"\r\n\r\n#define APSTUDIO_READONLY_SYMBOLS\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// Generated from the TEXTINCLUDE 2 resource.\r\n//\r\n#include \"atlres.h\"\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n#undef APSTUDIO_READONLY_SYMBOLS\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// Chinese (Simplified, PRC) resources\r\n\r\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)\r\nLANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED\r\n#pragma code_page(936)\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// Menu\r\n//\r\n\r\nIDR_MAINFRAME MENU\r\nBEGIN\r\n    POPUP \"ļ(&F)\"\r\n    BEGIN\r\n        MENUITEM \"Զ°汾\",                     ID_FILE_CHK_UPDATE, CHECKED\r\n        MENUITEM \"Ŀ¼\",                     ID_FILE_OPEN_FOLDER, CHECKED\r\n        MENUITEM \"ϸ־\",                      ID_FILE_DBG_LOGS\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"iTunesݵ\",                  ID_FILE_EXP_ITUNES\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"˳(&X)\",                      ID_APP_EXIT\r\n    END\r\n    POPUP \"ʽ (&T)\"\r\n    BEGIN\r\n        MENUITEM \"HTML\",                        ID_FORMAT_HTML\r\n        MENUITEM \"ı\",                          ID_FORMAT_TEXT\r\n        MENUITEM \"PDF\",                         ID_FORMAT_PDF\r\n    END\r\n    POPUP \"ѡ(&O)\"\r\n    BEGIN\r\n        MENUITEM \"Ϣʱ䵹򵼳\",                   ID_OPT_DESC_ORDER\r\n        MENUITEM \"رļ\",                      ID_OPT_DL_EMOJI, CHECKED\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"ϻظ¼\",                  ID_OPT_LM_ONSCROLL\r\n        MENUITEM \"ҳ 1000Ϣ/ҳ\",            ID_OPT_NORMALPAGINATION\r\n        MENUITEM \"ݷҳ\",                       ID_OPT_PAGINATION_YEAR\r\n        MENUITEM \"·ݷҳ\",                       ID_OPT_PAGINATION_MONTH\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"ʾϢ\",                      ID_OPT_FILTER\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"\",                        ID_OPT_INCREMENTALEXP\r\n        MENUITEM \"ں\",                       ID_OPT_SUBSCRIPTIONS\r\n    END\r\n    POPUP \"(&H)\"\r\n    BEGIN\r\n        MENUITEM \"ҳ\",                          ID_HELP_HOMEPAGE\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"&About WechatExporter\",       ID_APP_ABOUT\r\n    END\r\nEND\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// Dialog\r\n//\r\n\r\nIDD_ABOUTBOX DIALOGEX 0, 0, 187, 102\r\nSTYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU\r\nCAPTION \"\"\r\nFONT 9, \"Segoe UI\", 0, 0, 0x0\r\nBEGIN\r\n    DEFPUSHBUTTON   \"ȷ\",IDOK,130,81,50,14\r\n    CTEXT           \"WechatExporter Application v1.0\\n\\n(c) Copyright 2020\",IDC_VERSION,25,57,78,32\r\n    ICON            IDR_MAINFRAME,IDC_STATIC,55,26,20,20\r\n    GROUPBOX        \"\",IDC_STATIC,7,7,115,88\r\nEND\r\n\r\nIDD_WECHATEXPORTER_FORM DIALOGEX 0, 0, 393, 196\r\nSTYLE DS_SETFONT | WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS\r\nEXSTYLE WS_EX_CLIENTEDGE\r\nFONT 9, \"Segoe UI\", 0, 0, 0x1\r\nBEGIN\r\n    LTEXT           \"iTunesĿ¼\",IDC_STATIC_BACKUP,7,7,379,8\r\n    CONTROL         \"\",IDC_BACKUP,\"ComboBoxEx32\",CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP,7,18,362,60\r\n    PUSHBUTTON      \"...\",IDC_CHOOSE_BKP,371,17,15,14\r\n    LTEXT           \"¼Ŀ¼\",IDC_STATIC,7,33,361,8\r\n    EDITTEXT        IDC_OUTPUT,7,44,362,14,ES_AUTOHSCROLL | ES_READONLY\r\n    PUSHBUTTON      \"...\",IDC_CHOOSE_OUTPUT,371,44,15,14\r\n    GROUPBOX        \"΢˺ź¼ѡ\",IDC_GRP_USR_CHAT,7,62,379,108,0,WS_EX_TRANSPARENT\r\n    COMBOBOX        IDC_USERS,14,74,252,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP\r\n    EDITTEXT        IDC_VERSIONS,270,76,109,14,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER\r\n    CONTROL         \"\",IDC_SESSIONS,\"SysListView32\",LVS_REPORT | LVS_ALIGNLEFT | WS_BORDER | WS_TABSTOP,14,90,365,73\r\n    LTEXT           \"100%\",IDC_PROGRESS_TEXT,14,76,121,8,NOT WS_VISIBLE,WS_EX_TRANSPARENT\r\n    CONTROL         \"\",IDC_PROGRESS,\"msctls_progress32\",NOT WS_VISIBLE | WS_BORDER,270,74,109,12\r\n    CONTROL         \"\",IDC_SESS_PROGRESS,\"SysListView32\",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | NOT WS_VISIBLE | WS_BORDER | WS_TABSTOP,14,90,365,73\r\n    GROUPBOX        \"־(CTRL+Aѡ־/CTRL+Cѡ־)\",IDC_GRP_LOGS,7,62,379,108,NOT WS_VISIBLE,WS_EX_TRANSPARENT\r\n    LISTBOX         IDC_LOGS,14,74,365,89,LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | NOT WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP\r\n    CONTROL         \"ʾ־\",IDC_SHOW_LOGS,\"Button\",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,7,175,50,14\r\n    PUSHBUTTON      \"ȡ(&C)\",IDC_CANCEL,336,175,50,14,NOT WS_VISIBLE\r\n    PUSHBUTTON      \"iTunes\",IDC_EXP_ITNS,72,175,80,14,NOT WS_VISIBLE\r\n    PUSHBUTTON      \"(&E)\",IDC_EXPORT,279,175,50,14\r\n    PUSHBUTTON      \"ر(&X)\",IDC_CLOSE,336,175,50,14\r\nEND\r\n\r\nIDD_BACKUP_DLG DIALOGEX 0, 0, 328, 73\r\nSTYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION\r\nCAPTION \"΢\"\r\nFONT 8, \"MS Shell Dlg\", 400, 0, 0x1\r\nBEGIN\r\n    PUSHBUTTON      \"ȡ\",IDCANCEL,271,43,50,14\r\n    CONTROL         \"\",IDC_PROGRESS1,\"msctls_progress32\",WS_BORDER,7,43,239,14\r\n    LTEXT           \"ƶ豸΢...\",IDC_TITLE,7,7,314,27\r\nEND\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// DESIGNINFO\r\n//\r\n\r\n#ifdef APSTUDIO_INVOKED\r\nGUIDELINES DESIGNINFO\r\nBEGIN\r\n    IDD_ABOUTBOX, DIALOG\r\n    BEGIN\r\n        LEFTMARGIN, 7\r\n        RIGHTMARGIN, 180\r\n        TOPMARGIN, 7\r\n        BOTTOMMARGIN, 95\r\n    END\r\n\r\n    IDD_WECHATEXPORTER_FORM, DIALOG\r\n    BEGIN\r\n        LEFTMARGIN, 7\r\n        RIGHTMARGIN, 386\r\n        TOPMARGIN, 7\r\n        BOTTOMMARGIN, 189\r\n    END\r\n\r\n    IDD_BACKUP_DLG, DIALOG\r\n    BEGIN\r\n        LEFTMARGIN, 7\r\n        RIGHTMARGIN, 321\r\n        TOPMARGIN, 7\r\n        BOTTOMMARGIN, 66\r\n    END\r\nEND\r\n#endif    // APSTUDIO_INVOKED\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// AFX_DIALOG_LAYOUT\r\n//\r\n\r\nIDD_WECHATEXPORTER_FORM AFX_DIALOG_LAYOUT\r\nBEGIN\r\n    0\r\nEND\r\n\r\nIDD_ABOUTBOX AFX_DIALOG_LAYOUT\r\nBEGIN\r\n    0\r\nEND\r\n\r\nIDD_BACKUP_DLG AFX_DIALOG_LAYOUT\r\nBEGIN\r\n    0\r\nEND\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// Icon\r\n//\r\n\r\n// Icon with lowest ID value placed first to ensure application icon\r\n// remains consistent on all systems.\r\nIDI_IPHONE              ICON                    \"res\\\\iPhone.ico\"\r\n\r\nIDI_ITUNES              ICON                    \"res\\\\iTunes.ico\"\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// String Table\r\n//\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    IDR_MAINFRAME           \"Wechat Exporter\"\r\n    IDS_SEL_BACKUP_DIR      \"ѡiTunesĿ¼\"\r\n    IDS_SEL_OUTPUT_DIR      \"ѡ¼Ŀ¼\"\r\n    IDS_CANCEL_PROMPT       \"ȷҪȡ\"\r\n    IDS_STATIC_BACKUP       \"iTunesĿ¼(:%s)\"\r\n    IDS_SESSION_IDX         \"\"\r\n    IDS_SESSION_NAME        \"\"\r\n    IDS_SESSION_COUNT       \"¼\"\r\n    IDS_ALL_USERS           \"΢˺\"\r\n    IDS_SESSION_USER        \"΢˺\"\r\n    IDS_ITUNES_VERSION      \"iTunes Ѱװ 汾%s\"\r\n    IDS_ITUNES_NOT_INSTALLED \"iTunes δװ\"\r\n    IDS_ENC_BKP_NOT_SUPPORTED \"ּ֧ܵiTunes BackupʹòʽiPhone/iPad豸\"\r\n    IDS_FAILED_TO_LOAD_BKP  \"iTunes Backupʧܡ\"\r\n    IDS_INVALID_OUTPUT_DIR  \"ЧĿ¼ѡ\"\r\n    IDS_TOOLTIP_LOGS        \"Ctrl+A/Ctrl+C ־\"\r\nEND\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    ID_APP_EXIT             \"˳Ӧá\\n˳\"\r\nEND\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    ATL_IDS_IDLEMESSAGE     \"Ready\"\r\nEND\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    IDS_NEW_VERSION         \"°汾%sǷȥأ\"\r\n    IDS_SESSION_NUMBER      \"\"\r\n    IDS_SESSION_STATUS      \"\"\r\n    IDS_SESSION_DONE        \"ѵ\"\r\n    IDS_SESSION_PROGRESS    \"%u / %u\"\r\n    IDS_SESSION_CANCELLED   \"ȡ\"\r\n    IDS_SHOW_LOGS           \"ʾ־\"\r\n    IDS_HIDE_LOGS           \"־\"\r\n    IDS_VERSIONS            \"iTunes汾%s, iOS汾%s, ΢Ű汾%s\"\r\n    IDS_NO_PREV_EXP         \"Ŀ¼²ǰһεϢԡ\"\r\n    IDS_PREV_EXP_FOUND      \"Ŀ¼·%sĵݣεáģʽǰһεá\"\r\n    IDS_DELETED_SESSION     \"(ɾ)\"\r\n    IDS_EXPORTING_MSGS      \"ڵ¼ (%d%%)\"\r\n    IDS_DOWNLOADING_EMOJI   \"رļʣ: %d\"\r\n    IDS_EXP_ITNS            \"΢ļiTunes\"\r\n    IDS_SESSION_LAST_MSG    \"Ϣ\"\r\nEND\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    IDS_INVLD_INCEXP_FOR_MULTI_USERS \"ƴ󣬶ڶ΢˺ŵݸԭܲµĿ¼\"\r\nEND\r\n\r\n#endif    // Chinese (Simplified, PRC) resources\r\n/////////////////////////////////////////////////////////////////////////////\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// English resources\r\n\r\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\r\nLANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL\r\n#pragma code_page(1252)\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// Menu\r\n//\r\n\r\nIDR_MAINFRAME MENU\r\nBEGIN\r\n    POPUP \"&File\"\r\n    BEGIN\r\n        MENUITEM \"Check Update Automatically\",  ID_FILE_CHK_UPDATE, CHECKED\r\n        MENUITEM \"Open Folder After Exporting\", ID_FILE_OPEN_FOLDER, CHECKED\r\n        MENUITEM \"Output Detailed Logs\",        ID_FILE_DBG_LOGS\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"Export iTunes Backup\",        ID_FILE_EXP_ITUNES\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"E&xit\",                       ID_APP_EXIT\r\n    END\r\n    POPUP \"Forma&t \"\r\n    BEGIN\r\n        MENUITEM \"HTML\",                        ID_FORMAT_HTML\r\n        MENUITEM \"Text\",                        ID_FORMAT_TEXT\r\n        MENUITEM \"PDF\",                         ID_FORMAT_PDF\r\n    END\r\n    POPUP \"&Options\"\r\n    BEGIN\r\n        MENUITEM \"From Newer To Earlier\",       ID_OPT_DESC_ORDER\r\n        MENUITEM \"Download Emoji Files to Local Disk\", ID_OPT_DL_EMOJI, CHECKED\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"Load Messages On Scrolling\",  ID_OPT_LM_ONSCROLL\r\n        MENUITEM \"Normal Pagination (Every 1000 Messages)\", ID_OPT_NORMALPAGINATION\r\n        MENUITEM \"Pagination Based on Year\",    ID_OPT_PAGINATION_YEAR\r\n        MENUITEM \"Pagination Based on Month\",   ID_OPT_PAGINATION_MONTH\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"Show Message Filter\",         ID_OPT_FILTER\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"Incremental Exporting\",       ID_OPT_INCREMENTALEXP\r\n        MENUITEM \"Including Subscriptions\",     ID_OPT_SUBSCRIPTIONS\r\n    END\r\n    POPUP \"&Help\"\r\n    BEGIN\r\n        MENUITEM \"Home Page\",                   ID_HELP_HOMEPAGE\r\n        MENUITEM SEPARATOR\r\n        MENUITEM \"&About WechatExporter\",       ID_APP_ABOUT\r\n    END\r\nEND\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// Dialog\r\n//\r\n\r\nIDD_WECHATEXPORTER_FORM DIALOGEX 0, 0, 393, 196\r\nSTYLE DS_SETFONT | WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS\r\nEXSTYLE WS_EX_CLIENTEDGE\r\nFONT 9, \"Segoe UI\", 0, 0, 0x1\r\nBEGIN\r\n    LTEXT           \"iTunes Backup Directory:\",IDC_STATIC_BACKUP,7,7,379,8\r\n    CONTROL         \"\",IDC_BACKUP,\"ComboBoxEx32\",CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP,7,18,362,60\r\n    PUSHBUTTON      \"...\",IDC_CHOOSE_BKP,371,17,15,14\r\n    LTEXT           \"Output Directory:\",IDC_STATIC,7,33,361,8\r\n    EDITTEXT        IDC_OUTPUT,7,44,362,14,ES_AUTOHSCROLL | ES_READONLY\r\n    PUSHBUTTON      \"...\",IDC_CHOOSE_OUTPUT,371,44,15,14\r\n    GROUPBOX        \"Wechat Accounts and Chats\",IDC_GRP_USR_CHAT,7,62,379,108,0,WS_EX_TRANSPARENT\r\n    COMBOBOX        IDC_USERS,14,74,252,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP\r\n    EDITTEXT        IDC_VERSIONS,270,76,109,14,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER\r\n    CONTROL         \"\",IDC_SESSIONS,\"SysListView32\",LVS_REPORT | LVS_ALIGNLEFT | WS_BORDER | WS_TABSTOP,14,90,365,73\r\n    LTEXT           \"100%\",IDC_PROGRESS_TEXT,14,76,121,8,NOT WS_VISIBLE,WS_EX_TRANSPARENT\r\n    CONTROL         \"\",IDC_PROGRESS,\"msctls_progress32\",NOT WS_VISIBLE | WS_BORDER,270,74,109,12\r\n    CONTROL         \"\",IDC_SESS_PROGRESS,\"SysListView32\",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | NOT WS_VISIBLE | WS_BORDER | WS_TABSTOP,14,90,365,73\r\n    GROUPBOX        \"Logs(CTRL+A:Select All Logs/CTRL+C:Copy Logs)\",IDC_GRP_LOGS,7,62,379,108,NOT WS_VISIBLE,WS_EX_TRANSPARENT\r\n    LISTBOX         IDC_LOGS,14,74,365,89,LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | NOT WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP\r\n    CONTROL         \"Show Logs\",IDC_SHOW_LOGS,\"Button\",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,7,175,50,14\r\n    PUSHBUTTON      \"&Cancel\",IDC_CANCEL,336,175,50,14,NOT WS_VISIBLE\r\n    PUSHBUTTON      \"Export iTunes Backup\",IDC_EXP_ITNS,72,175,80,14,NOT WS_VISIBLE\r\n    PUSHBUTTON      \"&Export\",IDC_EXPORT,279,175,50,14\r\n    PUSHBUTTON      \"Close\",IDC_CLOSE,336,175,50,14\r\nEND\r\n\r\nIDD_ABOUTBOX DIALOGEX 0, 0, 187, 102\r\nSTYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU\r\nCAPTION \"About\"\r\nFONT 9, \"Segoe UI\", 0, 0, 0x0\r\nBEGIN\r\n    DEFPUSHBUTTON   \"OK\",IDOK,130,81,50,14\r\n    CTEXT           \"WechatExporter Application v1.0\\n\\n(c) Copyright 2020\",IDC_VERSION,25,57,78,32\r\n    ICON            IDR_MAINFRAME,IDC_STATIC,55,26,20,20\r\n    GROUPBOX        \"\",IDC_STATIC,7,7,115,88\r\nEND\r\n\r\nIDD_BACKUP_DLG DIALOGEX 0, 0, 328, 73\r\nSTYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION\r\nCAPTION \"Export WeChat Data\"\r\nFONT 8, \"MS Shell Dlg\", 400, 0, 0x1\r\nBEGIN\r\n    PUSHBUTTON      \"Cancel\",IDCANCEL,271,43,50,14\r\n    CONTROL         \"\",IDC_PROGRESS1,\"msctls_progress32\",WS_BORDER,7,43,239,14\r\n    LTEXT           \"Exporting WeChat data from device... \",IDC_TITLE,7,7,314,27\r\nEND\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// DESIGNINFO\r\n//\r\n\r\n#ifdef APSTUDIO_INVOKED\r\nGUIDELINES DESIGNINFO\r\nBEGIN\r\n    IDD_WECHATEXPORTER_FORM, DIALOG\r\n    BEGIN\r\n        LEFTMARGIN, 7\r\n        RIGHTMARGIN, 386\r\n        TOPMARGIN, 7\r\n        BOTTOMMARGIN, 189\r\n    END\r\n\r\n    IDD_ABOUTBOX, DIALOG\r\n    BEGIN\r\n        LEFTMARGIN, 7\r\n        RIGHTMARGIN, 180\r\n        TOPMARGIN, 7\r\n        BOTTOMMARGIN, 95\r\n    END\r\n\r\n    IDD_BACKUP_DLG, DIALOG\r\n    BEGIN\r\n        LEFTMARGIN, 7\r\n        RIGHTMARGIN, 321\r\n        TOPMARGIN, 7\r\n        BOTTOMMARGIN, 66\r\n    END\r\nEND\r\n#endif    // APSTUDIO_INVOKED\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// AFX_DIALOG_LAYOUT\r\n//\r\n\r\nIDD_WECHATEXPORTER_FORM AFX_DIALOG_LAYOUT\r\nBEGIN\r\n    0\r\nEND\r\n\r\nIDD_ABOUTBOX AFX_DIALOG_LAYOUT\r\nBEGIN\r\n    0\r\nEND\r\n\r\nIDD_BACKUP_DLG AFX_DIALOG_LAYOUT\r\nBEGIN\r\n    0\r\nEND\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// Icon\r\n//\r\n\r\n// Icon with lowest ID value placed first to ensure application icon\r\n// remains consistent on all systems.\r\nIDR_MAINFRAME           ICON                    \"res\\\\idr_main.ico\"\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// Version\r\n//\r\n\r\nVS_VERSION_INFO VERSIONINFO\r\n FILEVERSION 1,9,6,0\r\n PRODUCTVERSION 1,9,6,0\r\n FILEFLAGSMASK 0x3fL\r\n#ifdef _DEBUG\r\n FILEFLAGS 0x1L\r\n#else\r\n FILEFLAGS 0x0L\r\n#endif\r\n FILEOS 0x4L\r\n FILETYPE 0x2L\r\n FILESUBTYPE 0x0L\r\nBEGIN\r\n    BLOCK \"StringFileInfo\"\r\n    BEGIN\r\n        BLOCK \"000904b0\"\r\n        BEGIN\r\n            VALUE \"FileDescription\", \"WechatExporter Module\"\r\n            VALUE \"FileVersion\", \"1.9.6.0\"\r\n            VALUE \"InternalName\", \"WechatExporter\"\r\n            VALUE \"LegalCopyright\", \"Copyright 2020\"\r\n            VALUE \"OriginalFilename\", \"WechatExporter.exe\"\r\n            VALUE \"ProductName\", \"WechatExporter Module\"\r\n            VALUE \"ProductVersion\", \"1.9.6.0\"\r\n        END\r\n    END\r\n    BLOCK \"VarFileInfo\"\r\n    BEGIN\r\n        VALUE \"Translation\", 0x9, 1200\r\n    END\r\nEND\r\n\r\n#endif    // English resources\r\n/////////////////////////////////////////////////////////////////////////////\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n// English (United States) resources\r\n\r\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\r\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\r\n#pragma code_page(1252)\r\n\r\n#ifdef APSTUDIO_INVOKED\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// TEXTINCLUDE\r\n//\r\n\r\n1 TEXTINCLUDE \r\nBEGIN\r\n    \"resource.h\\0\"\r\nEND\r\n\r\n2 TEXTINCLUDE \r\nBEGIN\r\n    \"#include \"\"atlres.h\"\"\\r\\n\"\r\n    \"\\0\"\r\nEND\r\n\r\n3 TEXTINCLUDE \r\nBEGIN\r\n    \"\\0\"\r\nEND\r\n\r\n#endif    // APSTUDIO_INVOKED\r\n\r\n\r\n/////////////////////////////////////////////////////////////////////////////\r\n//\r\n// String Table\r\n//\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    IDR_MAINFRAME           \"Wechat Exporter\"\r\n    IDS_SEL_BACKUP_DIR      \"Please choose an iTunes backup directory.\"\r\n    IDS_SEL_OUTPUT_DIR      \"Please choose an output directory.\"\r\n    IDS_CANCEL_PROMPT       \"Sure to cancel the exporting?\"\r\n    IDS_STATIC_BACKUP       \"iTunes Backup Directory:(%s)\"\r\n    IDS_SESSION_IDX         \"No.\"\r\n    IDS_SESSION_NAME        \"Name\"\r\n    IDS_SESSION_COUNT       \"Number of Msgs\"\r\n    IDS_ALL_USERS           \"All Wechat Accounts\"\r\n    IDS_SESSION_USER        \"Wechat Account\"\r\n    IDS_ITUNES_VERSION      \"iTunes Installed, Version:%s\"\r\n    IDS_ITUNES_NOT_INSTALLED \"iTunes NOT Installed\"\r\n    IDS_ENC_BKP_NOT_SUPPORTED \"Encrypted iTunes Backup is not supported.\"\r\n    IDS_FAILED_TO_LOAD_BKP  \"Failed to load iTunes backup data.\"\r\n    IDS_INVALID_OUTPUT_DIR  \"The output directory is invalid.\"\r\n    IDS_TOOLTIP_LOGS        \"Ctrl+A/Ctrl+C Copy Logs\"\r\nEND\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    IDS_NEW_VERSION         \"Find a new version:%s, Download it now?\"\r\n    IDS_SESSION_NUMBER      \"NO.\"\r\n    IDS_SESSION_STATUS      \"Status\"\r\n    IDS_SESSION_DONE        \"Done\"\r\n    IDS_SESSION_PROGRESS    \"%u / %u\"\r\n    IDS_SESSION_CANCELLED   \"Cancelled\"\r\n    IDS_SHOW_LOGS           \"Show Logs\"\r\n    IDS_HIDE_LOGS           \"Hide Logs\"\r\n    IDS_VERSIONS            \"iTunes Version: %s, iOS Version: %s, Wechat Version: %s\"\r\n    IDS_NO_PREV_EXP         \"There is no previous exporting in output directory and \"\"Incremental Exporting\"\" will be ignored.\"\r\n    IDS_PREV_EXP_FOUND      \"The previous exporting at %s found, will reuse previous options.\"\r\n    IDS_DELETED_SESSION     \"(Deleted)\"\r\n    IDS_EXPORTING_MSGS      \"Exporting Messages (%d%%)\"\r\n    IDS_DOWNLOADING_EMOJI   \"Downloading Emoji Files, Remaing: %d\"\r\n    IDS_EXP_ITNS            \"Export iTunes Backup Files of WeChat\"\r\n    IDS_SESSION_LAST_MSG    \"Last Message\"\r\nEND\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    ATL_IDS_IDLEMESSAGE     \"Ready\"\r\nEND\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    ID_APP_EXIT             \"Quit the application; prompts to save documents\\nExit\"\r\nEND\r\n\r\nSTRINGTABLE\r\nBEGIN\r\n    IDS_INVLD_INCEXP_FOR_MULTI_USERS \r\n                            \"Incremental Exporting will be invalid for multiple WeChat accounts as for wrong design. \\r\\nPlease choose another directory for exporting.\"\r\nEND\r\n\r\n#endif    // English (United States) resources\r\n/////////////////////////////////////////////////////////////////////////////\r\n\r\n\r\n"
  },
  {
    "path": "vcproject/WechatExporter.sln",
    "content": "﻿\r\nMicrosoft Visual Studio Solution File, Format Version 12.00\r\n# Visual Studio 15\r\nVisualStudioVersion = 15.0.28307.1259\r\nMinimumVisualStudioVersion = 10.0.40219.1\r\nProject(\"{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}\") = \"WechatExporter\", \"WechatExporter.vcxproj\", \"{F57219E8-8B1F-41E3-B553-03C1884E8E4D}\"\r\nEndProject\r\nProject(\"{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}\") = \"WechatExporterCmd\", \"WechatExporterCmd.vcxproj\", \"{C0637D9A-3C23-495C-B661-5FDCD2169583}\"\r\nEndProject\r\nGlobal\r\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n\t\tDebug|x64 = Debug|x64\r\n\t\tDebug|x86 = Debug|x86\r\n\t\tRelease|x64 = Release|x64\r\n\t\tRelease|x86 = Release|x86\r\n\tEndGlobalSection\r\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n\t\t{F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Debug|x64.ActiveCfg = Debug|x64\r\n\t\t{F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Debug|x64.Build.0 = Debug|x64\r\n\t\t{F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Debug|x86.ActiveCfg = Debug|Win32\r\n\t\t{F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Debug|x86.Build.0 = Debug|Win32\r\n\t\t{F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Release|x64.ActiveCfg = Release|x64\r\n\t\t{F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Release|x64.Build.0 = Release|x64\r\n\t\t{F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Release|x86.ActiveCfg = Release|Win32\r\n\t\t{F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Release|x86.Build.0 = Release|Win32\r\n\t\t{C0637D9A-3C23-495C-B661-5FDCD2169583}.Debug|x64.ActiveCfg = Debug|x64\r\n\t\t{C0637D9A-3C23-495C-B661-5FDCD2169583}.Debug|x64.Build.0 = Debug|x64\r\n\t\t{C0637D9A-3C23-495C-B661-5FDCD2169583}.Debug|x86.ActiveCfg = Debug|Win32\r\n\t\t{C0637D9A-3C23-495C-B661-5FDCD2169583}.Debug|x86.Build.0 = Debug|Win32\r\n\t\t{C0637D9A-3C23-495C-B661-5FDCD2169583}.Release|x64.ActiveCfg = Release|x64\r\n\t\t{C0637D9A-3C23-495C-B661-5FDCD2169583}.Release|x64.Build.0 = Release|x64\r\n\t\t{C0637D9A-3C23-495C-B661-5FDCD2169583}.Release|x86.ActiveCfg = Release|Win32\r\n\t\t{C0637D9A-3C23-495C-B661-5FDCD2169583}.Release|x86.Build.0 = Release|Win32\r\n\tEndGlobalSection\r\n\tGlobalSection(SolutionProperties) = preSolution\r\n\t\tHideSolutionNode = FALSE\r\n\tEndGlobalSection\r\n\tGlobalSection(ExtensibilityGlobals) = postSolution\r\n\t\tSolutionGuid = {D4BEDF3C-C880-4E96-939D-8D1BCBEF9ED3}\r\n\tEndGlobalSection\r\nEndGlobal\r\n"
  },
  {
    "path": "vcproject/WechatExporter.vcxproj",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<Project DefaultTargets=\"Build\" ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\r\n  <ItemGroup Label=\"ProjectConfigurations\">\r\n    <ProjectConfiguration Include=\"Debug|Win32\">\r\n      <Configuration>Debug</Configuration>\r\n      <Platform>Win32</Platform>\r\n    </ProjectConfiguration>\r\n    <ProjectConfiguration Include=\"Release|Win32\">\r\n      <Configuration>Release</Configuration>\r\n      <Platform>Win32</Platform>\r\n    </ProjectConfiguration>\r\n    <ProjectConfiguration Include=\"Debug|x64\">\r\n      <Configuration>Debug</Configuration>\r\n      <Platform>x64</Platform>\r\n    </ProjectConfiguration>\r\n    <ProjectConfiguration Include=\"Release|x64\">\r\n      <Configuration>Release</Configuration>\r\n      <Platform>x64</Platform>\r\n    </ProjectConfiguration>\r\n  </ItemGroup>\r\n  <PropertyGroup Label=\"Globals\">\r\n    <VCProjectVersion>15.0</VCProjectVersion>\r\n    <ProjectGuid>{F57219E8-8B1F-41E3-B553-03C1884E8E4D}</ProjectGuid>\r\n    <WindowsTargetPlatformVersion>10.0.17763.0</WindowsTargetPlatformVersion>\r\n    <ProjectName />\r\n  </PropertyGroup>\r\n  <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.Default.props\" />\r\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|Win32'\" Label=\"Configuration\">\r\n    <ConfigurationType>Application</ConfigurationType>\r\n    <UseDebugLibraries>true</UseDebugLibraries>\r\n    <PlatformToolset>v141</PlatformToolset>\r\n    <CharacterSet>Unicode</CharacterSet>\r\n  </PropertyGroup>\r\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|Win32'\" Label=\"Configuration\">\r\n    <ConfigurationType>Application</ConfigurationType>\r\n    <UseDebugLibraries>false</UseDebugLibraries>\r\n    <PlatformToolset>v141</PlatformToolset>\r\n    <CharacterSet>Unicode</CharacterSet>\r\n  </PropertyGroup>\r\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\" Label=\"Configuration\">\r\n    <ConfigurationType>Application</ConfigurationType>\r\n    <UseDebugLibraries>true</UseDebugLibraries>\r\n    <PlatformToolset>v141</PlatformToolset>\r\n    <CharacterSet>Unicode</CharacterSet>\r\n  </PropertyGroup>\r\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\" Label=\"Configuration\">\r\n    <ConfigurationType>Application</ConfigurationType>\r\n    <UseDebugLibraries>false</UseDebugLibraries>\r\n    <PlatformToolset>v141</PlatformToolset>\r\n    <CharacterSet>Unicode</CharacterSet>\r\n  </PropertyGroup>\r\n  <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.props\" />\r\n  <ImportGroup Label=\"ExtensionSettings\">\r\n  </ImportGroup>\r\n  <ImportGroup Label=\"Shared\">\r\n  </ImportGroup>\r\n  <ImportGroup Label=\"PropertySheets\" Condition=\"'$(Configuration)|$(Platform)'=='Debug|Win32'\">\r\n    <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" Label=\"LocalAppDataPlatform\" />\r\n  </ImportGroup>\r\n  <ImportGroup Label=\"PropertySheets\" Condition=\"'$(Configuration)|$(Platform)'=='Release|Win32'\">\r\n    <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" Label=\"LocalAppDataPlatform\" />\r\n  </ImportGroup>\r\n  <ImportGroup Label=\"PropertySheets\" Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\">\r\n    <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" Label=\"LocalAppDataPlatform\" />\r\n  </ImportGroup>\r\n  <ImportGroup Label=\"PropertySheets\" Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\">\r\n    <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" Label=\"LocalAppDataPlatform\" />\r\n  </ImportGroup>\r\n  <PropertyGroup Label=\"UserMacros\" />\r\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|Win32'\">\r\n    <LinkIncremental>true</LinkIncremental>\r\n    <LibraryPath>$(ProjectDir)..\\releases\\windows-static\\x86\\dbg\\lib;$(LibraryPath)</LibraryPath>\r\n    <IncludePath>$(ProjectDir)..\\releases\\windows-static\\include;$(ProjectDir)..\\WechatExporter\\core;$(IncludePath)</IncludePath>\r\n    <OutDir>$(SolutionDir)$(Platform)\\$(Configuration)\\</OutDir>\r\n    <IntDir>$(Platform)\\$(Configuration)\\</IntDir>\r\n  </PropertyGroup>\r\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\">\r\n    <LinkIncremental>true</LinkIncremental>\r\n    <ExecutablePath>$(ExecutablePath)</ExecutablePath>\r\n    <IncludePath>$(ProjectDir)..\\WechatExporter\\core;$(ProjectDir)..\\releases\\windows-libs\\include;$(IncludePath)</IncludePath>\r\n    <LibraryPath>$(ProjectDir)..\\releases\\windows-libs\\x64\\dbg\\lib;$(LibraryPath)</LibraryPath>\r\n  </PropertyGroup>\r\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|Win32'\">\r\n    <LinkIncremental>false</LinkIncremental>\r\n    <LibraryPath>$(ProjectDir)..\\releases\\windows-static\\x86\\lib;$(LibraryPath)</LibraryPath>\r\n    <IncludePath>$(ProjectDir)..\\releases\\windows-static\\include;$(ProjectDir)..\\WechatExporter\\core;$(IncludePath)</IncludePath>\r\n    <OutDir>$(SolutionDir)$(Platform)\\$(Configuration)\\</OutDir>\r\n    <IntDir>$(Platform)\\$(Configuration)\\</IntDir>\r\n  </PropertyGroup>\r\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\">\r\n    <LinkIncremental>false</LinkIncremental>\r\n    <LibraryPath>$(ProjectDir)..\\releases\\windows-libs\\x64\\rel\\lib;$(LibraryPath)</LibraryPath>\r\n    <IncludePath>$(ProjectDir)..\\WechatExporter\\core;$(ProjectDir)..\\releases\\windows-libs\\include;$(IncludePath)</IncludePath>\r\n  </PropertyGroup>\r\n  <PropertyGroup Label=\"Vcpkg\" Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\">\r\n    <VcpkgEnabled>false</VcpkgEnabled>\r\n    <VcpkgUseStatic>true</VcpkgUseStatic>\r\n  </PropertyGroup>\r\n  <PropertyGroup Label=\"Vcpkg\" Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\">\r\n    <VcpkgEnabled>false</VcpkgEnabled>\r\n  </PropertyGroup>\r\n  <ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|Win32'\">\r\n    <ClCompile>\r\n      <PrecompiledHeader>NotUsing</PrecompiledHeader>\r\n      <WarningLevel>Level3</WarningLevel>\r\n      <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>\r\n      <DebugInformationFormat>EditAndContinue</DebugInformationFormat>\r\n      <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>\r\n      <Optimization>Disabled</Optimization>\r\n      <PreprocessorDefinitions>WIN32;_WINDOWS;STRICT;_DEBUG;CURL_STATICLIB;COMPILE_SDK;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n    </ClCompile>\r\n    <Link>\r\n      <SubSystem>Windows</SubSystem>\r\n      <GenerateDebugInformation>true</GenerateDebugInformation>\r\n      <AdditionalDependencies>Shell32.lib;Ws2_32.lib;Wldap32.lib;Version.lib;Crypt32.lib;Urlmon.lib;libprotobufd.lib;libcrypto.lib;libcharset.lib;libiconv.lib;lzmad.lib;zlibd.lib;libssl.lib;libcurl-d.lib;libxml2.lib;sqlite3.lib;jsoncpp.lib;SKP_Silk_FIX.lib;libmp3lame-static.lib;libmpghip-static.lib;libplist.lib;%(AdditionalDependencies)</AdditionalDependencies>\r\n    </Link>\r\n    <ResourceCompile>\r\n      <Culture>0x0409</Culture>\r\n      <AdditionalIncludeDirectories>$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>\r\n      <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n    </ResourceCompile>\r\n    <Midl>\r\n      <MkTypLibCompatible>false</MkTypLibCompatible>\r\n      <TargetEnvironment>Win32</TargetEnvironment>\r\n      <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n      <HeaderFileName>WechatExporter.h</HeaderFileName>\r\n      <InterfaceIdentifierFileName>WechatExporter_i.c</InterfaceIdentifierFileName>\r\n      <ProxyFileName>WechatExporter_p.c</ProxyFileName>\r\n      <GenerateStublessProxies>true</GenerateStublessProxies>\r\n      <TypeLibraryName>$(IntDir)/WechatExporter.tlb</TypeLibraryName>\r\n      <DllDataFileName />\r\n    </Midl>\r\n    <PostBuildEvent>\r\n      <Command>if not exist \"$(OutDir)res\" mkdir \"$(OutDir)res\"\r\nxcopy \"$(ProjectDir)..\\WechatExporter\\res\"  \"$(OutDir)res\"  /Y /S /E</Command>\r\n    </PostBuildEvent>\r\n  </ItemDefinitionGroup>\r\n  <ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\">\r\n    <ClCompile>\r\n      <WarningLevel>Level3</WarningLevel>\r\n      <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>\r\n      <DebugInformationFormat>EditAndContinue</DebugInformationFormat>\r\n      <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>\r\n      <Optimization>Disabled</Optimization>\r\n      <PreprocessorDefinitions>_WINDOWS;STRICT;_DEBUG;CURL_STATICLIB;COMPILE_SDK;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n      <AdditionalOptions>/source-charset:utf-8 %(AdditionalOptions)</AdditionalOptions>\r\n      <PrecompiledHeader>NotUsing</PrecompiledHeader>\r\n      <AssemblerOutput>NoListing</AssemblerOutput>\r\n    </ClCompile>\r\n    <Link>\r\n      <SubSystem>Windows</SubSystem>\r\n      <GenerateDebugInformation>true</GenerateDebugInformation>\r\n      <AdditionalDependencies>Shell32.lib;Ws2_32.lib;Wldap32.lib;Version.lib;Crypt32.lib;Urlmon.lib;Rpcrt4.lib;libprotobufd.lib;libcrypto.lib;charset.lib;iconv.lib;lzmad.lib;zlibd.lib;libssl.lib;libcurl-d.lib;libxml2.lib;sqlite3.lib;jsoncpp.lib;libamr.lib;SKP_Silk_FIX.lib;libmp3lame.lib;libplist-2.0.lib;libimobiledevice-1.0.lib;%(AdditionalDependencies)</AdditionalDependencies>\r\n      <DelayLoadDLLs>libcrypto-1_1-x64.dll;libssl-1_1-x64.dll;libimobiledevice-1.0.dll;libplist-2.0.dll;libmp3lame.dll;iconv-2.dll;lzmad.dll</DelayLoadDLLs>\r\n    </Link>\r\n    <ResourceCompile>\r\n      <Culture>0x0409</Culture>\r\n      <AdditionalIncludeDirectories>$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>\r\n      <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n    </ResourceCompile>\r\n    <Midl>\r\n      <MkTypLibCompatible>false</MkTypLibCompatible>\r\n      <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n      <HeaderFileName>WechatExporter.h</HeaderFileName>\r\n      <InterfaceIdentifierFileName>WechatExporter_i.c</InterfaceIdentifierFileName>\r\n      <ProxyFileName>WechatExporter_p.c</ProxyFileName>\r\n      <GenerateStublessProxies>true</GenerateStublessProxies>\r\n      <TypeLibraryName>$(IntDir)/WechatExporter.tlb</TypeLibraryName>\r\n      <DllDataFileName />\r\n    </Midl>\r\n    <PostBuildEvent>\r\n      <Command>if not exist \"$(OutDir)res\" mkdir \"$(OutDir)res\"\r\nxcopy \"$(ProjectDir)..\\WechatExporter\\res\"  \"$(OutDir)res\"  /Y /S /E /D\r\n\r\nif not exist \"$(OutDir)Dlls\" mkdir \"$(OutDir)Dlls\"\r\nxcopy \"$(ProjectDir)..\\releases\\windows-libs\\x64\\dbg\\bin\\*.dll\"  \"$(OutDir)Dlls\"  /Y /S /E /D\r\nxcopy \"$(ProjectDir)..\\releases\\windows-libs\\x64\\dbg\\bin\\*.pdb\"  \"$(OutDir)Dlls\\\"  /Y /S /E /D</Command>\r\n    </PostBuildEvent>\r\n  </ItemDefinitionGroup>\r\n  <ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|Win32'\">\r\n    <ClCompile>\r\n      <PrecompiledHeader>NotUsing</PrecompiledHeader>\r\n      <WarningLevel>Level3</WarningLevel>\r\n      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>\r\n      <ExceptionHandling />\r\n      <DebugInformationFormat />\r\n      <PreprocessorDefinitions>WIN32;_WINDOWS;STRICT;NDEBUG;CURL_STATICLIB;COMPILE_SDK;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n    </ClCompile>\r\n    <Link>\r\n      <SubSystem>Windows</SubSystem>\r\n      <AdditionalDependencies>Shell32.lib;Ws2_32.lib;Wldap32.lib;Version.lib;Crypt32.lib;Urlmon.lib;libprotobuf.lib;libcrypto.lib;libcharset.lib;libiconv.lib;zlib.lib;lzma.lib;libssl.lib;libcurl.lib;libxml2.lib;sqlite3.lib;jsoncpp.lib;SKP_Silk_FIX.lib;libmp3lame-static.lib;libmpghip-static.lib;libplist.lib;%(AdditionalDependencies)</AdditionalDependencies>\r\n    </Link>\r\n    <ResourceCompile>\r\n      <Culture>0x0409</Culture>\r\n      <AdditionalIncludeDirectories>$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>\r\n      <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n    </ResourceCompile>\r\n    <Midl>\r\n      <MkTypLibCompatible>false</MkTypLibCompatible>\r\n      <TargetEnvironment>Win32</TargetEnvironment>\r\n      <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n      <HeaderFileName>WechatExporter.h</HeaderFileName>\r\n      <InterfaceIdentifierFileName>WechatExporter_i.c</InterfaceIdentifierFileName>\r\n      <ProxyFileName>WechatExporter_p.c</ProxyFileName>\r\n      <GenerateStublessProxies>true</GenerateStublessProxies>\r\n      <TypeLibraryName>$(IntDir)/WechatExporter.tlb</TypeLibraryName>\r\n      <DllDataFileName />\r\n    </Midl>\r\n    <PostBuildEvent>\r\n      <Command>if not exist \"$(OutDir)res\" mkdir \"$(OutDir)res\"\r\nxcopy \"$(ProjectDir)..\\WechatExporter\\res\"  \"$(OutDir)res\"  /Y /S /E</Command>\r\n    </PostBuildEvent>\r\n  </ItemDefinitionGroup>\r\n  <ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\">\r\n    <ClCompile>\r\n      <WarningLevel>Level3</WarningLevel>\r\n      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>\r\n      <ExceptionHandling />\r\n      <DebugInformationFormat />\r\n      <PreprocessorDefinitions>_WINDOWS;STRICT;NDEBUG;CURL_STATICLIB;COMPILE_SDK;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n      <AssemblerOutput>NoListing</AssemblerOutput>\r\n    </ClCompile>\r\n    <Link>\r\n      <SubSystem>Windows</SubSystem>\r\n      <AdditionalDependencies>Shell32.lib;Ws2_32.lib;Wldap32.lib;Version.lib;Crypt32.lib;Urlmon.lib;Rpcrt4.lib;libprotobuf.lib;libcrypto.lib;charset.lib;iconv.lib;zlib.lib;lzma.lib;libssl.lib;libcurl.lib;libxml2.lib;sqlite3.lib;jsoncpp.lib;libamr.lib;SKP_Silk_FIX.lib;libmp3lame.lib;libplist-2.0.lib;libimobiledevice-1.0.lib;%(AdditionalDependencies)</AdditionalDependencies>\r\n      <DelayLoadDLLs>libcrypto-1_1-x64.dll;libssl-1_1-x64.dll;libimobiledevice-1.0.dll;libplist-2.0.dll;libmp3lame.dll;iconv-2.dll;lzma.dll</DelayLoadDLLs>\r\n    </Link>\r\n    <ResourceCompile>\r\n      <Culture>0x0409</Culture>\r\n      <AdditionalIncludeDirectories>$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>\r\n      <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n    </ResourceCompile>\r\n    <Midl>\r\n      <MkTypLibCompatible>false</MkTypLibCompatible>\r\n      <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>\r\n      <HeaderFileName>WechatExporter.h</HeaderFileName>\r\n      <InterfaceIdentifierFileName>WechatExporter_i.c</InterfaceIdentifierFileName>\r\n      <ProxyFileName>WechatExporter_p.c</ProxyFileName>\r\n      <GenerateStublessProxies>true</GenerateStublessProxies>\r\n      <TypeLibraryName>$(IntDir)/WechatExporter.tlb</TypeLibraryName>\r\n      <DllDataFileName />\r\n    </Midl>\r\n    <PostBuildEvent>\r\n      <Command>if not exist \"$(OutDir)res\" mkdir \"$(OutDir)res\"\r\nxcopy \"$(ProjectDir)..\\WechatExporter\\res\"  \"$(OutDir)res\"  /Y /S /E /D\r\n\r\nxcopy \"$(ProjectDir)..\\WechatExporter\\LICENSES\"  \"$(OutDir)LICENSES\"  /Y /S /E /D /I\r\n\r\nif not exist \"$(OutDir)Dlls\" mkdir \"$(OutDir)Dlls\"\r\nxcopy \"$(ProjectDir)..\\releases\\windows-libs\\x64\\rel\\bin\\*.dll\"  \"$(OutDir)Dlls\\\"  /Y /D</Command>\r\n    </PostBuildEvent>\r\n  </ItemDefinitionGroup>\r\n  <ItemGroup>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\AsyncExecutor.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\AsyncTask.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Downloader.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Exporter.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\FileSystem.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\IDeviceBackup.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\ITunesParser.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\MessageParser.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\RawMessage.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\ResManager.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\TaskManager.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Template.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Updater.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_audio.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_md5.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_protobuf.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_silk.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_thread.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\WechatParser.cpp\" />\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\XmlParser.cpp\" />\r\n    <ClCompile Include=\"AppConfiguration.cpp\" />\r\n    <ClCompile Include=\"stdafx.cpp\">\r\n      <PrecompiledHeader Condition=\"'$(Configuration)|$(Platform)'=='Debug|Win32'\">Create</PrecompiledHeader>\r\n      <PrecompiledHeader Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\">Create</PrecompiledHeader>\r\n      <PrecompiledHeader Condition=\"'$(Configuration)|$(Platform)'=='Release|Win32'\">Create</PrecompiledHeader>\r\n      <PrecompiledHeader Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\">Create</PrecompiledHeader>\r\n    </ClCompile>\r\n    <ClCompile Include=\"ViewHelper.cpp\" />\r\n    <ClCompile Include=\"WechatExporter.cpp\" />\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\AsyncExecutor.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\AsyncTask.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Downloader.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportContext.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Exporter.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportNotifier.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportOption.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\FileSystem.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\IDeviceBackup.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ITunesParser.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Logger.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\MbdbReader.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\MessageParser.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\RawMessage.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ResManager.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\semaphore.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\TaskManager.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Template.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Updater.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Utils.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatObjects.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatParser.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatSource.h\" />\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\XmlParser.h\" />\r\n    <ClInclude Include=\"AboutDlg.h\" />\r\n    <ClInclude Include=\"ColoredControls.h\" />\r\n    <ClInclude Include=\"Core.h\" />\r\n    <ClInclude Include=\"AppConfiguration.h\" />\r\n    <ClInclude Include=\"PdfConverterImpl.h\" />\r\n    <ClInclude Include=\"BackupDlg.h\" />\r\n    <ClInclude Include=\"ProgressListViewCtrl.h\" />\r\n    <ClInclude Include=\"ExportNotifierImpl.h\" />\r\n    <ClInclude Include=\"ITunesDetector.h\" />\r\n    <ClInclude Include=\"LoggerImpl.h\" />\r\n    <ClInclude Include=\"LogListBox.h\" />\r\n    <ClInclude Include=\"MainFrm.h\" />\r\n    <ClInclude Include=\"resource.h\" />\r\n    <ClInclude Include=\"stdafx.h\" />\r\n    <ClInclude Include=\"TextProgressBarCtrl.h\" />\r\n    <ClInclude Include=\"ToolTipButton.h\" />\r\n    <ClInclude Include=\"VersionDetector.h\" />\r\n    <ClInclude Include=\"View.h\" />\r\n    <ClInclude Include=\"ViewHelper.h\" />\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <ResourceCompile Include=\"WechatExporter.rc\" />\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <Image Include=\"res\\idr_main.ico\" />\r\n    <Image Include=\"res\\iPhone.ico\" />\r\n    <Image Include=\"res\\iTunes.ico\" />\r\n    <Image Include=\"res\\toolbar.bmp\" />\r\n    <Image Include=\"res\\WechatExporter.ico\" />\r\n  </ItemGroup>\r\n  <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.targets\" />\r\n  <ImportGroup Label=\"ExtensionTargets\">\r\n  </ImportGroup>\r\n</Project>"
  },
  {
    "path": "vcproject/WechatExporter.vcxproj.filters",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\r\n  <ItemGroup>\r\n    <Filter Include=\"Source Files\">\r\n      <UniqueIdentifier>{ecd560c4-f70e-440e-ace2-0ff312561b22}</UniqueIdentifier>\r\n      <Extensions>cpp;c;cxx;def;odl;idl;hpj;bat;asm</Extensions>\r\n    </Filter>\r\n    <Filter Include=\"Header Files\">\r\n      <UniqueIdentifier>{235b586f-6b11-48f8-9ef6-99f6b8fb9470}</UniqueIdentifier>\r\n      <Extensions>h;hpp;hxx;hm;inl;inc</Extensions>\r\n    </Filter>\r\n    <Filter Include=\"Resource Files\">\r\n      <UniqueIdentifier>{3a67dad0-ed25-40b7-9241-a759a966ea27}</UniqueIdentifier>\r\n      <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;jpg;jpeg;jpe;manifest</Extensions>\r\n    </Filter>\r\n    <Filter Include=\"core\">\r\n      <UniqueIdentifier>{acf6df2f-565c-4832-a716-eb5f91d982e1}</UniqueIdentifier>\r\n    </Filter>\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <ClCompile Include=\"stdafx.cpp\">\r\n      <Filter>Source Files</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"WechatExporter.cpp\">\r\n      <Filter>Source Files</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Exporter.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\ITunesParser.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\RawMessage.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_md5.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_protobuf.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\WechatParser.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_audio.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_silk.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Downloader.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\XmlParser.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_thread.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"AppConfiguration.cpp\">\r\n      <Filter>Source Files</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\MessageParser.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\FileSystem.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Updater.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"ViewHelper.cpp\">\r\n      <Filter>Source Files</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\AsyncExecutor.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\AsyncTask.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\TaskManager.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\ResManager.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\IDeviceBackup.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n    <ClCompile Include=\"..\\WechatExporter\\core\\Template.cpp\">\r\n      <Filter>core</Filter>\r\n    </ClCompile>\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <ClInclude Include=\"stdafx.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"MainFrm.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"View.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"AboutDlg.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"resource.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Exporter.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ITunesParser.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Logger.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\RawMessage.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Utils.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatObjects.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatParser.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"LoggerImpl.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"Core.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\semaphore.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportNotifier.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"ExportNotifierImpl.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Downloader.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\XmlParser.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"ColoredControls.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"LogListBox.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"ITunesDetector.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"VersionDetector.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"AppConfiguration.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\MessageParser.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\FileSystem.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Updater.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"ViewHelper.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"ProgressListViewCtrl.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"PdfConverterImpl.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\AsyncExecutor.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\AsyncTask.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\TaskManager.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\MbdbReader.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"TextProgressBarCtrl.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ResManager.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"ToolTipButton.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\IDeviceBackup.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatSource.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"BackupDlg.h\">\r\n      <Filter>Header Files</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportContext.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\Template.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportOption.h\">\r\n      <Filter>core</Filter>\r\n    </ClInclude>\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <ResourceCompile Include=\"WechatExporter.rc\">\r\n      <Filter>Resource Files</Filter>\r\n    </ResourceCompile>\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <Image Include=\"res\\toolbar.bmp\">\r\n      <Filter>Resource Files</Filter>\r\n    </Image>\r\n    <Image Include=\"res\\WechatExporter.ico\">\r\n      <Filter>Resource Files</Filter>\r\n    </Image>\r\n    <Image Include=\"res\\idr_main.ico\">\r\n      <Filter>Resource Files</Filter>\r\n    </Image>\r\n    <Image Include=\"res\\iPhone.ico\">\r\n      <Filter>Resource Files</Filter>\r\n    </Image>\r\n    <Image Include=\"res\\iTunes.ico\">\r\n      <Filter>Resource Files</Filter>\r\n    </Image>\r\n  </ItemGroup>\r\n</Project>"
  },
  {
    "path": "vcproject/WechatExporterCmd.cpp",
    "content": "// WechatExporterCmd.cpp : This file contains the 'main' function. Program execution begins and ends there.\n//\n\n#include <iostream>\n#include <string>\n#include <vector>\n\n\n#include <windows.h>\n#include <shlwapi.h>\n#include <string.h>\n#include <tchar.h>\n#include <atlstr.h>\n#include <fcntl.h>\n#include \"Core.h\"\n#include <WechatExporterCmd.h>\n\nstd::string getCurrentLanguageCode();\n\nstatic const WCHAR UNICODE_BOM = 0xFEFF;\n\nvoid UPrint(LPCWSTR String) {\n\n#ifndef NDEBUG\n\t// std::wcout << String;\n\t// return;\n#endif\n\n\tDWORD ConsoleMode;\n\tBOOL ConsoleOutput;\n\tDWORD FileType;\n\tBOOL Result;\n\tHANDLE StdOut;\n\tDWORD StringCharCount;\n\tDWORD Written;\n\n\t//\n\t// StdOut describes the standard output device.  This can be the console\n\t// or (if output has been redirected) a file or some other device type.\n\t//\n\tStdOut = GetStdHandle(STD_OUTPUT_HANDLE);\n\n\tif (StdOut == INVALID_HANDLE_VALUE) {\n\t\tgoto PrintExit;\n\t}\n\n\t//\n\t// Check whether the handle describes a character device.  If it does, then\n\t// it may be a console device.  A call to GetConsoleMode will fail with\n\t// ERROR_INVALID_HANDLE if it is not a console device.\n\t//\n\tFileType = GetFileType(StdOut);\n\n\tif ((FileType == FILE_TYPE_UNKNOWN) && (GetLastError() != ERROR_SUCCESS))\n\t{\n\t\tgoto PrintExit;\n\t}\n\n\tFileType &= ~(FILE_TYPE_REMOTE);\n\n\tif (FileType == FILE_TYPE_CHAR)\n\t{\n\t\tResult = GetConsoleMode(StdOut, &ConsoleMode);\n\n\t\tif ((Result == FALSE) && (GetLastError() == ERROR_INVALID_HANDLE))\n\t\t{\n\t\t\tConsoleOutput = FALSE;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tConsoleOutput = TRUE;\n\t\t}\n\t}\n\telse\n\t{\n\t\tConsoleOutput = FALSE;\n\t}\n\n\t//\n\t// If StdOut is a console device then just use the UNICODE console write\n\t// API.  This API doesn't work if StdOut has been redirected to a file or\n\t// some other device.  In this case, write to StdOut using WriteFile.\n\t//\n\n\tStringCharCount = (DWORD)wcslen(String);\n\n\tif (ConsoleOutput != FALSE)\n\t{\n\t\tWriteConsoleW(StdOut, (PVOID)String, StringCharCount, &Written, NULL);\n\t}\n\telse\n\t{\n\t\t//\n\t\t// Write out a Unicode BOM to ensure proper processing by text readers\n\t\t//\n\t\tWriteFile(StdOut, (PVOID)&UNICODE_BOM, sizeof(UNICODE_BOM), &Written, NULL);\n\n\t\t//\n\t\t// The number of bytes to write to standard output must exclude the null\n\t\t// terminating character.\n\t\t//\n\t\tWriteFile(StdOut, (PVOID)String, (StringCharCount * sizeof(WCHAR)), &Written, NULL);\n\t}\n\nPrintExit:\n\treturn;\n}\n\nclass LoggerImpl : public Logger\n{\nprotected:\n\tUINT m_cp;\n\npublic:\n\n\tLoggerImpl()\n\t{\n\t\tm_cp = GetConsoleOutputCP();\n\t\tif (m_cp != CP_UTF8)\n\t\t{\n#ifdef NDEBUG\n\t\t\t// _setmode(_fileno(stdout), _O_U16TEXT);\n#endif\n\t\t}\n\t\tstd::wcout << L\"CodePage:\" << m_cp << std::endl;\n\t}\n\n\tvoid write(const std::string& log)\n\t{\n\t\tif (m_cp == CP_UTF8)\n\t\t{\n\t\t\tstd::cout << log << std::endl;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tCA2W str(log.c_str(), CP_UTF8);\n\t\t\t// CW2W targetStr(str, m_cp);\n\t\t\tUPrint((LPCWSTR)str);\n\t\t\tUPrint(L\"\\r\\n\");\n\t\t}\n\t}\n\n\tvoid debug(const std::string& log)\n\t{\n\t\tif (m_cp == CP_UTF8)\n\t\t{\n\t\t\tstd::cout << \"DBG:: \" << log << std::endl;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tCA2W str(log.c_str(), CP_UTF8);\n\t\t\t// CW2W targetStr(str, m_cp);\n\t\t\tUPrint(L\"DBG::\");\n\t\t\tUPrint((LPCWSTR)str);\n\t\t\tUPrint(L\"\\r\\n\");\n\t\t}\n\t}\n};\n\nstd::string parseArgumentwithQuatoW(LPCWSTR path)\n{\n\tstd::string parsedPath;\n\tif (NULL == path)\n\t{\n\t\treturn parsedPath;\n\t}\n\n\tsize_t start = 0;\n\tsize_t length = lstrlenW(path);\n\tif (length > 1 && path[length - 1] == '\"')\n\t{\n\t\tlength--;\n\t}\n\tif (path[0] == '\"')\n\t{\n\t\tstart = 1;\n\t\tlength--;\n\t}\n\n\tstd::wstring value(path, start, length);\n\n\treturn std::string((LPCSTR)CW2A(value.c_str(), CP_UTF8));\n}\n\nint main()\n{\n\tif (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED))\n\t{\n\t\t::SetThreadUILanguage(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED));\n\t}\n\telse\n\t{\n\t\t::SetThreadUILanguage(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US));\n\t}\n\n\tTCHAR curDir[MAX_PATH] = { 0 };\n\tDWORD dwRet = GetModuleFileName(NULL, curDir, MAX_PATH);\n\tif (dwRet == 0)\n\t{\n\t\treturn 1;\n\t}\n\t\n\tif (!PathRemoveFileSpec(curDir))\n\t{\n\t\treturn 1;\n\t}\n\n\tTCHAR buffer[MAX_PATH] = { 0 };\n\t_tcscpy(buffer, curDir);\n\tPathAppend(buffer, TEXT(\"Dlls\"));\n\tSetDllDirectory(buffer);\n\n    int outputFormat = OUTPUT_FORMAT_HTML;\n    int asyncLoading = HTML_OPTION_ONSCROLL;\n    bool outputFilter = false;\n    std::string backupDir;\n    std::string outputDir;\n    std::string account;\n    std::vector<std::string> sessions;\n\n\tLPWSTR *szArglist = NULL;\n\tint nArgs;\n\n\tstd::unique_ptr<LPWSTR, HLOCAL(__stdcall *)(HLOCAL)> szArglistPtr(szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs), ::LocalFree);\n\tif (NULL == szArglist)\n\t{\n\t\treturn 1;\n\t}\n\n\tfor (int idx = 1; idx < nArgs; idx++)\n    {\n        if (lstrcmpW(L\"--help\", szArglist[idx]) == 0)\n        {\n\t\t\tCW2A fileName(PathFindFileNameW(szArglist[0]));\n            printHelp((LPCSTR)fileName);\n            return 0;\n        }\n        \n\t\tLPWSTR equals_pos = wcschr(szArglist[idx], '=');\n        if (equals_pos == NULL)\n        {\n            continue;\n        }\n        \n        std::wstring name = std::wstring(szArglist[idx], equals_pos - szArglist[idx]);\n        if (name == L\"--backup\")\n        {\n            backupDir = parseArgumentwithQuatoW(equals_pos + 1);\n        }\n        else if (name == L\"--output\")\n        {\n            outputDir = parseArgumentwithQuatoW(equals_pos + 1);\n        }\n        else if (name == L\"--account\")\n        {\n            account = parseArgumentwithQuatoW(equals_pos + 1);\n        }\n        else if (name == L\"--session\")\n        {\n            sessions.push_back(parseArgumentwithQuatoW(equals_pos + 1));\n        }\n        else if (name == L\"--asyncloading\")\n        {\n            if (lstrcmpW(L\"sync\", equals_pos + 1) == 0)\n            {\n                asyncLoading = HTML_OPTION_SYNC;\n            }\n            else if (lstrcmpW(L\"oninit\", equals_pos + 1) == 0)\n            {\n                asyncLoading = HTML_OPTION_ONINIT;\n            }\n        }\n        else if (name == L\"--filter\")\n        {\n            if (lstrcmpW(L\"yes\", equals_pos + 1) == 0)\n            {\n                outputFilter = true;\n            }\n        }\n    }\n    \n    if (backupDir.empty() || !existsDirectory(backupDir))\n    {\n        std::cout << \"Please input valid iTunes backup directory.\" << std::endl;\n        return 1;\n    }\n    if (outputDir.empty() || !existsDirectory(outputDir))\n    {\n        std::cout << \"Please input valid output directory.\" << std::endl;\n        return 1;\n    }\n    if (account.empty())\n    {\n        std::cout << \"Please input account name.\" << std::endl;\n        return 1;\n    }\n   \n    std::string languageCode = getCurrentLanguageCode();\n\tCW2A workDir(CT2W(curDir), CP_UTF8);\n\n\tLoggerImpl logger;\n\n    return exportSessions(languageCode, &logger, (LPCSTR)workDir, backupDir, outputDir, account, sessions, outputFormat, asyncLoading, outputFilter);\n}\n\nstd::string getCurrentLanguageCode()\n{\n\tstd::string languageCode;\n\tif (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED))\n\t{\n\t\tlanguageCode = \"zh-Hans\";\n\t}\n\n\treturn languageCode;\n}\n\n"
  },
  {
    "path": "vcproject/WechatExporterCmd.vcxproj",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project DefaultTargets=\"Build\" ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <ItemGroup Label=\"ProjectConfigurations\">\n    <ProjectConfiguration Include=\"Debug|Win32\">\n      <Configuration>Debug</Configuration>\n      <Platform>Win32</Platform>\n    </ProjectConfiguration>\n    <ProjectConfiguration Include=\"Release|Win32\">\n      <Configuration>Release</Configuration>\n      <Platform>Win32</Platform>\n    </ProjectConfiguration>\n    <ProjectConfiguration Include=\"Debug|x64\">\n      <Configuration>Debug</Configuration>\n      <Platform>x64</Platform>\n    </ProjectConfiguration>\n    <ProjectConfiguration Include=\"Release|x64\">\n      <Configuration>Release</Configuration>\n      <Platform>x64</Platform>\n    </ProjectConfiguration>\n  </ItemGroup>\n  <ItemGroup>\n    <ClCompile Include=\"..\\WechatExporter\\core\\AsyncExecutor.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\AsyncTask.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Downloader.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Exporter.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\FileSystem.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\IDeviceBackup.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\ITunesParser.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\MessageParser.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\RawMessage.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\ResManager.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\TaskManager.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Template.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Updater.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_audio.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_md5.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_protobuf.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_silk.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_thread.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\WechatParser.cpp\" />\n    <ClCompile Include=\"..\\WechatExporter\\core\\XmlParser.cpp\" />\n    <ClCompile Include=\"WechatExporterCmd.cpp\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ClInclude Include=\"..\\WechatExporter\\core\\AsyncExecutor.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\AsyncTask.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\Downloader.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\endianness.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportContext.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\Exporter.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportNotifier.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportOption.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\FileSystem.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\IDeviceBackup.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\ITunesParser.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\Logger.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\MbdbReader.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\MessageParser.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\MMKVReader.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\PdfConverter.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\RawMessage.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\ResManager.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\semaphore.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\TaskManager.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\Template.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\Updater.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\Utils.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatObjects.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatParser.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatSource.h\" />\n    <ClInclude Include=\"..\\WechatExporter\\core\\XmlParser.h\" />\n    <ClInclude Include=\"Core.h\" />\n  </ItemGroup>\n  <PropertyGroup Label=\"Globals\">\n    <VCProjectVersion>15.0</VCProjectVersion>\n    <ProjectGuid>{C0637D9A-3C23-495C-B661-5FDCD2169583}</ProjectGuid>\n    <Keyword>Win32Proj</Keyword>\n    <RootNamespace>WechatExporterCmd</RootNamespace>\n    <WindowsTargetPlatformVersion>10.0.17763.0</WindowsTargetPlatformVersion>\n  </PropertyGroup>\n  <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.Default.props\" />\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|Win32'\" Label=\"Configuration\">\n    <ConfigurationType>Application</ConfigurationType>\n    <UseDebugLibraries>true</UseDebugLibraries>\n    <PlatformToolset>v141</PlatformToolset>\n    <CharacterSet>Unicode</CharacterSet>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|Win32'\" Label=\"Configuration\">\n    <ConfigurationType>Application</ConfigurationType>\n    <UseDebugLibraries>false</UseDebugLibraries>\n    <PlatformToolset>v141</PlatformToolset>\n    <WholeProgramOptimization>true</WholeProgramOptimization>\n    <CharacterSet>Unicode</CharacterSet>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\" Label=\"Configuration\">\n    <ConfigurationType>Application</ConfigurationType>\n    <UseDebugLibraries>true</UseDebugLibraries>\n    <PlatformToolset>v141</PlatformToolset>\n    <CharacterSet>Unicode</CharacterSet>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\" Label=\"Configuration\">\n    <ConfigurationType>Application</ConfigurationType>\n    <UseDebugLibraries>false</UseDebugLibraries>\n    <PlatformToolset>v141</PlatformToolset>\n    <WholeProgramOptimization>true</WholeProgramOptimization>\n    <CharacterSet>Unicode</CharacterSet>\n  </PropertyGroup>\n  <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.props\" />\n  <ImportGroup Label=\"ExtensionSettings\">\n  </ImportGroup>\n  <ImportGroup Label=\"Shared\">\n  </ImportGroup>\n  <ImportGroup Label=\"PropertySheets\" Condition=\"'$(Configuration)|$(Platform)'=='Debug|Win32'\">\n    <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" Label=\"LocalAppDataPlatform\" />\n  </ImportGroup>\n  <ImportGroup Label=\"PropertySheets\" Condition=\"'$(Configuration)|$(Platform)'=='Release|Win32'\">\n    <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" Label=\"LocalAppDataPlatform\" />\n  </ImportGroup>\n  <ImportGroup Label=\"PropertySheets\" Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\">\n    <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" Label=\"LocalAppDataPlatform\" />\n  </ImportGroup>\n  <ImportGroup Label=\"PropertySheets\" Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\">\n    <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" Label=\"LocalAppDataPlatform\" />\n  </ImportGroup>\n  <PropertyGroup Label=\"UserMacros\" />\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\">\n    <LinkIncremental>false</LinkIncremental>\n    <IncludePath>$(ProjectDir)..\\WechatExporterCmd;$(ProjectDir)..\\WechatExporter\\core;$(ProjectDir)..\\releases\\windows-libs\\include;$(IncludePath)</IncludePath>\n    <LibraryPath>$(ProjectDir)..\\releases\\windows-libs\\x64\\rel\\lib;$(LibraryPath)</LibraryPath>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|Win32'\">\n    <LinkIncremental>true</LinkIncremental>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\">\n    <LinkIncremental>true</LinkIncremental>\n    <IncludePath>$(ProjectDir)..\\WechatExporterCmd;$(ProjectDir)..\\WechatExporter\\core;$(ProjectDir)..\\releases\\windows-libs\\include;$(IncludePath)</IncludePath>\n    <LibraryPath>$(ProjectDir)..\\releases\\windows-libs\\x64\\dbg\\lib;$(LibraryPath)</LibraryPath>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|Win32'\">\n    <LinkIncremental>false</LinkIncremental>\n  </PropertyGroup>\n  <ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\">\n    <ClCompile>\n      <PrecompiledHeader>\n      </PrecompiledHeader>\n      <WarningLevel>Level3</WarningLevel>\n      <Optimization>MaxSpeed</Optimization>\n      <FunctionLevelLinking>true</FunctionLevelLinking>\n      <IntrinsicFunctions>true</IntrinsicFunctions>\n      <SDLCheck>true</SDLCheck>\n      <PreprocessorDefinitions>NDEBUG;_CONSOLE;NOMINMAX;_CRT_SECURE_NO_WARNINGS;CURL_STATICLIB;COMPILE_SDK;%(PreprocessorDefinitions)</PreprocessorDefinitions>\n      <ConformanceMode>true</ConformanceMode>\n      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>\n    </ClCompile>\n    <Link>\n      <SubSystem>Console</SubSystem>\n      <EnableCOMDATFolding>true</EnableCOMDATFolding>\n      <OptimizeReferences>true</OptimizeReferences>\n      <GenerateDebugInformation>true</GenerateDebugInformation>\n      <AdditionalDependencies>Shell32.lib;Ws2_32.lib;Wldap32.lib;Crypt32.lib;Rpcrt4.lib;libprotobuf.lib;libcrypto.lib;charset.lib;iconv.lib;zlib.lib;lzma.lib;libssl.lib;libcurl.lib;libxml2.lib;sqlite3.lib;jsoncpp.lib;libamr.lib;SKP_Silk_FIX.lib;libmp3lame.lib;libplist-2.0.lib;libimobiledevice-1.0.lib;%(AdditionalDependencies)</AdditionalDependencies>\n      <DelayLoadDLLs>libcrypto-1_1-x64.dll;libssl-1_1-x64.dll;libimobiledevice-1.0.dll;libplist-2.0.dll;libmp3lame.dll;iconv-2.dll;lzma.dll</DelayLoadDLLs>\n    </Link>\n    <PostBuildEvent>\n      <Command>if not exist \"$(OutDir)res\" mkdir \"$(OutDir)res\"\nxcopy \"$(ProjectDir)..\\WechatExporter\\res\"  \"$(OutDir)res\"  /Y /S /E /D\n\nxcopy \"$(ProjectDir)..\\WechatExporter\\LICENSES\"  \"$(OutDir)LICENSES\"  /Y /S /E /D /I\n\nif not exist \"$(OutDir)Dlls\" mkdir \"$(OutDir)Dlls\"\nxcopy \"$(ProjectDir)..\\releases\\windows-libs\\x64\\rel\\bin\\*.dll\"  \"$(OutDir)Dlls\\\"  /Y /D</Command>\n    </PostBuildEvent>\n  </ItemDefinitionGroup>\n  <ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|Win32'\">\n    <ClCompile>\n      <PrecompiledHeader>\n      </PrecompiledHeader>\n      <WarningLevel>Level3</WarningLevel>\n      <Optimization>Disabled</Optimization>\n      <SDLCheck>true</SDLCheck>\n      <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>\n      <ConformanceMode>true</ConformanceMode>\n    </ClCompile>\n    <Link>\n      <SubSystem>Console</SubSystem>\n      <GenerateDebugInformation>true</GenerateDebugInformation>\n    </Link>\n  </ItemDefinitionGroup>\n  <ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\">\n    <ClCompile>\n      <PrecompiledHeader>\n      </PrecompiledHeader>\n      <WarningLevel>Level3</WarningLevel>\n      <Optimization>Disabled</Optimization>\n      <SDLCheck>true</SDLCheck>\n      <PreprocessorDefinitions>_DEBUG;_CONSOLE;NOMINMAX;_CRT_SECURE_NO_WARNINGS;CURL_STATICLIB;COMPILE_SDK;%(PreprocessorDefinitions)</PreprocessorDefinitions>\n      <ConformanceMode>true</ConformanceMode>\n      <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>\n    </ClCompile>\n    <Link>\n      <SubSystem>Console</SubSystem>\n      <GenerateDebugInformation>true</GenerateDebugInformation>\n      <AdditionalDependencies>Ws2_32.lib;Crypt32.lib;Rpcrt4.lib;libprotobufd.lib;libcrypto.lib;charset.lib;iconv.lib;lzmad.lib;zlibd.lib;libssl.lib;libcurl-d.lib;libxml2.lib;sqlite3.lib;jsoncpp.lib;libamr.lib;SKP_Silk_FIX.lib;libmp3lame.lib;libplist-2.0.lib;libimobiledevice-1.0.lib;%(AdditionalDependencies)</AdditionalDependencies>\n      <DelayLoadDLLs>libcrypto-1_1-x64.dll;libssl-1_1-x64.dll;libimobiledevice-1.0.dll;libplist-2.0.dll;libmp3lame.dll;iconv-2.dll;lzmad.dll</DelayLoadDLLs>\n    </Link>\n    <PostBuildEvent>\n      <Command>if not exist \"$(OutDir)res\" mkdir \"$(OutDir)res\"\nxcopy \"$(ProjectDir)..\\WechatExporter\\res\"  \"$(OutDir)res\"  /Y /S /E /D\n\nif not exist \"$(OutDir)Dlls\" mkdir \"$(OutDir)Dlls\"\nxcopy \"$(ProjectDir)..\\releases\\windows-libs\\x64\\dbg\\bin\\*.dll\"  \"$(OutDir)Dlls\"  /Y /S /E /D\nxcopy \"$(ProjectDir)..\\releases\\windows-libs\\x64\\dbg\\bin\\*.pdb\"  \"$(OutDir)Dlls\\\"  /Y /S /E /D</Command>\n    </PostBuildEvent>\n  </ItemDefinitionGroup>\n  <ItemDefinitionGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|Win32'\">\n    <ClCompile>\n      <PrecompiledHeader>\n      </PrecompiledHeader>\n      <WarningLevel>Level3</WarningLevel>\n      <Optimization>MaxSpeed</Optimization>\n      <FunctionLevelLinking>true</FunctionLevelLinking>\n      <IntrinsicFunctions>true</IntrinsicFunctions>\n      <SDLCheck>true</SDLCheck>\n      <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>\n      <ConformanceMode>true</ConformanceMode>\n    </ClCompile>\n    <Link>\n      <SubSystem>Console</SubSystem>\n      <EnableCOMDATFolding>true</EnableCOMDATFolding>\n      <OptimizeReferences>true</OptimizeReferences>\n      <GenerateDebugInformation>true</GenerateDebugInformation>\n    </Link>\n  </ItemDefinitionGroup>\n  <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.targets\" />\n  <ImportGroup Label=\"ExtensionTargets\">\n  </ImportGroup>\n</Project>"
  },
  {
    "path": "vcproject/WechatExporterCmd.vcxproj.filters",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <ItemGroup>\n    <Filter Include=\"Source Files\">\n      <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>\n      <Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>\n    </Filter>\n    <Filter Include=\"Header Files\">\n      <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>\n      <Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>\n    </Filter>\n    <Filter Include=\"Resource Files\">\n      <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>\n      <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>\n    </Filter>\n    <Filter Include=\"core\">\n      <UniqueIdentifier>{1c08ec05-7c7c-4054-8d71-e0d198fe35a3}</UniqueIdentifier>\n    </Filter>\n  </ItemGroup>\n  <ItemGroup>\n    <ClCompile Include=\"..\\WechatExporter\\core\\AsyncExecutor.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\AsyncTask.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Downloader.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Exporter.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\FileSystem.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\IDeviceBackup.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\ITunesParser.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\MessageParser.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\RawMessage.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\ResManager.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\TaskManager.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Updater.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_audio.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_md5.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_protobuf.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_silk.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Utils_thread.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\WechatParser.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\XmlParser.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n    <ClCompile Include=\"WechatExporterCmd.cpp\">\n      <Filter>Source Files</Filter>\n    </ClCompile>\n    <ClCompile Include=\"..\\WechatExporter\\core\\Template.cpp\">\n      <Filter>core</Filter>\n    </ClCompile>\n  </ItemGroup>\n  <ItemGroup>\n    <ClInclude Include=\"..\\WechatExporter\\core\\AsyncExecutor.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\AsyncTask.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\Downloader.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\endianness.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportContext.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\Exporter.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportNotifier.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\FileSystem.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\IDeviceBackup.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\ITunesParser.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\Logger.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\MbdbReader.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\MessageParser.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\MMKVReader.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\PdfConverter.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\RawMessage.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\ResManager.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\semaphore.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\TaskManager.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\Updater.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\Utils.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatObjects.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatParser.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\WechatSource.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\XmlParser.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"Core.h\">\n      <Filter>Header Files</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\Template.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n    <ClInclude Include=\"..\\WechatExporter\\core\\ExportOption.h\">\n      <Filter>core</Filter>\n    </ClInclude>\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "vcproject/resource.h",
    "content": "﻿//{{NO_DEPENDENCIES}}\r\n// Microsoft Visual C++ generated include file.\r\n// Used by WechatExporter.rc\r\n//\r\n#define IDD_ABOUTBOX                    100\r\n#define IDR_MAINFRAME                   128\r\n#define IDD_WECHATEXPORTER_FORM         129\r\n#define IDS_SEL_BACKUP_DIR              129\r\n#define IDS_SEL_OUTPUT_DIR              130\r\n#define IDS_CANCEL_PROMPT               131\r\n#define IDS_STATIC_BACKUP               132\r\n#define IDS_SESSION_IDX                 133\r\n#define IDS_SESSION_NAME                134\r\n#define IDS_SESSION_COUNT               135\r\n#define IDS_ALL_USERS                   136\r\n#define IDS_SESSION_USER                137\r\n#define IDS_ITUNES_VERSION              138\r\n#define IDS_ITUNES_NOT_INSTALLED        139\r\n#define IDS_ENC_BKP_NOT_SUPPORTED       140\r\n#define IDS_FAILED_TO_LOAD_BKP          141\r\n#define IDS_INVALID_OUTPUT_DIR          142\r\n#define IDS_TOOLTIP_LOGS                143\r\n#define IDS_NEW_VERSION                 144\r\n#define IDS_SESSION_NUMBER              145\r\n#define IDS_SESSION_STATUS              146\r\n#define IDS_SESSION_DONE                147\r\n#define IDS_SESSION_PROGRESS            148\r\n#define IDS_SESSION_CANCELLED           149\r\n#define IDS_SHOW_LOGS                   150\r\n#define IDS_HIDE_LOGS                   151\r\n#define IDS_VERSIONS                    152\r\n#define IDS_NO_PREV_EXP                 153\r\n#define IDS_PREV_EXP_FOUND              154\r\n#define IDS_DELETED_SESSION             155\r\n#define IDS_EXPORTING_MSGS              156\r\n#define IDS_DOWNLOADING_EMOJI           157\r\n#define IDS_EXP_ITNS                    158\r\n#define IDS_SESSION_LAST_MSG            159\r\n#define IDS_INVLD_INCEXP_FOR_MULTI_USERS 160\r\n#define IDD_DIALOG1                     204\r\n#define IDD_PROGRESS_DLG                204\r\n#define IDD_BACKUP_DLG                  204\r\n#define IDD_BACKUP_DLG1                 205\r\n#define IDI_ITUNES                      206\r\n#define IDI_IPHONE                      207\r\n#define IDI_ICON2                       208\r\n#define IDC_BACKUP                      1000\r\n#define IDC_CHOOSE_BKP                  1001\r\n#define IDC_OUTPUT                      1002\r\n#define IDC_CHOOSE_OUTPUT               1003\r\n#define IDC_LOGS                        1004\r\n#define IDC_EXPORT                      1005\r\n#define IDC_PROGRESS                    1006\r\n#define IDC_CANCEL                      1007\r\n#define IDC_EXPORT2                     1008\r\n#define IDC_EXP_ITNS                    1008\r\n#define IDC_STATIC_BACKUP               1010\r\n#define IDC_VERSION                     1011\r\n#define IDC_USERS                       1012\r\n#define IDC_CLOSE                       1013\r\n#define IDC_SESSIONS                    1014\r\n#define IDC_GRP_LOGS                    1015\r\n#define IDC_GRP_USR_CHAT                1016\r\n#define IDC_SESS_PROGRESS               1017\r\n#define IDC_CHECK1                      1018\r\n#define IDC_SHOW_LOGS                   1018\r\n#define IDC_PROGRESS_TEXT               1019\r\n#define IDC_VERSIONS                    1020\r\n#define IDC_PROGRESS1                   1021\r\n#define IDC_TITLE                       1022\r\n#define ID_FILE_CHK_UPDATE              32775\r\n#define ID_FORMAT_HTML                  32776\r\n#define ID_FORMAT_TEXT                  32777\r\n#define ID_FORMAT_PDF                   32778\r\n#define ID_OPT_DESC_ORDER               32779\r\n#define ID_OPT_DL_EMOJI                 32780\r\n#define ID_OPT_LM_ONSCROLL              32781\r\n#define ID_OPT_NORMALPAGINATION         32782\r\n#define ID_OPT_PAGINATION_YEAR          32783\r\n#define ID_OPT_PAGINATION_MONTH         32784\r\n#define ID_OPT_FILTER                   32785\r\n#define ID_OPT_INCREMENTALEXP           32786\r\n#define ID_OPT_SUBSCRIPTIONS            32787\r\n#define ID_HELP_HOMEPAGE                32788\r\n#define ID_FILE_EXP_ITUNES              32789\r\n#define ID_FILE_ITUNESBACKUPEXPORT      32790\r\n#define ID_FILE_DBG_LOGS                32791\r\n#define ID_FILE_OPENNING_FOLDER         32792\r\n#define ID_FILE_OPEN_FOLDER             32793\r\n#define ID_FILE_OPE_FOLDER              32794\r\n// Next default values for new objects\r\n// \r\n#ifdef APSTUDIO_INVOKED\r\n#ifndef APSTUDIO_READONLY_SYMBOLS\r\n#define _APS_NEXT_RESOURCE_VALUE        208\r\n#define _APS_NEXT_COMMAND_VALUE         32795\r\n#define _APS_NEXT_CONTROL_VALUE         1023\r\n#define _APS_NEXT_SYMED_VALUE           101\r\n#endif\r\n#endif\r\n"
  },
  {
    "path": "vcproject/stdafx.cpp",
    "content": "// stdafx.cpp : source file that includes just the standard includes\n//\tWechatExporter.pch will be the pre-compiled header\n//\tstdafx.obj will contain the pre-compiled type information\n\n#include \"stdafx.h\"\n"
  },
  {
    "path": "vcproject/stdafx.h",
    "content": "// stdafx.h : include file for standard system include files,\n//  or project specific include files that are used frequently, but\n//  are changed infrequently\n//\n\n#pragma once\n\n// Change these values to use different versions\n#define WINVER\t\t0x0601\t\t\t// Windows 7\n#define _WIN32_WINNT\t0x0601\n#define _WIN32_IE\t0x0700\n#define _RICHEDIT_VER\t0x0500\n\n#include <atlbase.h>\n#include <atlconv.h>\n#include <atlapp.h>\n\nextern CAppModule _Module;\n\n#include <atlwin.h>\n#include <atlstr.h>\n\n#if defined _M_IX86\n  #pragma comment(linker, \"/manifestdependency:\\\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\\\"\")\n#elif defined _M_IA64\n  #pragma comment(linker, \"/manifestdependency:\\\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df' language='*'\\\"\")\n#elif defined _M_X64\n  #pragma comment(linker, \"/manifestdependency:\\\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\\\"\")\n#else\n  #pragma comment(linker, \"/manifestdependency:\\\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\\\"\")\n#endif\n"
  }
]