Repository: BlueMatthew/WechatExporter Branch: main Commit: 8f8ce412fc57 Files: 187 Total size: 1.4 MB Directory structure: gitextract_4jypkpqy/ ├── .gitignore ├── LICENSE ├── README.md ├── WechatExporter/ │ ├── AppConfiguration.h │ ├── AppConfiguration.mm │ ├── AppDelegate.h │ ├── AppDelegate.mm │ ├── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── MainMenuCN.imageset/ │ │ │ └── Contents.json │ │ └── MainMenuEN.imageset/ │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── Main.storyboard │ ├── ExportNotifierImpl.h │ ├── HttpHelper.h │ ├── HttpHelper.mm │ ├── Info.plist │ ├── LICENSES/ │ │ ├── jsoncpp.LICENSE │ │ ├── lame.LICENSE │ │ ├── libcurl.COPYING │ │ ├── libimobiledevice-glue.COPYING │ │ ├── libimobiledevice.COPYING.LESSER │ │ ├── libplist.COPYING.LESSER │ │ ├── libusbmuxd.COPYING │ │ ├── libxml2.Copyright │ │ ├── opencore-amr.LICENSE │ │ ├── openssl.LICENSE │ │ └── protobuf.LICENSE │ ├── LoggerImpl.h │ ├── PdfConverterImpl.h │ ├── SessionDataSource.h │ ├── SessionDataSource.mm │ ├── ViewController copy.mm │ ├── ViewController.h │ ├── ViewController.mm │ ├── WechatExporter.entitlements │ ├── core/ │ │ ├── AsyncExecutor.cpp │ │ ├── AsyncExecutor.h │ │ ├── AsyncTask.cpp │ │ ├── AsyncTask.h │ │ ├── ByteArrayLocater.h │ │ ├── DownloadPool.cpp │ │ ├── DownloadPool.h │ │ ├── Downloader.cpp │ │ ├── Downloader.h │ │ ├── ExportContext.h │ │ ├── ExportNotifier.h │ │ ├── ExportOption.h │ │ ├── Exporter.cpp │ │ ├── Exporter.h │ │ ├── FileSystem.cpp │ │ ├── FileSystem.h │ │ ├── IDeviceBackup.cpp │ │ ├── IDeviceBackup.h │ │ ├── ITunesParser.cpp │ │ ├── ITunesParser.h │ │ ├── Logger.h │ │ ├── MMKVReader.h │ │ ├── MbdbReader.h │ │ ├── MessageParser.cpp │ │ ├── MessageParser.h │ │ ├── PdfConverter.h │ │ ├── RawMessage.cpp │ │ ├── RawMessage.h │ │ ├── ResManager.cpp │ │ ├── ResManager.h │ │ ├── TaskManager.cpp │ │ ├── TaskManager.h │ │ ├── Template.cpp │ │ ├── Template.h │ │ ├── Updater.cpp │ │ ├── Updater.h │ │ ├── Utils.cpp │ │ ├── Utils.h │ │ ├── Utils_audio.cpp │ │ ├── Utils_md5.cpp │ │ ├── Utils_protobuf.cpp │ │ ├── Utils_silk.cpp │ │ ├── Utils_thread.cpp │ │ ├── Utils_xml.cpp │ │ ├── WechatObjects.h │ │ ├── WechatParser.cpp │ │ ├── WechatParser.h │ │ ├── WechatSource.h │ │ ├── XmlParser.cpp │ │ ├── XmlParser.h │ │ ├── endianness.h │ │ ├── md5.c │ │ ├── md5.h │ │ └── semaphore.h │ ├── en.lproj/ │ │ ├── Localizable.strings │ │ └── Main.strings │ ├── main.m │ ├── res/ │ │ ├── emoji/ │ │ │ └── emoji.json │ │ ├── en.txt │ │ ├── templates/ │ │ │ ├── audio.html │ │ │ ├── card.html │ │ │ ├── channels.html │ │ │ ├── emoji.html │ │ │ ├── filter.html │ │ │ ├── frame.html │ │ │ ├── frame_filter.html │ │ │ ├── image.html │ │ │ ├── listframe.html │ │ │ ├── listitem.html │ │ │ ├── member.html │ │ │ ├── members.html │ │ │ ├── msg.html │ │ │ ├── notice.html │ │ │ ├── plainshare.html │ │ │ ├── refermsg.html │ │ │ ├── scripts.html │ │ │ ├── share.html │ │ │ ├── system.html │ │ │ ├── thumb.html │ │ │ ├── transfer.html │ │ │ ├── video.html │ │ │ ├── videonew.html │ │ │ └── wxemoji.html │ │ ├── templates_txt/ │ │ │ ├── audio.html │ │ │ ├── card.html │ │ │ ├── channels.html │ │ │ ├── emoji.html │ │ │ ├── frame.html │ │ │ ├── image.html │ │ │ ├── listframe.html │ │ │ ├── listitem.html │ │ │ ├── msg.html │ │ │ ├── notice.html │ │ │ ├── plainshare.html │ │ │ ├── refermsg.html │ │ │ ├── scripts.html │ │ │ ├── share.html │ │ │ ├── system.html │ │ │ ├── thumb.html │ │ │ ├── transfer.html │ │ │ ├── video.html │ │ │ └── wxemoji.html │ │ └── zh-Hans.txt │ └── zh-Hans.lproj/ │ ├── Localizable.strings │ └── Main.strings ├── WechatExporter.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ └── WechatExporter.xcscheme ├── WechatExporter.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── WechatExporterCmd/ │ ├── WechatExporter.cpp │ └── WechatExporterCmd.h ├── WechatExporterCmd.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ └── WechatExporterCmd.xcscheme ├── docs/ │ ├── index.html │ └── update.conf ├── libplist.README.md ├── release └── vcproject/ ├── AboutDlg.h ├── AppConfiguration.cpp ├── AppConfiguration.h ├── BackupDlg.h ├── ColoredControls.h ├── Core.h ├── ExportNotifierImpl.h ├── ITunesDetector.h ├── LogListBox.h ├── LoggerImpl.h ├── MainFrm.h ├── PdfConverterImpl.h ├── ProgressListViewCtrl.h ├── TextProgressBarCtrl.h ├── ToolTipButton.h ├── VersionDetector.h ├── View.h ├── ViewHelper.cpp ├── ViewHelper.h ├── WechatExporter.cpp ├── WechatExporter.h ├── WechatExporter.rc ├── WechatExporter.sln ├── WechatExporter.vcxproj ├── WechatExporter.vcxproj.filters ├── WechatExporterCmd.cpp ├── WechatExporterCmd.vcxproj ├── WechatExporterCmd.vcxproj.filters ├── resource.h ├── stdafx.cpp └── stdafx.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## User settings xcuserdata/ ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint *.xccheckout ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) release/ releases/ DerivedData/ *.moved-aside *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 ## Obj-C/Swift specific *.hmap ## App packaging *.ipa *.dSYM.zip *.dSYM # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ # # Add this line if you want to avoid checking in source code from the Xcode workspace # *.xcworkspace # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build/ # fastlane # # It is recommended to not store the screenshots in the git repo. # Instead, use fastlane to re-generate the screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output # Code Injection # # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ .DS_Store # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # ASP.NET Scaffolding ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool coverage*.json coverage*.xml coverage*.info # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: README.md ================================================ # WechatExporter > **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)。** 本程序参考 https://github.com/stomakun/WechatExport-iOS 修改成C++来实现,便于在各个平台以更少依赖运行。同时增加了聊天群名称的解析支持和更多消息类型的导出支持。导出支持Text、HTML、PDF三种格式。 - 导出的聊天记录页面可以设置为打开时一次性加载完成(默认方式)、打开时异步加载、页面滑动到底部时加载更多三种方式,可以在菜单“选项”中修改加载方式。 - 可以在导出的页面增加过滤功能,功能也需要在菜单“选项”中设置。 - PDF格式,实质是导出打开时一次性加载完成的HTML页面,然后通过Google Chrome或者Microsoft Edge浏览器的功能转成PDF文件,转PDF文件耗时较长,请不要关闭自动弹出的命令行窗口。 - 增量导出:菜单“选项”中,如果设置了增量导出,则会仅仅导出上一次导出的最后一条消息之后的部分,通过此功能,再一次备份之后,微信中的聊天记录可以删除,下一次导出,可以把同一个聊天群的消息合并在一起。 ## 操作步骤: 1. 通过iTunes将手机备份到电脑上(备份时不要选择设置口令),Windows操作系统一般位于目录:C:\用户[用户名]\AppData\Roaming\Apple Computer\MobileSync\Backup\。Android手机可以找一个iPad/iPhone设备,把聊天记录迁移到iPad/iPhone设备上,然后通过iTunes备份到电脑上。 ![iTunesBackup-960](https://user-images.githubusercontent.com/37573096/125906418-090d4ac8-a2ba-4a26-9db2-c6dbed4b0a3c.png) 2. 下载本代码的执行文件:[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),然后解压压缩文件 3. 执行解压出来的WechatExport.exe/WechatExporter (Windows下如果运行报缺少必须的dll文件,请安装[Visual C++ 2017 redist](https://aka.ms/vs/16/release/vc_redist.x64.exe)后再尝试运行) 4. 按界面提示进行操作。 ![Windows界面截屏](https://src.wakin.org/github/wxexp/screenshots/win.png) ![MacOS界面截屏](https://src.wakin.org/github/wxexp/screenshots/mac.png###) 5. 导出后的页面示例: ![导出后的页面示例截屏](https://src.wakin.org/github/wxexp/demo/demo.png) [点击链接可打开网页:https://src.wakin.org/github/wxexp/demo/](https://src.wakin.org/github/wxexp/demo/) ## 模版修改 解压目录下的res\templates(MacOS版本位于Contents\Resources\res)子目录里存放了输出聊天记录的html页面模版,其中通过两个%包含起来的字符串,譬如,%%NAME%%,不要修改之外,其它页面内容和格式都可以自行调整。 特别感谢Chao.M帮忙优化当前的模版。 ## 系统依赖: Windows版本: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) MacOS版本:MacOS 10.10(Yosemite)+ ## 程序编译 程序依赖如下第三方库: - libxml2: http://www.xmlsoft.org/ - libcurl: https://curl.se/libcurl/ - libsqlite3: https://www.sqlite.org/index.html - libprotobuf: https://github.com/protocolbuffers/protobuf - libjsoncpp: https://github.com/open-source-parsers/jsoncpp - lame: http://lame.sourceforge.net/ - silk: https://github.com/collects/silk (也参考了: https://github.com/kn007/silk-v3-decoder) - libplist: https://github.com/libimobiledevice/libplist https://github.com/libimobiledevice-win32/libplist - libiconv(windows only): https://www.gnu.org/software/libiconv/ - openssl(windows only):https://github.com/openssl/openssl - WTL (windows only):https://sourceforge.net/projects/wtl/ MacOS下,libxml2,libcurl,libsqlite3直接使用了Xcode自带的库,其它第三方库需自行编译。 libmp3lame需手动删除文件include/libmp3lame.sym中的行:lame_init_old Windows环境下,silk自带Visual Studio工程文件,可以直接利用Visual Studio编译,其余除了libplist之外,都通过vcpkg可以编译。libplist在vcpkg中也存在,但是在编译x64-windows-static target的时候报了错,于是直接通过Visual Studio建了工程进行编译。 https://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x64-windows-static.zip https://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x86-windows-static.zip https://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x64-windows-static-dbg.zip https://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x86-windows-static-dbg.zip https://github.com/BlueMatthew/WechatExporter/releases/download/v1.0/x64-macos-static.zip 已测试iTunes和微信版本 iTunes 12.3.3.17 + 微信6.5.9 iTunes 12.5.1.21 + 微信6.3.30 iTunes 12.10.10.2 + 微信7.0.2 iTunes 12.10.9.3 + 微信 7.0.15 iTunes 12.9.5.5 + 微信 7.0.2 Windows 10 + iTunes 12.11.0.26(Microsoft Store) + 微信 7.0.2 Windows 10 + iTunes 12.11.0.26(Microsoft Store) + 微信 8.0.1 Mac Catalina (Embedded iTunes) + 微信 8.0.1/8.0.2 Windows 7 + iTunes 12.10.9.3 + 微信版本 8.0.2 Windows 10 + iTunes 12.11.3.17 + 微信 8.0.7 Windows 7 + iTunes 12.10.9.3/Mac Catalina (Embedded iTunes) + 微信 7.0.2 + iOS 9.3.5 Windows + iTunes 12.10.3.1+ 微信 7.0.10 + iOS 13.3 (@lazybug163) MacOS 11.6(Embedded iTunes)+ iOS Version: 15.0 + 微信 8.0.9 ================================================ FILE: WechatExporter/AppConfiguration.h ================================================ // // AppConfiguration.h // WechatExporter // // Created by Matthew on 2021/3/18. // Copyright © 2021 Matthew. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, OUTPUT_FORMAT) { OUTPUT_FORMAT_HTML = 0, OUTPUT_FORMAT_TEXT, OUTPUT_FORMAT_PDF, OUTPUT_FORMAT_LAST }; @interface AppConfiguration : NSObject + (void)setDescOrder:(BOOL)descOrder; + (BOOL)getDescOrder; + (BOOL)IsPdfSupported; + (NSInteger)getOutputFormat; + (BOOL)isHtmlMode; + (BOOL)isTextMode; + (BOOL)isPdfMode; + (void)setOutputFormat:(NSInteger)outputFormat; + (void)setSavingInSession:(BOOL)savingInSession; + (BOOL)getSavingInSession; + (void)setSyncLoading; + (BOOL)getSyncLoading; + (void)setIncrementalExporting:(BOOL)incrementalExp; + (BOOL)getIncrementalExporting; + (void)setLastOutputDir:(NSString *)outputDir; + (NSString *)getLastOrDefaultOutputDir; + (NSString *)getDefaultOutputDir; + (void)setLastBackupDir:(NSString *)backupDir; + (NSString *)getLastBackupDir; + (NSString *)getDefaultBackupDir:(BOOL)checkExistence; // YES + (NSInteger)getLastCheckUpdateTime; + (void)setLastCheckUpdateTime; + (void)setLastCheckUpdateTime:(NSInteger)lastCheckUpdateTime; + (void)setCheckingUpdateDisabled:(BOOL)disabled; + (BOOL)isCheckingUpdateDisabled; + (void)setLoadingDataOnScroll; + (BOOL)getLoadingDataOnScroll; + (void)setNormalPagination; + (BOOL)getNormalPagination; + (void)setPaginationOnYear; + (BOOL)getPaginationOnYear; + (void)setPaginationOnMonth; + (BOOL)getPaginationOnMonth; + (void)setSupportingFilter:(BOOL)supportingFilter; + (BOOL)getSupportingFilter; + (void)setOutputDebugLogs:(BOOL)dbgLogs; + (BOOL)outputDebugLogs; + (void)setIncludingSubscriptions:(BOOL)includingSubscriptions; + (BOOL)includeSubscriptions; + (void)setOpenningFolderAfterExp:(BOOL)openningFolderAfterExp; + (BOOL)getOpenningFolderAfterExp; + (void)setSkipGuide:(BOOL)skipGuide; + (BOOL)getSkipGuide; + (void)upgrade; + (uint64_t)buildOptions; @end NS_ASSUME_NONNULL_END ================================================ FILE: WechatExporter/AppConfiguration.mm ================================================ // // AppConfiguration.m // WechatExporter // // Created by Matthew on 2021/3/18. // Copyright © 2021 Matthew. All rights reserved. // #import "AppConfiguration.h" #include "Utils.h" #include "PdfConverterImpl.h" #include "ExportOption.h" #define ASYNC_NONE 0 #define ASYNC_ONSCROLL 1 #define ASYNC_PAGER_NORMAL 2 #define ASYNC_PAGER_ON_YEAR 3 #define ASYNC_PAGER_ON_MONTH 4 @implementation AppConfiguration + (NSInteger)getAsyncLoadingValue { NSObject *obj = [[NSUserDefaults standardUserDefaults] objectForKey:@"AsyncLoading"]; if (nil == obj) { return ASYNC_ONSCROLL; } return [[NSUserDefaults standardUserDefaults] integerForKey:@"AsyncLoading"]; } + (void)setDescOrder:(BOOL)descOrder { [[NSUserDefaults standardUserDefaults] setBool:descOrder forKey:@"DescOrder"]; } + (BOOL)getDescOrder { return [[NSUserDefaults standardUserDefaults] boolForKey:@"DescOrder"]; } + (BOOL)IsPdfSupported { PdfConverterImpl converter(NULL); return converter.isPdfSupported() ? YES : NO; } + (NSInteger)getOutputFormat { return [[NSUserDefaults standardUserDefaults] integerForKey:@"OutputFormat"]; } + (BOOL)isHtmlMode { return [self getOutputFormat] == OUTPUT_FORMAT_HTML; } + (BOOL)isTextMode { return [self getOutputFormat] == OUTPUT_FORMAT_TEXT; } + (BOOL)isPdfMode { return [self getOutputFormat] == OUTPUT_FORMAT_PDF; } + (void)setOutputFormat:(NSInteger)outputFormat { [[NSUserDefaults standardUserDefaults] setInteger:outputFormat forKey:@"OutputFormat"]; } + (void)setSavingInSession:(BOOL)savingInSession { [[NSUserDefaults standardUserDefaults] setBool:(!savingInSession) forKey:@"UniversalFolder"]; } + (BOOL)getSavingInSession { return ![[NSUserDefaults standardUserDefaults] boolForKey:@"UniversalFolder"]; } + (void)setSyncLoading { [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_NONE forKey:@"AsyncLoading"]; } + (BOOL)getSyncLoading { return [AppConfiguration getAsyncLoadingValue] == ASYNC_NONE; } + (void)setIncrementalExporting:(BOOL)incrementalExp { [[NSUserDefaults standardUserDefaults] setBool:incrementalExp forKey:@"IncrementalExp"]; } + (BOOL)getIncrementalExporting { return [[NSUserDefaults standardUserDefaults] boolForKey:@"IncrementalExp"]; } + (void)setLastOutputDir:(NSString *)outputDir { [[NSUserDefaults standardUserDefaults] setObject:outputDir forKey:@"OutputDir"]; } + (NSString *)getLastOrDefaultOutputDir { NSString *outputDir = [[NSUserDefaults standardUserDefaults] stringForKey:@"OutputDir"]; if (nil != outputDir && outputDir.length > 0) { return outputDir; } return [self getDefaultOutputDir]; } + (NSString *)getDefaultOutputDir { NSMutableArray *components = [NSMutableArray array]; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); if (nil == paths && paths.count > 0) { [components addObject:[paths objectAtIndex:0]]; } else { [components addObject:NSHomeDirectory()]; [components addObject:@"Documents"]; } [components addObject:@"WechatHistory"]; return [NSString pathWithComponents:components]; } + (void)setLastBackupDir:(NSString *)backupDir { [[NSUserDefaults standardUserDefaults] setObject:backupDir forKey:@"BackupDir"]; } + (NSString *)getLastBackupDir { return [[NSUserDefaults standardUserDefaults] stringForKey:@"BackupDir"]; } + (NSString *)getDefaultBackupDir:(BOOL)checkExistence // YES { NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *appSupport = [fileManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil]; NSArray *components = @[[appSupport path], @"MobileSync", @"Backup"]; NSString *backupDir = [NSString pathWithComponents:components]; if (!checkExistence) { return backupDir; } BOOL isDir = NO; if ([fileManager fileExistsAtPath:backupDir isDirectory:&isDir] && isDir) { return backupDir; } return nil; } + (NSInteger)getLastCheckUpdateTime { return [[NSUserDefaults standardUserDefaults] integerForKey:@"LastChkUpdateTime"]; } + (void)setLastCheckUpdateTime { [self setLastCheckUpdateTime:0]; } + (void)setLastCheckUpdateTime:(NSInteger)lastCheckUpdateTime { if (0 == lastCheckUpdateTime) { lastCheckUpdateTime = static_cast(getUnixTimeStamp()); } [[NSUserDefaults standardUserDefaults] setInteger:lastCheckUpdateTime forKey:@"LastChkUpdateTime"]; } + (void)setCheckingUpdateDisabled:(BOOL)disabled { [[NSUserDefaults standardUserDefaults] setBool:disabled forKey:@"ChkUpdateDisabled"]; } + (BOOL)isCheckingUpdateDisabled { return [[NSUserDefaults standardUserDefaults] boolForKey:@"ChkUpdateDisabled"]; } + (void)setLoadingDataOnScroll { [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_ONSCROLL forKey:@"AsyncLoading"]; } + (BOOL)getLoadingDataOnScroll { return [AppConfiguration getAsyncLoadingValue] == ASYNC_ONSCROLL; } + (void)setNormalPagination { [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_PAGER_NORMAL forKey:@"AsyncLoading"]; } + (BOOL)getNormalPagination { return [AppConfiguration getAsyncLoadingValue] == ASYNC_PAGER_NORMAL; } + (void)setPaginationOnYear { [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_PAGER_ON_YEAR forKey:@"AsyncLoading"]; } + (BOOL)getPaginationOnYear { return [AppConfiguration getAsyncLoadingValue] == ASYNC_PAGER_ON_YEAR; } + (void)setPaginationOnMonth { [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_PAGER_ON_MONTH forKey:@"AsyncLoading"]; } + (BOOL)getPaginationOnMonth { return [AppConfiguration getAsyncLoadingValue] == ASYNC_PAGER_ON_MONTH; } + (void)setSupportingFilter:(BOOL)supportingFilter { [[NSUserDefaults standardUserDefaults] setBool:(!supportingFilter) forKey:@"NoFilter"]; } + (BOOL)getSupportingFilter { return ![[NSUserDefaults standardUserDefaults] boolForKey:@"NoFilter"]; } + (void)setOutputDebugLogs:(BOOL)dbgLogs { [[NSUserDefaults standardUserDefaults] setBool:dbgLogs forKey:@"OutputDebugLogs"]; } + (BOOL)outputDebugLogs { return [[NSUserDefaults standardUserDefaults] boolForKey:@"OutputDebugLogs"]; } + (void)setIncludingSubscriptions:(BOOL)includingSubscriptions { [[NSUserDefaults standardUserDefaults] setBool:includingSubscriptions forKey:@"IncludingSubscriptions"]; } + (BOOL)includeSubscriptions { return [[NSUserDefaults standardUserDefaults] boolForKey:@"IncludingSubscriptions"]; } + (void)setOpenningFolderAfterExp:(BOOL)openningFolderAfterExp { [[NSUserDefaults standardUserDefaults] setBool:openningFolderAfterExp forKey:@"OpenningFolderAfterExp"]; } + (BOOL)getOpenningFolderAfterExp { NSObject *obj = [[NSUserDefaults standardUserDefaults] objectForKey:@"OpenningFolderAfterExp"]; return (nil == obj) ? YES : [[NSUserDefaults standardUserDefaults] boolForKey:@"OpenningFolderAfterExp"]; } + (void)setSkipGuide:(BOOL)skipGuide { [[NSUserDefaults standardUserDefaults] setBool:skipGuide forKey:@"SkipGuide"]; } + (BOOL)getSkipGuide { return [[NSUserDefaults standardUserDefaults] boolForKey:@"SkipGuide"]; } + (void)upgrade { NSObject *obj = [[NSUserDefaults standardUserDefaults] objectForKey:@"SyncLoading"]; if (obj != nil) { BOOL val = [[NSUserDefaults standardUserDefaults] boolForKey:@"SyncLoading"]; [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"SyncLoading"]; if (val) { [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_NONE forKey:@"AsyncLoading"]; } } obj = [[NSUserDefaults standardUserDefaults] objectForKey:@"LoadingOnScroll"]; if (obj != nil) { BOOL val = [[NSUserDefaults standardUserDefaults] boolForKey:@"LoadingOnScroll"]; [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"LoadingOnScroll"]; if (val) { [[NSUserDefaults standardUserDefaults] setInteger:ASYNC_ONSCROLL forKey:@"AsyncLoading"]; } } } + (uint64_t)buildOptions { ExportOption options; if ([AppConfiguration isTextMode]) { options.setTextMode(); } if ([AppConfiguration isPdfMode]) { options.setPdfMode(); } options.setOrder(![AppConfiguration getDescOrder]); // getSavingInSession if ([AppConfiguration getSyncLoading]) { options.setSyncLoading(); } else { // options.setSyncLoading(false); if ([AppConfiguration getLoadingDataOnScroll]) { options.setLoadingDataOnScroll([AppConfiguration getLoadingDataOnScroll]); } if ([AppConfiguration getNormalPagination]) { options.setPager(); } if ([AppConfiguration getPaginationOnYear]) { options.setPagerByYear(); } if ([AppConfiguration getPaginationOnMonth]) { options.setPagerByMonth(); } // options.set([AppConfiguration getLoadingDataOnScroll]); } options.setIncrementalExporting([AppConfiguration getIncrementalExporting]); options.supportsFilter([AppConfiguration getSupportingFilter]); options.outputDebugLogs([AppConfiguration outputDebugLogs]); if ([AppConfiguration includeSubscriptions]) { options.includesSubscription(); } return (uint64_t)options; } @end ================================================ FILE: WechatExporter/AppDelegate.h ================================================ // // AppDelegate.h // WechatExporter // // Created by Matthew on 2020/9/29. // Copyright © 2020 Matthew. All rights reserved. // #import @interface AppDelegate : NSObject - (IBAction)fileMenuItemClick:(NSMenuItem *)sender; - (IBAction)formatMenuItemClick:(NSMenuItem *)sender; - (IBAction)optionsMenuItemClick:(NSMenuItem *)sender; @end ================================================ FILE: WechatExporter/AppDelegate.mm ================================================ // // AppDelegate.m // WechatExporter // // Created by Matthew on 2020/9/29. // Copyright © 2020 Matthew. All rights reserved. // #import "AppDelegate.h" #include "Exporter.h" #import "AppConfiguration.h" @interface AppDelegate () { BOOL m_pdfSupported; } @end @implementation AppDelegate - (void)applicationWillBecomeActive:(NSNotification *)notification { } - (void)applicationDidFinishLaunching:(NSNotification *)notification { [AppConfiguration upgrade]; Exporter::initializeExporter(); m_pdfSupported = [AppConfiguration IsPdfSupported]; NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu]; NSMenuItem *fileMenu = [mainMenu itemAtIndex:1]; for (NSMenuItem *menuItem in fileMenu.submenu.itemArray) { if ([@"updater" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration isCheckingUpdateDisabled] ? NSControlStateValueOff : NSControlStateValueOn; } else if ([@"openFolder" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration getOpenningFolderAfterExp] ? NSControlStateValueOn : NSControlStateValueOff; } else if ([@"outputDbgLogs" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration outputDebugLogs] ? NSControlStateValueOn : NSControlStateValueOff; } } BOOL htmlMode = [AppConfiguration isHtmlMode]; NSMenuItem *formatMenu = [mainMenu itemAtIndex:2]; for (NSMenuItem *menuItem in formatMenu.submenu.itemArray) { if ([@"htmlMode" isEqual:menuItem.identifier]) { menuItem.state = htmlMode ? NSControlStateValueOn : NSControlStateValueOff; } else if ([@"textMode" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration isTextMode] ? NSControlStateValueOn : NSControlStateValueOff; } else if ([@"pdfMode" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration isPdfMode] && m_pdfSupported ? NSControlStateValueOn : NSControlStateValueOff; } } NSMenuItem *optionsMenu = [mainMenu itemAtIndex:3]; for (NSMenuItem *menuItem in optionsMenu.submenu.itemArray) { if ([@"descOrder" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration getDescOrder] ? NSControlStateValueOn : NSControlStateValueOff; } else if ([@"asyncLoadingOnScroll" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration getLoadingDataOnScroll] ? NSControlStateValueOn : NSControlStateValueOff; menuItem.enabled = htmlMode; } else if ([@"asyncPagerNormal" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration getNormalPagination] ? NSControlStateValueOn : NSControlStateValueOff; menuItem.enabled = htmlMode; } else if ([@"asyncPagerYear" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration getPaginationOnYear] ? NSControlStateValueOn : NSControlStateValueOff; menuItem.enabled = htmlMode; } else if ([@"asyncPagerMonth" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration getPaginationOnMonth] ? NSControlStateValueOn : NSControlStateValueOff; menuItem.enabled = htmlMode; } else if ([@"filter" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration getSupportingFilter] ? NSControlStateValueOn : NSControlStateValueOff; menuItem.enabled = htmlMode; } else if ([@"incrementalExp" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration getIncrementalExporting] ? NSControlStateValueOn : NSControlStateValueOff; } else if ([@"includingSubscriptions" isEqual:menuItem.identifier]) { menuItem.state = [AppConfiguration includeSubscriptions] ? NSControlStateValueOn : NSControlStateValueOff; #ifndef NDEBUG menuItem.hidden = NO; #endif } else { } } } - (void)applicationWillTerminate:(NSNotification *)aNotification { // Insert code here to tear down your application Exporter::uninitializeExporter(); } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication { return YES; } - (IBAction)fileMenuItemClick:(NSMenuItem *)sender { if ([sender.identifier isEqualToString:@"openFolder"]) { [AppConfiguration setOpenningFolderAfterExp:(sender.state == NSControlStateValueOff)]; sender.state = (sender.state == NSControlStateValueOff) ? NSControlStateValueOn : NSControlStateValueOff; } else if ([sender.identifier isEqualToString:@"outputDbgLogs"]) { [AppConfiguration setOutputDebugLogs:(sender.state == NSControlStateValueOff)]; sender.state = (sender.state == NSControlStateValueOff) ? NSControlStateValueOn : NSControlStateValueOff; } else if ([sender.identifier isEqualToString:@"updater"]) { [AppConfiguration setCheckingUpdateDisabled:(sender.state == NSControlStateValueOn)]; sender.state = (sender.state == NSControlStateValueOff) ? NSControlStateValueOn : NSControlStateValueOff; } } - (IBAction)formatMenuItemClick:(NSMenuItem *)sender { if ([sender.identifier isEqualToString:@"htmlMode"] || [sender.identifier isEqualToString:@"textMode"] || [sender.identifier isEqualToString:@"pdfMode"]) { if (sender.state == NSControlStateValueOff) { NSInteger outputFormat = [sender.identifier isEqualToString:@"htmlMode"] ? OUTPUT_FORMAT_HTML : ([sender.identifier isEqualToString:@"pdfMode"] ? OUTPUT_FORMAT_PDF : OUTPUT_FORMAT_TEXT); [AppConfiguration setOutputFormat:outputFormat]; BOOL htmlMode = [AppConfiguration isHtmlMode]; NSMenuItem *menuItem = nil; NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu]; NSMenuItem *formatMenu = [mainMenu itemAtIndex:2]; for (NSInteger idx = 0; idx < 3; idx++) { menuItem = [formatMenu.submenu itemAtIndex:idx]; menuItem.state = [menuItem.identifier isEqualToString:sender.identifier] ? NSControlStateValueOn : NSControlStateValueOff; } NSMenuItem *optionsMenu = [mainMenu itemAtIndex:3]; for (NSMenuItem *menuItem in optionsMenu.submenu.itemArray) { if ([menuItem.identifier hasPrefix:@"async"]) { menuItem.enabled = htmlMode; } else if ([@"filter" isEqual:menuItem.identifier]) { menuItem.enabled = htmlMode; } } } } } - (IBAction)optionsMenuItemClick:(NSMenuItem *)sender { BOOL newValue = sender.state == NSControlStateValueOff; // if (![sender.identifier hasPrefix:@"pager"]) { sender.state = newValue ? NSControlStateValueOn : NSControlStateValueOff; } if ([sender.identifier isEqualToString:@"includingSubscriptions"]) { [AppConfiguration setIncludingSubscriptions:newValue]; } else if ([sender.identifier isEqualToString:@"descOrder"]) { [AppConfiguration setDescOrder:newValue]; } else if ([sender.identifier isEqualToString:@"savingInSessionFolder"]) { [AppConfiguration setSavingInSession:newValue]; } /* else if ([sender.identifier isEqualToString:@"asyncLoading"]) { [AppConfiguration setAsyncLoading:newValue]; NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu]; NSMenuItem *optionsMenu = [mainMenu itemAtIndex:3]; for (NSMenuItem *menuItem in optionsMenu.submenu.itemArray) { if ([@"loadingOnScroll" isEqual:menuItem.identifier]) { menuItem.enabled = [AppConfiguration isHtmlMode] && [AppConfiguration getAsyncLoading]; } } } else if ([sender.identifier isEqualToString:@"asyncLoadingOnScroll"]) { [AppConfiguration setLoadingDataOnScroll:newValue]; } */ else if ([sender.identifier isEqualToString:@"filter"]) { [AppConfiguration setSupportingFilter:newValue]; } else if ([sender.identifier isEqualToString:@"incrementalExp"]) { [AppConfiguration setIncrementalExporting:newValue]; } else if ([sender.identifier hasPrefix:@"async"]) { if (newValue) { for (NSMenuItem *menuItem in sender.parentItem.submenu.itemArray) { if ((menuItem != sender) && [menuItem.identifier hasPrefix:@"async"]) { if (menuItem.state == NSControlStateValueOn) { menuItem.state = NSControlStateValueOff; } } } if ([sender.identifier isEqualToString:@"asyncLoadingOnScroll"]) { [AppConfiguration setLoadingDataOnScroll]; } else if ([sender.identifier isEqualToString:@"asyncPagerNormal"]) { [AppConfiguration setNormalPagination]; } else if ([sender.identifier isEqualToString:@"asyncPagerYear"]) { [AppConfiguration setPaginationOnYear]; } else if ([sender.identifier isEqualToString:@"asyncPagerMonth"]) { [AppConfiguration setPaginationOnMonth]; } } else { [AppConfiguration setSyncLoading]; } } } @end ================================================ FILE: WechatExporter/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "32.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "64.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "256.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "512.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "1024.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: WechatExporter/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: WechatExporter/Assets.xcassets/MainMenuCN.imageset/Contents.json ================================================ { "images" : [ { "filename" : "main-menu-cn@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "main-menu-cn@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "main-menu-cn@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: WechatExporter/Assets.xcassets/MainMenuEN.imageset/Contents.json ================================================ { "images" : [ { "filename" : "main-menu-en@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "main-menu-en@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "main-menu-en@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: WechatExporter/Base.lproj/Main.storyboard ================================================ ================================================ FILE: WechatExporter/ExportNotifierImpl.h ================================================ #pragma once #include "ExportNotifier.h" #include "ViewController.h" class ExportNotifierImpl : public ExportNotifier { protected: __weak ViewController *m_viewController; public: ExportNotifierImpl(ViewController* viewController) { m_viewController = viewController; } ~ExportNotifierImpl() { m_viewController = nil; } void onStart() const { __block __weak ViewController* viewController = m_viewController; dispatch_async(dispatch_get_main_queue(), ^{ __strong __typeof(viewController)strongVC = viewController; if (strongVC) { [strongVC onStart]; strongVC = nil; } }); } void onProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const { } void onComplete(bool cancelled) const { __block __weak ViewController* viewController = m_viewController; __block BOOL localCancelled = cancelled; dispatch_async(dispatch_get_main_queue(), ^{ __strong __typeof(viewController)strongVC = viewController; if (strongVC) { [strongVC onComplete:localCancelled]; strongVC = nil; } }); } void onUserSessionStart(const std::string& usrName, uint32_t numberOfSessions) const { } void onUserSessionComplete(const std::string& usrName) const { } void onSessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages) const { __block __weak ViewController* viewController = m_viewController; __block NSInteger row = (NSInteger)sessionData; __block NSString *usrName = [NSString stringWithUTF8String:sessionUsrName.c_str()]; dispatch_async(dispatch_get_main_queue(), ^{ __strong __typeof(viewController)strongVC = viewController; if (strongVC) { [strongVC onSessionStart:usrName row:row]; strongVC = nil; } }); } void onSessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const { __block __weak ViewController* viewController = m_viewController; __block NSInteger row = (NSInteger)sessionData; __block NSUInteger numberOfMsgs = (NSUInteger)numberOfMessages; __block NSUInteger numberOfTotalMsgs = (NSUInteger)numberOfTotalMessages; __block NSString *usrName = [NSString stringWithUTF8String:sessionUsrName.c_str()]; dispatch_async(dispatch_get_main_queue(), ^{ __strong __typeof(viewController)strongVC = viewController; if (strongVC) { [strongVC onSessionProgress:usrName row:row numberOfMessages:numberOfMsgs numberOfTotalMessages:numberOfTotalMsgs]; strongVC = nil; } }); } void onSessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled) const { __block __weak ViewController* viewController = m_viewController; // __block BOOL localCancelled = cancelled; __block NSString *usrName = [NSString stringWithUTF8String:sessionUsrName.c_str()]; dispatch_async(dispatch_get_main_queue(), ^{ __strong __typeof(viewController)strongVC = viewController; if (strongVC) { [strongVC onSessionComplete:usrName]; strongVC = nil; } }); } void onTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks) const { } void onTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalMessages) const { } void onTasksComplete(const std::string& usrName, bool cancelled) const { } }; ================================================ FILE: WechatExporter/HttpHelper.h ================================================ // // HttpHelper.h // WechatExporter // // Created by Matthew on 2021/3/9. // Copyright © 2021 Matthew. All rights reserved. // #ifndef HttpHelper_h #define HttpHelper_h #import @interface HttpHelper : NSObject + (NSString *)standardUserAgent; @end #endif /* HttpHelper_h */ ================================================ FILE: WechatExporter/HttpHelper.mm ================================================ // // HttpHelper.m // WechatExporter // // Created by Matthew on 2021/3/9. // Copyright © 2021 Matthew. All rights reserved. // #import "HttpHelper.h" #if defined(__ppc__) || defined(__ppc64__) #define PROCESSOR "PPC" #elif defined(__i386__) || defined(__x86_64__) #define PROCESSOR "Intel" #else #error Unknown architecture #endif @implementation HttpHelper static inline int callGestalt(OSType selector) { SInt32 value = 0; Gestalt(selector, &value); return value; } // Uses underscores instead of dots because if "4." ever appears in a user agent string, old DHTML libraries treat it as Netscape 4. + (NSString *)macOSXVersionString { // Can't use -[NSProcessInfo operatingSystemVersionString] because it has too much stuff we don't want. int major = callGestalt(gestaltSystemVersionMajor); // ASSERT(major); int minor = callGestalt(gestaltSystemVersionMinor); int bugFix = callGestalt(gestaltSystemVersionBugFix); if (bugFix) return [NSString stringWithFormat:@"%d_%d_%d", major, minor, bugFix]; if (minor) return [NSString stringWithFormat:@"%d_%d", major, minor]; return [NSString stringWithFormat:@"%d", major]; } + (NSString *)userVisibleWebKitVersionString { // If the version is 4 digits long or longer, then the first digit represents // the version of the OS. Our user agent string should not include this first digit, // so strip it off and report the rest as the version. NSString *fullVersion = [[NSBundle bundleForClass:NSClassFromString(@"WKView")] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]; NSRange nonDigitRange = [fullVersion rangeOfCharacterFromSet:[[NSCharacterSet decimalDigitCharacterSet] invertedSet]]; if (nonDigitRange.location == NSNotFound && [fullVersion length] >= 4) return [fullVersion substringFromIndex:1]; if (nonDigitRange.location != NSNotFound && nonDigitRange.location >= 4) return [fullVersion substringFromIndex:1]; return fullVersion; } + (NSString *)standardUserAgent { // https://opensource.apple.com/source/WebKit2/WebKit2-7536.26.14/UIProcess/mac/WebPageProxyMac.mm.auto.html NSString *osVersion = [self macOSXVersionString]; NSString *webKitVersion = [self userVisibleWebKitVersionString]; return [NSString stringWithFormat:@"Mozilla/5.0 (Macintosh; %@ Mac OS X %@) AppleWebKit/%@ (KHTML, like Gecko) ", @PROCESSOR, osVersion, webKitVersion]; } @end ================================================ FILE: WechatExporter/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2020 Matthew. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass NSApplication NSSupportsAutomaticTermination NSSupportsSuddenTermination ================================================ FILE: WechatExporter/LICENSES/jsoncpp.LICENSE ================================================ The JsonCpp library's source code, including accompanying documentation, tests and demonstration applications, are licensed under the following conditions... Baptiste Lepilleur and The JsonCpp Authors explicitly disclaim copyright in all jurisdictions which recognize such a disclaimer. In such jurisdictions, this software is released into the Public Domain. In jurisdictions which do not recognize Public Domain property (e.g. Germany as of 2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur and The JsonCpp Authors, and is released under the terms of the MIT License (see below). In jurisdictions which recognize Public Domain property, the user of this software may choose to accept it either as 1) Public Domain, 2) under the conditions of the MIT License (see below), or 3) under the terms of dual Public Domain/MIT License conditions described here, as they choose. The MIT License is about as close to Public Domain as a license can get, and is described in clear, concise terms at: http://en.wikipedia.org/wiki/MIT_License The full text of the MIT License follows: ======================================================================== Copyright (c) 2007-2010 Baptiste Lepilleur and The JsonCpp Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== (END LICENSE TEXT) The MIT license is compatible with both the GPL and commercial software, affording one all of the rights of Public Domain with the minor nuisance of being required to keep the above copyright notice and license text in the source code. Note also that by accepting the Public Domain "license" you can re-license your copy using whatever license you like. ================================================ FILE: WechatExporter/LICENSES/lame.LICENSE ================================================ Can I use LAME in my commercial program? Yes, you can, under the restrictions of the LGPL (see COPYING in this folder). The easiest way to do this is to: 1. Link to LAME as separate library (libmp3lame.a on unix or lame_enc.dll or libmp3lame.dll on windows) 2. Fully acknowledge that you are using LAME, and give a link to our web site, www.mp3dev.org 3. If you make modifications to LAME, you *must* release these modifications back to the LAME project, under the LGPL. ================================================ FILE: WechatExporter/LICENSES/libcurl.COPYING ================================================ COPYRIGHT AND PERMISSION NOTICE Copyright (c) 1996 - 2020, Daniel Stenberg, , and many contributors, see the THANKS file. All rights reserved. Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization of the copyright holder. ================================================ FILE: WechatExporter/LICENSES/libimobiledevice-glue.COPYING ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ================================================ FILE: WechatExporter/LICENSES/libimobiledevice.COPYING.LESSER ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ================================================ FILE: WechatExporter/LICENSES/libplist.COPYING.LESSER ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ================================================ FILE: WechatExporter/LICENSES/libusbmuxd.COPYING ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ================================================ FILE: WechatExporter/LICENSES/libxml2.Copyright ================================================ Except where otherwise noted in the source code (e.g. the files hash.c, list.c and the trio files, which are covered by a similar licence but with different Copyright notices) all the files are: Copyright (C) 1998-2012 Daniel Veillard. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is fur- nished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FIT- NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: WechatExporter/LICENSES/opencore-amr.LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: WechatExporter/LICENSES/openssl.LICENSE ================================================ LICENSE ISSUES ============== The OpenSSL toolkit stays under a double license, i.e. both the conditions of the OpenSSL License and the original SSLeay license apply to the toolkit. See below for the actual license texts. OpenSSL License --------------- /* ==================================================================== * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. All advertising materials mentioning features or use of this * software must display the following acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" * * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please contact * openssl-core@openssl.org. * * 5. Products derived from this software may not be called "OpenSSL" * nor may "OpenSSL" appear in their names without prior written * permission of the OpenSSL Project. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit (http://www.openssl.org/)" * * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * ==================================================================== * * This product includes cryptographic software written by Eric Young * (eay@cryptsoft.com). This product includes software written by Tim * Hudson (tjh@cryptsoft.com). * */ Original SSLeay License ----------------------- /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) * All rights reserved. * * This package is an SSL implementation written * by Eric Young (eay@cryptsoft.com). * The implementation was written so as to conform with Netscapes SSL. * * This library is free for commercial and non-commercial use as long as * the following conditions are aheared to. The following conditions * apply to all code found in this distribution, be it the RC4, RSA, * lhash, DES, etc., code; not just the SSL code. The SSL documentation * included with this distribution is covered by the same copyright terms * except that the holder is Tim Hudson (tjh@cryptsoft.com). * * Copyright remains Eric Young's, and as such any Copyright notices in * the code are not to be removed. * If this package is used in a product, Eric Young should be given attribution * as the author of the parts of the library used. * This can be in the form of a textual message at program startup or * in documentation (online or textual) provided with the package. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * "This product includes cryptographic software written by * Eric Young (eay@cryptsoft.com)" * The word 'cryptographic' can be left out if the rouines from the library * being used are not cryptographic related :-). * 4. If you include any Windows specific code (or a derivative thereof) from * the apps directory (application code) you must include an acknowledgement: * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" * * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * The licence and distribution terms for any publically available version or * derivative of this code cannot be changed. i.e. this code cannot simply be * copied and put under another distribution licence * [including the GNU Public Licence.] */ ================================================ FILE: WechatExporter/LICENSES/protobuf.LICENSE ================================================ Copyright 2008 Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Code generated by the Protocol Buffer compiler is owned by the owner of the input file used when generating it. This code is not standalone and requires a support library to be linked with it. This support library is itself covered by the above license. ================================================ FILE: WechatExporter/LoggerImpl.h ================================================ // // LoggerImpl.h // WechatExporter // // Created by Matthew on 2020/10/1. // Copyright © 2020 Matthew. All rights reserved. // #include "core/Logger.h" #include #include "ViewController.h" #ifndef LoggerImpl_h #define LoggerImpl_h class LoggerImpl : public Logger { protected: __weak ViewController *m_viewController; NSLock *m_lock; char m_logFile[1024]; public: LoggerImpl(ViewController* viewController) { m_viewController = viewController; m_lock = [[NSLock alloc] init]; } ~LoggerImpl() { m_viewController = nil; m_lock = nil; } void setLogPath(const char* logPath) { #if !defined(NDEBUG) || defined(DBG_PERF) [m_lock lock]; strcpy(m_logFile, logPath); if (!endsWith(logPath, DIR_SEP_STR)) { strcat(m_logFile, DIR_SEP_STR); } strcat(m_logFile, "log.txt"); FILE *file = fopen(m_logFile, "w"); if (NULL != file) { fclose(file); } [m_lock unlock]; #endif } void write(const std::string& log) { #if !defined(NDEBUG) || defined(DBG_PERF) std::string timeString = getTimestampString(false, true) + ": "; #else std::string timeString = getTimestampString() + ": "; #endif #if !defined(NDEBUG) || defined(DBG_PERF) [m_lock lock]; if (strlen(m_logFile) > 0) { FILE *file = fopen(m_logFile, "a"); if (NULL != file) { fputs(timeString.c_str(), file); fputs(log.c_str(), file); fputs("\r", file); fclose(file); } } [m_lock unlock]; #endif __block NSString *logString = [NSString stringWithUTF8String:(timeString + log).c_str()]; __block __weak ViewController* viewController = m_viewController; dispatch_async(dispatch_get_main_queue(), ^{ __strong __typeof(viewController)strongVC = viewController; if (strongVC) { [strongVC writeLog:logString]; strongVC = nil; } }); } void debug(const std::string& log) { write(log); #if !defined(NDEBUG) || defined(DBG_PERF) NSString *logString = [NSString stringWithUTF8String:log.c_str()]; NSLog(@"%@", logString); #endif } }; #endif /* LoggerImpl_h */ ================================================ FILE: WechatExporter/PdfConverterImpl.h ================================================ // // PdfConverterImpl.h // WechatExporter // // Created by Matthew on 2021/4/22. // Copyright © 2021 Matthew. All rights reserved. // #include "PdfConverter.h" #import #include "FileSystem.h" #include "Utils.h" #ifndef PdfConverterImpl_h #define PdfConverterImpl_h class PdfConverterImpl : public PdfConverter { public: PdfConverterImpl(const char *outputDir) : m_pdfSupported(false) { if (detectChromeInstalled()) { m_pdfSupported = true; m_param = @[@"--headless", @"--disable-extensions", @"--disable-gpu", @"--print-to-pdf-no-header"]; // m_param = "--headless --disable-extensions --disable-gpu --print-to-pdf=\"%%DEST%%\" --print-to-pdf-no-header \"file://%%SRC%%\""; } else if (detectEdgeInstalled()) { m_pdfSupported = true; // m_param = @[@"--headless", @"--disable-extensions", @"--disable-gpu", @"--print-to-pdf-no-header"]; m_param = @[@"--headless", @"--print-to-pdf-no-header"]; } if (NULL != outputDir) { initShellFile(outputDir); } } bool isPdfSupported() const { return m_pdfSupported; } ~PdfConverterImpl() { } void executeCommand() { NSString *shellPathString = [NSString stringWithUTF8String:m_shellPath.c_str()]; if (![[NSFileManager defaultManager] fileExistsAtPath:shellPathString]) { return; } NSURL *terminalUrl = [[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:@"com.apple.Terminal"]; if (nil == terminalUrl) { return; } [[NSWorkspace sharedWorkspace] openFile:shellPathString withApplication:terminalUrl.path]; } void setWorkDir(NSString* workDir) { m_workDir = [NSString stringWithString:workDir]; } bool makeUserDirectory(const std::string& dirName) { // std::string command = replaceAll(dirName, " ", "\\ "); std::string command = NEW_LINE + "[ -d \"pdf/" + dirName + "\" ] || mkdir \"pdf/" + dirName + "\"" + NEW_LINE; appendFile(m_shellPath, reinterpret_cast(command.c_str()), command.size()); return true; } bool convert(const std::string& htmlPath, const std::string& pdfPath) { if (!m_pdfSupported) { return false; } NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:htmlPath.c_str()]]; std::string command = "chrome "; command += "--headless --disable-gpu --disable-extensions --print-to-pdf-no-header --print-to-pdf=\""; command += pdfPath; command += "\" \""; command += [[url absoluteString] UTF8String]; command += "\" >/dev/null 2>&1" + NEW_LINE; command += "echo \"" + pdfPath.substr(m_output.size()) + "\"" + NEW_LINE; appendFile(m_shellPath, reinterpret_cast(command.c_str()), command.size()); return true; /* std::string command = [m_assemblyPath UTF8String]; command += " "; NSString *src = [NSString stringWithUTF8String:htmlPath.c_str()]; NSString *dest = [NSString stringWithUTF8String:pdfPath.c_str()]; NSURL *url = [NSURL fileURLWithPath:src]; NSMutableArray *params = [NSMutableArray arrayWithArray:m_param]; [params addObject:[NSString stringWithFormat:@"--print-to-pdf=\"%@\"", dest]]; [params addObject:[url absoluteString]]; NSString* args = [params componentsJoinedByString:@" "]; NSString* arg1 = [NSString stringWithFormat:@"--print-to-pdf=\"%@\"", dest]; command += [args UTF8String]; // system(command.c_str()); execlp([m_assemblyPath UTF8String], [m_assemblyPath UTF8String], "--headless", "--print-to-pdf-no-header", [arg1 UTF8String], [[url absoluteString] UTF8String], NULL); */ /* // std::string param = m_param; // replaceAll(param, "%%SRC%%", htmlPath); // replaceAll(param, "%%DEST%%", pdfPath); NSTask *task = [[NSTask alloc] init]; task.launchPath = m_assemblyPath; task.arguments = params; task.currentDirectoryPath = m_workDir; // task.standardOutput = pipe; NSPipe *pipe = [NSPipe pipe]; NSFileHandle *file = pipe.fileHandleForReading; task.standardOutput = pipe; [task launch]; NSData *data = [file readDataToEndOfFile]; [file closeFile]; // [[outputPipe fileHandleForReading] readToEndOfFileInBackgroundAndNotify]; [task waitUntilExit]; int status = [task terminationStatus]; return status == 0; */ } protected: bool detectChromeInstalled() { NSString *appPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@"com.google.Chrome"]; if (nil != appPath) { appPath = [appPath stringByAppendingPathComponent:@"Contents/MacOS/Google Chrome"]; if ([[NSFileManager defaultManager] fileExistsAtPath:appPath]) { m_assemblyPath = [NSString stringWithString:appPath]; return true; } } return false; } bool detectEdgeInstalled() { NSString *appPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@"com.microsoft.edgemac"]; if (nil != appPath) { appPath = [appPath stringByAppendingPathComponent:@"Contents/MacOS/Microsoft Edge"]; if ([[NSFileManager defaultManager] fileExistsAtPath:appPath]) { m_assemblyPath = [NSString stringWithString:appPath]; return true; } } return false; } void initShellFile(const char *outputDir) { m_output = outputDir; if (!endsWith(m_output, "/")) { m_output += "/"; } m_shellPath = combinePath(m_output, "pdf.sh"); deleteFile(m_shellPath); std::string aliasCmd = [m_assemblyPath UTF8String]; replaceAll(aliasCmd, " ", "\\ "); aliasCmd = "#!/bin/sh" + NEW_LINE + NEW_LINE + "alias chrome=\"" + aliasCmd + "\"" + NEW_LINE + NEW_LINE; std::string output = m_output; // replaceAll(output, " ", "\\ "); aliasCmd += "cd \"" + output + "\"" + NEW_LINE; aliasCmd += "[ -d pdf ] || mkdir pdf" + NEW_LINE; NSDictionary *attributes = @{NSFilePosixPermissions : [NSNumber numberWithShort:0777]}; NSData *contents = [NSData dataWithBytes:aliasCmd.c_str() length:aliasCmd.size()]; NSString *shellPath = [NSString stringWithUTF8String:m_shellPath.c_str()]; [[NSFileManager defaultManager] createFileAtPath:shellPath contents:contents attributes:attributes]; // appendFile(m_shellPath, reinterpret_cast(aliasCmd.c_str()), aliasCmd.size()); } private: bool m_pdfSupported; NSString *m_assemblyPath; NSArray *m_param; std::string m_output; std::string m_shellPath; NSString *m_workDir; const std::string NEW_LINE = "\n"; }; #endif /* PdfConverterImpl_h */ ================================================ FILE: WechatExporter/SessionDataSource.h ================================================ // // SessionDataSource.h // WechatExporter // // Created by Matthew on 2021/2/1. // Copyright © 2021 Matthew. All rights reserved. // #ifndef SessionDataSource_h #define SessionDataSource_h #import #include #include #include #import "WechatObjects.h" @interface SessionItem : NSObject @property (assign) NSInteger orgIndex; @property (assign) NSInteger userIndex; @property (assign) BOOL checked; @property (strong) NSString *sessionUsrName; @property (strong) NSString *displayName; @property (assign) NSInteger recordCount; @property (strong) NSString *userDisplayName; @property (strong) NSString *usrName; @property (strong) NSString *lastMessage; #ifndef NDEBUG @property (assign) NSUInteger lastMessageTime; #endif // @property (assign) NSInteger userPointer; // @property (assign) NSInteger sessionPointer; // - (NSComparisonResult)orgIndexCompare:(SessionItem *)sessionItem:(BOOL)ascending; - (NSComparisonResult)displayNameCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending; - (NSComparisonResult)recordCountCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending; - (NSComparisonResult)userIndexCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending; @end @interface SessionDataSource : NSObject @property (nonatomic, assign) NSInteger rowInProgress; @property (nonatomic, assign) NSUInteger numberOfMsgExported; - (void)loadData:(const std::vector>> *)usersAndSessions withAllUsers:(BOOL)allUsers indexOfSelectedUser:(NSInteger)indexOfSelectedUser includesSubscription:(BOOL)includesSubscriptions; - (void)getSelectedUserAndSessions:(std::map>&)usersAndSessions; - (void)bindCellView:(NSTableCellView *)cellView atRow:(NSInteger)row andColumnId:(NSString *)identifier; - (NSControlStateValue)updateCheckStateAtRow:(NSInteger)row; - (void)checkAllSessions:(BOOL)checked; @end #endif /* SessionDataSource_h */ ================================================ FILE: WechatExporter/SessionDataSource.mm ================================================ // // SessionDataSource.m // WechatExporter // // Created by Matthew on 2021/2/1. // Copyright © 2021 Matthew. All rights reserved. // #import "SessionDataSource.h" @implementation SessionItem - (NSComparisonResult)orgIndexCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending { if (self.orgIndex < sessionItem.orgIndex) return NSOrderedAscending; else if (self.orgIndex > sessionItem.orgIndex) return NSOrderedDescending; return NSOrderedSame; } - (NSComparisonResult)displayNameCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending { NSComparisonResult result = [self.displayName caseInsensitiveCompare:sessionItem.displayName]; if (result == NSOrderedSame) { result = [self orgIndexCompare:sessionItem ascending:ascending]; } else { if (!ascending) { result = (result == NSOrderedAscending) ? NSOrderedDescending : NSOrderedAscending; } } return result; } - (NSComparisonResult)recordCountCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending { if (self.recordCount < sessionItem.recordCount) return ascending ? NSOrderedAscending : NSOrderedDescending; else if (self.recordCount > sessionItem.recordCount) return ascending ? NSOrderedDescending : NSOrderedAscending; return [self orgIndexCompare:sessionItem ascending:ascending]; } - (NSComparisonResult)userIndexCompare:(SessionItem *)sessionItem ascending:(BOOL)ascending { if (self.userIndex < sessionItem.userIndex) return ascending ? NSOrderedAscending : NSOrderedDescending; else if (self.userIndex > sessionItem.userIndex) return ascending ? NSOrderedDescending : NSOrderedAscending; return [self orgIndexCompare:sessionItem ascending:ascending]; } @end @interface SessionDataSource() { const std::vector>> *m_usersAndSessions; NSInteger m_indexOfSelectedUser; NSArray *m_sessions; } @end @implementation SessionDataSource @synthesize rowInProgress = m_rowInProgress; @synthesize numberOfMsgExported = m_numberOfMsgExported; - (instancetype)init { if (self = [super init]) { m_rowInProgress = -1; m_numberOfMsgExported = 0; m_usersAndSessions = NULL; m_indexOfSelectedUser = -1; m_sessions = nil; } return self; } - (void)setRowInProgress:(NSInteger)rowInProgress { if (m_rowInProgress != rowInProgress) { m_numberOfMsgExported = 0; m_rowInProgress = rowInProgress; } } - (void)getSelectedUserAndSessions:(std::map>&)usersAndSessions { NSInteger row = 0; for (SessionItem *sessionItem in m_sessions) { if (sessionItem.checked) { std::string usrName = [sessionItem.usrName UTF8String]; std::string sessionUsrName = [sessionItem.sessionUsrName UTF8String]; std::map>::iterator it = usersAndSessions.find(usrName); if (it == usersAndSessions.end()) { it = usersAndSessions.insert(usersAndSessions.end(), std::pair>(usrName, std::map())); } it->second.insert(std::pair(sessionUsrName, (void *)row)); } ++row; } } - (void)loadData:(const std::vector>> *)usersAndSessions withAllUsers:(BOOL)allUsers indexOfSelectedUser:(NSInteger)indexOfSelectedUser includesSubscription:(BOOL)includesSubscriptions { m_usersAndSessions = usersAndSessions; m_indexOfSelectedUser = indexOfSelectedUser; m_sessions = nil; if (!allUsers && indexOfSelectedUser == -1) { return; } NSInteger orgIndex = 0; NSInteger userIndex = 0; NSMutableArray *sessions = [NSMutableArray array]; for (std::vector>>::const_iterator it = usersAndSessions->cbegin(); it != usersAndSessions->cend(); ++it, ++userIndex) { if (!allUsers) { if (userIndex != indexOfSelectedUser) { continue; } } for (std::vector::const_iterator it2 = it->second.cbegin(); it2 != it->second.cend(); ++it2, ++orgIndex) { if (!includesSubscriptions && it2->isSubscription()) { continue; } SessionItem *sessionItem = [[SessionItem alloc] init]; sessionItem.orgIndex = orgIndex; sessionItem.userIndex = userIndex; sessionItem.checked = YES; sessionItem.displayName = [NSString stringWithUTF8String:it2->getDisplayName().c_str()]; if (it2->isDeleted()) { sessionItem.displayName = [sessionItem.displayName stringByAppendingString:NSLocalizedString(@"session-deleted", comment: "")]; } sessionItem.sessionUsrName = [NSString stringWithUTF8String:it2->getUsrName().c_str()]; sessionItem.recordCount = it2->getRecordCount(); sessionItem.usrName = [NSString stringWithUTF8String:it->first.getUsrName().c_str()]; sessionItem.userDisplayName = [NSString stringWithUTF8String:it->first.getDisplayName().c_str()]; #ifndef NDEBUG sessionItem.lastMessageTime = it2->getLastMessageTime(); #endif NSString *displayMsg = nil; std::string msg = it2->getLastMessage(); if (it2->isTextMessage()) { if (it2->hasLastMessageUserDisplayName()) { msg = it2->getLastMessageUserDisplayName() + ": " + msg; } displayMsg = [NSString stringWithUTF8String:msg.c_str()]; } else { displayMsg = NSLocalizedString(@"not-text-msg", comment: ""); } // NSLocalizedString(@"err-failed-to-parse-backup", comment: "") sessionItem.lastMessage = displayMsg; [sessions addObject:sessionItem]; } } m_sessions = sessions; } - (void)checkAllSessions:(BOOL)checked { for (NSInteger idx = 0; idx < m_sessions.count; ++idx) { SessionItem *sessionItem = [m_sessions objectAtIndex:idx]; sessionItem.checked = checked; } } - (NSControlStateValue)updateCheckStateAtRow:(NSInteger)row { if (row < m_sessions.count) { SessionItem *sessionItem = [m_sessions objectAtIndex:row]; if (sessionItem != nil) { sessionItem.checked = !sessionItem.checked; } BOOL checked = sessionItem.checked; BOOL allSame = YES; for (NSInteger idx = 0; idx < m_sessions.count; ++idx) { SessionItem *sessionItem = [m_sessions objectAtIndex:idx]; if (sessionItem.checked != checked) { allSame = NO; break; } } return allSame ? (checked ? NSControlStateValueOn : NSControlStateValueOff) : NSControlStateValueMixed; } return NSControlStateValueMixed; } - (void)clearSort { m_sessions = [m_sessions sortedArrayUsingComparator: ^(SessionItem *item1, SessionItem *item2) { return [item1 orgIndexCompare:item2 ascending:YES]; }]; } - (void)sortOnDisplayName:(BOOL)ascending { __block BOOL localAsc = ascending; m_sessions = [m_sessions sortedArrayUsingComparator: ^(SessionItem *item1, SessionItem *item2) { return [item1 displayNameCompare:item2 ascending:localAsc]; }]; } - (void)sortOnRecordCount:(BOOL)ascending { __block BOOL localAsc = ascending; m_sessions = [m_sessions sortedArrayUsingComparator: ^(SessionItem *item1, SessionItem *item2) { return [item1 recordCountCompare:item2 ascending:localAsc]; }]; } - (void)sortOnUserName:(BOOL)ascending { __block BOOL localAsc = ascending; m_sessions = [m_sessions sortedArrayUsingComparator: ^(SessionItem *item1, SessionItem *item2) { return [item1 userIndexCompare:item2 ascending:localAsc]; }]; } - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { return m_sessions.count; } /* - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { return nil; } */ - (void)tableView:(NSTableView *)tableView sortDescriptorsDidChange:(NSArray *)oldDescriptors { if (tableView.sortDescriptors.count > 0) { NSSortDescriptor *sortDescriptor = [tableView.sortDescriptors objectAtIndex:0]; if ([sortDescriptor.key isEqualToString:@"columnName"]) { [self sortOnDisplayName:sortDescriptor.ascending]; } else if ([sortDescriptor.key isEqualToString:@"columnRecordCount"]) { [self sortOnRecordCount:sortDescriptor.ascending]; } else if ([sortDescriptor.key isEqualToString:@"columnUser"]) { [self sortOnUserName:sortDescriptor.ascending]; } } else { [self clearSort]; } [tableView reloadData]; } - (void)bindCellView:(NSTableCellView *)cellView atRow:(NSInteger)row andColumnId:(NSString *)identifier { SessionItem *sessionItem = [m_sessions objectAtIndex:row]; if ([identifier isEqualToString:@"columnCheck"]) { NSButton *btn = (NSButton *)cellView.subviews.firstObject; if (btn) { btn.state = sessionItem.checked; } } else if([identifier isEqualToString:@"columnName"]) { cellView.textField.stringValue = sessionItem.displayName; } else if([identifier isEqualToString:@"columnRecordCount"]) { if (row == m_rowInProgress) { cellView.textField.stringValue = [NSString stringWithFormat:@"%ld / %ld", (long)m_numberOfMsgExported, (long)sessionItem.recordCount]; } else { cellView.textField.stringValue = [NSString stringWithFormat:@"%ld", (long)sessionItem.recordCount]; } } else if([identifier isEqualToString:@"columnUser"]) { cellView.textField.stringValue = sessionItem.userDisplayName; } else if([identifier isEqualToString:@"columnLastMsg"]) { cellView.textField.stringValue = sessionItem.lastMessage; } } @end ================================================ FILE: WechatExporter/ViewController copy.mm ================================================ // // ViewController.m // WechatExporter // // Created by Matthew on 2020/9/29. // Copyright © 2020 Matthew. All rights reserved. // #import "ViewController.h" #include "ITunesParser.h" #include "LoggerImpl.h" #include "ShellImpl.h" #include "ExportNotifierImpl.h" #include "RawMessage.h" #include "Utils.h" #include "Exporter.h" #include #include void errorLogCallback(void *pArg, int iErrCode, const char *zMsg) { NSString *log = [NSString stringWithUTF8String:zMsg]; NSLog(@"SQLITE3: %@", log); } @interface ViewController() { ShellImpl* m_shell; LoggerImpl* m_logger; ExportNotifierImpl *m_notifier; Exporter* m_exporter; std::vector m_manifests; NSInteger m_selectedIndex; } @end @implementation ViewController -(void)dealloc { [self stopExporting]; } - (void)stopExporting { if (NULL != m_exporter) { m_exporter->cancel(); m_exporter->waitForComplition(); delete m_exporter; m_exporter = NULL; } if (NULL != m_notifier) { delete m_notifier; m_notifier = NULL; } if (NULL != m_logger) { delete m_logger; m_logger = NULL; } if (NULL != m_shell) { delete m_shell; m_shell = NULL; } [self.btnBackup setAction:nil]; [self.btnOutput setAction:nil]; [self.btnExport setAction:nil]; [self.btnCancel setAction:nil]; [self.chkboxDesc setAction:nil]; [self.chkboxNoAudio setAction:nil]; // self.popupBackup.delegate = nil; } - (void)viewDidLoad { [super viewDidLoad]; #ifndef NDEBUG self.chkboxNoAudio.hidden = NO; #endif sqlite3_config(SQLITE_CONFIG_LOG, errorLogCallback, NULL); [self.view.window center]; // self.txtboxLogs. m_selectedIndex = 0; m_shell = new ShellImpl(); m_logger = new LoggerImpl(self); m_notifier = new ExportNotifierImpl(self); m_exporter = NULL; [self.btnBackup setAction:@selector(btnBackupClicked:)]; [self.btnOutput setAction:@selector(btnOutputClicked:)]; [self.btnExport setAction:@selector(btnExportClicked:)]; [self.btnCancel setAction:@selector(btnCancelClicked:)]; [self.chkboxDesc setAction:@selector(btnDescClicked:)]; [self.chkboxNoAudio setAction:@selector(btnIgnoreAudioClicked:)]; // self.popupBackup.delegate = self; BOOL descOrder = [[NSUserDefaults standardUserDefaults] boolForKey:@"Desc"]; self.chkboxDesc.state = descOrder ? NSOnState : NSOffState; BOOL ignoreAudio = [[NSUserDefaults standardUserDefaults] boolForKey:@"IgnoreAudio"]; self.chkboxNoAudio.state = ignoreAudio ? NSOnState : NSOffState; NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *outputDir = [[NSUserDefaults standardUserDefaults] objectForKey:@"OutputDir"]; if (nil == outputDir || [outputDir isEqualToString:@""]) { NSMutableArray *components = [NSMutableArray array]; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); if (nil == paths && paths.count > 0) { [components addObject:[paths objectAtIndex:0]]; } else { [components addObject:NSHomeDirectory()]; [components addObject:@"Documents"]; } [components addObject:@"WechatHistory"]; outputDir = [NSString pathWithComponents:components]; } self.txtboxOutput.stringValue = outputDir; NSURL *appSupport = [fileManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil]; NSArray *components = @[[appSupport path], @"MobileSync", @"Backup"]; NSString *backupDir = [NSString pathWithComponents:components]; BOOL isDir = NO; if ([fileManager fileExistsAtPath:backupDir isDirectory:&isDir] && isDir) { NSString *backupDir = [NSString pathWithComponents:components]; ManifestParser parser([backupDir UTF8String], m_shell); std::vector manifests; if (parser.parse(manifests)) { NSString *previoudBackupDir = [[NSUserDefaults standardUserDefaults] objectForKey:@"BackupDir"]; [self updateBackups:manifests withPreviousPath:previoudBackupDir]; } } self.popupBackup.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable; self.btnBackup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin; self.txtboxOutput.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable; self.btnOutput.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin; self.popupUsers.autoresizingMask = NSViewMinYMargin; self.sclSessions.autoresizingMask = NSViewMinYMargin | NSViewHeightSizable; self.sclViewLogs.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; self.progressBar.autoresizingMask = NSViewMaxYMargin; self.btnCancel.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin; self.btnExport.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin; }; - (void)updateBackups:(const std::vector&) manifests withPreviousPath:(NSString *)previousPath { if (manifests.empty()) { return; } size_t selectedIndex = (size_t)self.popupBackup.indexOfSelectedItem; for (std::vector::const_iterator it = manifests.cbegin(); it != manifests.cend(); ++it) { std::vector::const_iterator it2 = std::find(m_manifests.cbegin(), m_manifests.cend(), *it); if (it2 == m_manifests.cend()) { m_manifests.push_back(*it); } } // update [self.popupBackup removeAllItems]; for (std::vector::const_iterator it = m_manifests.cbegin(); it != m_manifests.cend(); ++it) { std::string itemTitle = it->toString(); NSString* item = [NSString stringWithUTF8String:itemTitle.c_str()]; [self.popupBackup addItemWithTitle:item]; if (selectedIndex == -1) { if (nil != previousPath && ![previousPath isEqualToString:@""]) { NSString* itemPath = [NSString stringWithUTF8String:it->getPath().c_str()]; if ([previousPath isEqualToString:itemPath]) { selectedIndex = std::distance(m_manifests.cbegin(), it); } } } } if (selectedIndex == -1 && self.popupBackup.numberOfItems > 0) { selectedIndex = 0; } if (selectedIndex != -1 && selectedIndex < self.popupBackup.numberOfItems) { [self.popupBackup selectItemAtIndex:selectedIndex]; } } -(void)comboBoxSelectionDidChange:(NSNotification *)notification { #ifndef NDEBUG if ([notification.name isEqualToString:NSComboBoxSelectionDidChangeNotification]) { NSComboBox *cmb = (NSComboBox *)notification.object; if (cmb == self.popupBackup) { if (self.popupBackup.indexOfSelectedItem != -1 && self.popupBackup.indexOfSelectedItem < m_manifests.size()) { const BackupManifest& manifest = m_manifests[self.popupBackup.indexOfSelectedItem]; std::string backup = manifest.getPath(); NSString *backupPath = [NSString stringWithUTF8String:backup.c_str()]; [[NSUserDefaults standardUserDefaults] setObject:backupPath forKey:@"BackupDir"]; } } } #endif } - (void)btnBackupClicked:(id)sender { NSOpenPanel *panel = [NSOpenPanel openPanel]; panel.canChooseFiles = NO; panel.canChooseDirectories = YES; panel.allowsMultipleSelection = NO; panel.canCreateDirectories = NO; panel.showsHiddenFiles = YES; [panel setDirectoryURL:[NSURL URLWithString:NSHomeDirectory()]]; // Set panel's default directory. // [panel setDirectoryURL:[NSURL URLWithString:@"/Users/matthew/Documents/reebes/Backup"]]; // Set panel's default directory. [panel beginSheetModalForWindow:[self.view window] completionHandler: (^(NSInteger result) { if (result == NSOKButton) { NSURL *backupUrl = panel.directoryURL; ManifestParser parser([backupUrl.path UTF8String], self->m_shell); std::vector manifests; if (parser.parse(manifests) && !manifests.empty()) { [self updateBackups:manifests withPreviousPath:nil]; } else { [self msgBox:@"解析iTunes Backup文件失败。"]; } } })]; } - (void)btnOutputClicked:(id)sender { NSOpenPanel *panel = [NSOpenPanel openPanel]; panel.canChooseFiles = NO; panel.canChooseDirectories = YES; panel.allowsMultipleSelection = NO; panel.canCreateDirectories = YES; panel.showsHiddenFiles = NO; NSString *outputPath = self.txtboxOutput.stringValue; if (nil == outputPath || [outputPath isEqualToString:@""]) { [panel setDirectoryURL:[NSURL URLWithString:NSHomeDirectory()]]; // Set panel's default directory. } else { [panel setDirectoryURL:[NSURL fileURLWithPath:outputPath]]; } [panel beginSheetModalForWindow:[self.view window] completionHandler: (^(NSInteger result){ if (result == NSOKButton) { NSURL *url = panel.directoryURL; [[NSUserDefaults standardUserDefaults] setObject:url.path forKey:@"OutputDir"]; self.txtboxOutput.stringValue = url.path; } })]; } - (void)btnExportClicked:(id)sender { if (NULL != m_exporter) { [self msgBox:@"导出已经在执行。"]; return; } if (self.popupBackup.indexOfSelectedItem == -1 || self.popupBackup.indexOfSelectedItem >= m_manifests.size()) { [self msgBox:@"请选择iTunes备份目录。"]; return; } const BackupManifest& manifest = m_manifests[self.popupBackup.indexOfSelectedItem]; if (manifest.isEncrypted()) { [self msgBox:@"不支持加密的iTunes Backup。请使用不加密形式备份iPhone/iPad设备。"]; return; } std::string backup = manifest.getPath(); NSString *backupPath = [NSString stringWithUTF8String:backup.c_str()]; BOOL isDir = NO; if (![[NSFileManager defaultManager] fileExistsAtPath:backupPath isDirectory:&isDir] || !isDir) { [self msgBox:@"iTunes备份目录不存在。"]; return; } NSString *outputPath = self.txtboxOutput.stringValue; if (nil == outputPath || [outputPath isEqualToString:@""]) { [self msgBox:@"请选择输出目录。"]; return; } if (![[NSFileManager defaultManager] fileExistsAtPath:outputPath isDirectory:&isDir] || !isDir) { [self msgBox:@"输出目录不存在。"]; // self.txtboxOutput focus return; } BOOL descOrder = (self.chkboxDesc.state == NSOnState); BOOL ignoreAudio = (self.chkboxNoAudio.state == NSOnState); BOOL saveFilesInSessionFolder = (self.chkboxSaveFilesInSessionFolder.state == NSOnState); m_logger->write("iTunes Backup:" + manifest.getPath()); m_logger->write("iTunes Version:" + manifest.getITunesVersion()); self.txtViewLogs.string = @""; [self onStart]; NSDictionary *dict = @{@"backup": backupPath, @"output": outputPath, @"descOrder": @(descOrder), @"ignoreAudio": @(ignoreAudio), @"saveFilesInSessionFolder": @(saveFilesInSessionFolder)}; [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:dict]; } - (void)btnCancelClicked:(id)sender { if (NULL == m_exporter) { // [self msgBox:@"当前未执行导出。"]; return; } m_exporter->cancel(); [self.btnCancel setEnabled:NO]; } - (void)btnDescClicked:(id)sender { BOOL descOrder = (self.chkboxDesc.state == NSOnState); [[NSUserDefaults standardUserDefaults] setBool:descOrder forKey:@"Desc"]; } - (void)btnIgnoreAudioClicked:(id)sender { BOOL ignoreAudio = (self.chkboxNoAudio.state == NSOnState); [[NSUserDefaults standardUserDefaults] setBool:ignoreAudio forKey:@"IgnoreAudio"]; } - (void)run:(NSDictionary *)dict { NSString *backup = [dict objectForKey:@"backup"]; NSString *output = [dict objectForKey:@"output"]; if (backup == nil || output == nil) { [self msgBox:@"参数错误。"]; return; } NSString *iTunesVersion = [dict objectForKey:@"iTunesVersion"]; NSNumber *ignoreAudio = [dict objectForKey:@"ignoreAudio"]; NSNumber *descOrder = [dict objectForKey:@"descOrder"]; NSNumber *saveFilesInSessionFolder = [dict objectForKey:@"saveFilesInSessionFolder"]; NSString *workDir = [[NSFileManager defaultManager] currentDirectoryPath]; workDir = [[NSBundle mainBundle] resourcePath]; m_exporter = new Exporter([workDir UTF8String], [backup UTF8String], [output UTF8String], m_shell, m_logger); if (nil != ignoreAudio && [ignoreAudio boolValue]) { m_exporter->ignoreAudio(); } if (nil != descOrder && [descOrder boolValue]) { m_exporter->setOrder(false); } if (nil != saveFilesInSessionFolder && [saveFilesInSessionFolder boolValue]) { m_exporter->saveFilesInSessionFolder(); } m_exporter->setNotifier(m_notifier); m_exporter->run(); } - (void)msgBox:(NSString *)msg { __block NSString *localMsg = [NSString stringWithString:msg]; __block NSString *title = [NSRunningApplication currentApplication].localizedName; dispatch_async(dispatch_get_main_queue(), ^{ NSAlert *alert = [[NSAlert alloc] init]; alert.messageText = localMsg; alert.window.title = title; [alert runModal]; }); } - (void)onStart { self.view.window.styleMask &= ~NSClosableWindowMask; [self.popupBackup setEnabled:NO]; [self.btnOutput setEnabled:NO]; [self.btnBackup setEnabled:NO]; [self.btnExport setEnabled:NO]; [self.btnCancel setEnabled:YES]; [self.chkboxDesc setEnabled:NO]; [self.chkboxNoAudio setEnabled:NO]; [self.chkboxSaveFilesInSessionFolder setEnabled:NO]; [self.progressBar startAnimation:nil]; } - (void)onComplete:(BOOL)cancelled { self.view.window.styleMask |= NSClosableWindowMask; [self.btnExport setEnabled:YES]; [self.btnCancel setEnabled:NO]; [self.popupBackup setEnabled:YES]; [self.btnOutput setEnabled:YES]; [self.btnBackup setEnabled:YES]; [self.chkboxDesc setEnabled:YES]; [self.chkboxNoAudio setEnabled:YES]; [self.chkboxSaveFilesInSessionFolder setEnabled:YES]; [self.progressBar stopAnimation:nil]; if (m_exporter) { m_exporter->waitForComplition(); delete m_exporter; m_exporter = NULL; } } - (void)writeLog:(NSString *)log { NSString *newLog = nil; if (nil == self.txtViewLogs.string || self.txtViewLogs.string.length == 0) { self.txtViewLogs.string = [log copy]; } else { newLog = [NSString stringWithFormat:@"%@\n%@", self.txtViewLogs.string, log]; self.txtViewLogs.string = newLog; } NSPoint newScrollOrigin; // assume that the scrollview is an existing variable if ([[self.sclViewLogs documentView] isFlipped]) { newScrollOrigin = NSMakePoint(0.0, NSMaxY([[self.sclViewLogs documentView] frame]) -NSHeight([[self.sclViewLogs contentView] bounds])); } else { newScrollOrigin = NSMakePoint(0.0,0.0); } [[self.sclViewLogs documentView] scrollPoint:newScrollOrigin]; } - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { return 0; } @end ================================================ FILE: WechatExporter/ViewController.h ================================================ // // ViewController.h // WechatExporter // // Created by Matthew on 2020/9/29. // Copyright © 2020 Matthew. All rights reserved. // #import @interface ViewController : NSViewController @property (weak) IBOutlet NSTextField *lblITunes; @property (weak) IBOutlet NSButton *btnExport; @property (weak) IBOutlet NSButton *btnCancel; @property (weak) IBOutlet NSButton *btnQuit; @property (weak) IBOutlet NSTextField *txtboxOutput; @property (weak) IBOutlet NSPopUpButton *popupBackup; @property (weak) IBOutlet NSPopUpButton *popupUsers; @property (weak) IBOutlet NSButton *btnToggleAll; @property (weak) IBOutlet NSScrollView *sclSessions; @property (weak) IBOutlet NSTableView *tblSessions; @property (weak) IBOutlet NSButton *btnBackup; @property (weak) IBOutlet NSButton *btnOutput; @property (weak) IBOutlet NSButton *btnShowLogs; @property (weak) IBOutlet NSButton *btnBackupDevice; @property (weak) IBOutlet NSProgressIndicator *progressBar; @property (weak) IBOutlet NSScrollView *sclViewLogs; @property (unsafe_unretained) IBOutlet NSTextView *txtViewLogs; - (void)writeLog:(NSString *)log; - (void)onStart; - (void)onComplete:(BOOL)cancelled; - (void)onSessionStart:(NSString *)usrName row:(NSInteger)row; - (void)onSessionProgress:(NSString *)sessionUsrName row:(NSInteger)row numberOfMessages:(NSUInteger)numberOfMessages numberOfTotalMessages:(NSUInteger)numberOfTotalMessages; - (void)onSessionComplete:(NSString *)usrName; @end ================================================ FILE: WechatExporter/ViewController.mm ================================================ // // ViewController.mm // WechatExporter // // Created by Matthew on 2020/9/29. // Copyright © 2020 Matthew. All rights reserved. // #import "ViewController.h" #import "SessionDataSource.h" #import "HttpHelper.h" #import "AppConfiguration.h" #include "LoggerImpl.h" #include "ExportNotifierImpl.h" #include "PdfConverterImpl.h" #include "Utils.h" #include "Exporter.h" #include "IDeviceBackup.h" #include "WechatSource.h" #include "Updater.h" @interface ViewController() { LoggerImpl* m_logger; ExportNotifierImpl *m_notifier; PdfConverterImpl *m_pdfConverter; Exporter* m_exporter; // std::vector m_manifests; std::vector m_manifests; std::vector>> m_usersAndSessions; SessionDataSource *m_dataSource; NSIndexSet *m_columns; NSColor *m_orgTextColor; } @end @implementation ViewController - (instancetype)init { if (self = [super init]) { m_dataSource = [[SessionDataSource alloc] init]; m_orgTextColor = [NSColor clearColor]; } return self; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { m_dataSource = [[SessionDataSource alloc] init]; m_orgTextColor = [NSColor clearColor]; } return self; } - (instancetype)initWithNibName:(NSNibName)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { m_dataSource = [[SessionDataSource alloc] init]; m_orgTextColor = [NSColor clearColor]; } return self; } -(void)dealloc { [self stopExporting]; } - (void)stopExporting { if (NULL != m_exporter) { m_exporter->cancel(); m_exporter->waitForComplition(); delete m_exporter; m_exporter = NULL; } if (NULL != m_notifier) { delete m_notifier; m_notifier = NULL; } if (NULL != m_logger) { delete m_logger; m_logger = NULL; } if (NULL != m_pdfConverter) { delete m_pdfConverter; m_pdfConverter = NULL; } #ifndef NDEBUG [self.tblSessions setTarget:nil]; [self.tblSessions setDoubleAction:nil]; #endif [self.btnBackup setAction:nil]; [self.btnBackup setTarget:nil]; [self.btnOutput setAction:nil]; [self.btnOutput setTarget:nil]; [self.btnExport setAction:nil]; [self.btnExport setTarget:nil]; [self.btnCancel setAction:nil]; [self.btnCancel setTarget:nil]; [self.btnQuit setAction:nil]; [self.btnQuit setTarget:nil]; [self.popupBackup setAction:nil]; [self.popupBackup setTarget:nil]; [self.popupUsers setAction:nil]; [self.popupUsers setTarget:nil]; [self.btnShowLogs setTarget:nil]; [self.btnShowLogs setAction:nil]; [self.btnToggleAll setAction:nil]; [self.btnToggleAll setTarget:nil]; #ifndef NDEBUG [self.btnBackupDevice setAction:nil]; [self.btnBackupDevice setTarget:nil]; #endif } - (void)viewDidLoad { [super viewDidLoad]; m_columns = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.tblSessions.numberOfColumns)]; self.popupBackup.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable; self.btnBackup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin; self.txtboxOutput.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable; self.btnOutput.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin; self.popupUsers.autoresizingMask = NSViewMinYMargin; self.tblSessions.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; self.sclSessions.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; self.btnShowLogs.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin; self.sclViewLogs.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; self.progressBar.autoresizingMask = NSViewMaxYMargin; self.btnCancel.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin; self.btnQuit.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin; self.btnExport.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin; #ifndef NDEBUG self.btnBackupDevice.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin; self.btnBackupDevice.hidden = NO; #endif m_logger = new LoggerImpl(self); m_notifier = new ExportNotifierImpl(self); m_exporter = NULL; [self.btnBackup setTarget:self]; [self.btnBackup setAction:@selector(btnBackupClicked:)]; [self.btnOutput setTarget:self]; [self.btnOutput setAction:@selector(btnOutputClicked:)]; [self.btnExport setTarget:self]; [self.btnExport setAction:@selector(btnExportClicked:)]; [self.btnCancel setTarget:self]; [self.btnCancel setAction:@selector(btnCancelClicked:)]; [self.btnQuit setTarget:self]; [self.btnQuit setAction:@selector(btnQuitClicked:)]; [self.btnShowLogs setTarget:self]; [self.btnShowLogs setAction:@selector(btnShowLogsClicked:)]; [self.popupBackup setTarget:self]; [self.popupBackup setAction:@selector(handlePopupButton:)]; [self.popupUsers setTarget:self]; [self.popupUsers setAction:@selector(handlePopupButton:)]; [self.btnToggleAll setTarget:self]; [self.btnToggleAll setAction:@selector(toggleAllSessions:)]; #ifndef NDEBUG [self.tblSessions setTarget:self]; [self.tblSessions setDoubleAction:@selector(tableViewDoubleClick:)]; #endif #ifndef NDEBUG [self.btnBackupDevice setTarget:self]; [self.btnBackupDevice setAction:@selector(btnBackupDeviceClicked:)]; #endif NSRect frame = [self.tblSessions.headerView headerRectOfColumn:0]; NSRect btnFrame = self.btnToggleAll.frame; btnFrame.size.width = btnFrame.size.height; btnFrame.origin.x = (frame.size.width - btnFrame.size.width) / 2; btnFrame.origin.y = (frame.size.height - btnFrame.size.height) / 2; self.btnToggleAll.frame = btnFrame; [self.tblSessions.headerView addSubview:self.btnToggleAll]; for (NSTableColumn *tableColumn in self.tblSessions.tableColumns) { NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:tableColumn.identifier ascending:YES selector:@selector(compare:)]; [tableColumn setSortDescriptorPrototype:sortDescriptor]; } self.tblSessions.dataSource = m_dataSource; self.tblSessions.delegate = self; self.txtboxOutput.stringValue = [AppConfiguration getLastOrDefaultOutputDir]; #ifdef NDEBUG NSString *previoudBackupDir = [AppConfiguration getLastBackupDir]; previoudBackupDir = nil; if (nil != previoudBackupDir) { #ifndef USING_DECODED_ITUNESBACKUP std::unique_ptr parser(new ManifestParser([previoudBackupDir UTF8String], false)); #else std::unique_ptr parser(new DecodedManifestParser([previoudBackupDir UTF8String], false)); #endif std::vector manifests; if (parser->parse(manifests)) { [self updateBackups:manifests withPreviousPath:previoudBackupDir]; } else { m_logger->debug(parser->getLastError()); } } #else NSString *backupDir = [AppConfiguration getDefaultBackupDir:YES]; if (nil != backupDir) { #ifndef USING_DECODED_ITUNESBACKUP std::unique_ptr parser(new ManifestParser([backupDir UTF8String], false)); #else std::unique_ptr parser(new DecodedManifestParser([backupDir UTF8String], false)); #endif // std::unique_ptr parser(new ManifestParser([backupDir UTF8String], false)); std::vector manifests; if (parser->parse(manifests)) { NSString *previoudBackupDir = [AppConfiguration getLastBackupDir]; [self updateBackups:manifests withPreviousPath:previoudBackupDir]; } else { m_logger->debug(parser->getLastError()); } } #endif BOOL checkUpdateDisabled = [AppConfiguration isCheckingUpdateDisabled]; NSInteger lastChkUpdateTime = [AppConfiguration getLastCheckUpdateTime]; #ifndef NDEBUG lastChkUpdateTime = 0; #endif if (!checkUpdateDisabled && ((getUnixTimeStamp() - (uint32_t)lastChkUpdateTime) > 86400)) { #ifndef NDEBUG [self performSelector:@selector(checkUpdate) withObject:nil afterDelay:1]; #else [self performSelector:@selector(checkUpdate) withObject:nil afterDelay:5]; #endif } #ifndef NDEBUG [self performSelector:@selector(showGuide) withObject:nil afterDelay:1.0]; #endif if (@available(macOS 10.14, *)) { if (self.popupBackup.itemArray.count == 0) { NSFileManager * fm = [NSFileManager defaultManager]; NSString *backupDir = [AppConfiguration getDefaultBackupDir:NO]; BOOL readable = [fm isReadableFileAtPath:backupDir]; if (!readable) { typeof(self) __weak weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) strongSelf = weakSelf; // strong by default if (nil == strongSelf) { return; } NSAlert *alert = [[NSAlert alloc] init]; alert.messageText = NSLocalizedString(@"grant-full-disk-access", nil); alert.window.title = [NSRunningApplication currentApplication].localizedName; NSButton *btnGrant = [alert addButtonWithTitle:NSLocalizedString(@"btn-grant-full-disk-access", nil)]; [btnGrant setTarget:strongSelf]; [btnGrant setAction:@selector(btnGrantClicked:)]; [alert addButtonWithTitle:NSLocalizedString(@"btn-ok", nil)]; [alert runModal]; [btnGrant setTarget:nil]; [btnGrant setAction:nil]; }); } } } } - (void)updateBackups:(const std::vector&) manifests withPreviousPath:(NSString *)previousPath { if (manifests.empty()) { return; } size_t selectedIndex = (size_t)self.popupBackup.indexOfSelectedItem; for (std::vector::const_iterator it = manifests.cbegin(); it != manifests.cend(); ++it) { std::vector::const_iterator it2 = std::find(m_manifests.cbegin(), m_manifests.cend(), *it); if (it2 == m_manifests.cend()) { m_manifests.push_back(*it); } } // update [self.popupBackup removeAllItems]; for (std::vector::const_iterator it = m_manifests.cbegin(); it != m_manifests.cend(); ++it) { std::string itemTitle = it->toString(); NSString* item = [NSString stringWithUTF8String:itemTitle.c_str()]; [self.popupBackup addItemWithTitle:item]; if (selectedIndex == -1) { if (nil != previousPath && ![previousPath isEqualToString:@""]) { NSString* itemPath = [NSString stringWithUTF8String:it->getPath().c_str()]; if ([previousPath isEqualToString:itemPath]) { selectedIndex = std::distance(m_manifests.cbegin(), it); } } } } if (selectedIndex == -1 && self.popupBackup.numberOfItems > 0) { selectedIndex = 0; } if (selectedIndex != -1 && selectedIndex < [self.popupBackup numberOfItems]) { [self setPopupButton:self.popupBackup selectedItemAt:selectedIndex]; } } - (IBAction)handlePopupButton:(NSPopUpButton *)popupButton { if (popupButton == self.popupBackup) { m_usersAndSessions.clear(); [self.popupUsers removeAllItems]; self.txtViewLogs.string = @""; // Clear Users and Sessions [m_dataSource loadData:&m_usersAndSessions withAllUsers:YES indexOfSelectedUser:-1 includesSubscription:[AppConfiguration includeSubscriptions]]; [self.tblSessions reloadData]; if (self.popupBackup.indexOfSelectedItem == -1 || self.popupBackup.indexOfSelectedItem >= m_manifests.size()) { return; } const BackupItem& manifest = m_manifests[self.popupBackup.indexOfSelectedItem]; if (manifest.isEncrypted()) { [self msgBox:NSLocalizedString(@"err-encrypted-bkp-not-supported", comment: "")]; return; } NSString *backupPath = [NSString stringWithUTF8String:manifest.getPath().c_str()]; #ifndef NDEBUG [AppConfiguration setLastBackupDir:backupPath]; #endif [self performSelector:@selector(loadDataForBackup:) withObject:backupPath afterDelay:0.016]; } else if (popupButton == self.popupUsers) { NSInteger indexOfSelectedItem = self.popupUsers.indexOfSelectedItem; BOOL allUsers = (indexOfSelectedItem == 0); if (indexOfSelectedItem != -1) { indexOfSelectedItem--; } [m_dataSource loadData:&m_usersAndSessions withAllUsers:allUsers indexOfSelectedUser:indexOfSelectedItem includesSubscription:[AppConfiguration includeSubscriptions]]; self.btnToggleAll.state = NSControlStateValueOn; [self.tblSessions reloadData]; } } - (void)loadDataForBackup:(NSString *)backupPath { #ifndef NDEBUG m_logger->write("Start loading users and sessions."); #endif __block NSString *backupDir = [NSString stringWithString:backupPath]; __block NSString *workDir = [[NSBundle mainBundle] resourcePath]; typeof(self) __weak weakSelf = self; [self setUIEnabled:NO withCancellable:NO]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ __strong typeof(weakSelf) strongSelf = weakSelf; // strong by default if (nil != strongSelf) { Exporter exp([workDir UTF8String], [backupDir UTF8String], "", strongSelf->m_logger, NULL); ExportOption options; options.outputDebugLogs([AppConfiguration outputDebugLogs]); exp.setOptions(options); exp.setLanguageCode([[self getCurrentLanguageCode] UTF8String]); // exp.setLanguageCode([[self getCurrentLanguageCode] UTF8String]); exp.loadUsersAndSessions(); exp.swapUsersAndSessions(strongSelf->m_usersAndSessions); } // update UI on the main thread dispatch_async(dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) strongSelf = weakSelf; // strong by default if (strongSelf) { #ifndef NDEBUG strongSelf->m_logger->write("Data Loaded."); #endif [strongSelf loadUsers]; [strongSelf setUIEnabled:YES withCancellable:NO]; } }); }); } - (void)filterSubscriptions { if ([AppConfiguration includeSubscriptions]) { } } - (void)toggleAllSessions:(id)sender { NSButton *btn = (NSButton *)sender; if (btn.state == NSControlStateValueMixed) { [self.btnToggleAll setNextState]; } if (btn.state == NSControlStateValueOn) { [m_dataSource checkAllSessions:YES]; } else if (btn.state == NSControlStateValueOff) { [m_dataSource checkAllSessions:NO]; } [self.tblSessions reloadData]; } - (void)checkButtonTapped:(id)sender { NSButton *btn = (NSButton *)sender; NSControlStateValue state = [m_dataSource updateCheckStateAtRow:btn.tag]; self.btnToggleAll.state = state; } - (void)tableViewDoubleClick:(id)sender { NSTableView *tableView = (NSTableView *)sender; if (1 != tableView.clickedColumn && 3 != tableView.clickedColumn) { return; } NSView *view = [tableView viewAtColumn:tableView.clickedColumn row:tableView.clickedRow makeIfNecessary:NO]; if (nil == view) { return; } NSTextField *textField = (NSTextField *)view.subviews.firstObject; if (nil != textField && nil != textField.stringValue) { [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; [[NSPasteboard generalPasteboard] setString:textField.stringValue forType:NSStringPboardType]; } } - (void)btnGrantClicked:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"]]; } - (void)btnShowLogsClicked:(id)sender { BOOL logsHidden = self.sclViewLogs.hidden; self.sclViewLogs.hidden = !logsHidden; self.sclSessions.hidden = logsHidden; self.btnShowLogs.title = NSLocalizedString((logsHidden ? @"hide-logs" : @"show-logs"), comment: ""); } #ifndef NDEBUG - (void)btnBackupDeviceClicked:(id)sender { NSButton* btn = (NSButton*)sender; std::vector devices; IDeviceBackup::queryDevices(devices); [btn setEnabled:NO]; if (!devices.empty()) { NSString *outputPath = self.txtboxOutput.stringValue; outputPath = [outputPath stringByAppendingPathComponent:@"Backup"]; IDeviceBackup deviceBackup(devices.front(), [outputPath UTF8String]); deviceBackup.backup(); } [btn setEnabled:YES]; } #endif - (void)btnBackupClicked:(id)sender { NSOpenPanel *panel = [NSOpenPanel openPanel]; panel.canChooseFiles = NO; panel.canChooseDirectories = YES; panel.allowsMultipleSelection = NO; panel.canCreateDirectories = NO; panel.showsHiddenFiles = YES; NSString *backupDir = [AppConfiguration getDefaultBackupDir:NO]; [panel setDirectoryURL:[NSURL fileURLWithPath:backupDir]]; // Set panel's default directory. [panel beginSheetModalForWindow:[self.view window] completionHandler: (^(NSInteger result) { if (result == NSOKButton) { NSURL *backupUrl = panel.URL; #ifndef USING_DECODED_ITUNESBACKUP std::unique_ptr parser(new ManifestParser([backupUrl.path UTF8String], false)); #else std::unique_ptr parser(new DecodedManifestParser([backupUrl.path UTF8String], false)); #endif std::vector manifests; if (parser->parse(manifests) && !manifests.empty()) { [self updateBackups:manifests withPreviousPath:nil]; } else { [self msgBox:NSLocalizedString(@"err-failed-to-parse-backup", comment: "")]; } } })]; } - (void)btnOutputClicked:(id)sender { NSOpenPanel *panel = [NSOpenPanel openPanel]; panel.canChooseFiles = NO; panel.canChooseDirectories = YES; panel.allowsMultipleSelection = NO; panel.canCreateDirectories = YES; panel.showsHiddenFiles = NO; NSString *outputPath = self.txtboxOutput.stringValue; if (nil == outputPath || [outputPath isEqualToString:@""]) { [panel setDirectoryURL:[NSURL URLWithString:NSHomeDirectory()]]; // Set panel's default directory. } else { [panel setDirectoryURL:[NSURL fileURLWithPath:outputPath]]; } [panel beginSheetModalForWindow:[self.view window] completionHandler: (^(NSInteger result){ if (result == NSOKButton) { NSURL *url = panel.directoryURL; [AppConfiguration setLastOutputDir:url.path]; self.txtboxOutput.stringValue = url.path; } })]; } - (void)btnExportClicked:(id)sender { if (NULL != m_exporter) { [self msgBox:NSLocalizedString(@"err-exp-is-running", nil)]; return; } if (self.popupBackup.indexOfSelectedItem == -1 || self.popupBackup.indexOfSelectedItem >= m_manifests.size()) { [self msgBox:NSLocalizedString(@"err-no-backup-dir", nil)]; return; } const BackupItem& manifest = m_manifests[self.popupBackup.indexOfSelectedItem]; if (manifest.isEncrypted()) { [self msgBox:NSLocalizedString(@"err-encrypted-bkp-not-supported", nil)]; return; } std::string backup = manifest.getPath(); NSString *backupPath = [NSString stringWithUTF8String:backup.c_str()]; BOOL isDir = NO; if (![[NSFileManager defaultManager] fileExistsAtPath:backupPath isDirectory:&isDir] || !isDir) { [self msgBox:NSLocalizedString(@"err-backup-dir-doesnt-exist", nil)]; return; } NSString *outputPath = self.txtboxOutput.stringValue; if (nil == outputPath || [outputPath isEqualToString:@""]) { [self msgBox:NSLocalizedString(@"err-no-output-dir", comment: "")]; return; } if (![[NSFileManager defaultManager] fileExistsAtPath:outputPath isDirectory:&isDir] || !isDir) { [self msgBox:NSLocalizedString(@"err-output-dir-doesnt-exist", comment: "")]; // self.txtboxOutput focus return; } if ([AppConfiguration getIncrementalExporting]) { uint64_t options = 0; std::string exportTime; std::string version; if (Exporter::hasPreviousExporting([outputPath UTF8String], options, exportTime, version)) { if (version.empty() && (m_usersAndSessions.size() > 1)) { [self msgBox:NSLocalizedString(@"invld-inc-exp-for-multi-users", comment: "")]; return; } NSString* prevExpFound = [NSString stringWithFormat:NSLocalizedString(@"prev-exp-found", comment: ""), [NSString stringWithUTF8String:exportTime.c_str()]]; #ifdef NDEBUG [self msgBox:prevExpFound]; #endif } else { #ifdef NDEBUG [self msgBox:NSLocalizedString(@"no-prev-exp-found", comment: "")]; #endif // MessageBoxTimeout(m_hWnd, text, TEXT(""), MB_OK, 0, 4000); } } #ifndef NDEBUG // ITunesDb iTunesDb(backup, ""); // std::vector domains; // domains.push_back("AppDomain-com.tencent.xin"); // domains.push_back("AppDomainGroup-group.com.tencent.xin"); // iTunesDb.copy([outputPath UTF8String], domains); #endif #if !defined(NDEBUG) || defined(DBG_PERF) m_logger->setLogPath([outputPath UTF8String]); #endif BOOL descOrder = [AppConfiguration getDescOrder]; BOOL textMode = [AppConfiguration isTextMode]; BOOL syncLoading = ![AppConfiguration getSyncLoading]; BOOL saveFilesInSessionFolder = [AppConfiguration getSavingInSession]; NSInteger outputFormat = [AppConfiguration getOutputFormat]; if (outputFormat == OUTPUT_FORMAT_PDF) { syncLoading = YES; // loadingDataOnScroll = false; // supportingFilter = false; } self.txtViewLogs.string = @""; [self onStart]; NSDictionary *dict = @{@"backup": backupPath, @"output": outputPath, @"descOrder": @(descOrder), @"textMode": @(textMode), @"syncLoading": @(syncLoading), @"saveFilesInSessionFolder": @(saveFilesInSessionFolder)}; // [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:dict]; [self run:dict]; [self performSelector:@selector(showGuide) withObject:nil afterDelay:1.0]; } - (void)btnCancelClicked:(id)sender { if (NULL == m_exporter) { // [self msgBox:@"当前未执行导出。"]; return; } m_exporter->cancel(); [self.btnCancel setEnabled:NO]; } - (void)btnQuitClicked:(id)sender { [self.view.window.windowController close]; } - (void)run:(NSDictionary *)dict { NSString *backup = [dict objectForKey:@"backup"]; NSString *output = [dict objectForKey:@"output"]; if (backup == nil || output == nil) { [self msgBox:NSLocalizedString(@"err-wrong-param", comment: "")]; return; } ExportOption options = [AppConfiguration buildOptions]; // NSString *iTunesVersion = [dict objectForKey:@"iTunesVersion"]; NSNumber *textMode = [dict objectForKey:@"textMode"]; NSNumber *descOrder = [dict objectForKey:@"descOrder"]; NSNumber *syncLoading = [dict objectForKey:@"syncLoading"]; NSNumber *saveFilesInSessionFolder = [dict objectForKey:@"saveFilesInSessionFolder"]; NSString *workDir = [[NSFileManager defaultManager] currentDirectoryPath]; workDir = [[NSBundle mainBundle] resourcePath]; std::map> usersAndSessions; [m_dataSource getSelectedUserAndSessions:usersAndSessions]; if (NULL != m_pdfConverter) { delete m_pdfConverter; m_pdfConverter = NULL; } if ([AppConfiguration isPdfMode]) { m_pdfConverter = new PdfConverterImpl([output UTF8String]); m_pdfConverter->setWorkDir(output); } m_exporter = new Exporter([workDir UTF8String], [backup UTF8String], [output UTF8String], m_logger, m_pdfConverter); m_exporter->setLanguageCode([[self getCurrentLanguageCode] UTF8String]); // m_exporter->setIncrementalExporting([AppConfiguration getIncrementalExporting]); // m_exporter->supportsFilter([AppConfiguration getSupportingFilter]); // m_exporter->outputDebugLogs([AppConfiguration outputDebugLogs]); // if ([AppConfiguration includeSubscriptions]) { // m_exporter->includesSubscription(); } if (options.isTextMode()) { m_exporter->setExtName("txt"); m_exporter->setTemplatesName("templates_txt"); } if (options.isPdfMode()) { options.setSyncLoading(); options.supportsFilter(false); } m_exporter->setOptions(options); m_exporter->setNotifier(m_notifier); m_exporter->filterUsersAndSessions(usersAndSessions); m_exporter->run(); } - (void)loadUsers { [self.popupUsers removeAllItems]; if (m_usersAndSessions.empty()) { return; } [self.popupUsers addItemWithTitle:NSLocalizedString(@"txt-all-wechat-users", comment: "")]; // CComboBox cbmBox = GetDlgItem(IDC_USERS); for (std::vector>>::const_iterator it = m_usersAndSessions.cbegin(); it != m_usersAndSessions.cend(); ++it) { NSString *displayName = [NSString stringWithUTF8String:it->first.getDisplayName().c_str()]; [self.popupUsers addItemWithTitle:displayName]; } if ([self.popupUsers numberOfItems] > 0) { [self setPopupButton:self.popupUsers selectedItemAt:0]; } } - (void)setPopupButton:(NSPopUpButton *)popupButton selectedItemAt:(NSInteger)index { [popupButton.menu performActionForItemAtIndex:index]; } - (void)msgBox:(NSString *)msg { __block NSString *localMsg = [NSString stringWithString:msg]; __block NSString *title = [NSRunningApplication currentApplication].localizedName; dispatch_async(dispatch_get_main_queue(), ^{ NSAlert *alert = [[NSAlert alloc] init]; alert.messageText = localMsg; alert.window.title = title; [alert runModal]; }); } - (void)setUIEnabled:(BOOL)enabled withCancellable:(BOOL)cancellable { if (enabled) { self.view.window.styleMask |= NSClosableWindowMask; [self.progressBar stopAnimation:nil]; for (NSTableColumn *tableColumn in self.tblSessions.tableColumns) { NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:tableColumn.identifier ascending:YES selector:@selector(compare:)]; [tableColumn setSortDescriptorPrototype:sortDescriptor]; } } else { self.view.window.styleMask &= ~NSClosableWindowMask; [self.progressBar startAnimation:nil]; for (NSTableColumn *tableColumn in self.tblSessions.tableColumns) { // NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:tableColumn.identifier ascending:YES selector:@selector(compare:)]; [tableColumn setSortDescriptorPrototype:nil]; } } [self.popupBackup setEnabled:enabled]; [self.btnOutput setEnabled:enabled]; [self.btnBackup setEnabled:enabled]; [self.btnExport setEnabled:enabled]; [self.btnCancel setEnabled:!enabled && cancellable]; [self.btnCancel setHidden:enabled]; [self.btnQuit setHidden:!enabled]; // [self.chkboxDesc setEnabled:enabled]; // [self.chkboxTextMode setEnabled:enabled]; // [self.chkboxSaveFilesInSessionFolder setEnabled:enabled]; // self.tblSessions.allowsColumnReordering = enabled; } - (void)onStart { [self setUIEnabled:NO withCancellable:YES]; } - (void)onSessionStart:(NSString *)usrName row:(NSInteger)row { [self updateRow:row]; } - (void)onSessionProgress:(NSString *)sessionUsrName row:(NSInteger)row numberOfMessages:(NSUInteger)numberOfMessages numberOfTotalMessages:(NSUInteger)numberOfTotalMessages { m_dataSource.numberOfMsgExported = numberOfMessages; NSTableCellView *cellView = [self.tblSessions viewAtColumn:2 row:row makeIfNecessary:NO]; if (nil != cellView) { cellView.textField.stringValue = [NSString stringWithFormat:@"%ld / %ld", (long)numberOfMessages, (long)numberOfTotalMessages]; // tableViewCell.textField.stringValue = [NSString stringWithFormat:@"%ld", (long)sessionItem.recordCount]; } } - (void)onSessionComplete:(NSString *)usrName { [self updateRow:-1]; } - (void)updateRow:(NSInteger)row { if (row == m_dataSource.rowInProgress) { return; } NSMutableIndexSet *rows = (row != -1) ? [NSMutableIndexSet indexSetWithIndex:row] : [NSMutableIndexSet indexSet]; if (m_dataSource.rowInProgress != -1) { [rows addIndex:m_dataSource.rowInProgress]; } m_dataSource.rowInProgress = row; BOOL rowVisible = NO; NSRange visibleRange = [self.tblSessions rowsInRect:self.tblSessions.visibleRect]; if (row >= visibleRange.location && row < (visibleRange.location + visibleRange.length)) { rowVisible = YES; } if (!rowVisible) { [self.tblSessions scrollRowToVisible:row]; } [self.tblSessions reloadDataForRowIndexes:rows columnIndexes:m_columns]; } - (void)onComplete:(BOOL)cancelled { [self updateRow:-1]; if (NULL != m_pdfConverter) { if (!cancelled) { m_pdfConverter->executeCommand(); } delete m_pdfConverter; m_pdfConverter = NULL; } [self setUIEnabled:YES withCancellable:YES]; if (m_exporter) { m_exporter->waitForComplition(); delete m_exporter; m_exporter = NULL; } #ifdef NDEBUG // Don't open the folder on debug mode if (!cancelled && [AppConfiguration getOpenningFolderAfterExp]) { NSString *outputPath = self.txtboxOutput.stringValue; NSURL *url = [NSURL fileURLWithPath:outputPath]; [[NSWorkspace sharedWorkspace] openURL: url]; } #endif } - (void)writeLog:(NSString *)log { NSString *newLog = nil; if (nil == self.txtViewLogs.string || self.txtViewLogs.string.length == 0) { self.txtViewLogs.string = [log copy]; } else { newLog = [NSString stringWithFormat:@"%@\n%@", self.txtViewLogs.string, log]; self.txtViewLogs.string = newLog; } NSPoint newScrollOrigin; // assume that the scrollview is an existing variable if ([[self.sclViewLogs documentView] isFlipped]) { newScrollOrigin = NSMakePoint(0.0, NSMaxY([[self.sclViewLogs documentView] frame]) -NSHeight([[self.sclViewLogs contentView] bounds])); } else { newScrollOrigin = NSMakePoint(0.0,0.0); } [[self.sclViewLogs documentView] scrollPoint:newScrollOrigin]; } - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { NSTableCellView *cellView = [tableView makeViewWithIdentifier:[tableColumn identifier] owner:self]; if ([[tableColumn identifier] isEqualToString:@"columnCheck"]) { NSButton *btn = (NSButton *)cellView.subviews.firstObject; if (btn) { [btn setTarget:self]; [btn setAction:@selector(checkButtonTapped:)]; btn.tag = row; } } [m_dataSource bindCellView:cellView atRow:row andColumnId:[tableColumn identifier]]; BOOL clearClr = (m_orgTextColor == [NSColor clearColor]); if (m_dataSource.rowInProgress == row) { if (clearClr) { m_orgTextColor = cellView.textField.textColor; } cellView.textField.textColor = [NSColor redColor]; } else { if (!clearClr) { cellView.textField.textColor = m_orgTextColor; } } return cellView; } - (void)tableViewColumnDidResize:(NSNotification *)notification { if ([notification.name isEqualToString:NSTableViewColumnDidResizeNotification]) { NSTableView *tableView = (NSTableView *)notification.object; NSTableColumn *column = [notification.userInfo objectForKey:@"NSTableColumn"]; if (tableView == self.tblSessions && [column.identifier isEqualToString:@"columnCheck"]) { NSRect frame = [self.tblSessions.headerView headerRectOfColumn:0]; NSRect btnFrame = self.btnToggleAll.frame; btnFrame.origin.x = (frame.size.width - btnFrame.size.width) / 2; btnFrame.origin.y = (frame.size.height - btnFrame.size.height) / 2; self.btnToggleAll.frame = btnFrame; } } } - (void)showNewVersion:(NSString *)version withUrl:(NSString *)url { NSAlert *alert = [[NSAlert alloc] init]; NSString *message = [NSString stringWithFormat:NSLocalizedString(@"prompt-new-version-found", comment: ""), version]; NSString *title = [NSRunningApplication currentApplication].localizedName; [alert addButtonWithTitle:NSLocalizedString(@"btn-yes", comment: "")]; [alert addButtonWithTitle:NSLocalizedString(@"btn-no", comment: "")]; alert.messageText = message; alert.window.title = title; [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSModalResponseOK) { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; } }]; } - (void)showGuide { if ([AppConfiguration getSkipGuide]) { return; } __block NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSInformationalAlertStyle]; alert.messageText = NSLocalizedString(@"alert-update-options", nil); alert.window.title = [NSRunningApplication currentApplication].localizedName; // NSButton *btnGrant = [alert addButtonWithTitle:NSLocalizedString(@"btn-grant-full-disk-access", nil)]; // [btnGrant setTarget:strongSelf]; // [btnGrant setAction:@selector(btnGrantClicked:)]; BOOL isCN = [self isCurrentLanguageCN]; NSString *resName = isCN ? @"MainMenuCN" : @"MainMenuEN"; NSRect frame = isCN ? NSMakeRect(0, 0, 480, 183) : NSMakeRect(0, 0, 545, 183); NSImageView *imageView = [[NSImageView alloc] initWithFrame:frame]; NSBundle *bundle = [NSBundle mainBundle]; imageView.image = [bundle imageForResource:resName]; alert.accessoryView = imageView; alert.showsSuppressionButton = YES; [alert addButtonWithTitle:NSLocalizedString(@"btn-ok", nil)]; [alert layout]; NSRect frame1 = alert.accessoryView.superview.frame; NSRect frame2 = alert.suppressionButton.frame; if (frame2.origin.y > frame1.origin.y) { NSRect frame = frame1; frame1.origin = NSMakePoint(frame1.origin.x, frame2.origin.y + frame2.size.height - frame1.size.height); frame2.origin = NSMakePoint(frame2.origin.x, frame.origin.y); alert.accessoryView.superview.frame = frame1; alert.suppressionButton.frame = frame2; } [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse returnCode) { if (alert.suppressionButton.state == NSOnState) { [AppConfiguration setSkipGuide:YES]; } }]; // [btnGrant setTarget:nil]; // [btnGrant setAction:nil]; } - (void)checkUpdate { NSBundle *bundle = [NSBundle mainBundle]; NSDictionary *info = [bundle infoDictionary]; NSString *shortVersion = info[@"CFBundleShortVersionString"]; NSString *build = info[@"CFBundleVersion"]; __block NSString *version = [NSString stringWithFormat:@"%@.%@", shortVersion, build]; __block typeof(self) __weak weakSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *userAgent = [HttpHelper standardUserAgent]; Updater updater([version UTF8String]); updater.setUserAgent([userAgent UTF8String]); bool hasNewVersion = updater.checkUpdate(); NSInteger lastChkUpdateTime = static_cast(getUnixTimeStamp()); [AppConfiguration setLastCheckUpdateTime:lastChkUpdateTime]; if (!hasNewVersion) { return; } __strong typeof(weakSelf) strongSelf = weakSelf; // strong by default if (nil == strongSelf) { return; } // update UI on the main thread dispatch_async(dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) strongSelf = weakSelf; // strong by default if (strongSelf) { NSString *newVersion = [NSString stringWithUTF8String:updater.getNewVersion().c_str()]; NSString *url = [NSString stringWithUTF8String:updater.getUpdateUrl().c_str()]; [strongSelf showNewVersion:newVersion withUrl:url]; } }); }); } - (NSString *)getCurrentLanguageCode { NSString *preferredLanguage = [[NSLocale preferredLanguages] objectAtIndex:0]; if ([preferredLanguage hasPrefix:@"zh-Hans"]) { preferredLanguage = @"zh-Hans"; } else { preferredLanguage = @"en"; } #ifndef NDEBUG preferredLanguage = @"zh-Hans"; #endif return preferredLanguage; } - (BOOL)isCurrentLanguageCN { return [@"zh-Hans" isEqualToString:[self getCurrentLanguageCode]]; } @end ================================================ FILE: WechatExporter/WechatExporter.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.assets.pictures.read-only com.apple.security.cs.disable-library-validation com.apple.security.files.downloads.read-only com.apple.security.files.user-selected.read-write ================================================ FILE: WechatExporter/core/AsyncExecutor.cpp ================================================ // // AsyncExecutor.cpp // WechatExporter // // Created by Matthew on 2021/4/17. // Copyright © 2021 Matthew. All rights reserved. // #include "AsyncExecutor.h" #include "Utils.h" std::atomic_uint32_t AsyncExecutor::m_nextTaskId(1u); uint32_t AsyncExecutor::genNextTaskId() { return m_nextTaskId.fetch_add(1); } AsyncExecutor::Thread::Thread(AsyncExecutor *executor) : m_executor(executor), m_thread(new std::thread(&AsyncExecutor::Thread::ThreadFunc, this)) { } AsyncExecutor::Thread::~Thread() { m_thread->join(); m_thread.reset(); } void AsyncExecutor::Thread::ThreadFunc() { #if !defined(NDEBUG) || defined(DBG_PERF) std::string tname = m_executor->m_tag + std::to_string(++m_executor->m_tid); setThreadName(tname.c_str()); #endif m_executor->ThreadFunc(); std::unique_lock lock(m_executor->m_mutex); m_executor->m_nthreads--; m_executor->m_dead_threads.push_back(this); if ((m_executor->m_shutdown) && (m_executor->m_nthreads == 0)) { m_executor->m_shutdown_cv.notify_one(); } } void AsyncExecutor::addTask(AsyncExecutor::Task* task) { std::lock_guard lock(m_mutex); // Add works to the callbacks list m_tasks.push(task); // Increase pool size or notify as needed if (m_threads_waiting == 0 && m_nthreads < m_max_threads) { // Kick off a new thread m_nthreads++; new Thread(this); } else { m_cv.notify_one(); } // Also use this chance to harvest dead threads if (!m_dead_threads.empty()) { DestroyThreads(&m_dead_threads); } } AsyncExecutor::AsyncExecutor(int reserve_threads, int max_threads, Callback *callback) : m_callback(callback), m_shutdown(false), m_reserve_threads(reserve_threads), m_max_threads(max_threads), m_nthreads(0), m_threads_waiting(0) { #ifndef NDEBUG m_tid = 0; #endif /* for (int i = 0; i < m_reserve_threads; i++) { std::lock_guard lock(m_mutex); m_nthreads++; new Thread(this); } */ } AsyncExecutor::~AsyncExecutor() { std::unique_lock lock(m_mutex); m_shutdown = true; m_cv.notify_all(); while (m_nthreads != 0) { m_shutdown_cv.wait(lock); } DestroyThreads(&m_dead_threads); } void AsyncExecutor::DestroyThreads(std::list *threads) { for (auto it = threads->begin(); it != threads->end(); it = threads->erase(it)) { delete *it; } } size_t AsyncExecutor::getNumberOfQueue() const { std::unique_lock lock(m_mutex); size_t size = m_tasks.size(); return size; } void AsyncExecutor::shutdown() { std::unique_lock lock(m_mutex); m_shutdown = true; m_cv.notify_all(); } // true: completed, false: timeout bool AsyncExecutor::waitForCompltion(unsigned int ms) { std::unique_lock lock(m_mutex); if (m_nthreads != 0) { if (ms == 0) { m_shutdown_cv.wait(lock); return false; } else { if (m_shutdown_cv.wait_for(lock, std::chrono::milliseconds(ms)) == std::cv_status::timeout) { return false; } } } return true; } void AsyncExecutor::cancel() { std::queue tasks; { std::unique_lock lock(m_mutex); tasks.swap(m_tasks); m_shutdown = true; m_cv.notify_all(); } while (!tasks.empty()) { auto task = tasks.front(); tasks.pop(); delete task; } } void AsyncExecutor::ThreadFunc() { for (;;) { std::unique_lock lock(m_mutex); // Wait until work is available or we are shutting down. if (!m_shutdown && m_tasks.empty()) { // If there are too many threads waiting, then quit this thread if (m_threads_waiting >= m_reserve_threads) { break; } m_threads_waiting++; m_cv.wait(lock); m_threads_waiting--; } // Drain callbacks before considering shutdown to ensure all work gets completed. if (!m_tasks.empty()) { auto task = m_tasks.front(); m_tasks.pop(); lock.unlock(); if (NULL != m_callback) { m_callback->onTaskStart(this, task); } bool succeeded = task->run(); if (NULL != m_callback) { m_callback->onTaskComplete(this, task, succeeded); } delete task; } else if (m_shutdown) { break; } } } ================================================ FILE: WechatExporter/core/AsyncExecutor.h ================================================ // // AsyncExecutor.h // WechatExporter // // Created by Matthew on 2021/4/17. // Copyright © 2021 Matthew. All rights reserved. // #ifndef AsyncExecutor_h #define AsyncExecutor_h #include #include #include #include #include #include class AsyncExecutor { public: class Task { public: virtual bool run() = 0; virtual int getType() const = 0; virtual std::string getName() const { return ""; } virtual bool hasError() const { return false; } virtual std::string getError() const { return ""; } Task() : m_taskId(0u), m_userData(NULL) { } virtual ~Task() {} uint32_t getTaskId() const { return m_taskId; } void setTaskId(uint32_t taskId) { m_taskId = taskId; } const void* getUserData() const { return m_userData; } void setUserData(const void* userData) { m_userData = userData; } private: uint32_t m_taskId; const void* m_userData; }; class Callback { public: virtual void onTaskStart(const AsyncExecutor* executor, const Task *task) = 0; virtual void onTaskComplete(const AsyncExecutor* executor, const Task *task, bool succeeded) = 0; virtual ~Callback() {} }; protected: class Thread { public: Thread(AsyncExecutor* executor); ~Thread(); private: AsyncExecutor* m_executor; std::unique_ptr m_thread; void ThreadFunc(); }; public: explicit AsyncExecutor(int reserve_threads, int max_threads, Callback *callback); ~AsyncExecutor(); static uint32_t genNextTaskId(); void addTask(Task *task); size_t getNumberOfQueue() const; void shutdown(); void cancel(); // true: completed, false: timeout bool waitForCompltion(unsigned int ms); #if !defined(NDEBUG) || defined(DBG_PERF) void setTag(const std::string& tag) { m_tag = tag; } #endif protected: Callback* m_callback; #if !defined(NDEBUG) || defined(DBG_PERF) std::string m_tag; uint32_t m_tid; #endif mutable std::mutex m_mutex; std::condition_variable m_cv; std::condition_variable m_shutdown_cv; bool m_shutdown; std::queue m_tasks; int m_reserve_threads; int m_max_threads; int m_nthreads; int m_threads_waiting; std::list m_dead_threads; static std::atomic_uint32_t m_nextTaskId; void ThreadFunc(); static void DestroyThreads(std::list* m_threads); }; #endif /* AsyncExecutor_h */ ================================================ FILE: WechatExporter/core/AsyncTask.cpp ================================================ // // AsyncTask.cpp // WechatExporter // // Created by Matthew on 2021/4/20. // Copyright © 2021 Matthew. All rights reserved. // #include "AsyncTask.h" #include #include #include #ifdef _WIN32 #include #ifndef NDEBUG #include #endif #endif #include "FileSystem.h" #include "Utils.h" // #define FAKE_DOWNLOAD size_t writeHttpDataToBuffer(void *buffer, size_t size, size_t nmemb, void *user_p) { std::vector* body = reinterpret_cast *>(user_p); if (NULL != body) { size_t bytes = size * nmemb; unsigned char *ptr = reinterpret_cast(buffer); std::copy(ptr, ptr + bytes, back_inserter(*body)); return bytes; } return 0; } void DownloadTask::initialize() { curl_global_init(CURL_GLOBAL_ALL); } void DownloadTask::uninitialize() { curl_global_cleanup(); } bool DownloadTask::httpGet(const std::string& url, const std::vector>& headers, long& httpStatus, std::vector& body) { httpStatus = 0; CURLcode res = CURLE_OK; CURL *curl = NULL; body.clear(); #ifndef FAKE_DOWNLOAD // User-Agent: WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0 curl = curl_easy_init(); #ifndef NDEBUG struct curl_slist *host = NULL; #endif struct curl_slist *chunk = NULL; for (std::vector>::const_iterator it = headers.cbegin(); it != headers.cend(); ++it) { if (it->first == "User-Agent") { curl_easy_setopt(curl, CURLOPT_USERAGENT, it->second.c_str()); } #ifndef NDEBUG else if (it->first == "RESOLVE") { host = curl_slist_append(host, it->second.c_str()); } #endif else { std::string header = it->first + ": " + it->second; chunk = curl_slist_append(chunk, header.c_str()); } } #ifndef NDEBUG if (NULL != host) { curl_easy_setopt(curl, CURLOPT_RESOLVE, host); } #endif curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); if (NULL != chunk) { curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); } // curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &::writeHttpDataToBuffer); curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast(&body)); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); res = curl_easy_perform(curl); if (res == CURLE_OK) { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus); // m_error = curl_easy_strerror(res); } curl_easy_cleanup(curl); #ifndef NDEBUG if (NULL != host) { curl_slist_free_all(host); } #endif if (NULL != chunk) { curl_slist_free_all(chunk); } #endif // no FAKE_DOWNLOAD return res == CURLE_OK; } size_t writeTaskHttpData(void *buffer, size_t size, size_t nmemb, void *user_p) { DownloadTask *task = reinterpret_cast(user_p); if (NULL != task) { return task->writeData(buffer, size, nmemb); } return 0; } DownloadTask::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) { #ifndef NDEBUG if (m_output.empty()) { assert(false); } #endif } unsigned int DownloadTask::getRetries() const { return m_retries; } bool DownloadTask::run() { std::string* urls[] = { &m_url, &m_urlBackup }; for (int item = 0; item < sizeof(urls) / sizeof(std::string*); ++item) { if (urls[item]->empty()) { continue; } for (int idx = 0; idx < DEFAULT_RETRIES; idx++) { if (downloadFile(*urls[item])) { return true; } } if (startsWith(*urls[item], "http://")) { std::string url = *urls[item]; url.replace(0, 7, "https://"); if (downloadFile(url)) { return true; } } } if (!m_default.empty()) { if (copyFile(m_default, m_output)) { return true; } else { m_error += "\r\nFailed to copy default file: " + m_default + " => " + m_output; } } return false; } bool DownloadTask::downloadFile(const std::string& url) { ++m_retries; m_outputTmp = m_output + ".tmp"; deleteFile(m_outputTmp); CURLcode res = CURLE_OK; CURL *curl = NULL; #ifndef NDEBUG std::string logPath = m_output + ".http.log"; #ifdef _WIN32 CA2W pszW(logPath.c_str(), CP_UTF8); FILE* logFile = _wfopen((LPCWSTR)pszW, L"wb"); #else FILE* logFile = fopen(logPath.c_str(), "wb"); #endif #endif std::string userAgent = m_userAgent.empty() ? "WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0" : m_userAgent; long httpStatus = 0; #ifndef FAKE_DOWNLOAD // User-Agent: WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0 curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &::writeTaskHttpData); curl_easy_setopt(curl, CURLOPT_WRITEDATA, this); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); #ifndef NDEBUG curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); curl_easy_setopt(curl, CURLOPT_STDERR, logFile); #endif res = curl_easy_perform(curl); if (res != CURLE_OK) { m_error = "Failed " + m_name + "\r\n"; m_error += curl_easy_strerror(res); if (m_retries >= DEFAULT_RETRIES) { fprintf(stderr, "%s: %s\n", m_error.c_str(), m_url.c_str()); } } else { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus); #ifndef NDEBUG char *lastUrl = NULL; CURLcode res2 = curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &lastUrl); if((CURLE_OK == res2) && lastUrl && m_url != lastUrl) { m_error += " \r\nRedirect: "; m_error += lastUrl; } #endif } curl_easy_cleanup(curl); #ifndef NDEBUG if (NULL != logFile) { fclose(logFile); } #endif if (res == CURLE_OK && httpStatus == 200) { ::moveFile(m_outputTmp, m_output); if (m_mtime > 0) { updateFileTime(m_output, m_mtime); } #ifndef NDEBUG ::deleteFile(logPath); #endif return true; } #else return true; #endif // no FAKE_DOWNLOAD if (m_error.empty()) { m_error = "HTTP Status:" + std::to_string(httpStatus); } return false; } size_t DownloadTask::writeData(void *buffer, size_t size, size_t nmemb) { size_t bytesToWrite = size * nmemb; if (appendFile(m_outputTmp, reinterpret_cast(buffer), bytesToWrite)) { return bytesToWrite; } return 0; } CopyTask::CopyTask(const std::string &src, const std::string& dest, const std::string& name) : m_src(src), m_dest(dest), m_name(name) { } bool CopyTask::run() { if (::copyFile(m_src, m_dest)) { return true; } if (!existsFile(m_src)) { m_error = "Failed CP: " + m_src + "(not existed) => " + m_dest; } else { m_error = "Failed CP: " + m_src + " => " + m_dest; #ifdef _WIN32 DWORD lastError = ::GetLastError(); m_error += "LastError:" + std::to_string(lastError); #endif } return false; } Mp3Task::Mp3Task(const std::string &pcm, const std::string& mp3, unsigned int mtime) : m_pcm(pcm), m_mp3(mp3), m_mtime(mtime) { } void Mp3Task::swapBuffer(std::vector& buffer) { m_pcmData.swap(buffer); } bool Mp3Task::run() { std::vector pcmData; bool isSilk = false; bool res = silkToPcm(m_pcm, pcmData, isSilk, &m_error) && !pcmData.empty(); if (res) { res = pcmToMp3(pcmData, m_mp3); } else if (!isSilk) { res = amrToPcm(m_pcm, pcmData) && !pcmData.empty(); if (res) { res = amrPcmToMp3(pcmData, m_mp3); } } if (res) { updateFileTime(m_mp3, m_mtime); return true; } /* if (res) { m_error = "Failed pcmToMp3: " + m_pcm + " => " + m_mp3; } else { m_error = "Failed silkTpPcm: " + m_pcm; } */ return false; } PdfTask::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) { } bool PdfTask::run() { return m_pdfConverter->convert(m_src, m_dest); } ================================================ FILE: WechatExporter/core/AsyncTask.h ================================================ // // AsyncTask.h // WechatExporter // // Created by Matthew on 2021/4/20. // Copyright © 2021 Matthew. All rights reserved. // #ifndef AsyncTask_h #define AsyncTask_h #include #include "AsyncExecutor.h" #include "PdfConverter.h" #define TASK_TYPE_DOWNLOAD 1 #define TASK_TYPE_COPY 2 #define TASK_TYPE_AUDIO 3 #define TASK_TYPE_PDF 4 class DownloadTask : public AsyncExecutor::Task { private: std::string m_url; std::string m_urlBackup; std::string m_output; std::string m_default; std::string m_outputTmp; std::string m_error; std::string m_userAgent; time_t m_mtime; unsigned int m_retries; std::string m_name; public: static const unsigned int DEFAULT_RETRIES = 3; DownloadTask(const std::string &url, const std::string& output, const std::string& defaultFile, time_t mtime, const std::string& name = ""); virtual ~DownloadTask() {} virtual int getType() const { return TASK_TYPE_DOWNLOAD; } virtual std::string getName() const { return m_name; } void setUserAgent(const std::string& userAgent) { m_userAgent = userAgent; } inline std::string getUrl() const { return m_url; } inline std::string getOutput() const { return m_output; } bool hasError() const { return !m_error.empty(); } std::string getError() const { return m_error; } static void initialize(); static void uninitialize(); static bool httpGet(const std::string& url, const std::vector>& headers, long& httpStatus, std::vector& body); size_t writeData(void *buffer, size_t size, size_t nmemb); unsigned int getRetries() const; bool run(); protected: bool downloadFile(const std::string& url); }; class CopyTask : public AsyncExecutor::Task { public: CopyTask(const std::string &src, const std::string& dest, const std::string& name); virtual ~CopyTask() {} virtual int getType() const { return TASK_TYPE_COPY; } virtual std::string getName() const { return m_name; } bool hasError() const { return !m_error.empty(); } std::string getError() const { return m_error; } bool run(); private: std::string m_src; std::string m_dest; std::string m_name; std::string m_error; }; class Mp3Task : public AsyncExecutor::Task { public: Mp3Task(const std::string &pcm, const std::string& mp3, unsigned int mtime); virtual ~Mp3Task() {} virtual int getType() const { return TASK_TYPE_AUDIO; } virtual std::string getName() const { return "Mp3: " + m_mp3; } bool hasError() const { return !m_error.empty(); } std::string getError() const { return m_error; } void swapBuffer(std::vector& buffer); bool run(); private: std::string m_pcm; std::string m_mp3; unsigned int m_mtime; std::string m_error; std::vector m_pcmData; }; class PdfTask : public AsyncExecutor::Task { public: PdfTask(PdfConverter* pdfConveter, const std::string &src, const std::string& dest, const std::string& name); virtual ~PdfTask() {} virtual int getType() const { return TASK_TYPE_PDF; } virtual std::string getName() const { return "PDF: " + m_src + " => " + m_dest; } bool run(); private: PdfConverter *m_pdfConverter; std::string m_src; std::string m_dest; std::string m_name; }; #endif /* AsyncTask_h */ ================================================ FILE: WechatExporter/core/ByteArrayLocater.h ================================================ // // ByteArrayLocater.h // WechatExporter // // Created by Matthew on 2020/10/19. // Copyright © 2020 Matthew. All rights reserved. // #include #ifndef ByteArrayLocater_h #define ByteArrayLocater_h class ByteArrayLocater { public: std::vector locate(const unsigned char* data, int length, const unsigned char* candidate, int candidateLength) { std::vector positions; if (isEmptyLocate(data, length, candidate, candidateLength)) return positions; for (int i = 0; i < length; i++) { if (!isMatch(data, length, i, candidate, candidateLength)) continue; positions.push_back(i); } return positions; } std::vector> locatePair(const unsigned char* data, int length, const unsigned char* startCandidate, int startCandidateLength, const unsigned char* endCandidate, int endCandidateLength) { std::vector> positions; if (isEmptyLocate(data, length, startCandidate, startCandidateLength)) return positions; for (int i = 0; i < length;) { if (!isMatch(data, length, i, startCandidate, startCandidateLength)) { i++; continue; } int j = i + startCandidateLength; for (; j < length;) { if (!isMatch(data, length, j, endCandidate, endCandidateLength)) { j++; continue; } positions.push_back(std::make_pair(i, j)); break; } if (j == length) { // No endTag found break; } i = j + endCandidateLength; } return positions; } bool isMatch(const unsigned char* data, int length, int position, const unsigned char* candidate, int candidateLength) { if (candidateLength > (length - position)) return false; for (int i = 0; i < candidateLength; i++) if (data[position + i] != candidate[i]) return false; return true; } bool isEmptyLocate(const unsigned char* data, int length, const unsigned char* candidate, int candidateLength) { return data == NULL || candidate == NULL || length == 0 || candidateLength == 0 || candidateLength > length; } }; #endif /* ByteArrayLocater_h */ ================================================ FILE: WechatExporter/core/DownloadPool.cpp ================================================ // // DownloadPool.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include "DownloadPool.h" #include #include #include #include #include #include "OSDef.h" size_t writeData(void *buffer, size_t size, size_t nmemb, void *user_p) { Task *task = reinterpret_cast(user_p); if (NULL != task) { return task->writeData(buffer, size, nmemb); } return 0; } void Task::run() { std::ofstream output(m_output, std::fstream::in | std::fstream::out | std::fstream::trunc); output.close(); CURL *curl_handler = curl_easy_init(); curl_easy_setopt(curl_handler, CURLOPT_URL, m_url.c_str()); curl_easy_setopt(curl_handler, CURLOPT_TIMEOUT, 60); curl_easy_setopt(curl_handler, CURLOPT_WRITEFUNCTION, &::writeData); curl_easy_setopt(curl_handler, CURLOPT_WRITEDATA, this); curl_easy_perform(curl_handler); curl_easy_cleanup(curl_handler); } size_t Task::writeData(void *buffer, size_t size, size_t nmemb) { std::ofstream file; file.open (m_output, std::fstream::in | std::fstream::out | std::fstream::app | std::fstream::binary); // file.write(buffer, size); size_t bytesToWrite = size * nmemb; file.write(reinterpret_cast(buffer), bytesToWrite); file.close(); return bytesToWrite; } DownloadPool::DownloadPool() { m_noMoreTask = false; curl_global_init(CURL_GLOBAL_ALL); for (int idx = 0; idx < 4; idx++) { m_threads.push_back(std::thread(&DownloadPool::run, this)); } // vecOfThreads.push_back(std::thread(func)); } DownloadPool::~DownloadPool() { curl_global_cleanup(); } void DownloadPool::addTask(const std::string &url, const std::string& output) { std::string formatedPath = output; std::replace(formatedPath.begin(), formatedPath.end(), DIR_SEP_R, DIR_SEP); #ifndef NDEBUG struct stat buffer; if (stat (formatedPath.c_str(), &buffer) == 0) { return; } #endif std::string uid = url + output; bool existed = false; Task task(url, formatedPath); m_mtx.lock(); if (!(existed = (m_urls.find(uid) != m_urls.cend()))) { m_urls.insert(uid); m_queue.push(task); } m_mtx.unlock(); if (existed) { #ifndef NDEBUG printf("URL Existed: %s", url.c_str()); #endif } } void DownloadPool::setNoMoreTask() { m_mtx.lock(); m_noMoreTask = true; m_mtx.unlock(); } void DownloadPool::run() { while(1) { bool found = false; bool noMoreTask = false; Task task; m_mtx.lock(); size_t queueSize = m_queue.size(); if (queueSize > 0) { task = m_queue.front(); m_queue.pop(); found = true; } noMoreTask = m_noMoreTask; m_mtx.unlock(); if (found) { // run task task.run(); continue; } if (m_noMoreTask) { break; } else { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } } void DownloadPool::finishAndWaitForExit() { setNoMoreTask(); for (std::vector::iterator it = m_threads.begin(); it != m_threads.end(); ++it) { it->join(); } } ================================================ FILE: WechatExporter/core/DownloadPool.h ================================================ // // DownloadPool.h // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #ifndef DownloadPool_h #define DownloadPool_h #include #include #include #include #include #include #include class Task { protected: std::string m_url; std::string m_output; public: Task() { } Task(const std::string &url, const std::string& output) { m_url = url; m_output = output; } Task& operator=(const Task& task) { if (this != &task) { m_url = task.m_url; m_output = task.m_output; } return *this; } size_t writeData(void *buffer, size_t size, size_t nmemb); void run(); }; class DownloadPool { protected: std::queue m_queue; std::set m_urls; std::mutex m_mtx; bool m_noMoreTask; std::vector m_threads; public: DownloadPool(); ~DownloadPool(); void addTask(const std::string &url, const std::string& output); void setNoMoreTask(); void run(); void finishAndWaitForExit(); }; #endif /* DownloadPool_h */ ================================================ FILE: WechatExporter/core/Downloader.cpp ================================================ // // Downloader.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #ifdef USING_DOWNLOADER #include "Downloader.h" #include #include #include #include #include #include "Utils.h" #include "FileSystem.h" #ifdef _WIN32 #include #ifndef NDEBUG #include #endif #endif // #define FAKE_DOWNLOAD size_t writeDataToBuffer(void *buffer, size_t size, size_t nmemb, void *user_p) { std::vector* body = reinterpret_cast *>(user_p); if (NULL != body) { size_t bytes = size * nmemb; unsigned char *ptr = reinterpret_cast(buffer); std::copy(ptr, ptr + bytes, back_inserter(*body)); return bytes; } return 0; } bool Downloader::httpGet(const std::string& url, const std::vector>& headers, long& httpStatus, std::vector& body) { httpStatus = 0; CURLcode res = CURLE_OK; CURL *curl = NULL; body.clear(); #ifndef FAKE_DOWNLOAD // User-Agent: WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0 curl = curl_easy_init(); #ifndef NDEBUG struct curl_slist *host = NULL; #endif struct curl_slist *chunk = NULL; for (std::vector>::const_iterator it = headers.cbegin(); it != headers.cend(); ++it) { if (it->first == "User-Agent") { curl_easy_setopt(curl, CURLOPT_USERAGENT, it->second.c_str()); } #ifndef NDEBUG else if (it->first == "RESOLVE") { host = curl_slist_append(host, it->second.c_str()); } #endif else { std::string header = it->first + ": " + it->second; chunk = curl_slist_append(chunk, header.c_str()); } } #ifndef NDEBUG if (NULL != host) { curl_easy_setopt(curl, CURLOPT_RESOLVE, host); } #endif curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); if (NULL != chunk) { curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); } // curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &::writeDataToBuffer); curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast(&body)); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); res = curl_easy_perform(curl); if (res == CURLE_OK) { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus); // m_error = curl_easy_strerror(res); } curl_easy_cleanup(curl); #ifndef NDEBUG if (NULL != host) { curl_slist_free_all(host); } #endif if (NULL != chunk) { curl_slist_free_all(chunk); } #endif // no FAKE_DOWNLOAD return res == CURLE_OK; } size_t writeTaskData(void *buffer, size_t size, size_t nmemb, void *user_p) { Task *task = reinterpret_cast(user_p); if (NULL != task) { return task->writeData(buffer, size, nmemb); } return 0; } Task::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) { #ifndef NDEBUG if (m_output.empty()) { assert(false); } #endif } unsigned int Task::getRetries() const { return m_retries; } bool Task::run() { return m_localCopy ? copyFile() : downloadFile(); } bool Task::downloadFile() { ++m_retries; m_outputTmp = m_output + ".tmp"; deleteFile(m_outputTmp); CURLcode res = CURLE_OK; CURL *curl = NULL; #ifndef NDEBUG std::string logPath = m_output + ".log"; #ifdef _WIN32 CA2W pszW(logPath.c_str(), CP_UTF8); FILE* logFile = _wfopen((LPCWSTR)pszW, L"wb"); #else FILE* logFile = fopen(logPath.c_str(), "wb"); #endif #endif std::string userAgent = m_userAgent.empty() ? "WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0" : m_userAgent; #ifndef FAKE_DOWNLOAD // User-Agent: WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0 curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, m_url.c_str()); curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &::writeTaskData); curl_easy_setopt(curl, CURLOPT_WRITEDATA, this); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); #ifndef NDEBUG curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); curl_easy_setopt(curl, CURLOPT_STDERR, logFile); #endif long httpStatus = 0; res = curl_easy_perform(curl); if (res != CURLE_OK) { m_error = curl_easy_strerror(res); if (m_retries >= MAX_RETRIES) { fprintf(stderr, "%s: %s\n", m_error.c_str(), m_url.c_str()); } } else { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus); } curl_easy_cleanup(curl); #endif // no FAKE_DOWNLOAD #ifndef NDEBUG if (NULL != logFile) { fclose(logFile); } #endif if (res == CURLE_OK && httpStatus == 200) { ::moveFile(m_outputTmp, m_output); if (m_mtime > 0) { updateFileTime(m_output, m_mtime); } #ifndef NDEBUG ::deleteFile(logPath); #endif } return res == CURLE_OK; } bool Task::copyFile() { return ::copyFile(m_url, m_output); } size_t Task::writeData(void *buffer, size_t size, size_t nmemb) { size_t bytesToWrite = size * nmemb; if (appendFile(m_outputTmp, reinterpret_cast(buffer), bytesToWrite)) { return bytesToWrite; } return 0; } std::atomic_uint32_t Downloader::m_nextTaskId(1u); Downloader::Downloader(Logger* logger) : m_logger(logger) { m_noMoreTask = false; m_downloadTaskSize = 0; } Downloader::~Downloader() { } void Downloader::initialize() { curl_global_init(CURL_GLOBAL_ALL); } void Downloader::uninitialize() { curl_global_cleanup(); } void Downloader::setUserAgent(const std::string& userAgent) { m_userAgent = userAgent; } uint32_t Downloader::addTask(const std::string &url, const std::string& output, time_t mtime, std::string type/* = ""*/) { uint32_t taskId = m_nextTaskId.fetch_add(1); #ifndef NDEBUG if (url == "/0" || url.empty() || output.empty()) { int aa = 0; } if (!startsWith(url, "http://") && !startsWith(url, "https://") && !startsWith(url, "file://")) { assert(false); } #endif m_mtx.lock(); if (m_threads.empty()) { for (int idx = 0; idx < 4; idx++) { m_threads.push_back(std::thread(&Downloader::run, this, idx)); } } m_mtx.unlock(); std::string formatedPath = output; std::replace(formatedPath.begin(), formatedPath.end(), DIR_SEP_R, DIR_SEP); std::string uid = url + output; bool existed = false; m_mtx.lock(); if (startsWith(url, "file://")) { Task task(taskId, url.substr(7), formatedPath, mtime, true); m_copyQueue.push(task); } else { std::map::const_iterator it = m_urls.find(url); existed = (it != m_urls.cend()); if (!existed) { m_urls[url] = output; Task task(taskId, url, formatedPath, mtime); task.setUserAgent(m_userAgent); m_queue.push(task); m_downloadTaskSize++; #ifndef NDEBUG std::string key = type.empty() ? "unkownd" : type; std::map::iterator it = m_statsType.find(key); if (it == m_statsType.end()) { m_statsType.insert(std::pair(key, 1)); } else { ++(it->second); } #endif } else if (output != it->second) { Task task(taskId, it->second, formatedPath, mtime, true); m_copyQueue.push(task); } } m_mtx.unlock(); if (existed) { #ifndef NDEBUG // printf("URL Existed: %s\r\n", url.c_str()); #endif } return taskId; } #ifndef NDEBUG std::string Downloader::getStats() const { std::string stats; for (std::map::const_iterator it = m_statsType.cbegin(); it != m_statsType.cend(); ++it) { stats.append(it->first + ":" + std::to_string(it->second) + " "); } return stats; } #endif void Downloader::setNoMoreTask() { m_mtx.lock(); m_noMoreTask = true; m_mtx.unlock(); } void Downloader::run(int idx) { while(1) { bool found = false; bool noMoreTask = false; Task task; m_mtx.lock(); noMoreTask = m_noMoreTask && m_downloadTaskSize == 0; size_t queueSize = m_queue.size(); if (queueSize > 0) { task = m_queue.front(); m_queue.pop(); found = true; } else if (noMoreTask) { if (!m_copyQueue.empty()) { task = m_copyQueue.front(); m_copyQueue.pop(); found = true; } } m_mtx.unlock(); if (found) { // run task #ifndef NDEBUG if (!task.isLocalCopy()) { std::string log = "Start downloading: " + task.getUrl() + " (" + std::to_string(task.getRetries() + 1) + ")"; // m_logger->debug(log); } #endif bool succeeded = task.run(); if (!task.isLocalCopy()) { m_mtx.lock(); // unsigned int dlTaskSizeChanging = 0; if (!succeeded && task.getRetries() < Task::MAX_RETRIES) { // Retry to download it m_queue.push(task); } if (succeeded || task.getRetries() >= Task::MAX_RETRIES) { m_downloadTaskSize--; } m_mtx.unlock(); #ifndef NDEBUG std::string log = "Downloading "; log += succeeded ? "Succeeded" : "Failed"; log += ":" + task.getUrl() + " => " + task.getOutput() + " (" + std::to_string(task.getRetries() + 1) + ")"; // m_logger->debug(log); #endif if (!succeeded/* && task.getRetries() >= Task::MAX_RETRIES*/) { std::string log = "Failed Download(" + std::to_string(task.getRetries()) + "): " + task.getUrl() + " => " + task.getOutput() + " ERR:" + task.getError(); m_logger->write(log); } else if (task.getRetries() > 1) // succeeded but ever failed { std::string log = "Succeeded Download(" + std::to_string(task.getRetries()) + "): " + task.getUrl() + " => " + task.getOutput(); m_logger->write(log); } } continue; } if (noMoreTask) { break; } else { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } } void Downloader::cancel() { std::queue empty; std::queue empty2; m_mtx.lock(); m_downloadTaskSize -= m_queue.size(); m_queue.swap(empty); m_copyQueue.swap(empty2); m_mtx.unlock(); } void Downloader::shutdown() { setNoMoreTask(); for (std::vector::iterator it = m_threads.begin(); it != m_threads.end(); ++it) { it->join(); } } int Downloader::getCount() const { return static_cast(m_urls.size()); } int Downloader::getRunningCount() const { size_t count = 0; m_mtx.lock(); count = m_queue.size(); m_mtx.unlock(); return static_cast(count); } #endif // USING_DOWNLOADER ================================================ FILE: WechatExporter/core/Downloader.h ================================================ // // Downloader.h // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #ifndef Downloader_h #define Downloader_h #include #include #include #include #include #include #include #include #include "Logger.h" #ifdef USING_DOWNLOADER class Task { protected: uint32_t m_taskId; std::string m_url; std::string m_output; std::string m_outputTmp; std::string m_error; std::string m_userAgent; time_t m_mtime; bool m_localCopy; unsigned int m_retries; public: static const unsigned int MAX_RETRIES = 3; public: Task() : m_taskId(0), m_localCopy(false) { } Task(uint32_t taskId, const std::string &url, const std::string& output, time_t mtime, bool localCopy = false); void setUserAgent(const std::string& userAgent) { m_userAgent = userAgent; } inline std::string getUrl() const { return m_url; } inline std::string getOutput() const { return m_output; } inline std::string getError() const { return m_error; } bool isLocalCopy() const { return m_localCopy; } Task& operator=(const Task& task) { if (this != &task) { m_taskId = task.m_taskId; m_url = task.m_url; m_output = task.m_output; m_mtime = task.m_mtime; m_localCopy = task.m_localCopy; m_retries = task.m_retries; } return *this; } size_t writeData(void *buffer, size_t size, size_t nmemb); bool run(); unsigned int getRetries() const; protected: bool downloadFile(); bool copyFile(); }; class Downloader { protected: std::queue m_queue; std::queue m_copyQueue; std::map m_urls; // url => local file path for first download mutable std::mutex m_mtx; bool m_noMoreTask; size_t m_downloadTaskSize; // +1 when task is added, -1 when download is completed std::vector m_threads; std::string m_userAgent; static std::atomic_uint32_t m_nextTaskId; Logger* m_logger; public: Downloader(Logger* logger); ~Downloader(); void setUserAgent(const std::string& userAgent); // return taskId uint32_t addTask(const std::string &url, const std::string& output, time_t mtime, std::string type = ""); void setNoMoreTask(); void run(int idx); void cancel(); void shutdown(); int getCount() const; int getRunningCount() const; static void initialize(); static void uninitialize(); static bool httpGet(const std::string& url, const std::vector>& headers, long& httpStatus, std::vector& body); #ifndef NDEBUG std::string getStats() const; #endif protected: const Task& dequeue(); #ifndef NDEBUG std::map m_statsType; #endif }; #endif // USING_DOWNLOADER #endif /* Downloader_h */ ================================================ FILE: WechatExporter/core/ExportContext.h ================================================ // // ExportContext.h // WechatExporter // // Created by Matthew on 2021/7/15. // Copyright © 2021 Matthew. All rights reserved. // #ifndef ExportContext_h #define ExportContext_h #define EXPORT_CONTEXT_VERSION_2_0 "2.0" #define EXPORT_CONTEXT_VERSION "3.0" #include #include #include #include #include #include #define RETURN_FALSE_IF_FAILED(rc) if (SQLITE_OK != rc) { return false; } #define WXEXP_DATA_FOLDER ".wxexp" #ifndef NDEBUG #define WXEXP_DATA_FILE "wxexp.txt" #else #define WXEXP_DATA_FILE "wxexp.dat" #endif class ExportContext { private: std::string m_version; uint64_t m_options; std::time_t m_exportTime; std::string m_usrName; std::vector>> m_accountAndSessions; std::map m_maxIdForSessions; // sqlite3* m_db; sqlite3_stmt* m_stmt; uint64_t m_maxIdForSession; private: void closeDb() { if (NULL != m_stmt) { sqlite3_finalize(m_stmt); m_stmt = NULL; } if (NULL != m_db) { sqlite3_close(m_db); m_db = NULL; } } public: ExportContext() : m_db(NULL), m_stmt(NULL), m_maxIdForSession(0) { } ~ExportContext() { closeDb(); } uint64_t getOptions() const { return m_options; } void setOptions(uint64_t options) { m_options = options; } void refreshExportTime() { std::time(&m_exportTime); } std::time_t getExportTime() const { return m_exportTime; } size_t getNumberOfSessions() const { return m_maxIdForSessions.size(); } size_t getNumberOfAccounts() const { return m_accountAndSessions.size(); } std::string getVersion() const { return m_version; } size_t getNumberOfSessions(const std::string& account) const { size_t numberOfSessions = 0; for (auto it = m_accountAndSessions.cbegin(); it != m_accountAndSessions.cend(); ++it) { if (it->first == account) { numberOfSessions = it->second.size(); break; } } return numberOfSessions; } bool getMaxId(const std::string& accountUsrName, const std::string& sessionUsrName, int64_t& maxId) const { std::string key = accountUsrName + "\t" + sessionUsrName; std::map::const_iterator it = m_maxIdForSessions.find(key); if (it != m_maxIdForSessions.cend()) { maxId = it->second; return true; } return false; } void setMaxId(const std::string& accountUsrName, const std::string& sessionUsrName, int64_t maxId) { auto it = m_accountAndSessions.begin(); for (; it != m_accountAndSessions.end(); ++it) { if (it->first == accountUsrName) { break; } } // m_accountAndSessions if (it == m_accountAndSessions.end()) { it = m_accountAndSessions.insert(it, std::pair>(accountUsrName, std::list())); } it->second.push_back(sessionUsrName); std::string key = accountUsrName + "\t" + sessionUsrName; std::map::iterator it2 = m_maxIdForSessions.find(key); if (it2 != m_maxIdForSessions.end()) { maxId = it2->second; } else { m_maxIdForSessions.insert(it2, std::pair(key, maxId)); } } bool prepareUserDatabase(const Friend& user, const std::string& outputDir) { closeDb(); std::string dbPath = combinePath(outputDir, WXEXP_DATA_FOLDER, user.getHash() + ".db"); int rc = openSqlite3Database(dbPath, &m_db, false); if (SQLITE_OK == rc) { sqlite3_exec(m_db, "PRAGMA mmap_size=268435456;PRAGMA synchronous=OFF;", NULL, NULL, NULL); // 256M:268435456 2M 2097152 } return (rc == SQLITE_OK); } bool prepareSessionTable(const Session& session) { if (NULL == m_db) { return false; } if (NULL != m_stmt) { sqlite3_finalize(m_stmt); m_stmt = NULL; } 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);"; sql += "CREATE INDEX IF NOT EXISTS Chat_" + session.getHash() + "_Index ON Chat_" + session.getHash() + "(MesSvrID);"; sql += "CREATE INDEX IF NOT EXISTS Chat_" + session.getHash() + "_Index2 ON Chat_" + session.getHash() + "(CreateTime)"; int rc = sqlite3_exec(m_db, sql.c_str(), NULL, NULL, NULL); RETURN_FALSE_IF_FAILED(rc); m_maxIdForSession = 0; sql = "SELECT MAX(MesLocalID) from Chat_" + session.getHash(); rc = sqlite3_prepare_v2(m_db, sql.c_str(), (int)(sql.size()), &m_stmt, NULL); if (rc == SQLITE_OK) { if ((rc = sqlite3_step(m_stmt)) == SQLITE_ROW) { m_maxIdForSession = sqlite3_column_int64(m_stmt, 0); } sqlite3_finalize(m_stmt); m_stmt = NULL; } sql = "INSERT INTO Chat_" + session.getHash() + "(CreateTime,Des,MesLocalID,Message,MesSvrID,Status,TableVer,Type) VALUES(?,?,?,?,?,?,?,?)"; rc = sqlite3_prepare_v2(m_db, sql.c_str(), (int)(sql.size()), &m_stmt, NULL); return (rc == SQLITE_OK); } bool insertMessage(const Session& session, const WXMSG& msg) { if (msg.msgIdValue <= m_maxIdForSession) { return true; } if (NULL == m_db || NULL == m_stmt) { return false; } sqlite3_reset(m_stmt); sqlite3_clear_bindings(m_stmt); int rc = sqlite3_bind_int(m_stmt, 1, (int)msg.createTime); rc = sqlite3_bind_int(m_stmt, 2, msg.des); RETURN_FALSE_IF_FAILED(rc); rc = sqlite3_bind_int64(m_stmt, 3, msg.msgIdValue); RETURN_FALSE_IF_FAILED(rc); rc = sqlite3_bind_text(m_stmt, 4, msg.content.c_str(), (int)(msg.content.size()), NULL); RETURN_FALSE_IF_FAILED(rc); rc = sqlite3_bind_int64(m_stmt, 5, (sqlite_int64)msg.msgSvrId); RETURN_FALSE_IF_FAILED(rc); rc = sqlite3_bind_int(m_stmt, 6, msg.status); RETURN_FALSE_IF_FAILED(rc); rc = sqlite3_bind_int(m_stmt, 7, msg.tableVersion); RETURN_FALSE_IF_FAILED(rc); rc = sqlite3_bind_int(m_stmt, 8, msg.type); RETURN_FALSE_IF_FAILED(rc); rc = sqlite3_step(m_stmt); if (SQLITE_DONE != rc && SQLITE_ROW != rc) { const char* err = sqlite3_errmsg(m_db); int aa = 0; } return (rc == SQLITE_DONE || rc == SQLITE_ROW); } void saveMessage(const WXMSG& msg) { // 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) // CREATE INDEX IF NOT EXISTS Chat_%%CHAT_ID%%_ct ON Chat_%%CHAT_ID%% (CreateTime) } std::string serialize() const { Json::Value contextObj(Json::objectValue); contextObj["version"] = Json::Value(EXPORT_CONTEXT_VERSION); contextObj["options"] = Json::Value(m_options); contextObj["exportTime"] = Json::Value(static_cast(m_exportTime)); contextObj["accounts"] = Json::Value(Json::arrayValue); Json::Value& accounts = contextObj["accounts"]; int64_t maxId = 0; auto it3 = m_maxIdForSessions.cend(); for (auto it = m_accountAndSessions.cbegin(); it != m_accountAndSessions.cend(); ++it) { Json::Value& accountObj = accounts.append(Json::Value(Json::objectValue)); accountObj["usrName"] = Json::Value(it->first); accountObj["sessions"] = Json::Value(Json::arrayValue); Json::Value& sessionsObj = accountObj["sessions"]; const std::list& sessions = it->second; std::set uniqueSessions; for (auto it2 = sessions.cbegin(); it2 != sessions.cend(); ++it2) { Json::Value& itemObj = sessionsObj.append(Json::Value(Json::objectValue)); std::string key = it->first + "\t" + *it2; itemObj["usrName"] = Json::Value(*it2); maxId = 0; it3 = m_maxIdForSessions.find(key); if (it3 != m_maxIdForSessions.cend()) { maxId = it3->second; } itemObj["maxId"] = Json::Value(maxId); } } Json::StreamWriterBuilder builder; #ifndef NDEBUG builder["indentation"] = "\t"; builder["emitUTF8"] = true; #else builder["indentation"] = ""; #endif return Json::writeString(builder, contextObj); } bool upgrade(const std::string& data) { Json::CharReaderBuilder builder; std::unique_ptr reader(builder.newCharReader()); Json::Value contextObj; Json::String error; if (!reader->parse(data.c_str(), data.c_str() + data.size(), &contextObj, &error)) { return false; } if (contextObj.isMember("version")) { m_version = contextObj["version"].asString(); } else { m_version.clear(); } if (m_version == EXPORT_CONTEXT_VERSION) { return true; } // First version if (!contextObj.isObject() || !contextObj.isMember("options") || !contextObj.isMember("exportTime") || !contextObj.isMember("sessions")) { return false; } const Json::Value& maxIdForSessions = contextObj["sessions"]; if (!maxIdForSessions.isArray()) { return false; } if (m_version != EXPORT_CONTEXT_VERSION) { int options = contextObj["options"].asInt(); ExportOption eo; eo.fromSessionParsingOptions(options); m_options = (uint64_t)eo; } else { m_options = contextObj["options"].asUInt64(); } // m_options = contextObj["options"].asInt(); m_exportTime = static_cast(contextObj["exportTime"].asInt64()); m_maxIdForSessions.clear(); for (Json::ArrayIndex idx = 0; idx < maxIdForSessions.size(); idx++) { const Json::Value& itemObj = maxIdForSessions[idx]; if (!itemObj.isObject() || !itemObj.isMember("usrName") || !itemObj.isMember("maxId")) { return false; } m_maxIdForSessions.insert(std::pair(m_usrName + "\t" + itemObj["usrName"].asString(), itemObj["maxId"].asInt64())); } return true; } bool unserialize(const std::string& data) { Json::CharReaderBuilder builder; std::unique_ptr reader(builder.newCharReader()); Json::Value contextObj; Json::String error; if (!reader->parse(data.c_str(), data.c_str() + data.size(), &contextObj, &error)) { return false; } if (contextObj.isMember("version")) { m_version = contextObj["version"].asString(); } else { m_version.clear(); } if (m_version.empty()) { // First version if (!contextObj.isObject() || !contextObj.isMember("options") || !contextObj.isMember("exportTime") || !contextObj.isMember("sessions")) { return false; } const Json::Value& maxIdForSessions = contextObj["sessions"]; if (!maxIdForSessions.isArray()) { return false; } m_options = contextObj["options"].asUInt64(); m_exportTime = static_cast(contextObj["exportTime"].asInt64()); m_accountAndSessions.clear(); m_maxIdForSessions.clear(); for (Json::ArrayIndex idx = 0; idx < maxIdForSessions.size(); idx++) { const Json::Value& itemObj = maxIdForSessions[idx]; if (!itemObj.isObject() || !itemObj.isMember("usrName") || !itemObj.isMember("maxId")) { return false; } m_maxIdForSessions.insert(std::pair(m_usrName + "\t" + itemObj["usrName"].asString(), itemObj["maxId"].asInt64())); } } else { if (!contextObj.isObject() || !contextObj.isMember("options") || !contextObj.isMember("exportTime") || !contextObj.isMember("accounts")) { return false; } const Json::Value& accounts = contextObj["accounts"]; if (!accounts.isArray()) { return false; } if (m_version == EXPORT_CONTEXT_VERSION_2_0) { int options = contextObj["options"].asInt(); ExportOption eo; eo.fromSessionParsingOptions(options); m_options = (uint64_t)eo; } else { m_options = contextObj["options"].asUInt64(); } m_exportTime = static_cast(contextObj["exportTime"].asInt64()); m_accountAndSessions.clear(); m_maxIdForSessions.clear(); if (!accounts.empty()) { m_accountAndSessions.reserve(accounts.size()); } for (Json::ArrayIndex idx = 0; idx < accounts.size(); idx++) { const Json::Value& accountObj = accounts[idx]; if (!accountObj.isObject() || !accountObj.isMember("usrName") || !accountObj.isMember("sessions")) { return false; } const Json::Value& sessionsObj = accountObj["sessions"]; if (!sessionsObj.isArray()) { return false; } std::string account = accountObj["usrName"].asString(); auto it = m_accountAndSessions.insert(m_accountAndSessions.end(), std::pair>(account, std::list())); for (Json::ArrayIndex idx = 0; idx < sessionsObj.size(); idx++) { const Json::Value& itemObj = sessionsObj[idx]; if (!itemObj.isObject() || !itemObj.isMember("usrName") || !itemObj.isMember("maxId")) { return false; } auto it2 = it->second.insert(it->second.begin(), itemObj["usrName"].asString()); int64_t maxId = itemObj["maxId"].asInt64(); if (maxId != 0) { m_maxIdForSessions.insert(std::pair(account + "\t" + (*it2), itemObj["maxId"].asInt64())); } } } } return true; } }; class PageInfo { private: uint32_t m_page; uint32_t m_count; uint16_t m_year; uint16_t m_month; std::string m_text; std::string m_fileName; public: 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) { } 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) { } uint32_t getPage() const { return m_page; } uint32_t getCount() const { return m_count; } void setCount(uint32_t count) { m_count = count; } uint16_t getYear() const { return m_year; } uint16_t getMonth() const { return m_month; } std::string getText() const { return m_text; } std::string getFileName() const { return m_fileName; } }; class WXMSG; class Pager { public: Pager() : m_last(m_pages.end()), m_totalNumberOfPreviousPages(0) { } virtual ~Pager() { } virtual bool buildNewPage(const WXMSG *msg, const std::vector& messages) { return false; } const std::vector& getPages() const { return m_pages; } bool hasPages() const { return !m_pages.empty(); } protected: std::vector m_pages; std::vector::iterator m_last; uint64_t m_totalNumberOfPreviousPages; }; class NumberPager : public Pager { public: NumberPager(uint32_t pageSize) : Pager(), m_pageSize(pageSize), m_numberOfRawMsgs(0) { } virtual ~NumberPager() { } virtual bool buildNewPage(const WXMSG *msg, const std::vector& messages) { m_numberOfRawMsgs++; bool newPage = ((m_numberOfRawMsgs % m_pageSize) == 1); if (newPage) { 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)); m_totalNumberOfPreviousPages = messages.size(); return false; } else { m_last->setCount(m_last->getCount() + (uint32_t)(messages.size() - m_totalNumberOfPreviousPages)); m_totalNumberOfPreviousPages = messages.size(); } return true; return newPage; } private: uint32_t m_pageSize; uint64_t m_numberOfRawMsgs; }; class YearPager : public Pager { public: YearPager() : Pager(), m_previousYear(0) { } bool buildNewPage(const WXMSG *msg, const std::vector& messages) { std::time_t ts = msg->createTime; std::tm* t1 = std::localtime(&ts); uint16_t year = t1->tm_year + 1900; if (m_previousYear != year) { 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)); m_totalNumberOfPreviousPages = messages.size(); m_previousYear = year; return false; } else { m_last->setCount(m_last->getCount() + (uint32_t)(messages.size() - m_totalNumberOfPreviousPages)); m_totalNumberOfPreviousPages = messages.size(); } return true; } protected: uint16_t m_previousYear; }; class YearMonthPager : public YearPager { public: YearMonthPager() : YearPager(), m_previousMonth(0) { } bool buildNewPage(const WXMSG *msg, const std::vector& messages) { std::time_t ts = msg->createTime; std::tm* t1 = std::localtime(&ts); uint16_t year = t1->tm_year + 1900; uint16_t month = t1->tm_mon + 1; if ((m_previousYear != year) || (m_previousMonth != month)) { 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)); m_totalNumberOfPreviousPages = messages.size(); m_previousYear = year; m_previousMonth = month; return false; } else { m_last->setCount(m_last->getCount() + (uint32_t)(messages.size() - m_totalNumberOfPreviousPages)); m_totalNumberOfPreviousPages = messages.size(); } return true; } protected: uint16_t m_previousMonth; }; #endif /* ExportContext_h */ ================================================ FILE: WechatExporter/core/ExportNotifier.h ================================================ #ifndef ExportNotifier_h #define ExportNotifier_h class ExportNotifier { public: virtual ~ExportNotifier() {} virtual void onStart() const = 0; virtual void onProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const = 0; virtual void onComplete(bool cancelled) const = 0; virtual void onUserSessionStart(const std::string& usrName, uint32_t numberOfSessions) const = 0; virtual void onUserSessionComplete(const std::string& usrName) const = 0; virtual void onSessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages) const = 0; virtual void onSessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const = 0; virtual void onSessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled) const = 0; virtual void onTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks) const = 0; virtual void onTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalMessages) const = 0; virtual void onTasksComplete(const std::string& usrName, bool cancelled) const = 0; }; #endif /* ExportNotifier_h */ ================================================ FILE: WechatExporter/core/ExportOption.h ================================================ // // ExportOption.h // WechatExporter // // Created by Matthew on 2022/7/10. // Copyright © 2022 Matthew. All rights reserved. // #ifndef ExportOption_h #define ExportOption_h enum SessionParsingOption { SPO_IGNORE_AVATAR = 1 << 0, SPO_IGNORE_AUDIO = 1 << 1, SPO_IGNORE_IMAGE = 1 << 2, SPO_IGNORE_VIDEO = 1 << 3, SPO_IGNORE_EMOJI = 1 << 4, SPO_IGNORE_FILE = 1 << 5, SPO_IGNORE_CARD = 1 << 6, SPO_IGNORE_SHARING = 1 << 7, SPO_IGNORE_HTML_ENC = 1 << 8, SPO_TEXT_MODE = 0xFFFF, SPO_PDF_MODE = 1 << 16, SPO_DOC_MODE = 1 << 17, SPO_USING_REMOTE_EMOJI = 1 << 19, SPO_DESC = 1 << 20, SPO_ICON_IN_SESSION = 1 << 21, // Put Head Icon and Emoji files in the folder of session SPO_SYNC_LOADING = 1 << 22, SPO_SUPPORT_FILTER = 1 << 23, SPO_INCLUDING_SUBSCRIPTION = 1 << 24, SPO_OVERWRITE_EXISTING_FIlE = 1 << 26, SPO_EXP_GROUP_MEMBERS = 1 << 27, SPO_EXP_CONTACTS = 1 << 28, SPO_OUTPUT_DBG_LOGS = 1 << 29, SPO_INCREMENTAL_EXP = 1 << 30, SPO_END }; enum EXPORT_OPTION : uint64_t { EO_INCREMENTAL_EXP = 1ull << 0, EO_OUTPUT_DBG_LOGS = 1ull << 1, EO_OVERWRT_EXISTING_FIlE = 1ull << 2, EO_FILTER_BY_NAME = 1ull << 3, EO_EXP_GROUP_MEMBERS = 1ull << 4, EO_EXP_CONTACTS = 1ull << 5, EO_TEXT_MODE = 1ull << 12, EO_PDF_MODE = 1ull << 13, EO_DOC_MODE = 1ull << 14, EO_TIME_DESC = 1ull << 20, EO_USING_REMOTE_EMOJI = 1ull << 21, // Sync // Async // onscroll // normal pager // EO_LOADING_ONSCRLL = 1ull << 28, EO_PAGER_NORMAL = 1ull << 29, EO_PAGER_YEAR = 1ull << 30, EO_PAGER_MONTH = 1ull << 31, EO_ASYNC_MASK = 0xFull << 28, // bits EO_PAGER_MASK = 0x7ull << 29, // bits EO_SUPPORT_FILTER = 1ull << 40, EO_INCLUDING_SUBSCRIPTION = 1ull << 41, EO_END, // EO_LARGR_VALUE = }; class ExportOption { public: ExportOption() : m_options(0) { } ExportOption(uint64_t options) : m_options(options) { } ExportOption& operator=(uint64_t options) { m_options = options; return *this; } void setTextMode(bool textMode = true) { if (textMode) m_options |= EO_TEXT_MODE; else m_options &= ~EO_TEXT_MODE; } bool isHtmlMode() const { return (m_options & EO_TEXT_MODE) == 0 && (m_options & EO_PDF_MODE) == 0; } bool isTextMode() const { return (m_options & EO_TEXT_MODE) == EO_TEXT_MODE; } void setPdfMode(bool pdfMode = true) { setTextMode(!pdfMode); // html mode if (pdfMode) m_options |= EO_PDF_MODE; else m_options &= ~EO_PDF_MODE; } bool isPdfMode() const { return (m_options & EO_PDF_MODE) == EO_PDF_MODE; } void setOrder(bool asc = true) { if (asc) m_options &= ~EO_TIME_DESC; else m_options |= EO_TIME_DESC; } bool isDesc() const { return (m_options & EO_TIME_DESC) == EO_TIME_DESC; } void saveFilesInSessionFolder(bool flag = true) { } void filterByName() { m_options |= EO_FILTER_BY_NAME; } bool isFilteredByName() const { return (m_options & EO_FILTER_BY_NAME) == EO_FILTER_BY_NAME; } void setIncrementalExporting(bool incrementalExporting) { if (incrementalExporting) m_options |= EO_INCREMENTAL_EXP; else m_options &= ~EO_INCREMENTAL_EXP; } bool isIncrementalExporting() const { return (m_options & EO_INCREMENTAL_EXP) == EO_INCREMENTAL_EXP; } void supportsFilter(bool supportsFilter = true) { if (supportsFilter) m_options |= EO_SUPPORT_FILTER; else m_options &= ~EO_SUPPORT_FILTER; } bool isSupportingFilter() const { return (m_options & EO_SUPPORT_FILTER) == EO_SUPPORT_FILTER; } void outputDebugLogs(bool outputDebugLogs) { if (outputDebugLogs) m_options |= EO_OUTPUT_DBG_LOGS; else m_options &= ~EO_OUTPUT_DBG_LOGS; } bool isOutputtingDebugLogs() const { return (m_options & EO_OUTPUT_DBG_LOGS) == EO_OUTPUT_DBG_LOGS; } void useRemoteEmoji(bool useEmojiUrl) { if (useEmojiUrl) m_options |= EO_USING_REMOTE_EMOJI; else m_options &= ~EO_USING_REMOTE_EMOJI; } bool isUsingRemoteEmoji() const { return (m_options & EO_USING_REMOTE_EMOJI) == EO_USING_REMOTE_EMOJI; } void includesSubscription() { m_options |= EO_INCLUDING_SUBSCRIPTION; } bool isIncludingSubscription() const { return (m_options & EO_INCLUDING_SUBSCRIPTION) == EO_INCLUDING_SUBSCRIPTION; } bool isSyncLoading() const { return (m_options & EO_ASYNC_MASK) == 0; } bool isAsyncLoading() const { return (m_options | EO_ASYNC_MASK) != 0; } void setSyncLoading() { m_options &= ~EO_ASYNC_MASK; } void setLoadingDataOnScroll(bool loadingDataOnScroll = true) { if (loadingDataOnScroll) m_options |= EO_LOADING_ONSCRLL; else m_options &= ~EO_LOADING_ONSCRLL; } bool getLoadingDataOnScroll() const { return (m_options & EO_LOADING_ONSCRLL) == EO_LOADING_ONSCRLL; } bool hasPager() const { return (m_options & EO_PAGER_MASK) != 0; } void setPager() { m_options |= EO_PAGER_NORMAL; m_options &= ~EO_PAGER_YEAR; m_options &= ~EO_PAGER_MONTH; } void setPagerByYear() { m_options &= ~EO_PAGER_NORMAL; m_options |= EO_PAGER_YEAR; m_options &= ~EO_PAGER_MONTH; } bool isPagerByYear() const { return (m_options & EO_PAGER_YEAR) == EO_PAGER_YEAR; } void setPagerByMonth() { m_options &= ~EO_PAGER_NORMAL; m_options &= ~EO_PAGER_YEAR; m_options |= EO_PAGER_MONTH; } bool isPagerByMonth() const { return (m_options & EO_PAGER_MONTH) == EO_PAGER_MONTH; } operator uint64_t() const { return m_options; } void fromSessionParsingOptions(int options) { m_options = 0; if ((options & SPO_TEXT_MODE) == SPO_TEXT_MODE) { m_options |= EO_TEXT_MODE; } if ((options & SPO_PDF_MODE) == SPO_PDF_MODE) { m_options |= EO_PDF_MODE; } if ((options & SPO_DOC_MODE) == SPO_DOC_MODE) { m_options |= EO_DOC_MODE; } if ((options & SPO_USING_REMOTE_EMOJI) == SPO_USING_REMOTE_EMOJI) { m_options |= EO_USING_REMOTE_EMOJI; } if ((options & SPO_DESC) == SPO_DESC) { m_options |= EO_TIME_DESC; } if ((options & SPO_SYNC_LOADING) == SPO_SYNC_LOADING) { m_options &= ~EO_ASYNC_MASK; } if ((options & SPO_SUPPORT_FILTER) == SPO_SUPPORT_FILTER) { m_options |= EO_SUPPORT_FILTER; } if ((options & SPO_INCLUDING_SUBSCRIPTION) == SPO_INCLUDING_SUBSCRIPTION) { m_options |= EO_INCLUDING_SUBSCRIPTION; } if ((options & SPO_OVERWRITE_EXISTING_FIlE) == SPO_OVERWRITE_EXISTING_FIlE) { m_options |= EO_OVERWRT_EXISTING_FIlE; } if ((options & SPO_EXP_GROUP_MEMBERS) == SPO_EXP_GROUP_MEMBERS) { m_options |= EO_EXP_GROUP_MEMBERS; } if ((options & SPO_EXP_CONTACTS) == SPO_EXP_CONTACTS) { m_options |= EO_EXP_CONTACTS; } if ((options & SPO_OUTPUT_DBG_LOGS) == SPO_OUTPUT_DBG_LOGS) { m_options |= EO_OUTPUT_DBG_LOGS; } if ((options & SPO_INCREMENTAL_EXP) == SPO_INCREMENTAL_EXP) { m_options |= EO_INCREMENTAL_EXP; } } private: uint64_t m_options; }; #endif /* ExportOption_h */ ================================================ FILE: WechatExporter/core/Exporter.cpp ================================================ // // Exporter.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include "Exporter.h" #include #ifdef USING_DOWNLOADER #include "Downloader.h" #else #include "AsyncTask.h" #endif #include "TaskManager.h" #include "WechatParser.h" #include "ExportContext.h" #ifdef _WIN32 #include #endif #define USING_NEW_TEMPLATE 1 #ifndef NDEBUG static const size_t PAGE_SIZE = 100; #else static const size_t PAGE_SIZE = 1000; #endif Exporter::Exporter(const std::string& workDir, const std::string& backup, const std::string& output, Logger* logger, PdfConverter* pdfConverter) { m_running = false; m_iTunesDb = NULL; m_iTunesDbShare = NULL; m_workDir = workDir; m_backup = backup; m_output = output; m_logger = logger; m_pdfConverter = pdfConverter; m_notifier = NULL; m_cancelled = false; m_options = 0; // m_filterByName = false; m_extName = "html"; m_templatesName = "templates"; m_exportContext = NULL; } Exporter::~Exporter() { if (NULL != m_exportContext) { delete m_exportContext; m_exportContext = NULL; } releaseITunes(); m_logger = NULL; m_notifier = NULL; } void Exporter::initializeExporter() { // Disable Memory Stats in sqlite sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0); #ifdef USING_DOWNLOADER Downloader::initialize(); #else DownloadTask::initialize(); #endif } void Exporter::uninitializeExporter() { #ifdef USING_DOWNLOADER Downloader::uninitialize(); #else DownloadTask::uninitialize(); #endif } void Exporter::setOptions(const ExportOption& options) { m_options = options; } /* void Exporter::includesSubscription() { m_options.includesSubscription(); } */ bool Exporter::hasDebugLogs() const { return m_options.isOutputtingDebugLogs(); } bool Exporter::isSubscriptionIncluded() const { return m_options.isIncludingSubscription(); } bool Exporter::hasPreviousExporting(const std::string& outputDir, uint64_t& options, std::string& exportTime, std::string& version) { std::string fileName = combinePath(outputDir, WXEXP_DATA_FOLDER, WXEXP_DATA_FILE); if (!existsFile(fileName)) { return false; } ExportContext context; if (!loadExportContext(fileName, &context)) { return false; } options = context.getOptions(); version = context.getVersion(); std::time_t ts = context.getExportTime(); std::tm * ptm = std::localtime(&ts); char buffer[32]; std::strftime(buffer, 32, "%Y-%m-%d %H:%M", ptm); exportTime = buffer; return true; } bool Exporter::loadExportContext(const std::string& contextFile, ExportContext *context) { std::string contents = readFile(contextFile); if (contents.empty()) { return false; } if (!context->unserialize(contents) || context->getNumberOfSessions() == 0) { return false; } return true; } void Exporter::setNotifier(ExportNotifier *notifier) { m_notifier = notifier; } bool Exporter::isRunning() const { return m_running; } void Exporter::cancel() { m_cancelled = true; } void Exporter::waitForComplition() { if (!isRunning()) { return; } m_thread.join(); } void Exporter::setExtName(const std::string& extName) { m_extName = extName; } void Exporter::setTemplatesName(const std::string& templatesName) { m_templatesName = templatesName; } void Exporter::setLanguageCode(const std::string& languageCode) { m_languageCode = languageCode; } void Exporter::filterUsersAndSessions(const std::map>& usersAndSessions) { m_usersAndSessionsFilter = usersAndSessions; } bool Exporter::run() { if (isRunning() || m_thread.joinable()) { m_logger->write(m_resManager.getLocaleString("Previous task has not completed.")); return false; } if (!existsDirectory(m_output)) { m_logger->write(formatString(m_resManager.getLocaleString("Can't access output directory: %s"), m_output.c_str())); return false; } m_running = true; std::thread th(&Exporter::runImpl, this); m_thread.swap(th); return true; } bool Exporter::loadUsersAndSessions() { m_usersAndSessions.clear(); m_resManager.initLocaleResource(m_workDir, m_languageCode); if (!loadITunes(false)) { m_logger->write(formatString(m_resManager.getLocaleString("Failed to parse the backup data of iTunes in the directory: %s"), m_backup.c_str())); notifyComplete(); return false; } m_logger->debug("ITunes Database loaded."); WechatInfoParser wechatInfoParser(m_iTunesDb); if (wechatInfoParser.parse(m_wechatInfo)) { 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())); } std::vector users; LoginInfo2Parser loginInfo2Parser(m_iTunesDb, hasDebugLogs() ? m_logger : NULL); if (!loginInfo2Parser.parse(users)) { if (hasDebugLogs()) { m_logger->debug(loginInfo2Parser.getError()); } return false; } m_logger->debug("WeChat Users loaded."); m_usersAndSessions.reserve(users.size()); // Avoid re-allocation and causing the pointer changed for (std::vector::const_iterator it = users.cbegin(); it != users.cend(); ++it) { std::vector>>::iterator it2 = m_usersAndSessions.emplace(m_usersAndSessions.cend(), std::pair>(*it, std::vector())); Friends friends; loadUserFriendsAndSessions(it2->first, friends, it2->second, false); } return true; } void Exporter::swapUsersAndSessions(std::vector>>& usersAndSessions) { usersAndSessions.swap(m_usersAndSessions); } bool Exporter::runImpl() { #if !defined(NDEBUG) || defined(DBG_PERF) setThreadName("exp"); #endif time_t startTime; std::time(&startTime); notifyStart(); #if !defined(NDEBUG) || defined(DBG_PERF) makeDirectory(combinePath(m_output, "dbg")); #endif if (!m_resManager.initResources(m_workDir, m_languageCode, m_templatesName)) { m_logger->write(formatString("Failed to load resources in %s.", m_workDir.c_str())); if (hasDebugLogs()) { std::string emptyTemplates = m_resManager.checkEmptyTemplates(); if (!emptyTemplates.empty()) { m_logger->write("Empty template: " + emptyTemplates); } } } m_logger->write(formatString(m_resManager.getLocaleString("iTunes Backup: %s"), m_backup.c_str())); if (!loadITunes()) { m_logger->write(formatString(m_resManager.getLocaleString("Failed to parse the backup data of iTunes in the directory: %s"), m_backup.c_str())); notifyComplete(); return false; } m_logger->debug("ITunes Database loaded."); WechatInfoParser wechatInfoParser(m_iTunesDb); if (wechatInfoParser.parse(m_wechatInfo)) { m_logger->write(formatString(m_resManager.getLocaleString("iTunes Version: %s, WeChat Version: %s"), m_iTunesDb->getVersion().c_str(), m_wechatInfo.getShortVersion().c_str())); } m_logger->write(m_resManager.getLocaleString("Finding WeChat accounts...")); std::vector users; LoginInfo2Parser loginInfo2Parser(m_iTunesDb, hasDebugLogs() ? m_logger : NULL); if (!loginInfo2Parser.parse(users)) { m_logger->write(m_resManager.getLocaleString("Failed to find WeChat account.")); if (hasDebugLogs()) { m_logger->debug(loginInfo2Parser.getError()); } notifyComplete(); return false; } m_logger->write(formatString(m_resManager.getLocaleString("%d WeChat account(s) found."), (int)(users.size()))); // if (m_options.isIncrementalExporting()) { std::string path = combinePath(m_output, WXEXP_DATA_FOLDER); makeDirectory(path); } if (NULL == m_exportContext) { m_exportContext = new ExportContext(); } uint64_t orgOptions = m_options; std::string contextFileName = combinePath(m_output, WXEXP_DATA_FOLDER, WXEXP_DATA_FILE); if ((m_options.isIncrementalExporting()) && loadExportContext(contextFileName, m_exportContext)) { // Use the previous options m_options = m_exportContext->getOptions(); m_options.setIncrementalExporting(true); m_logger->write(m_resManager.getLocaleString("Incremental Exporting")); } else { // If there is no export context, save current options m_exportContext->setOptions(m_options); } std::string htmlBody; std::set userFileNames; for (std::vector::iterator it = users.begin(); it != users.end(); ++it) { if (m_cancelled) { break; } if (!m_usersAndSessionsFilter.empty()) { const std::string& nameForFilter = m_options.isFilteredByName() ? it->getDisplayName() : it->getUsrName(); if (m_usersAndSessionsFilter.find(nameForFilter) == m_usersAndSessionsFilter.cend()) { continue; } } if (!buildFileNameForUser(*it, userFileNames)) { m_logger->write(formatString(m_resManager.getLocaleString("Can't build directory name for user: %s. Skip it."), it->getUsrName().c_str())); continue; } std::string userOutputPath; exportUser(*it, userOutputPath); std::string userItem = m_resManager.getTemplate("listitem"); replaceAll(userItem, "%%ITEMPICPATH%%", userOutputPath + "/Portrait/" + it->getLocalPortrait()); if (!m_options.isTextMode()) { replaceAll(userItem, "%%ITEMLINK%%", encodeUrl(it->getOutputFileName()) + "/index." + m_extName); replaceAll(userItem, "%%ITEMTEXT%%", safeHTML(it->getDisplayName())); } else { replaceAll(userItem, "%%ITEMLINK%%", it->getOutputFileName() + "/index." + m_extName); replaceAll(userItem, "%%ITEMTEXT%%", it->getDisplayName()); } htmlBody += userItem; } std::string fileName = combinePath(m_output, "index." + m_extName); std::string html = m_resManager.getTemplate("listframe"); replaceAll(html, "%%USERNAME%%", ""); replaceAll(html, "%%TBODY%%", htmlBody); writeFile(fileName, html); m_options = orgOptions; if (m_exportContext->getNumberOfSessions() > 0) { m_exportContext->refreshExportTime(); fileName = combinePath(m_output, WXEXP_DATA_FOLDER, WXEXP_DATA_FILE); writeFile(fileName, m_exportContext->serialize()); } delete m_exportContext; m_exportContext = NULL; time_t endTime = 0; std::time(&endTime); int seconds = static_cast(difftime(endTime, startTime)); std::ostringstream stream; int minutes = seconds / 60; int hours = minutes / 60; stream << std::setfill('0') << std::setw(2) << hours << ':' << std::setfill('0') << std::setw(2) << (minutes % 60) << ':' << std::setfill('0') << std::setw(2) << (seconds % 60); m_logger->write(formatString(m_resManager.getLocaleString((m_cancelled ? "Cancelled in %s." : "Completed in %s.")), stream.str().c_str())); notifyComplete(m_cancelled); return true; } bool Exporter::exportUser(Friend& user, std::string& userOutputPath) { std::string uidMd5 = user.getHash(); std::string userBase = combinePath("Documents", uidMd5); // Use display name first, it it can't be created, use uid hash userOutputPath = user.getOutputFileName(); std::string outputBase = combinePath(m_output, userOutputPath); if (!existsDirectory(outputBase)) { if (!makeDirectory(outputBase)) { userOutputPath = user.getHash(); outputBase = combinePath(m_output, userOutputPath); if (!existsDirectory(outputBase)) { if (!makeDirectory(outputBase)) { return false; } } } } if (!m_options.isTextMode()) { std::string portraitPath = combinePath(outputBase, "Portrait"); makeDirectory(portraitPath); std::string defaultPortrait = combinePath(portraitPath, "DefaultAvatar.png"); copyFile(combinePath(m_workDir, "res", "DefaultAvatar.png"), defaultPortrait, true); } /* if (false && (m_options & SPO_IGNORE_EMOJI) == 0) { std::string emojiPath = combinePath(outputBase, "Emoji"); makeDirectory(emojiPath); } */ // if (m_options.isIncrementalExporting()) { std::string path = combinePath(m_output, WXEXP_DATA_FOLDER, user.getUsrName()); makeDirectory(path); m_exportContext->prepareUserDatabase(user, m_output); } m_logger->write(formatString(m_resManager.getLocaleString("Handling account: %s, WeChat Id: %s"), user.getDisplayName().c_str(), user.getUsrName().c_str())); m_logger->write(m_resManager.getLocaleString("Reading account info.")); m_logger->write(m_resManager.getLocaleString("Reading chat info")); Friends friends; std::vector sessions; loadUserFriendsAndSessions(user, friends, sessions); m_logger->write(formatString(m_resManager.getLocaleString("%d chats found."), (int)(sessions.size()))); Friend* myself = friends.getFriend(user.getHash()); if (NULL == myself) { Friend& newUser = friends.addFriend(user.getHash()); newUser = user; myself = &user; } std::string userBody; std::map>::const_iterator itUser = m_usersAndSessionsFilter.cend(); if (!m_usersAndSessionsFilter.empty()) { std::string nameForFilter = m_options.isFilteredByName() ? user.getDisplayName() : user.getUsrName(); itUser = m_usersAndSessionsFilter.find(nameForFilter); } bool pdfOutput = (m_options.isPdfMode() && NULL != m_pdfConverter); if (pdfOutput) { m_pdfConverter->makeUserDirectory(userOutputPath); } #ifdef USING_DOWNLOADER Downloader downloader(m_logger); #else TaskManager taskManager(m_logger); #endif #ifndef NDEBUG m_logger->debug("UA: " + m_wechatInfo.buildUserAgent()); #endif #ifdef USING_DOWNLOADER downloader.setUserAgent(m_wechatInfo.buildUserAgent()); #else taskManager.setUserAgent(m_wechatInfo.buildUserAgent()); #endif MessageParser msgParser(*m_iTunesDb, *m_iTunesDbShare, taskManager, friends, *myself, m_options, m_workDir, outputBase, m_resManager); if (!m_options.isTextMode()) { #ifndef NDEBUG m_logger->debug("Download avatar: *" + user.getPortrait() + "* => " + combinePath(outputBase, "Portrait", user.getLocalPortrait())); #endif msgParser.copyPortraitIcon(NULL, user, combinePath(outputBase, "Portrait")); // downloader.addTask(user.getPortrait(), combinePath(outputBase, "Portrait", user.getLocalPortrait()), 0); } // Export Contacts // std::map // WeChat Id/Display Name/Portrait URL // csv std::string csvContents; std::vectorfriendList; friends.toArraySortedByDisplayName(friendList); std::string oneQuato = "\""; std::string twoQuatos = "\"\""; std::string twoQuatos2 = "\",\""; for (auto it = friendList.cbegin(); it != friendList.cend(); ++it) { #ifdef NDEBUG // Release if ((*it)->isSubscription() || (*it)->isChatroom()) { continue; } std::string wechatId = (*it)->getWxName(); std::string displayName = (*it)->getDisplayName(); replaceAll(displayName, oneQuato, twoQuatos); csvContents += oneQuato + wechatId + twoQuatos2 + displayName + twoQuatos2 + (*it)->getPortrait() + twoQuatos2 + (*it)->buildTagDesc(m_tags) + "\"\r\n"; #else std::string wechatId = (*it)->getWxName(); if (wechatId == (*it)->getUsrName()) { wechatId.clear(); } std::string displayName = (*it)->getDisplayName(); replaceAll(displayName, oneQuato, twoQuatos); csvContents += oneQuato + (*it)->getUsrName() + twoQuatos2 + wechatId + twoQuatos2 + displayName + twoQuatos2 + (*it)->buildTagDesc(m_tags) + twoQuatos2 + (*it)->getPortrait() + "\"\r\n"; #endif } std::string contactPath = combinePath(m_output, userOutputPath + "_Contacts.csv"); writeFile(contactPath, csvContents); std::string weChatIdFormat = m_resManager.getLocaleString(" (WeChat ID: %s)"); std::set sessionFileNames; for (std::vector::iterator it = sessions.begin(); it != sessions.end(); ++it) { if (m_cancelled) { break; } if (!m_usersAndSessionsFilter.empty() && itUser != m_usersAndSessionsFilter.cend() && !(itUser->second.empty())) { std::map::const_iterator itSession = itUser->second.cend(); const std::string& nameForFilter = m_options.isFilteredByName() ? it->getDisplayName() : it->getUsrName(); if ((itSession = itUser->second.find(nameForFilter)) == itUser->second.cend()) { continue; } it->setData(itSession->second); } int recordCount = it->getRecordCount(); if (m_options.isIncrementalExporting()) { int64_t maxMsgId = 0; m_exportContext->getMaxId(user.getUsrName(), it->getUsrName(), maxMsgId); if (maxMsgId > 0) { recordCount = SessionParser::calcNumberOfMessages(*it, maxMsgId); } } notifySessionStart(it->getUsrName(), it->getData(), recordCount); if (!buildFileNameForUser(*it, sessionFileNames)) { m_logger->write(formatString(m_resManager.getLocaleString("Can't build directory name for chat: %s. Skip it."), it->getDisplayName().c_str())); notifySessionComplete(it->getUsrName(), it->getData(), m_cancelled); continue; } std::string sessionDisplayName = it->getDisplayName(); #ifndef NDEBUG 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()); #else 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())); #endif if (!isSubscriptionIncluded() && it->isSubscription()) { m_logger->write(formatString(m_resManager.getLocaleString("Skip subscription: %s"), sessionDisplayName.c_str())); notifySessionComplete(it->getUsrName(), it->getData(), m_cancelled); continue; } if (!m_options.isTextMode()) { // Download avatar for session msgParser.copyPortraitIcon(&(*it), *it, combinePath(outputBase, "Portrait")); } int count = exportSession(*myself, msgParser, *it, userBase, outputBase); m_logger->write(formatString(m_resManager.getLocaleString("Succeeded handling %d messages."), count)); if (count > 0) { std::string userItem = m_resManager.getTemplate("listitem"); std::string userItemText = sessionDisplayName; if (!it->isChatroom()) { std::string wxName = it->isWxNameEmpty() ? "" : it->getWxName(); if (!wxName.empty() && userItemText != wxName) { userItemText += formatString(weChatIdFormat, wxName.c_str()); } } replaceAll(userItem, "%%ITEMPICPATH%%", "Portrait/" + it->getLocalPortrait()); if (!m_options.isTextMode()) { replaceAll(userItem, "%%ITEMLINK%%", encodeUrl(it->getOutputFileName()) + "/index." + m_extName); replaceAll(userItem, "%%ITEMTEXT%%", safeHTML(userItemText)); } else { replaceAll(userItem, "%%ITEMLINK%%", it->getOutputFileName() + "." + m_extName); replaceAll(userItem, "%%ITEMTEXT%%", userItemText); } userBody += userItem; } notifySessionComplete(it->getUsrName(), it->getData(), m_cancelled); if (pdfOutput) { // std::string std::string htmlFileName = combinePath(outputBase, it->getOutputFileName(), "index." + m_extName); if (existsFile(htmlFileName)) { std::string pdfFileName = combinePath(m_output, "pdf", userOutputPath, it->getOutputFileName() + ".pdf"); // taskManager.convertPdf(&(*it), htmlFileName, pdfFileName, m_pdfConverter); m_pdfConverter->convert(htmlFileName, pdfFileName); } } } std::string html = m_resManager.getTemplate("listframe"); replaceAll(html, "%%USERNAME%%", " - " + user.getDisplayName()); replaceAll(html, "%%TBODY%%", userBody); std::string fileName = combinePath(outputBase, "index." + m_extName); writeFile(fileName, html); size_t dlCount = 0; size_t prevDlCount = 0; if (m_cancelled) { #ifdef USING_DOWNLOADER downloader.cancel(); #else taskManager.cancel(); #endif } else { #ifdef USING_DOWNLOADER dlCount = downloader.getRunningCount(); prevDlCount = dlCount; if (dlCount > 0) { m_logger->write("Waiting for tasks: " + std::to_string(dlCount)); } #else std::string queueDesc; dlCount = taskManager.getNumberOfQueue(queueDesc); prevDlCount = dlCount; if (dlCount > 0) { m_logger->write("Waiting for tasks: " + queueDesc); } taskManager.shutdown(); #endif } notifyTasksStart(user.getUsrName(), static_cast(dlCount)); #ifdef USING_DOWNLOADER downloader.shutdown(); #else unsigned int timeout = m_cancelled ? 0 : 512; for (int idx = 1; ; ++idx) { if (taskManager.waitForCompltion(timeout)) { break; } if (m_cancelled) { taskManager.cancel(); timeout = 0; } else if ((idx % 2) == 0) { std::string queueDesc; size_t curDlCount = taskManager.getNumberOfQueue(queueDesc); if (curDlCount != prevDlCount) { notifyTasksProgress(user.getUsrName(), static_cast(prevDlCount - curDlCount), static_cast(dlCount)); prevDlCount = curDlCount; } } } #endif if (dlCount != prevDlCount) { notifyTasksProgress(user.getUsrName(), static_cast(dlCount - prevDlCount), static_cast(dlCount)); } notifyTasksComplete(user.getUsrName(), m_cancelled); #ifndef NDEBUG // m_logger->debug(formatString("Total Downloads: %d", downloader.getCount())); // m_logger->debug("Download Stats: " + downloader.getStats()); #endif return true; } bool Exporter::loadUserFriendsAndSessions(const Friend& user, Friends& friends, std::vector& sessions, bool detailedInfo/* = true*/) { std::string uidMd5 = user.getHash(); std::string userBase = combinePath("Documents", uidMd5); std::string wcdbPath = m_iTunesDb->findRealPath(combinePath(userBase, "DB", "WCDB_Contact.sqlite")); FriendsParser friendsParser(detailedInfo); #ifndef NDEBUG friendsParser.setOutputPath(m_output); #endif friendsParser.parseWcdb(wcdbPath, friends); m_logger->debug("WeChat Friends(" + std::to_string(friends.friends.size()) + ") for: " + user.getDisplayName() + " loaded."); m_tags.clear(); if (detailedInfo) { friendsParser.parseFriendTags(m_iTunesDb, user.getHash(), m_tags); } SessionsParser sessionsParser(m_iTunesDb, m_iTunesDbShare, friends, m_wechatInfo.getCellDataVersion(), m_logger, detailedInfo); sessionsParser.parse(user, sessions); std::sort(sessions.begin(), sessions.end(), SessionLastMsgTimeCompare()); #ifndef NDEBUG int idx = 0; for (auto it = sessions.cbegin(); it != sessions.cend(); ++it) { if (it->isSubscription()) { continue; } printf("%u : %s\t%s\r\n",it->getLastMessageTime(), it->getDisplayName().c_str(), it->getUsrName().c_str()); idx ++; if (idx > 20) { // break; } } idx = 0; #endif // m_logger->debug("WeChat Sessions for: " + user.getDisplayName() + " loaded."); return true; } bool Exporter::buildScriptFile(const std::string& fileName, std::vector::const_iterator b, std::vector::const_iterator e, const PageInfo& page) const { std::string scripts = m_resManager.getTemplate("scripts"); Json::Value jsonMsgs(Json::arrayValue); for (auto it = b; it != e; ++it) { jsonMsgs.append(*it); } Json::StreamWriterBuilder builder; #ifndef NDEBUG builder["indentation"] = "\t"; // assume default for comments is None builder["emitUTF8"] = true; #else builder["indentation"] = ""; #endif std::string moreMsgs = Json::writeString(builder, jsonMsgs); replaceAll(scripts, "%%JSON_DATA%%", moreMsgs); // std::string dataFileName = combinePath(dataPath, "msg-" + page.getFileName() + ".js"); writeFile(fileName, scripts); return true; } int Exporter::exportSession(const Friend& user, const MessageParser& msgParser, const Session& session, const std::string& userBase, const std::string& outputBase) { if (session.isDbFileEmpty()) { return 0; } std::string sessionBasePath = combinePath(outputBase, session.getOutputFileName(), "Files"); if (!m_options.isTextMode()) { std::string portraitPath = combinePath(sessionBasePath, "Portrait"); makeDirectory(portraitPath); // std::string defaultPortrait = combinePath(portraitPath, "DefaultAvatar.png"); // copyFile(combinePath(m_workDir, "res", "DefaultAvatar.png"), defaultPortrait, true); makeDirectory(combinePath(sessionBasePath, "Emoji")); } std::vector messages; std::vector::size_type numberOfExportedMsgs = 0; if (session.getRecordCount() > 0) { messages.reserve(session.getRecordCount()); } int64_t maxMsgId = 0; m_exportContext->getMaxId(user.getUsrName(), session.getUsrName(), maxMsgId); if (m_options.isIncrementalExporting()) { m_exportContext->prepareSessionTable(session); } #if !defined(NDEBUG) || defined(DBG_PERF) m_logger->debug("DB: " + session.getDbFile() + " Table: Chat_" + session.getHash()); #endif int numberOfMsgs = 0; SessionParser sessionParser(m_options); std::unique_ptr enumerator(sessionParser.buildMsgEnumerator(session, maxMsgId)); std::vector tvs; std::unique_ptr pager; uint16_t year = 0; uint16_t month = 0; size_t msgPosOfPage = 0; std::string dataPath = combinePath(outputBase, session.getOutputFileName(), "Files", "Data"); makeDirectory(dataPath); if (m_options.isAsyncLoading()) { if (m_options.isPagerByYear()) { pager.reset((Pager *)(new YearPager())); } else if (m_options.isPagerByMonth()) { pager.reset((Pager *)(new YearMonthPager())); } else { pager.reset((Pager *)(new NumberPager(PAGE_SIZE))); } } else { pager.reset(new Pager()); } WXMSG msg; #if !defined(NDEBUG) || defined(DBG_PERF) m_logger->debug("Start exporting session"); #endif while (enumerator->nextMessage(msg)) { #if !defined(NDEBUG) || defined(DBG_PERF) // m_logger->debug("Export msg: " + msg.msgId); #endif if (m_options.isIncrementalExporting()) { m_exportContext->insertMessage(session, msg); } if (msg.msgIdValue > maxMsgId) { maxMsgId = msg.msgIdValue; } tvs.clear(); if (!msgParser.parse(msg, session, tvs)) { if (hasDebugLogs() && msgParser.hasError()) { m_logger->debug(msgParser.getError()); } } exportMessage(session, tvs, messages); ++numberOfMsgs; pager->buildNewPage(&msg, messages); notifySessionProgress(session.getUsrName(), session.getData(), numberOfMsgs, session.getRecordCount()); #if !defined(NDEBUG) || defined(DBG_PERF) // m_logger->debug("Finish exporting msg: " + msg.msgId); #endif if (m_cancelled) { break; } } #if !defined(NDEBUG) || defined(DBG_PERF) m_logger->debug("Finish exporting session"); #endif if (maxMsgId > 0) { m_exportContext->setMaxId(user.getUsrName(), session.getUsrName(), maxMsgId); } if (numberOfMsgs == 0) // no messages or no new messages for incremental exporting { return (maxMsgId > 0) ? 1 : numberOfMsgs; } std::string contentOfMembers; if (session.isChatroom()) { // Export memebers } std::string rawMsgFileName = combinePath(m_output, WXEXP_DATA_FOLDER, session.getOwner()->getUsrName(), session.getUsrName() + ".dat"); if (m_options.isIncrementalExporting()) { m_logger->debug("Merge incremental messages."); mergeMessages(rawMsgFileName, messages); } m_logger->debug("Save messages for incremental exporting."); serializeMessages(rawMsgFileName, messages); // m_logger->debug("After serializeMessages."); if (numberOfMsgs > 0) { Json::Value jsonPages(Json::arrayValue); if (pager->hasPages()) { numberOfExportedMsgs = 0; auto pages = pager->getPages(); for (auto it = pages.cbegin(); it != pages.cend(); ++it) { auto b = messages.cbegin() + numberOfExportedMsgs; auto e = b + it->getCount(); buildScriptFile(combinePath(dataPath, "msg-" + it->getFileName() + ".js"), b, e, *it); numberOfExportedMsgs += it->getCount(); Json::Value jsonPage(Json::objectValue); jsonPage["numnberOfMsgs"] = it->getCount(); jsonPage["text"] = it->getText(); jsonPage["page"] = it->getPage() + 1; if (it->getYear() > 0) { jsonPage["year"] = it->getYear(); } if (it->getMonth() > 0) { jsonPage["month"] = it->getMonth(); } jsonPages.append(jsonPage); } } Json::StreamWriterBuilder builder; #ifndef NDEBUG builder["indentation"] = "\t"; // assume default for comments is None builder["emitUTF8"] = true; #else builder["indentation"] = ""; #endif std::string pageData = Json::writeString(builder, jsonPages); auto b = messages.cbegin(); // No page for text mode auto e = (m_options.isTextMode() || m_options.isSyncLoading() || (messages.size() <= PAGE_SIZE)) ? messages.cend() : (b + PAGE_SIZE); // const size_t numberOfMessages = std::distance(e, messages.cend()); const size_t numberOfMessages = numberOfMsgs; const size_t numberOfPages = (messages.size() + PAGE_SIZE - 1) / PAGE_SIZE; #if USING_NEW_TEMPLATE std::map values; #ifndef NDEBUG values["%%USRNAME%%"] = user.getUsrName() + " - " + user.getHash(); values["%%SESSION_USRNAME%%"] = session.getUsrName() + " - " + session.getHash(); #else values["%%USRNAME%%"] = ""; values["%%SESSION_USRNAME%%"] = ""; #endif values["%%DISPLAYNAME%%"] = session.getDisplayName(); values["%%WX_CHAT_HISTORY%%"] = m_resManager.getLocaleString("WeChat Chat History"); values["%%ASYNC_LOADING_TYPE%%"] = m_options.getLoadingDataOnScroll() ? "onscroll" : "initial"; if (m_options.isAsyncLoading()) { if (m_options.hasPager()) { values["%%ASYNC_LOADING_TYPE%%"] = "pager"; if (m_options.isPagerByYear()) { values["%%PAGER_TYPE%%"] = "2"; } else if (m_options.isPagerByMonth()) { values["%%PAGER_TYPE%%"] = "3"; } else { values["%%PAGER_TYPE%%"] = "1"; } } else { values["%%ASYNC_LOADING_TYPE%%"] = "onscroll"; } } else { values["%%ASYNC_LOADING_TYPE%%"] = ""; } values["%%SIZE_OF_PAGE%%"] = std::to_string(PAGE_SIZE); values["%%NUMBER_OF_MSGS%%"] = std::to_string(numberOfMessages); values["%%NUMBER_OF_PAGES%%"] = std::to_string(numberOfPages); values["%%DATA_PATH%%"] = "Files/Data"; values["%%PAGE_DATA%%"] = pageData; // m_logger->debug("Before join"); if (!m_options.isHtmlMode() || m_options.isSyncLoading()) { values["%%BODY%%"] = join(messages.cbegin(), messages.cend(), ""); } // m_logger->debug("After join"); values["%%HEADER_FILTER%%"] = m_options.isSupportingFilter() ? m_resManager.getTemplate("filter") : ""; const std::string& html = m_resManager.buildFromTemplate("frame", values); m_logger->debug("After build html from template"); #else // NOT USING_NEW_TEMPLATE std::string html = m_resManager.getTemplate("frame"); #if !defined(NDEBUG) || defined(DBG_PERF) if (html.empty()) { m_logger->write("Template file is empty: frame"); } #endif #ifndef NDEBUG replaceAll(html, "%%USRNAME%%", user.getUsrName() + " - " + user.getHash()); replaceAll(html, "%%SESSION_USRNAME%%", session.getUsrName() + " - " + session.getHash()); #else replaceAll(html, "%%USRNAME%%", ""); replaceAll(html, "%%SESSION_USRNAME%%", ""); #endif replaceAll(html, "%%DISPLAYNAME%%", session.getDisplayName()); replaceAll(html, "%%WX_CHAT_HISTORY%%", m_resManager.getLocaleString("WeChat Chat History")); replaceAll(html, "%%ASYNC_LOADING_TYPE%%", m_options.getLoadingDataOnScroll() ? "onscroll" : "initial"); replaceAll(html, "%%SIZE_OF_PAGE%%", std::to_string(PAGE_SIZE)); replaceAll(html, "%%NUMBER_OF_MSGS%%", std::to_string(numberOfMessages)); replaceAll(html, "%%NUMBER_OF_PAGES%%", std::to_string(numberOfPages)); replaceAll(html, "%%DATA_PATH%%", "Files/Data"); replaceAll(html, "%%PAGE_DATA%%", pageData); replaceAll(html, "%%BODY%%", join(b, e, "")); replaceAll(html, "%%HEADER_FILTER%%", m_options.isSupportingFilter() ? m_resManager.getTemplate("filter") : ""); #endif std::string fileName = combinePath(outputBase, session.getOutputFileName(), "index." + m_extName); if (!writeFile(fileName, html)) { m_logger->write("Failed to write chat history file: " + fileName); } // m_logger->debug("Finish writing html file"); } // m_logger->debug("Session exporting ends."); return numberOfMsgs; } bool Exporter::exportMessage(const Session& session, const std::vector& tvs, std::vector& messages) { std::string content; for (std::vector::const_iterator it = tvs.cbegin(); it != tvs.cend(); ++it) { #if USING_NEW_TEMPLATE content.append(m_resManager.buildFromTemplate(it->getName(), it->getValues())); #else content.append(buildContentFromTemplateValues(*it)); #endif } messages.push_back(content); return m_cancelled; } bool Exporter::exportPageToFile(const Friend& user, const Session& session, const std::vector& tvs, std::vector& messages, const PageInfo& pageInfo, const std::string& outputBase) { if (messages.empty()) { return true; } std::string dataPath = combinePath(outputBase, session.getOutputFileName(), "Files", "Data"); makeDirectory(dataPath); std::string rawMsgFileName = combinePath(m_output, WXEXP_DATA_FOLDER, session.getOwner()->getUsrName(), session.getUsrName() + ".dat"); if (m_options.isIncrementalExporting()) { m_logger->debug("Merge incremental messages."); mergeMessages(rawMsgFileName, messages); } m_logger->debug("Save messages for incremental exporting."); serializeMessages(rawMsgFileName, messages); // m_logger->debug("After serializeMessages."); // auto b = messages.cbegin(); // No page for text mode // auto e = (((m_options & (SPO_TEXT_MODE | SPO_SYNC_LOADING)) != 0) || (messages.size() <= PAGE_SIZE)) ? messages.cend() : (b + PAGE_SIZE); // const size_t numberOfMessages = std::distance(e, messages.cend()); // const size_t numberOfPages = (numberOfMessages + PAGE_SIZE - 1) / PAGE_SIZE; #if USING_NEW_TEMPLATE std::map values; #ifndef NDEBUG values["%%USRNAME%%"] = user.getUsrName() + " - " + user.getHash(); values["%%SESSION_USRNAME%%"] = session.getUsrName() + " - " + session.getHash(); #else values["%%USRNAME%%"] = ""; values["%%SESSION_USRNAME%%"] = ""; #endif values["%%DISPLAYNAME%%"] = session.getDisplayName(); values["%%WX_CHAT_HISTORY%%"] = m_resManager.getLocaleString("WeChat Chat History"); values["%%ASYNC_LOADING_TYPE%%"] = m_options.getLoadingDataOnScroll() ? "onscroll" : "initial"; values["%%SIZE_OF_PAGE%%"] = std::to_string(PAGE_SIZE); // values["%%NUMBER_OF_MSGS%%"] = std::to_string(numberOfMessages); // values["%%NUMBER_OF_PAGES%%"] = std::to_string(numberOfPages); values["%%DATA_PATH%%"] = "Files/Data"; // m_logger->debug("Before join"); values["%%BODY%%"] = join(messages.cbegin(), messages.cend(), ""); // m_logger->debug("After join"); values["%%HEADER_FILTER%%"] = m_options.isSupportingFilter() ? m_resManager.getTemplate("filter") : ""; const std::string& html = m_resManager.buildFromTemplate("frame", values); m_logger->debug("After build html from template"); #else // NOT USING_NEW_TEMPLATE std::string html = m_resManager.getTemplate("frame"); #if !defined(NDEBUG) || defined(DBG_PERF) if (html.empty()) { m_logger->write("Template file is empty: frame"); } #endif #ifndef NDEBUG replaceAll(html, "%%USRNAME%%", user.getUsrName() + " - " + user.getHash()); replaceAll(html, "%%SESSION_USRNAME%%", session.getUsrName() + " - " + session.getHash()); #else replaceAll(html, "%%USRNAME%%", ""); replaceAll(html, "%%SESSION_USRNAME%%", ""); #endif replaceAll(html, "%%DISPLAYNAME%%", session.getDisplayName()); replaceAll(html, "%%WX_CHAT_HISTORY%%", m_resManager.getLocaleString("WeChat Chat History")); replaceAll(html, "%%ASYNC_LOADING_TYPE%%", m_options.getLoadingDataOnScroll() ? "onscroll" : "initial"); replaceAll(html, "%%SIZE_OF_PAGE%%", std::to_string(PAGE_SIZE)); // replaceAll(html, "%%NUMBER_OF_MSGS%%", std::to_string(numberOfMessages)); // replaceAll(html, "%%NUMBER_OF_PAGES%%", std::to_string(numberOfPages)); replaceAll(html, "%%DATA_PATH%%", "Files/Data"); replaceAll(html, "%%BODY%%", join(messages.cbegin(), messages.cend(), "")); replaceAll(html, "%%HEADER_FILTER%%", m_options.isSupportingFilter() ? m_resManager.getTemplate("filter") : ""); #endif std::string outputFileName = "index"; if (pageInfo.getPage() > 0) { outputFileName += "-" + std::to_string(pageInfo.getPage()); } outputFileName += m_extName; std::string fullFileName = combinePath(outputBase, session. getOutputFileName(), outputFileName); if (!writeFile(fullFileName, html)) { m_logger->write("Failed to write chat history file: " + fullFileName); } // m_logger->debug("Finish writing html file"); // if ((m_options & SPO_SYNC_LOADING) == 0 && numberOfPages > 0) { // makeDirectory(dataPath); // for (size_t page = 0; page < numberOfPages; ++page) { // b = e; std::string scripts = m_resManager.getTemplate("scripts"); // e = (page == (numberOfPages - 1)) ? messages.cend() : (b + PAGE_SIZE); Json::Value jsonMsgs(Json::arrayValue); for (auto it = messages.cbegin(); it != messages.cend(); ++it) { jsonMsgs.append(*it); } Json::StreamWriterBuilder builder; #ifndef NDEBUG builder["indentation"] = "\t"; // assume default for comments is None builder["emitUTF8"] = true; #else builder["indentation"] = ""; #endif std::string moreMsgs = Json::writeString(builder, jsonMsgs); replaceAll(scripts, "%%JSON_DATA%%", moreMsgs); fullFileName = combinePath(dataPath, "msg-" + std::to_string(pageInfo.getPage() + 1) + ".js"); writeFile(fullFileName, scripts); } } return true; } void Exporter::serializeMessages(const std::string& fileName, const std::vector& messages) { File file; size_t byteWriten = 0; if (file.open(fileName, false)) { uint32_t size = htonl(static_cast(messages.size())); file.write(reinterpret_cast(&size), sizeof(size), byteWriten); for (std::vector::const_iterator it = messages.cbegin(); it != messages.cend(); ++it) { size = htonl(static_cast(it->size())); file.write(reinterpret_cast(&size), sizeof(size), byteWriten); // appendFile(fileName, reinterpret_cast(&size), sizeof(size)); file.write(reinterpret_cast(it->c_str()), it->size(), byteWriten); } file.close(); } } void Exporter::unserializeMessages(const std::string& fileName, std::vector& messages) { std::vector data; if (!readFile(fileName, data)) { messages.clear(); return; } size_t dataSize = data.size(); if (dataSize < sizeof(uint32_t)) { return; } size_t offset = 0; uint32_t itemSize = 0; memcpy(&itemSize, &data[offset], sizeof(uint32_t)); offset += sizeof(uint32_t); itemSize = ntohl(itemSize); messages.clear(); messages.reserve(itemSize); uint32_t sizeOfString = 0; for (uint32_t idx = 0; idx < itemSize; ++idx) { if (offset + sizeof(uint32_t) > dataSize) { break; } memcpy(&sizeOfString, &data[offset], sizeof(uint32_t)); offset += sizeof(uint32_t); sizeOfString = ntohl(sizeOfString); if (offset + sizeOfString > dataSize) { break; } messages.emplace_back(reinterpret_cast(&data[offset]), sizeOfString); offset += sizeOfString; } } void Exporter::mergeMessages(const std::string& fileName, std::vector& messages) { std::vector orgMessages; unserializeMessages(fileName, orgMessages); // std::string contents = readFile(fileName); if (!m_options.isDesc()) { // ASC messages.swap(orgMessages); } messages.reserve(messages.size() + orgMessages.size()); messages.insert(messages.cend(), orgMessages.cbegin(), orgMessages.cend()); } bool Exporter::buildFileNameForUser(Friend& user, std::set& existingFileNames) { std::string names[] = {user.getDisplayName(), user.getUsrName(), user.getHash()}; bool succeeded = false; for (int idx = 0; idx < 3; ++idx) { std::string outputFileName = removeInvalidCharsForFileName(names[idx]); if (isValidFileName(outputFileName)) { if ( existingFileNames.find(outputFileName) != existingFileNames.cend()) { int idx = 1; while (idx++) { if (existingFileNames.find(outputFileName + "_" + std::to_string(idx)) == existingFileNames.cend()) { outputFileName += "_" + std::to_string(idx); break; } } } user.setOutputFileName(outputFileName); existingFileNames.insert(outputFileName); succeeded = true; break; } } return succeeded; } void Exporter::releaseITunes() { if (NULL != m_iTunesDb) { delete m_iTunesDb; m_iTunesDb = NULL; } if (NULL != m_iTunesDbShare) { delete m_iTunesDbShare; m_iTunesDbShare = NULL; } } bool Exporter::loadITunes(bool detailedInfo/* = true*/) { releaseITunes(); #ifndef USING_DECODED_ITUNESBACKUP m_iTunesDb = new ITunesDb(m_backup, "Manifest.db"); #else m_iTunesDb = new DecodedWechatITunesDb(m_backup, "Manifest.db"); #endif if (!detailedInfo) { std::function fn = std::bind(&Exporter::filterITunesFile, this, std::placeholders::_1, std::placeholders::_2); m_iTunesDb->setLoadingFilter(fn); } if (!m_iTunesDb->load("AppDomain-com.tencent.xin", !detailedInfo)) { return false; } #ifndef USING_DECODED_ITUNESBACKUP m_iTunesDbShare = new ITunesDb(m_backup, "Manifest.db"); #else m_iTunesDbShare = new DecodedWechatITunesDb(m_backup, "Manifest.db"); #endif if (!m_iTunesDbShare->load("AppDomainGroup-group.com.tencent.xin")) { // Optional // return false; } return true; } std::string Exporter::getITunesVersion() const { return NULL != m_iTunesDb ? m_iTunesDb->getVersion() : ""; } std::string Exporter::getIOSVersion() const { return NULL != m_iTunesDb ? m_iTunesDb->getIOSVersion() : ""; } std::string Exporter::getWechatVersion() const { return m_wechatInfo.getVersion(); } std::string Exporter::buildContentFromTemplateValues(const TemplateValues& tv) const { #if !defined(NDEBUG) && defined(SAMPLING_TMPL) std::string alignment = ""; #endif std::string content = m_resManager.getTemplate(tv.getName()); for (TemplateValues::const_iterator it = tv.cbegin(); it != tv.cend(); ++it) { if (startsWith(it->first, "%")) { replaceAll(content, it->first, it->second); } #if !defined(NDEBUG) && defined(SAMPLING_TMPL) if (it->first == "%%ALIGNMENT%%") { alignment = it->second; } #endif } std::string::size_type pos = 0; while ((pos = content.find("%%", pos)) != std::string::npos) { std::string::size_type posEnd = content.find("%%", pos + 2); if (posEnd == std::string::npos) { break; } content.erase(pos, posEnd + 2 - pos); } #if !defined(NDEBUG) && defined(SAMPLING_TMPL) std::string fileName = "sample_" + tv.getName() + alignment + ".html"; writeFile(combinePath(m_output, "dbg", fileName), content); #endif return content; } void Exporter::notifyStart() { if (m_notifier) { m_notifier->onStart(); } } void Exporter::notifyComplete(bool cancelled/* = false*/) { if (m_notifier) { m_notifier->onComplete(cancelled); } } void Exporter::notifyProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages) { if (m_notifier) { m_notifier->onProgress(numberOfMessages, numberOfTotalMessages); } } void Exporter::notifySessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages) { if (m_notifier) { m_notifier->onSessionStart(sessionUsrName, sessionData, numberOfTotalMessages); } } void Exporter::notifySessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled/* = false*/) { if (m_notifier) { m_notifier->onSessionComplete(sessionUsrName, sessionData, cancelled); } } void Exporter::notifySessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages) { if (m_notifier) { m_notifier->onSessionProgress(sessionUsrName, sessionData, numberOfMessages, numberOfTotalMessages); } } void Exporter::notifyTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks) { if (m_notifier) { m_notifier->onTasksStart(usrName, numberOfTotalTasks); } } void Exporter::notifyTasksComplete(const std::string& usrName, bool cancelled/* = false*/) { if (m_notifier) { m_notifier->onTasksComplete(usrName, cancelled); } } void Exporter::notifyTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalTasks) { if (m_notifier) { m_notifier->onTasksProgress(usrName, numberOfCompletedTasks, numberOfTotalTasks); } } bool Exporter::filterITunesFile(const char *file, int flags) const { if (startsWith(file, "Documents/MMappedKV/")) { return startsWith(file, "mmsetting", 20); } if (std::strncmp(file, "Documents/MapDocument/", 22) == 0 || std::strncmp(file, "Library/WebKit/", 15) == 0) { return false; } const char *str = std::strchr(file, '/'); if (str != NULL) { str = std::strchr(str + 1, '/'); if (str != NULL) { if (std::strncmp(str, "/Audio/", 7) == 0 || std::strncmp(str, "/Img/", 5) == 0 || std::strncmp(str, "/OpenData/", 10) == 0 || std::strncmp(str, "/Video/", 7) == 0 || std::strncmp(str, "/appicon/", 9) == 0 || std::strncmp(str, "/translate/", 11) == 0 || std::strncmp(str, "/Brand/", 7) == 0 || std::strncmp(str, "/Pattern_v3/", 12) == 0 || std::strncmp(str, "/WCPay/", 7) == 0) { return false; } } } return true; } ================================================ FILE: WechatExporter/core/Exporter.h ================================================ // // Exporter.h // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include #include #include #include #include "Logger.h" #include "PdfConverter.h" #include "WechatObjects.h" #include "ExportOption.h" #include "ITunesParser.h" #include "ExportNotifier.h" #include "ResManager.h" // #define USING_ASYNC_TASK_FOR_MP3 #ifndef Exporter_h #define Exporter_h class MessageParser; class TemplateValues; class ExportContext; class PageInfo; class Exporter { protected: std::atomic_bool m_running; std::thread m_thread; // semaphore& m_signal; std::string m_workDir; WechatInfo m_wechatInfo; std::string m_backup; std::string m_output; Logger* m_logger; PdfConverter* m_pdfConverter; ITunesDb *m_iTunesDb; ITunesDb *m_iTunesDbShare; ResManager m_resManager; std::map m_templates; std::map m_localeStrings; ExportNotifier* m_notifier; std::atomic m_cancelled; ExportOption m_options; // bool m_filterByName; std::string m_extName; std::string m_templatesName; std::map> m_usersAndSessionsFilter; std::vector>> m_usersAndSessions; ExportContext* m_exportContext; std::string m_languageCode; std::map m_tags; public: Exporter(const std::string& workDir, const std::string& backup, const std::string& output, Logger* logger, PdfConverter* pdfConverter); ~Exporter(); void setNotifier(ExportNotifier *notifier); bool loadUsersAndSessions(); void swapUsersAndSessions(std::vector>>& usersAndSessions); bool run(); bool isRunning() const; void cancel(); void waitForComplition(); void setOptions(const ExportOption& options); void filterUsersAndSessions(const std::map>& usersAndSessions); void setExtName(const std::string& extName); void setTemplatesName(const std::string& templatesName); void setFilterByName(); void setLanguageCode(const std::string& languageCode); std::string getITunesVersion() const; std::string getIOSVersion() const; std::string getWechatVersion() const; static void initializeExporter(); static void uninitializeExporter(); static bool hasPreviousExporting(const std::string& outputDir, uint64_t& options, std::string& exportTime, std::string& version); protected: bool runImpl(); bool exportUser(Friend& user, std::string& userOutputPath); // bool loadUserSessions(Friend& user, std::vector& sessions) const; bool loadUserFriendsAndSessions(const Friend& user, Friends& friends, std::vector& sessions, bool detailedInfo = true); int exportSession(const Friend& user, const MessageParser& msgParser, const Session& session, const std::string& userBase, const std::string& outputBase); bool exportMessage(const Session& session, const std::vector& tvs, std::vector& messages); bool buildScriptFile(const std::string& fileName, std::vector::const_iterator b, std::vector::const_iterator e, const PageInfo& page) const; void releaseITunes(); bool loadITunes(bool detailedInfo = true); bool hasDebugLogs() const; bool isSubscriptionIncluded() const; void notifyStart(); void notifyComplete(bool cancelled = false); void notifyProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages); void notifySessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages); void notifySessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled = false); void notifySessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages); void notifyTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks); void notifyTasksComplete(const std::string& usrName, bool cancelled = false); void notifyTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalTasks); bool buildFileNameForUser(Friend& user, std::set& existingFileNames); std::string buildContentFromTemplateValues(const TemplateValues& values) const; bool filterITunesFile(const char * file, int flags) const; bool exportPageToFile(const Friend& user, const Session& session, const std::vector& tvs, std::vector& messages, const PageInfo& pagenfo, const std::string& outputBase); void serializeMessages(const std::string& fileName, const std::vector& messages); void unserializeMessages(const std::string& fileName, std::vector& messages); void mergeMessages(const std::string& fileName, std::vector& messages); static bool loadExportContext(const std::string& contextFile, ExportContext *context); }; #endif /* Exporter_h */ ================================================ FILE: WechatExporter/core/FileSystem.cpp ================================================ // // FileSystem_Win.cpp // WechatExporter // // Created by Matthew on 2021/1/21. // Copyright © 2021 Matthew. All rights reserved. // #include "FileSystem.h" #include "Utils.h" #ifndef NDEBUG #include #endif // #include #include #include #ifdef _WIN32 #include #include #include #include #include #else // _WIN32 #ifdef __APPLE__ // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/copyfile.3.html #include #endif #include #include #include #include #include #include #endif // _WIN32 #ifdef _WIN32 inline size_t getFileSizeImpl(LPCTSTR path) #else inline size_t getFileSizeImpl(const std::string& path) #endif { #ifdef _WIN32 HANDLE hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) return -1; // error condition, could call GetLastError to find out more LARGE_INTEGER size; if (!GetFileSizeEx(hFile, &size)) { CloseHandle(hFile); return -1; // error condition, could call GetLastError to find out more } CloseHandle(hFile); return (size_t)size.QuadPart; #else struct stat sb; int rc = stat(path.c_str(), &sb); return rc == 0 ? sb.st_size : -1; #endif } size_t getFileSize(const std::string& path) { #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); return getFileSizeImpl((LPCTSTR)pszT); #else return getFileSizeImpl(path); #endif } #ifdef _WIN32 inline bool existsDirectoryImpl(LPCTSTR lpszPath) { DWORD dwAttrib = ::GetFileAttributes(lpszPath); return (dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY); } inline bool isDirectoryImpl(LPCTSTR lpszPath) { DWORD dwAttrib = ::GetFileAttributes(lpszPath); return (dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY); } time_t FileTimeToTime(FILETIME ft) { // takes the last modified date LARGE_INTEGER date, adjust; date.HighPart = ft.dwHighDateTime; date.LowPart = ft.dwLowDateTime; const uint64_t EpochShift = 116444736000000000; if (date.QuadPart < EpochShift) return -1; // 100-nanoseconds = milliseconds * 10000 // removes the diff between 1970 and 1601 date.QuadPart -= EpochShift; // converts back from 100-nanoseconds to seconds return date.QuadPart / 10000000; } #else inline bool isDirectoryImpl(const std::string& path) { struct stat sb; return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode)); } #endif bool existsDirectory(const std::string& path) { #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); return existsDirectoryImpl(pszT); #else struct stat sb; return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode)); #endif } #ifndef _WIN32 int makePathImpl(const std::string::value_type *path, mode_t mode) { struct stat st; int status = 0; if (stat(path, &st) != 0) { // Directory does not exist. EEXIST for race condition if (mkdir(path, mode) != 0 && errno != EEXIST) status = -1; } else if (!S_ISDIR(st.st_mode)) { // errno = ENOTDIR; status = -1; } return status; } #endif bool makeDirectory(const std::string& path) { 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/") { int aa = 0; } #ifndef NDEBUG // The path must be full/absolute path #ifdef _WIN32 assert(path.find(":\\") != std::string::npos); #else assert(startsWith(path, "/")); #endif #endif #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); return ::SHCreateDirectoryEx(NULL, (LPCTSTR)pszT, NULL) == ERROR_SUCCESS; #else std::vector copypath; copypath.reserve(path.size() + 1); std::copy(path.begin(), path.end(), std::back_inserter(copypath)); copypath.push_back('\0'); std::replace(copypath.begin(), copypath.end(), '\\', '/'); std::vector::iterator itStart = copypath.begin(); std::vector::iterator it; int status = 0; mode_t mode = 0755; while (status == 0 && (it = std::find(itStart, copypath.end(), '/')) != copypath.end()) { if (it != copypath.begin()) { // Neither root nor double slash in path *it = '\0'; status = makePathImpl(©path[0], mode); *it = '/'; } itStart = it + 1; } if (status == 0) { status = makePathImpl(©path[0], mode); } return status == 0; #endif } bool deleteFile(const std::string& path) { #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); return ::DeleteFile((LPCTSTR)pszT) == TRUE; #else return 0 == std::remove(path.c_str()); #endif } bool deleteDirectory(const std::string& path) { #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); size_t len = _tcslen(pszT); TCHAR* pszT2 = new TCHAR[len + 2]; _tcscpy(pszT2, pszT); pszT2[len + 1] = 0; pszT2[len] = 0; SHFILEOPSTRUCT file_op = { NULL, FO_DELETE, pszT2, NULL, FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT, FALSE, NULL, NULL }; bool result = (SHFileOperation(&file_op) == 0); delete[] pszT2; return result; #else int ret = 0; FTS *ftsp = NULL; FTSENT *curr; // Cast needed (in C) because fts_open() takes a "char * const *", instead // of a "const char * const *", which is only allowed in C++. fts_open() // does not modify the argument. char *files[] = { (char *) path.c_str(), NULL }; // FTS_NOCHDIR - Avoid changing cwd, which could cause unexpected behavior // in multithreaded programs // FTS_PHYSICAL - Don't follow symlinks. Prevents deletion of files outside // of the specified directory // FTS_XDEV - Don't cross filesystem boundaries ftsp = fts_open(files, FTS_NOCHDIR | FTS_PHYSICAL | FTS_XDEV, NULL); if (!ftsp) { // fprintf(stderr, "%s: fts_open failed: %s\n", dir, strerror(errno)); ret = -1; goto finish; } while ((curr = fts_read(ftsp))) { switch (curr->fts_info) { case FTS_NS: case FTS_DNR: case FTS_ERR: // fprintf(stderr, "%s: fts_read error: %s\n", curr->fts_accpath, strerror(curr->fts_errno)); break; case FTS_DC: case FTS_DOT: case FTS_NSOK: // Not reached unless FTS_LOGICAL, FTS_SEEDOT, or FTS_NOSTAT were // passed to fts_open() break; case FTS_D: // Do nothing. Need depth-first search, so directories are deleted // in FTS_DP break; case FTS_DP: case FTS_F: case FTS_SL: case FTS_SLNONE: case FTS_DEFAULT: if (remove(curr->fts_accpath) < 0) { // fprintf(stderr, "%s: Failed to remove: %s\n", curr->fts_path, strerror(curr->fts_errno)); ret = -1; } break; } } finish: if (ftsp) { fts_close(ftsp); } return ret == 0; #endif } #ifdef _WIN32 inline bool existsFileImpl(LPCTSTR path) #else inline bool existsFileImpl(const std::string& path) #endif { #ifdef _WIN32 DWORD dwAttrib = ::GetFileAttributes(path); return (dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY) == 0); #else struct stat sb; return (stat(path.c_str(), &sb) == 0); #endif } bool existsFile(const std::string& path) { #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); return existsFileImpl((LPCTSTR)pszT); #else return existsFileImpl(path); #endif } bool listSubDirectories(const std::string& path, std::vector& subDirectories) { #ifdef _WIN32 WIN32_FIND_DATA FindFileData; HANDLE hFind = INVALID_HANDLE_VALUE; std::string formatedPath = combinePath(path, "*.*"); std::replace(formatedPath.begin(), formatedPath.end(), ALT_DIR_SEP, DIR_SEP); CW2T localPath(CA2W(formatedPath.c_str(), CP_UTF8)); hFind = ::FindFirstFile((LPTSTR)localPath, &FindFileData); if (hFind == INVALID_HANDLE_VALUE) { return false; } do { if (_tcscmp(FindFileData.cFileName, TEXT(".")) == 0 || _tcscmp(FindFileData.cFileName, TEXT("..")) == 0) { continue; } if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { CW2A pszU8(CT2W(FindFileData.cFileName), CP_UTF8); subDirectories.push_back((LPCSTR)pszU8); } } while (::FindNextFile(hFind, &FindFileData)); ::FindClose(hFind); #else struct dirent *entry; DIR *dir = opendir(path.c_str()); if (dir == NULL) { return false; } while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } std::string subDir = entry->d_name; // TODO: Check directory or not if (isDirectoryImpl(combinePath(path, subDir))) { subDirectories.push_back(subDir); } } closedir(dir); #endif return true; } bool listDirectory(const std::string& path, std::vector& subDirectories, std::vector& subFiles) { #ifdef _WIN32 WIN32_FIND_DATA FindFileData; HANDLE hFind = INVALID_HANDLE_VALUE; std::string formatedPath = combinePath(path, "*.*"); std::replace(formatedPath.begin(), formatedPath.end(), ALT_DIR_SEP, DIR_SEP); CW2T localPath(CA2W(formatedPath.c_str(), CP_UTF8)); hFind = ::FindFirstFile((LPTSTR)localPath, &FindFileData); if (hFind == INVALID_HANDLE_VALUE) { return false; } do { if (_tcscmp(FindFileData.cFileName, TEXT(".")) == 0 || _tcscmp(FindFileData.cFileName, TEXT("..")) == 0) { continue; } CW2A pszU8(CT2W(FindFileData.cFileName), CP_UTF8); if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { subDirectories.push_back((LPCSTR)pszU8); } else { subFiles.push_back((LPCSTR)pszU8); } } while (::FindNextFile(hFind, &FindFileData)); ::FindClose(hFind); #else struct dirent *entry; DIR *dir = opendir(path.c_str()); if (dir == NULL) { return false; } while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } std::string subDir = entry->d_name; if (isDirectoryImpl(combinePath(path, subDir))) { subDirectories.push_back(subDir); } else { subFiles.push_back(subDir); } } closedir(dir); #endif return true; } #ifdef _WIN32 inline bool copyFileImpl(LPCTSTR src, LPCTSTR dest) #else inline bool copyFileImpl(const std::string& src, const std::string& dest) #endif { #ifdef _WIN32 BOOL bRet = ::CopyFile(src, dest, TRUE); #ifndef NDEBUG DWORD err = ::GetLastError(); TCHAR buffer[256] = { 0 }; _itot((int)err, buffer, 10); _tcscat(buffer, TEXT(" ")); _tcscat(buffer, dest); assert(buffer); #endif return (bRet == TRUE); #elif defined(__APPLE__) /* Initialize a state variable */ copyfile_state_t s; s = copyfile_state_alloc(); /* Copy the data and extended attributes of one file to another */ int ret = copyfile(src.c_str(), dest.c_str(), s, COPYFILE_ALL); /* Release the state variable */ copyfile_state_free(s); return (ret == 0); #else std::ifstream ss(src, std::ios::in | std::ios::binary); if (!ss.is_open()) { #ifndef NDEBUG // assert(false); #endif return false; } std::ofstream ds(dest, std::ios::out | std::ios::binary | std::ios::trunc); if (!ds.is_open()) { #ifndef NDEBUG assert(false); #endif ss.close(); return false; } ds << ss.rdbuf(); ss.close(); ds.close(); return true; #endif } bool copyFile(const std::string& src, const std::string& dest, bool overwrite) { #ifdef _WIN32 CW2T pszDest(CA2W(dest.c_str(), CP_UTF8)); if (::PathFileExists((LPCTSTR)pszDest) && !overwrite) { return true; } CW2T pszSrc(CA2W(src.c_str(), CP_UTF8)); return copyFileImpl(pszSrc, pszDest); #else if (existsFile(dest) && !overwrite) { return false; } return copyFileImpl(src, dest); #endif } #ifdef _WIN32 inline bool checkFileNewer(LPCTSTR src, LPCTSTR dest) #else inline bool checkFileNewer(const std::string& src, const std::string& dest) #endif { #ifdef _WIN32 if (!::PathFileExists(dest)) { return true; } // Check file size if (getFileSizeImpl(src) != getFileSizeImpl(dest)) { return true; } // Check last modified time HANDLE hFileSrc = CreateFile(src, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFileSrc == INVALID_HANDLE_VALUE) return true; HANDLE hFileDest = CreateFile(dest, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFileDest == INVALID_HANDLE_VALUE) { CloseHandle(hFileSrc); return true; } LARGE_INTEGER sizeSrc; LARGE_INTEGER sizeDest; if (!GetFileSizeEx(hFileSrc, &sizeSrc) || !GetFileSizeEx(hFileDest, &sizeDest) || sizeSrc.QuadPart != sizeDest.QuadPart) { CloseHandle(hFileSrc); CloseHandle(hFileDest); return true; } FILETIME ftSrc, ftDest; if (!GetFileTime(hFileSrc, NULL, NULL, &ftSrc) || !GetFileTime(hFileDest, NULL, NULL, &ftDest)) { CloseHandle(hFileSrc); CloseHandle(hFileDest); return true; } CloseHandle(hFileSrc); CloseHandle(hFileDest); return CompareFileTime(&ftSrc, &ftDest) != 0; #else if (!existsFile(dest)) { return true; } struct stat sbSrc, sbDest; int rc = stat(src.c_str(), &sbSrc); if (rc != 0) { return true; } rc = stat(dest.c_str(), &sbDest); if (rc != 0) { return true; } return (sbSrc.st_size != sbDest.st_size) || (sbSrc.st_mtime != sbDest.st_mtime); #endif return true; } bool copyFileIfNewer(const std::string& src, const std::string& dest) { #ifdef _WIN32 CW2T pszSrc(CA2W(src.c_str(), CP_UTF8)); CW2T pszDest(CA2W(dest.c_str(), CP_UTF8)); if (!checkFileNewer((LPCTSTR)pszSrc, (LPCTSTR)pszDest)) { return true; } return copyFileImpl(pszSrc, pszDest); #else if (!checkFileNewer(src, dest)) { return true; } return copyFileImpl(src, dest); #endif } bool copyDirectory(const std::string& src, const std::string& dest) { #ifdef _WIN32 TCHAR pszSrc[MAX_PATH] = { 0 }; _tcscpy(pszSrc, (LPCTSTR)CW2T(CA2W(src.c_str(), CP_UTF8))); if (!existsDirectoryImpl((LPCTSTR)pszSrc)) { return false; } PathAppend(pszSrc, TEXT("*")); TCHAR pszDest[MAX_PATH] = { 0 }; _tcscpy(pszDest, (LPCTSTR)CW2T(CA2W(dest.c_str(), CP_UTF8))); pszSrc[_tcslen(pszSrc) + 1] = 0; pszDest[_tcslen(pszDest) + 1] = 0; SHFILEOPSTRUCTW s = { 0 }; s.wFunc = FO_COPY; s.pTo = pszDest; s.pFrom = pszSrc; s.fFlags = FOF_SILENT | FOF_NOCONFIRMMKDIR | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NO_UI; int res = SHFileOperation( &s ); return res == 0; // #elif defined(__APPLE__) #else if (src.empty() || dest.empty()) { return false; } struct stat st; /* if src does not exist */ if ((stat(src.c_str(), &st) < 0) || !S_ISDIR(st.st_mode)) { // Source directory does not exist; return false; } /* if dst directory does not exist */ if ((stat(dest.c_str(), &st) < 0) || !S_ISDIR(st.st_mode)) { /* create it */ if (!makeDirectory(dest/*, 0755*/)) { // Unable to create destination directory; return false; } } std::queue> pairs; pairs.push(std::pair(src, dest)); std::vector>::reference ref = pairs.front(); if (ref.first[ref.first.size() - 1] != DIR_SEP) { ref.first += DIR_SEP; } if (ref.second[ref.second.size() - 1] != DIR_SEP) { ref.second += DIR_SEP; } bool res = true; while (!pairs.empty()) { ref = pairs.front(); /* loop over src directory contents */ DIR *cur_dir = opendir(ref.first.c_str()); if (cur_dir) { struct dirent* ep; while ((ep = readdir(cur_dir))) { if ((strcmp(ep->d_name, ".") == 0) || (strcmp(ep->d_name, "..") == 0)) { continue; } std::string srcpath = ref.first + ep->d_name; std::string dstpath = ref.second + ep->d_name; if (isDirectoryImpl(srcpath)) { pairs.push(std::pair(srcpath + DIR_SEP, dstpath + DIR_SEP)); } else { if (!copyFile(srcpath, dstpath)) { res = false; break; } } } closedir(cur_dir); } pairs.pop(); } return res; #endif } bool moveFile(const std::string& src, const std::string& dest, bool overwrite/* = true*/) { #ifndef NDEBUG if (src.empty()) { assert(!"src is empty"); } if (dest.empty()) { assert(!"dest is empty"); } #endif #ifdef _WIN32 CW2T pszSrc(CA2W(src.c_str(), CP_UTF8)); CW2T pszDest(CA2W(dest.c_str(), CP_UTF8)); if (overwrite && ::PathFileExists((LPCTSTR)pszDest)) { ::DeleteFile((LPCTSTR)pszDest); } bool bRet = false; if (::PathFileExists((LPCTSTR)pszSrc)) { bRet = ::MoveFile((LPCTSTR)pszSrc, (LPCTSTR)pszDest) == TRUE; #ifndef NDEBUG if (!bRet) { // MessageBox(NULL, (LPCTSTR)pszSrc, (LPCTSTR)pszDest, MB_OK); assert(!((LPCTSTR)pszDest)); } #endif } return bRet; #else if (overwrite) { remove(dest.c_str()); } else if (existsFile(dest)) { return false; } if (!existsFile(src)) { return false; } int ret = rename(src.c_str(), dest.c_str());; #ifndef NDEBUG assert(ret == 0); #endif return ret == 0; #endif } #ifdef _WIN32 CString removeInvalidCharsForFileName(const CString& fileName) { LPCTSTR invalidChars = TEXT("[\\/:*?\"<>|]"); CString validFileName = fileName; for (LPCTSTR ptr = invalidChars; *ptr != '\0'; ptr++) { validFileName.Remove(*ptr); } validFileName.TrimLeft(TEXT(".")); validFileName.TrimRight(TEXT(" .")); if (validFileName.GetLength() > 255) { validFileName = validFileName.Left(255); } return validFileName; } #endif std::string removeInvalidCharsForFileName(const std::string& fileName) { /* Windows NT: Do not use any of the following characters when naming your files or folders: / ? < > \ : * | ” and any character you can type with the Ctrl key File and folder names may be up to 256 characters long The maximum length of a full path is 260 characters Placing a space at the end of the name Placing a period at the end of the name MAC: The only illegal character for file and folder names in Mac OS X is the colon “:” File and folder names are not permitted to begin with a dot “.” File and folder names may be up to 255 characters in length */ #ifdef _WIN32 CW2T pszT(CA2W(fileName.c_str(), CP_UTF8)); CString validFileName = removeInvalidCharsForFileName(CString(pszT)); CW2A pszU8(CT2W(validFileName), CP_UTF8); return std::string(pszU8); #else std::string validFileName = fileName; static std::string invalidChars = "[\\/:*?\"<>|]"; for (unsigned int i = 0; i < invalidChars.size(); ++i) { validFileName.erase(remove(validFileName.begin(), validFileName.end(), invalidChars[i]), validFileName.end()); } size_t pos = validFileName.find_first_not_of("."); if (pos == std::string::npos) { return ""; } else if (pos != 0) { validFileName.erase(0, pos); } pos = validFileName.find_last_not_of(" ."); if (pos == std::string::npos) { return ""; } else { validFileName.erase(pos + 1); } if (validFileName.size() > 255) { validFileName = validFileName.substr(0, 255); } return validFileName; #endif } bool isValidFileName(const std::string& fileName) { #ifdef _WIN32 TCHAR tmpPath[MAX_PATH] = { 0 }; if (GetTempPath(MAX_PATH, tmpPath)) { // CT2A t2a(charPath); // tempDir = (LPCSTR)t2a; } TCHAR path[MAX_PATH] = { 0 }; CW2T pszT(CA2W(fileName.c_str(), CP_UTF8)); ::PathCombine(path, tmpPath, (LPCTSTR)pszT); bool valid = false; if (::CreateDirectory(path, NULL)) { valid = true; RemoveDirectory(path); } else { valid = (::GetLastError() == ERROR_ALREADY_EXISTS); } return valid; #else char const *tmpdir = getenv("TMPDIR"); std::string tempDir; if (tmpdir == NULL) { tempDir = "/tmp"; } else { tempDir = tmpdir; } std::string path = combinePath(tempDir, fileName); int status = mkdir(path.c_str(), 0); int lastErrorNo = errno; if (status == 0) { remove(path.c_str()); } return status == 0 || lastErrorNo == EEXIST; #endif } std::string readFile(const std::string& path) { std::string contents; std::vector data; if (readFile(path, data) && !data.empty()) { contents.append(reinterpret_cast(&data[0]), data.size()); } return contents; } bool readFile(const std::string& path, std::vector& data) { #ifdef _WIN32 CA2W pszW(path.c_str(), CP_UTF8); std::ifstream ifs(pszW, std::ios::in | std::ios::binary | std::ios::ate); #else std::ifstream ifs(path, std::ios::in | std::ios::binary | std::ios::ate); #endif if (ifs.is_open()) { std::streampos size = ifs.tellg(); if ((long long)size > 0) { std::vector buffer; data.resize(size); ifs.seekg (0, std::ios::beg); ifs.read((char *)(&data[0]), size); } else { data.clear(); } ifs.close(); return true; } return false; } bool writeFile(const std::string& path, const std::vector& data) { return writeFile(path, &(data[0]), data.size()); } bool writeFile(const std::string& path, const std::string& data) { return writeFile(path, reinterpret_cast(data.c_str()), data.size()); } bool writeFile(const std::string& path, const unsigned char* data, size_t dataLength) { #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); HANDLE hFile = CreateFile(pszT, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { return false; } DWORD dwBytesToWrite = static_cast(dataLength); DWORD dwBytesWritten = 0; BOOL bErrorFlag = WriteFile(hFile, data, dwBytesToWrite, &dwBytesWritten, NULL); ::CloseHandle(hFile); return (TRUE == bErrorFlag); #else std::ofstream ofs; ofs.open(path, std::ios::out | std::ios::binary | std::ios::trunc); if (ofs.is_open()) { ofs.write(reinterpret_cast(data), dataLength); ofs.close(); return true; } return false; #endif } bool appendFile(const std::string& path, const std::string& data) { return appendFile(path, reinterpret_cast(data.c_str()), data.size()); } bool appendFile(const std::string& path, const unsigned char* data, size_t dataLength) { #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); HANDLE hFile = ::CreateFile(pszT, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { return false; } ::SetFilePointer(hFile, 0, 0, FILE_END); DWORD dwBytesToWrite = static_cast(dataLength); DWORD dwBytesWritten = 0; BOOL bErrorFlag = WriteFile(hFile, data, dwBytesToWrite, &dwBytesWritten, NULL); ::CloseHandle(hFile); return (TRUE == bErrorFlag); #else std::ofstream ofs; ofs.open(path, std::fstream::in | std::fstream::out | std::fstream::app | std::fstream::binary); if (ofs.is_open()) { ofs.write(reinterpret_cast(data), dataLength); ofs.close(); return true; } return false; #endif } std::string combinePath(const std::string& p1, const std::string& p2) { if (p1.empty() && p2.empty()) { return ""; } std::string path; #ifdef _WIN32 TCHAR buffer[MAX_PATH] = { 0 }; CW2T pszT1(CA2W(p1.c_str(), CP_UTF8)); CW2T pszT2(CA2W(p2.c_str(), CP_UTF8)); LPTSTR psz = ::PathCombine(buffer, pszT1, pszT2); #ifndef NDEBUG assert(psz != NULL); #endif CW2A pszU8(CT2W(buffer), CP_UTF8); path = pszU8; #else if (!p1.empty()) { path = p1; if (path[path.size() - 1] != DIR_SEP && path[path.size() - 1] != ALT_DIR_SEP) { path += DIR_SEP; } path += p2; } else { path = p2; } #endif #ifndef NDEBUG if (path.empty()) { assert(false); } #endif return path; } std::string combinePath(const std::string& p1, const std::string& p2, const std::string& p3) { return combinePath(combinePath(p1, p2), p3); } std::string combinePath(const std::string& p1, const std::string& p2, const std::string& p3, const std::string& p4) { return combinePath(combinePath(p1, p2, p3), p4); } std::string normalizePath(const std::string& path) { std::string p = path; normalizePath(p); return p; } void normalizePath(std::string& path) { std::replace(path.begin(), path.end(), ALT_DIR_SEP, DIR_SEP); } int calcFreeSpace(const std::string& path, uint64_t& freeSpace) { int res = -1; #ifdef _WIN32 ULARGE_INTEGER value; value.QuadPart = 0; if (GetDiskFreeSpaceExA(path.c_str(), &value, NULL, NULL)) { res = 0; freeSpace = value.QuadPart; } #else struct statvfs fs; memset(&fs, '\0', sizeof(fs)); res = statvfs(path.c_str(), &fs); if (res == 0) { freeSpace = (uint64_t)fs.f_bavail * (uint64_t)fs.f_bsize; } #endif return res; } FileEnumerator::File::File() : m_isDir(false), m_isNormalFile(false), m_fileSize(0) { } const std::string& FileEnumerator::File::getFileName() const { return m_fileName; } const std::string& FileEnumerator::File::getFullPath() const { return m_fullPath; } bool FileEnumerator::File::isDirectory() const { return m_isDir; } bool FileEnumerator::File::isNormalFile() const { return m_isNormalFile; } time_t FileEnumerator::File::getModifiedTime() const { return m_modifiedTime; } uint64_t FileEnumerator::File::getFileSize() const { return m_fileSize; } FileEnumerator::FileEnumerator(const std::string& path) : m_path(path), m_handle(NULL), m_first(true) { #ifdef _WIN32 HANDLE hFind = INVALID_HANDLE_VALUE; m_handle = (void *)hFind; #else #endif } FileEnumerator::~FileEnumerator() { #ifdef _WIN32 HANDLE hFind = (HANDLE)m_handle; if (hFind != INVALID_HANDLE_VALUE) { ::FindClose(hFind); } #else DIR *dir = (DIR *)m_handle; if (dir != NULL) { closedir(dir); } #endif } bool FileEnumerator::nextFile(File& file) { #ifdef _WIN32 bool res = false; HANDLE hFind = (HANDLE)m_handle; WIN32_FIND_DATA FindFileData; if (m_first) { m_first = false; std::string formatedPath = combinePath(m_path, "*.*"); std::replace(formatedPath.begin(), formatedPath.end(), ALT_DIR_SEP, DIR_SEP); CW2T pszPath(CA2W(formatedPath.c_str(), CP_UTF8)); hFind = ::FindFirstFile((LPTSTR)pszPath, &FindFileData); m_handle = (void *)hFind; if (hFind == INVALID_HANDLE_VALUE) { return res; } do { if (_tcscmp(FindFileData.cFileName, TEXT(".")) == 0 || _tcscmp(FindFileData.cFileName, TEXT("..")) == 0) { continue; } res = true; break; } while (::FindNextFile(hFind, &FindFileData)); } else { if (hFind == INVALID_HANDLE_VALUE) { return res; } while (::FindNextFile(hFind, &FindFileData)) { if (_tcscmp(FindFileData.cFileName, TEXT(".")) == 0 || _tcscmp(FindFileData.cFileName, TEXT("..")) == 0) { continue; } res = true; break; } } if (res) { file.m_fileName = (LPCSTR)CW2A(CT2W(FindFileData.cFileName), CP_UTF8); file.m_fullPath = combinePath(m_path, file.m_fileName); file.m_isDir = ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY); file.m_isNormalFile = !file.m_isDir && ((FindFileData.dwFileAttributes & (FILE_ATTRIBUTE_NORMAL | FILE_ATTRIBUTE_ARCHIVE)) != 0); if (file.m_isDir) { file.m_fileSize = (FindFileData.nFileSizeHigh * (MAXDWORD + 1)) + FindFileData.nFileSizeLow; } else { struct stat st; stat(file.m_fullPath.c_str(), &st); } file.m_modifiedTime = FileTimeToTime(FindFileData.ftLastWriteTime); #ifndef NDEBUG struct __stat64 st; _tstat64(CW2T(CA2W(file.m_fullPath.c_str(), CP_UTF8)), &st); assert(file.m_isNormalFile == S_ISREG(st.st_mode)); if (!S_ISDIR(st.st_mode)) { assert(file.m_fileSize == st.st_size); } #endif } return res; #else DIR *dir = (DIR *)m_handle; bool res = false; if (m_first) { m_first = false; dir = opendir(m_path.c_str()); m_handle = (void *)dir; if (dir == NULL) { return res; } } struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } file.m_fileName = entry->d_name; file.m_fullPath = combinePath(m_path, file.m_fileName); struct stat st; stat(file.m_fullPath.c_str(), &st); file.m_fileSize = st.st_size; file.m_isDir = S_ISDIR(st.st_mode); file.m_isNormalFile = S_ISREG(st.st_mode); file.m_modifiedTime = st.st_mtime; res = true; break; } return res; #endif } File::File() : #ifdef _WIN32 m_file(INVALID_HANDLE_VALUE) #else m_file(NULL) #endif { } File::~File() { close(); } bool File::open(const std::string& path, bool readOnly/* = true*/) { #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); if (readOnly) { m_file = CreateFile((LPCTSTR)pszT, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); } else { m_file = CreateFile((LPCTSTR)pszT, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); } return (INVALID_HANDLE_VALUE != m_file); #else m_file = fopen(path.c_str(), readOnly ? "rb" : "wb"); return NULL != m_file; #endif } bool File::read(unsigned char* buffer, size_t bytesToRead, size_t& bytesRead) { #ifdef _WIN32 DWORD numberOfBytesRead = 0; BOOL res = ReadFile(m_file, buffer, (DWORD)bytesToRead, &numberOfBytesRead, NULL); if (res) { bytesRead = numberOfBytesRead; } return res; #else if (m_file != NULL && bytesToRead == 0) { bytesRead = 0; return true; } size_t res = fread(buffer, 1, bytesToRead, m_file); if (res < bytesToRead) { bytesRead = res; if (feof(m_file)) { return true; } return ferror(m_file) != 0; } return true; #endif } bool File::write(const unsigned char* buffer, size_t bytesToWrite, size_t& bytesWritten) { #ifdef _WIN32 DWORD numberOfBytesWritten = 0; BOOL res = WriteFile(m_file, buffer, static_cast(bytesToWrite), &numberOfBytesWritten, NULL); bytesWritten = res ? numberOfBytesWritten : 0; return (TRUE == res); #else if (NULL != m_file && bytesToWrite == 0) { bytesWritten = 0; return true; } bytesWritten = fwrite(buffer, 1, bytesToWrite, m_file); return (bytesWritten == bytesToWrite); #endif } void File::close() { #ifdef _WIN32 if (INVALID_HANDLE_VALUE != m_file) { CloseHandle(m_file); m_file = INVALID_HANDLE_VALUE; } #else if (NULL != m_file) { fclose(m_file); m_file = NULL; } #endif } ================================================ FILE: WechatExporter/core/FileSystem.h ================================================ // // FileSystem.h // WechatExporter // // Created by Matthew on 2021/1/21. // Copyright © 2021 Matthew. All rights reserved. // #ifndef FileSystem_h #define FileSystem_h #include #include #ifdef _WIN32 #include #if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG) #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) #endif #if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR) #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) #endif #endif #ifdef _WIN32 #define DIR_SEP '\\' #define DIR_SEP_STR "\\" #define ALT_DIR_SEP '/' #else #define DIR_SEP '/' #define DIR_SEP_STR "/" #define ALT_DIR_SEP '\\' #endif size_t getFileSize(const std::string& path); bool existsDirectory(const std::string& path); bool makeDirectory(const std::string& path); bool deleteFile(const std::string& path); bool deleteDirectory(const std::string& path); bool existsFile(const std::string& path); bool listSubDirectories(const std::string& path, std::vector& subDirectories); bool listDirectory(const std::string& path, std::vector& subDirectories, std::vector& subFiles); bool copyFile(const std::string& src, const std::string& dest, bool overwrite = true); bool copyFileIfNewer(const std::string& src, const std::string& dest); bool copyDirectory(const std::string& src, const std::string& dest); bool moveFile(const std::string& src, const std::string& dest, bool overwrite = true); // ref: https://blackbeltreview.wordpress.com/2015/01/27/illegal-filename-characters-on-windows-vs-mac-os/ bool isValidFileName(const std::string& fileName); std::string removeInvalidCharsForFileName(const std::string& fileName); std::string readFile(const std::string& path); bool readFile(const std::string& path, std::vector& data); bool writeFile(const std::string& path, const std::vector& data); bool writeFile(const std::string& path, const std::string& data); bool writeFile(const std::string& path, const unsigned char* data, size_t dataLength); bool appendFile(const std::string& path, const std::string& data); bool appendFile(const std::string& path, const unsigned char* data, size_t dataLength); std::string combinePath(const std::string& p1, const std::string& p2); std::string combinePath(const std::string& p1, const std::string& p2, const std::string& p3); std::string combinePath(const std::string& p1, const std::string& p2, const std::string& p3, const std::string& p4); std::string normalizePath(const std::string& path); void normalizePath(std::string& path); // return zero for succeeding and non-zero for failure int calcFreeSpace(const std::string& path, uint64_t& freeSpace); #ifdef _WIN32 time_t FileTimeToTime(FILETIME ft); #endif class FileEnumerator { public: class File { public: friend FileEnumerator; File(); const std::string& getFileName() const; const std::string& getFullPath() const; bool isDirectory() const; bool isNormalFile() const; time_t getModifiedTime() const; uint64_t getFileSize() const; private: std::string m_fileName; std::string m_fullPath; time_t m_modifiedTime; bool m_isDir; bool m_isNormalFile; uint64_t m_fileSize; }; FileEnumerator(const std::string& path); ~FileEnumerator(); bool isValid() const; bool nextFile(File& file); private: std::string m_path; void* m_handle; bool m_first; }; class File { public: File(); // Will close the file ~File(); bool open(const std::string& path, bool readOnly = true); bool read(unsigned char* buffer, size_t bytesToRead, size_t& bytesRead); bool write(const unsigned char* buffer, size_t bytesToWrite, size_t& bytesWritten); void close(); private: #ifdef _WIN32 HANDLE m_file; #else FILE* m_file; #endif }; #endif /* FileSystem_h */ ================================================ FILE: WechatExporter/core/IDeviceBackup.cpp ================================================ // // IDevice.cpp // WechatExporter // // Created by Matthew on 2021/12/7. // Copyright © 2021 Matthew. All rights reserved. // #include "IDeviceBackup.h" #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #include #include #include #else #include #include #endif #include #include #include #include #include #include #include #include #include #include "endianness.h" #include "Utils.h" #include "ITunesParser.h" #include "FileSystem.h" #ifdef _WIN32 int __cdecl printlog(const char *format, ...) { char str[1024]; va_list argptr; va_start(argptr, format); int ret = vsnprintf(str, sizeof(str), format, argptr); va_end(argptr); OutputDebugString((LPCTSTR)CW2T(CA2W(str, CP_UTF8))); return ret; } #else #ifndef NDEBUG #define printlog printf #else #define printlog(format, ...) (void)0 #endif #endif #define DEVICE_VERSION(maj, min, patch) (((maj & 0xFF) << 16) | ((min & 0xFF) << 8) | (patch & 0xFF)) #define PRINT_VERBOSE(min_level, ...) { printlog(__VA_ARGS__); } #define CODE_SUCCESS 0x00 #define CODE_ERROR_LOCAL 0x06 #define CODE_ERROR_REMOTE 0x0b #define CODE_FILE_DATA 0x0c #define MAC_EPOCH 978307200 #ifdef _WIN32 void usleep(__int64 usec) { HANDLE timer; LARGE_INTEGER ft; ft.QuadPart = -(10 * usec); // Convert to 100 nanosecond interval, negative value indicates relative time timer = CreateWaitableTimer(NULL, TRUE, NULL); SetWaitableTimer(timer, &ft, 0, NULL, NULL, 0); WaitForSingleObject(timer, INFINITE); CloseHandle(timer); } #endif enum plist_format_t { PLIST_FORMAT_XML, PLIST_FORMAT_BINARY }; std::string formatDiskSize(uint64_t size) { char buf[80]; double sz; if (size >= 1000000000000LL) { sz = ((double)size / 1000000000000.0f); sprintf(buf, "%0.1f TB", sz); } else if (size >= 1000000000LL) { sz = ((double)size / 1000000000.0f); sprintf(buf, "%0.1f GB", sz); } else if (size >= 1000000LL) { sz = ((double)size / 1000000.0f); sprintf(buf, "%0.1f MB", sz); } else if (size >= 1000LL) { sz = ((double)size / 1000.0f); sprintf(buf, "%0.1f KB", sz); } else { sprintf(buf, "%d Bytes", (int)size); } return std::string(buf); } int writePlistFile(plist_t plist, const std::string& filename, enum plist_format_t format) { char *buffer = NULL; uint32_t length; if (!plist || filename.empty()) return 0; if (format == PLIST_FORMAT_XML) plist_to_xml(plist, &buffer, &length); else if (format == PLIST_FORMAT_BINARY) plist_to_bin(plist, &buffer, &length); else return 0; bool res = writeFile(filename, (const unsigned char*)buffer, length); plist_mem_free(buffer); return res ? 1 : 0; } int readPlistFile(plist_t *plist, const std::string& filename) { if (filename.empty()) return 0; std::vector buffer; if (!readFile(filename, buffer)) { return 0; } plist_from_memory((const char *)&buffer[0], (uint32_t)buffer.size(), plist); return 1; } namespace fs { #ifdef _WIN32 static int win32err_to_errno(int err_value) { switch (err_value) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_INVALID_DRIVE: return ENOENT; case ERROR_ALREADY_EXISTS: case ERROR_FILE_EXISTS: return EEXIST; case ERROR_HANDLE_DISK_FULL: case ERROR_DISK_FULL: return ENOSPC; default: return EFAULT; } } static int win32err_to_device_error(DWORD errValue) { switch (errValue) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_INVALID_DRIVE: return -6; case ERROR_ALREADY_EXISTS: case ERROR_FILE_EXISTS: return -7; case ERROR_HANDLE_DISK_FULL: case ERROR_DISK_FULL: return -15; default: return -1; } } #endif static int convertErrnoToDeviceError(int errnoValue) { switch (errnoValue) { case ENOENT: /* No such file or directory */ return -6; case EEXIST: /* File exists */ return -7; case ENOTDIR: /* Not a directory */ return -8; case EISDIR: /* Is a directory */ return -9; case ELOOP: /* Too many levels of symbolic links */ return -10; case EIO: /* Input/output error */ return -11; case ENOSPC: /* No space left on device */ return -15; default: return -1; } } // return errno static int deleteFile(const std::string& path) { int e = 0; #ifdef _WIN32 CW2T szPath(CA2W(path.c_str(), CP_UTF8)); if (!DeleteFile((LPCTSTR)szPath)) { e = win32err_to_errno(GetLastError()); } #else if (unlink(path.c_str()) < 0) { e = errno; } #endif return e; } // return errno #ifdef _WIN32 static int deleteDirectoryRecursively(LPCTSTR szPath) { #ifndef NDEBUG assert((_tcslen(szPath) + 4) < MAX_PATH); #endif TCHAR szPath1[MAX_PATH] = { 0 }; _tcscpy(szPath1, szPath); PathAddBackslash(szPath1); PathAppend(szPath1, TEXT("*.*")); WIN32_FIND_DATA FindFileData; HANDLE hFind = INVALID_HANDLE_VALUE; hFind = ::FindFirstFile((LPCTSTR)szPath1, &FindFileData); if (hFind == INVALID_HANDLE_VALUE) { return win32err_to_errno(GetLastError()); } int res = 0; do { if (_tcscmp(FindFileData.cFileName, TEXT(".")) == 0 || _tcscmp(FindFileData.cFileName, TEXT("..")) == 0) { continue; } #ifndef NDEBUG assert((_tcslen(szPath) + 1 + _tcslen(FindFileData.cFileName)) < MAX_PATH); #endif _tcscpy(szPath1, szPath); PathAddBackslash(szPath1); PathAppend(szPath1, FindFileData.cFileName); if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { res = deleteDirectoryRecursively(szPath1); if (res != 0) { break; } } else { if (!DeleteFile(szPath1)) { res = win32err_to_errno(GetLastError()); break; } } } while (::FindNextFile(hFind, &FindFileData)); ::FindClose(hFind); if (res == 0) { if (!RemoveDirectory(szPath)) { res = win32err_to_errno(GetLastError()); } } return res; } #endif static int deleteDirectoryRecursively(const std::string& path) { #ifdef _WIN32 std::string p = path; std::replace(p.begin(), p.end(), ALT_DIR_SEP, DIR_SEP); CW2T pszPath(CA2W(p.c_str(), CP_UTF8)); return deleteDirectoryRecursively(pszPath); #else int res = 0; DIR *d = opendir(path.c_str()); if (NULL == d) { res = errno; return res; } size_t path_len = path.size(); struct dirent *p; res = 0; while (!res && (p = readdir(d))) { char *buf = NULL; size_t len = 0; /* Skip the names "." and ".." as we don't want to recurse on them. */ if (!strcmp(p->d_name, ".") || !strcmp(p->d_name, "..")) continue; len = path_len + strlen(p->d_name) + 2; buf = (char *)malloc(len); if (NULL == buf) { res = errno; break; } struct stat statbuf; strcpy(buf, path.c_str()); strcat(buf, "/"); strcat(buf, p->d_name); if (stat(buf, &statbuf) != 0) { res = errno; free(buf); break; } if (S_ISDIR(statbuf.st_mode)) res = deleteDirectoryRecursively(buf); else { if (unlink(buf) != 0) { res = errno; } } free(buf); if (res != 0) { break; } } closedir(d); if (!res) { if (rmdir(path.c_str()) != 0) { res = errno; } } return res; #endif } // return errno static int moveFile(const std::string& src, const std::string& dest) { int e = 0; #ifdef _WIN32 CW2T szSrc(CA2W(src.c_str(), CP_UTF8)); CW2T szDest(CA2W(dest.c_str(), CP_UTF8)); if (!MoveFile((LPCTSTR)szSrc, (LPCTSTR)szDest)) { e = win32err_to_errno(GetLastError()); } #else if (rename(src.c_str(), dest.c_str()) < 0) { e = errno; } #endif return e; } static int makeDirectory(const std::string& path, mode_t mode) { #ifdef _WIN32 CW2T pszPath(CA2W(path.c_str(), CP_UTF8)); if (PathFileExists(pszPath)) { return 0; } DWORD err = ::SHCreateDirectoryEx(NULL, (LPCTSTR)pszPath, NULL); return (err == ERROR_SUCCESS) ? 0 : win32err_to_errno(err); #else if (path.empty()) return -1; if (mkdir(path.c_str(), mode) == 0 || errno == EEXIST) { return 0; } std::vector copypath; copypath.reserve(path.size() + 1); std::copy(path.begin(), path.end(), std::back_inserter(copypath)); copypath.push_back('\0'); std::vector::iterator itStart = copypath.begin(); std::vector::iterator it; int status = 0; if (*itStart == '/') ++itStart; // Skip root path while (status == 0 && (it = std::find(itStart, copypath.end(), '/')) != copypath.end()) { // Neither root nor double slash in path *it = '\0'; if (mkdir(©path[0], mode) != 0) { status = (errno == EEXIST) ? 0 : errno; } *it = '/'; itStart = it + 1; } if (status == 0) { if (mkdir(©path[0], mode) != 0) { status = (errno == EEXIST) ? 0 : errno; } } return status; #endif } class FileEnumerator { public: class FileInfo { public: friend FileEnumerator; FileInfo() : m_isDir(false), m_isNormalFile(false), m_fileSize(0) { } const std::string& getFileName() const { return m_fileName; } bool isDirectory() const { return m_isDir; } bool isNormalFile() const { return m_isNormalFile; } time_t getModifiedTime() const { return m_modifiedTime; } uint64_t getFileSize() const { return m_fileSize; } private: std::string m_fileName; time_t m_modifiedTime; bool m_isDir; bool m_isNormalFile; uint64_t m_fileSize; }; FileEnumerator(const std::string& path) : m_path(path), #ifdef _WIN32 m_handle(INVALID_HANDLE_VALUE), #else m_handle(NULL), #endif m_first(true) { #ifdef _WIN32 _tcscpy(m_szPath, CW2T(CA2W(path.c_str(), CP_UTF8))); #endif } ~FileEnumerator() { #ifdef _WIN32 if (m_handle != INVALID_HANDLE_VALUE) { ::FindClose(m_handle); } #else if (NULL != m_handle) { closedir(m_handle); } #endif } bool isValid() const { #ifdef _WIN32 return INVALID_HANDLE_VALUE != m_handle; #else return NULL != m_handle; #endif } bool nextFile(FileInfo& file) { #ifdef _WIN32 bool res = false; WIN32_FIND_DATA FindFileData; if (m_first) { TCHAR findPath[MAX_PATH] = { 0 }; _tcscpy(findPath, m_szPath); PathAppend(findPath, TEXT("*.*")); m_first = false; m_handle = ::FindFirstFile(findPath, &FindFileData); if (m_handle == INVALID_HANDLE_VALUE) { return res; } do { if (_tcscmp(FindFileData.cFileName, TEXT(".")) == 0 || _tcscmp(FindFileData.cFileName, TEXT("..")) == 0) { continue; } res = true; break; } while (::FindNextFile(m_handle, &FindFileData)); } else { if (INVALID_HANDLE_VALUE == m_handle) { return res; } while (::FindNextFile(m_handle, &FindFileData)) { if (_tcscmp(FindFileData.cFileName, TEXT(".")) == 0 || _tcscmp(FindFileData.cFileName, TEXT("..")) == 0) { continue; } res = true; break; } } if (res) { file.m_fileName = (LPCSTR)CW2A(CT2W(FindFileData.cFileName), CP_UTF8); file.m_isDir = ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY); file.m_isNormalFile = !file.m_isDir && ((FindFileData.dwFileAttributes & (FILE_ATTRIBUTE_NORMAL | FILE_ATTRIBUTE_ARCHIVE)) != 0); if (file.m_isDir) { TCHAR szPath[MAX_PATH] = { 0 }; _tcscpy(szPath, m_szPath); PathAppend(szPath, FindFileData.cFileName); struct _stati64 st; _tstat64(szPath, &st); file.m_fileSize = st.st_size; file.m_modifiedTime = st.st_mtime; } else { file.m_fileSize = (FindFileData.nFileSizeHigh * (MAXDWORD + 1)) + FindFileData.nFileSizeLow; file.m_modifiedTime = ::FileTimeToTime(FindFileData.ftLastWriteTime); } } return res; #else bool res = false; if (m_first) { m_first = false; m_handle = opendir(m_path.c_str()); if (NULL == m_handle) { return res; } } struct dirent *entry; while ((entry = readdir(m_handle)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } file.m_fileName = entry->d_name; std::string fullPath = combinePath(m_path, file.m_fileName); struct stat st; stat(fullPath.c_str(), &st); file.m_fileSize = st.st_size; file.m_isDir = S_ISDIR(st.st_mode); file.m_isNormalFile = S_ISREG(st.st_mode); file.m_modifiedTime = st.st_mtime; res = true; break; } return res; #endif } private: std::string m_path; #ifdef _WIN32 TCHAR m_szPath[MAX_PATH]; HANDLE m_handle; #else DIR* m_handle; #endif bool m_first; }; } // namespace fs class IDeviceBackupClient { public: struct File { std::string fileId; size_t size; File() : size(0) { } File(const std::string& fid, size_t sz) : fileId(fid), size(sz) { } }; /* struct __string_less { // _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX11 bool operator()(const std::string& __x, const std::string& __y) const {return __x < __y;} bool operator()(const std::pair& __x, const std::string& __y) const {return __x.first < __y;} bool operator()(const File& __x, const std::string& __y) const {return __x.fileId < __y;} bool operator()(const File& __x, const File& __y) const {return __x.fileId < __y.fileId;} }; */ IDeviceBackupClient(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) { } ~IDeviceBackupClient() { unlockAfc(); if (NULL != m_mobilebackup2) { mobilebackup2_client_free(m_mobilebackup2); } if (NULL != m_afc) { afc_client_free(m_afc); } if (m_np) { np_client_free(m_np); } if (NULL != m_client) { lockdownd_client_free(m_client); } if (NULL != m_device) { idevice_free(m_device); } } bool init() { lockdownd_error_t err = LOCKDOWN_E_UNKNOWN_ERROR; idevice_error_t devErr = idevice_new_with_options(&m_device, m_udid.c_str(), (idevice_options)(IDEVICE_LOOKUP_USBMUX | IDEVICE_LOOKUP_NETWORK)); if (devErr == IDEVICE_E_SUCCESS && NULL != m_device) { err = lockdownd_client_new(m_device, &m_client, CLIENT_ID); } return LOCKDOWN_E_SUCCESS == err; } lockdownd_error_t initWithHandShake() { lockdownd_error_t err = LOCKDOWN_E_UNKNOWN_ERROR; idevice_error_t devErr = idevice_new_with_options(&m_device, m_udid.c_str(), (idevice_options)(IDEVICE_LOOKUP_USBMUX | IDEVICE_LOOKUP_NETWORK)); if (devErr == IDEVICE_E_SUCCESS && NULL != m_device) { err = lockdownd_client_new_with_handshake(m_device, &m_client, CLIENT_ID); } return err; } void updateDeviceInfo(DeviceInfo& device, lockdownd_error_t err) { if (LOCKDOWN_E_SUCCESS == err) { device.setLocked(false); device.setTrustPending(false); device.setTrusted(true); } else { if (LOCKDOWN_E_PASSWORD_PROTECTED == err) { device.setLocked(true); } if (LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING == err) { device.setTrustPending(true); } if (LOCKDOWN_E_PAIRING_FAILED == err || LOCKDOWN_E_PAIRING_PROHIBITED_OVER_THIS_CONNECTION) { device.setTrusted(false); } } } void freeClient() { if (NULL != m_client) { lockdownd_client_free(m_client); m_client = NULL; } } inline bool needUnlock(lockdownd_error_t err) const { return (LOCKDOWN_E_PASSWORD_PROTECTED == err); } inline bool needTrust(lockdownd_error_t err) const { return (LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING == err); } bool loadFiles(const std::string& backupPath, const std::string& udid) { m_files.clear(); ITunesDb iTunesDb(combinePath(backupPath, udid), ""); std::unique_ptr enumerator(iTunesDb.buildEnumerator(std::vector(), true)); if (enumerator->isInvalid()) { return false; } #ifndef NDEBUG uint32_t zeroFile = 0; uint32_t nonzeroFile = 0; #endif ITunesFile file; // m_files.reserve(2048); while (enumerator->nextFile(file)) { if (file.domain == "AppDomain-com.tencent.xin" || file.domain == "AppDomainGroup-group.com.tencent.xin") { continue; } if (!file.blobParsed) { ITunesDb::parseFileInfo(&file); } #ifndef NDEBUG if (file.size > 0) { nonzeroFile ++; } else { zeroFile ++; } #endif if (iTunesDb.isMbdb()) { m_files.insert(m_files.end(), std::pair(file.fileId, file.size)); } else { m_files.insert(m_files.end(), std::pair(file.fileId.substr(0, 2) + "/" + file.fileId, file.size)); } } #ifndef NDEBUG printlog("Manifest.db: zero: %u, nonzero: %u\r\n", zeroFile, nonzeroFile); #endif return true; } bool queryFileSize(const std::string& fileId, size_t& fileSize) const { std::map::const_iterator it = m_files.find(fileId); if (it == m_files.cend()) { return false; } fileSize = it->second; return true; } bool queryDeviceName(std::string& name) const { if (NULL != m_client) { char *deviceName = NULL; if ((LOCKDOWN_E_SUCCESS == lockdownd_get_device_name(m_client, &deviceName)) && NULL != deviceName) { name = deviceName; plist_mem_free(deviceName); return true; } } return false; } bool queryAppContainer(const std::string& bundleId, std::string& container) { if (NULL == m_device || NULL == m_client) { return false; } lockdownd_service_descriptor_t service = NULL; lockdownd_error_t lderr = LOCKDOWN_E_SUCCESS; if (((lderr = lockdownd_start_service(m_client, "com.apple.mobile.installation_proxy", &service)) != LOCKDOWN_E_SUCCESS) || NULL == service) { return false; } instproxy_client_t ipc = NULL; instproxy_error_t err = instproxy_client_new(m_device, service, &ipc); if (service) { lockdownd_service_descriptor_free(service); } service = NULL; bool res = false; if (err == INSTPROXY_E_SUCCESS) { char *value = NULL; err = getContainderForBundleIdentifier(ipc, bundleId.c_str(), &value); if (err == INSTPROXY_E_SUCCESS && NULL != value) { container = value; res = true; } if (NULL != value) { free(value); } instproxy_client_free(ipc); } return res;; } void setOverallProgressFromMessage(IDeviceBackup *pThis, plist_t message, char* identifier) { plist_t node = NULL; double progress = 0.0; if (!strcmp(identifier, "DLMessageDownloadFiles")) { node = plist_array_get_item(message, 3); } else if (!strcmp(identifier, "DLMessageUploadFiles")) { node = plist_array_get_item(message, 2); } else if (!strcmp(identifier, "DLMessageMoveFiles") || !strcmp(identifier, "DLMessageMoveItems")) { node = plist_array_get_item(message, 3); } else if (!strcmp(identifier, "DLMessageRemoveFiles") || !strcmp(identifier, "DLMessageRemoveItems")) { node = plist_array_get_item(message, 3); } if (node != NULL) { plist_get_real_val(node, &progress); pThis->setOverallProgress(progress); } } void doPostNotification(idevice_t device, const char *notification) { lockdownd_service_descriptor_t service = NULL; np_client_t np; lockdownd_client_t lockdown = NULL; if (lockdownd_client_new_with_handshake(device, &lockdown, CLIENT_ID) != LOCKDOWN_E_SUCCESS) { return; } lockdownd_error_t ldret = lockdownd_start_service(lockdown, NP_SERVICE_NAME, &service); if (service && service->port) { np_client_new(device, service, &np); if (np) { np_post_notification(np, notification); np_client_free(np); } } else { printlog("ERROR: Could not start service %s: %s\n", NP_SERVICE_NAME, lockdownd_strerror(ldret)); } if (service) { lockdownd_service_descriptor_free(service); service = NULL; } lockdownd_client_free(lockdown); } static void notifyCallback(const char *notification, void *userdata) { if (strlen(notification) == 0) { return; } if (!strcmp(notification, NP_SYNC_CANCEL_REQUEST)) { PRINT_VERBOSE(1, "User has cancelled the backup process on the device.\n"); IDeviceBackup *pThis = reinterpret_cast(userdata); pThis->cancel(); } else if (!strcmp(notification, NP_BACKUP_DOMAIN_CHANGED)) { /// backup_domain_changed = 1; } else { PRINT_VERBOSE(1, "Unhandled notification '%s' (TODO: implement)\n", notification); } } static int checkSnapshotState(const std::string& path, const std::string& udid, const char *matches) { int ret = 0; plist_t status_plist = NULL; std::string file_path = combinePath(path, udid, "Status.plist"); readPlistFile(&status_plist, file_path); if (!status_plist) { printlog("Could not read Status.plist!\n"); return ret; } plist_t node = plist_dict_get_item(status_plist, "SnapshotState"); if (node && PLIST_IS_STRING(node)) { const char* sval = plist_get_string_ptr(node, NULL); if (sval) { ret = (strcmp(sval, matches) == 0) ? 1 : 0; } } else { printlog("%s: ERROR could not get SnapshotState key from Status.plist!\n", __func__); } plist_free(status_plist); return ret; } instproxy_error_t getContainderForBundleIdentifier(instproxy_client_t client, const char* appid, char** container) { if (!client || !appid) return INSTPROXY_E_INVALID_ARG; plist_t apps = NULL; // create client options for any application types plist_t client_opts = instproxy_client_options_new(); instproxy_client_options_add(client_opts, "ApplicationType", "Any", NULL); // only return attributes we need instproxy_client_options_set_return_attributes(client_opts, "CFBundleIdentifier", "CFBundleExecutable", "Path", "Container", "EnvironmentVariables", NULL); // only query for specific appid const char* appids[] = {appid, NULL}; // query device for list of apps instproxy_error_t ierr = instproxy_lookup(client, appids, client_opts, &apps); instproxy_client_options_free(client_opts); if (ierr != INSTPROXY_E_SUCCESS) { return ierr; } plist_t app_found = plist_access_path(apps, 1, appid); if (!app_found) { if (apps) plist_free(apps); *container = NULL; return INSTPROXY_E_OP_FAILED; } const char* container_str = NULL; uint64_t strLength = 0; plist_t container_p = plist_dict_get_item(app_found, "Container"); if (container_p) { container_str = plist_get_string_ptr(container_p, &strLength); } if (!container_str) { plist_free(apps); return INSTPROXY_E_OP_FAILED; } char* ret = (char*)malloc(strLength + 1); strcpy(ret, container_str); *container = ret; plist_free(apps); return INSTPROXY_E_SUCCESS; } void handleListDirectory(plist_t message, const char *backupDir) { if (!message || (!PLIST_IS_ARRAY(message)) || plist_array_get_size(message) < 2 || !backupDir) return; plist_t node = plist_array_get_item(message, 1); char *str = NULL; if (plist_get_node_type(node) == PLIST_STRING) { plist_get_string_val(node, &str); } if (!str) { printlog("ERROR: Malformed DLContentsOfDirectory message\n"); // TODO error handling return; } std::string relativePath = str; if (relativePath.size() > (m_udid.size() + 1) && startsWith(relativePath, m_udid)) { relativePath = relativePath.substr(m_udid.size() + 1); } else { relativePath.clear(); } std::string path = combinePath(backupDir, str); plist_mem_free(str); plist_t dirlist = plist_new_dict(); fs::FileEnumerator fileEnumerator(path); fs::FileEnumerator::FileInfo fi; while (fileEnumerator.nextFile(fi)) { plist_t fdict = plist_new_dict(); const char *ftype = "DLFileTypeUnknown"; if (fi.isDirectory()) { ftype = "DLFileTypeDirectory"; } else if (fi.isNormalFile()) { ftype = "DLFileTypeRegular"; } size_t fileSize = fi.getFileSize(); if (!fi.isDirectory() && (fileSize == 0) && fi.isNormalFile()) { bool found = queryFileSize(relativePath + "/" + fi.getFileName(), fileSize); #ifndef NDEBUG if (found && fileSize > 0) { static int zeroCnt = 0; zeroCnt ++; // printlog("DBG::Found zero file: %u\r\n", zeroCnt); } #endif } plist_dict_set_item(fdict, "DLFileType", plist_new_string(ftype)); plist_dict_set_item(fdict, "DLFileSize", plist_new_uint(fileSize)); plist_dict_set_item(fdict, "DLFileModificationDate", plist_new_date((uint32_t)((uint64_t)fi.getModifiedTime() - MAC_EPOCH), 0)); plist_dict_set_item(dirlist, fi.getFileName().c_str(), fdict); } /* TODO error handling */ mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, 0, NULL, dirlist); plist_free(dirlist); if (err != MOBILEBACKUP2_E_SUCCESS) { printlog("Could not send status response, error %d\n", err); } } void handleMakeDirectory(plist_t message, const char *backup_dir) { if (!message || (!PLIST_IS_ARRAY(message)) || plist_array_get_size(message) < 2 || !backup_dir) return; plist_t dir = plist_array_get_item(message, 1); char *str = NULL; int errcode = 0; char *errdesc = NULL; plist_get_string_val(dir, &str); std::string newpath = combinePath(backup_dir, str); plist_mem_free(str); if ((errcode = fs::makeDirectory(newpath, 0755)) != 0) { errdesc = strerror(errcode); if (errno != EEXIST) { printlog("mkdir: %s (%d)\n", errdesc, errcode); } errcode = fs::convertErrnoToDeviceError(errcode); } mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, NULL); if (err != MOBILEBACKUP2_E_SUCCESS) { printlog("Could not send status response, error %d\n", err); } } int receiveFilename(IDeviceBackup *pThis, char** filename) { uint32_t nlen = 0; uint32_t rlen = 0; do { nlen = 0; rlen = 0; mobilebackup2_receive_raw(m_mobilebackup2, (char*)&nlen, 4, &rlen); nlen = be32toh(nlen); if ((nlen == 0) && (rlen == 4)) { // a zero length means no more files to receive return 0; } else if(rlen == 0) { // device needs more time, waiting... continue; } else if (nlen > 4096) { // filename length is too large printlog("ERROR: %s: too large filename length (%d)!\n", __func__, nlen); return 0; } if (*filename != NULL) { free(*filename); *filename = NULL; } *filename = (char*)malloc(nlen+1); rlen = 0; mobilebackup2_receive_raw(m_mobilebackup2, *filename, nlen, &rlen); if (rlen != nlen) { printlog("ERROR: %s: could not read filename\n", __func__); return 0; } char* p = *filename; p[rlen] = 0; break; } while(1 && !pThis->isCanclled()); return nlen; } int handleSendFile(const char *backup_dir, const char *path, plist_t *errplist) { uint32_t nlen = 0; uint32_t pathlen = (uint32_t)strlen(path); uint32_t bytes = 0; #ifdef _WIN32 std::string localfile = combinePath(backup_dir, normalizePath(path)); CA2W szLocalFile(localfile.c_str(), CP_UTF8); struct _stati64 fst; #else std::string localfile = combinePath(backup_dir, path); struct stat fst; #endif char buf[32768]; FILE *f = NULL; uint32_t slen = 0; int errcode = -1; int result = -1; uint32_t length; #ifdef _WIN32 uint64_t total; uint64_t sent; #else off_t total; off_t sent; #endif mobilebackup2_error_t err; /* send path length */ nlen = htobe32(pathlen); err = mobilebackup2_send_raw(m_mobilebackup2, (const char*)&nlen, sizeof(nlen), &bytes); if (err != MOBILEBACKUP2_E_SUCCESS) { goto leave_proto_err; } if (bytes != (uint32_t)sizeof(nlen)) { err = MOBILEBACKUP2_E_MUX_ERROR; goto leave_proto_err; } /* send path */ err = mobilebackup2_send_raw(m_mobilebackup2, path, pathlen, &bytes); if (err != MOBILEBACKUP2_E_SUCCESS) { goto leave_proto_err; } if (bytes != pathlen) { err = MOBILEBACKUP2_E_MUX_ERROR; goto leave_proto_err; } #ifdef _WIN32 if (_wstati64((LPCWSTR)szLocalFile, &fst) < 0) #else if (stat(localfile.c_str(), &fst) < 0) #endif { if (errno != ENOENT) printlog("%s: stat failed on '%s': %d\n", __func__, localfile.c_str(), errno); errcode = errno; goto leave; } total = fst.st_size; PRINT_VERBOSE(1, "Sending '%s' (%s)\n", path, formatDiskSize(total).c_str()); if (total == 0) { errcode = 0; goto leave; } #ifdef _WIN32 f = _wfopen((LPCWSTR)szLocalFile, L"rb"); #else f = fopen(localfile.c_str(), "rb"); #endif if (!f) { printlog("%s: Error opening local file '%s': %d\n", __func__, localfile.c_str(), errno); errcode = errno; goto leave; } sent = 0; do { length = ((total-sent) < (long long)sizeof(buf)) ? (uint32_t)total-sent : (uint32_t)sizeof(buf); /* send data size (file size + 1) */ nlen = htobe32(length+1); memcpy(buf, &nlen, sizeof(nlen)); buf[4] = CODE_FILE_DATA; err = mobilebackup2_send_raw(m_mobilebackup2, (const char*)buf, 5, &bytes); if (err != MOBILEBACKUP2_E_SUCCESS) { goto leave_proto_err; } if (bytes != 5) { goto leave_proto_err; } /* send file contents */ size_t r = fread(buf, 1, sizeof(buf), f); if (r <= 0) { printlog("%s: read error\n", __func__); errcode = errno; goto leave; } err = mobilebackup2_send_raw(m_mobilebackup2, buf, r, &bytes); if (err != MOBILEBACKUP2_E_SUCCESS) { goto leave_proto_err; } if (bytes != (uint32_t)r) { printlog("Error: sent only %d of %d bytes\n", bytes, (int)r); goto leave_proto_err; } sent += r; } while (sent < total); fclose(f); f = NULL; errcode = 0; leave: if (errcode == 0) { result = 0; nlen = 1; nlen = htobe32(nlen); memcpy(buf, &nlen, 4); buf[4] = CODE_SUCCESS; mobilebackup2_send_raw(m_mobilebackup2, buf, 5, &bytes); } else { if (!*errplist) { *errplist = plist_new_dict(); } char *errdesc = strerror(errcode); mb2_multi_status_add_file_error(*errplist, path, fs::convertErrnoToDeviceError(errcode), errdesc); length = strlen(errdesc); nlen = htobe32(length+1); memcpy(buf, &nlen, 4); buf[4] = CODE_ERROR_LOCAL; slen = 5; memcpy(buf+slen, errdesc, length); slen += length; err = mobilebackup2_send_raw(m_mobilebackup2, (const char*)buf, slen, &bytes); if (err != MOBILEBACKUP2_E_SUCCESS) { printlog("could not send message\n"); } if (bytes != slen) { printlog("could only send %d from %d\n", bytes, slen); } } leave_proto_err: if (f) fclose(f); return result; } void handleSendFiles(plist_t message, const char *backup_dir) { uint32_t cnt; uint32_t i = 0; uint32_t sent; plist_t errplist = NULL; if (!message || (plist_get_node_type(message) != PLIST_ARRAY) || (plist_array_get_size(message) < 2) || !backup_dir) return; plist_t files = plist_array_get_item(message, 1); cnt = plist_array_get_size(files); for (i = 0; i < cnt; i++) { plist_t val = plist_array_get_item(files, i); if (plist_get_node_type(val) != PLIST_STRING) { continue; } char *str = NULL; plist_get_string_val(val, &str); if (!str) continue; if (handleSendFile(backup_dir, str, &errplist) < 0) { plist_mem_free(str); printlog("Error when sending file '%s' to device\n", str); // TODO: perhaps we can continue, we've got a multi status response?! break; } plist_mem_free(str); } /* send terminating 0 dword */ uint32_t zero = 0; mobilebackup2_send_raw(m_mobilebackup2, (char*)&zero, 4, &sent); if (!errplist) { plist_t emptydict = plist_new_dict(); mobilebackup2_send_status_response(m_mobilebackup2, 0, NULL, emptydict); plist_free(emptydict); } else { mobilebackup2_send_status_response(m_mobilebackup2, -13, "Multi status", errplist); plist_free(errplist); } } int handleReceiveFiles(IDeviceBackup *pThis, plist_t message, const std::string& backupDir) { uint64_t backup_real_size = 0; uint64_t backup_total_size = 0; uint32_t blocksize = 0; uint32_t bdone; uint32_t rlen; uint32_t nlen = 0; uint32_t r; char buf[32768]; char *fname = NULL; char *dname = NULL; std::string bname; char code = 0; char last_code = 0; plist_t node = NULL; FILE *f = NULL; unsigned int file_count = 0; int errcode = 0; char *errdesc = NULL; if (!message || (!PLIST_IS_ARRAY(message)) || plist_array_get_size(message) < 4 || backupDir.empty()) return 0; node = plist_array_get_item(message, 3); if (plist_get_node_type(node) == PLIST_UINT) { plist_get_uint_val(node, &backup_total_size); } if (backup_total_size > 0) { PRINT_VERBOSE(1, "Receiving files\n"); } do { if (pThis->isCanclled()) break; nlen = receiveFilename(pThis, &dname); if (nlen == 0) { break; } nlen = receiveFilename(pThis, &fname); if (!nlen) { break; } std::string destFileName = (fname != NULL) ? fname : ""; bname = combinePath(backupDir, fname); #ifndef NDEBUG static uint32_t filtered_cnt = 0; #endif bool filtered = false; if (backup_total_size > 0) { filtered = pThis->filter(dname, fname); #ifndef NDEBUG if (filtered) filtered_cnt++; #endif } if (fname != NULL) { free(fname); fname = NULL; } #ifndef NDEBUG if (!filtered) { // printlog("DBG::dname=%s\r\n", dname); // printlog("DBG::Write File: %s\r\n", destFileName.c_str()); } // printlog("DBG::filtered_cnt = %u\r\n", filtered_cnt); #endif r = 0; nlen = 0; mobilebackup2_receive_raw(m_mobilebackup2, (char*)&nlen, 4, &r); if (r != 4) { printlog("ERROR: %s: could not receive code length!\n", __func__); break; } nlen = be32toh(nlen); last_code = code; code = 0; mobilebackup2_receive_raw(m_mobilebackup2, &code, 1, &r); if (r != 1) { printlog("ERROR: %s: could not receive code!\n", __func__); break; } /* TODO remove this */ if ((code != CODE_SUCCESS) && (code != CODE_FILE_DATA) && (code != CODE_ERROR_REMOTE)) { PRINT_VERBOSE(1, "Found new flag %02x\n", code); } fs::deleteFile(bname); #ifdef _WIN32 f = _wfopen((LPCWSTR)CA2W(bname.c_str(), CP_UTF8), L"wb"); #else f = fopen(bname.c_str(), "wb"); #endif while (f && (code == CODE_FILE_DATA)) { blocksize = nlen-1; bdone = 0; rlen = 0; while (bdone < blocksize) { if ((blocksize - bdone) < sizeof(buf)) { rlen = blocksize - bdone; } else { rlen = sizeof(buf); } mobilebackup2_receive_raw(m_mobilebackup2, buf, rlen, &r); if ((int)r <= 0) { break; } if (!filtered) { fwrite(buf, 1, r, f); } bdone += r; } if (bdone == blocksize) { backup_real_size += blocksize; } /* if (backup_total_size > 0) { print_progress(backup_real_size, backup_total_size); } */ if (pThis->isCanclled()) break; nlen = 0; mobilebackup2_receive_raw(m_mobilebackup2, (char*)&nlen, 4, &r); nlen = be32toh(nlen); if (nlen > 0) { last_code = code; mobilebackup2_receive_raw(m_mobilebackup2, &code, 1, &r); } else { break; } } if (f) { fclose(f); file_count++; if (filtered) { std::map::iterator it = m_files.find(destFileName); if (it == m_files.end()) { m_files.insert(it, std::pair(destFileName, blocksize)); } else { it->second = blocksize; } } } else { errcode = fs::convertErrnoToDeviceError(errno); errdesc = strerror(errno); printlog("Error opening '%s' for writing: %s\n", bname.c_str(), errdesc); break; } if (nlen == 0) { break; } /* check if an error message was received */ if (code == CODE_ERROR_REMOTE) { /* error message */ char *msg = (char*)malloc(nlen); mobilebackup2_receive_raw(m_mobilebackup2, msg, nlen-1, &r); msg[r] = 0; /* If sent using CODE_FILE_DATA, end marker will be CODE_ERROR_REMOTE which is not an error! */ if (last_code != CODE_FILE_DATA) { printlog("\nReceived an error message from device: %s\n", msg); } free(msg); } } while (1); if (fname != NULL) free(fname); /* if there are leftovers to read, finish up cleanly */ if ((int)nlen-1 > 0) { PRINT_VERBOSE(1, "\nDiscarding current data hunk.\n"); fname = (char*)malloc(nlen-1); mobilebackup2_receive_raw(m_mobilebackup2, fname, nlen-1, &r); free(fname); fs::deleteFile(bname); } /* clean up */ if (dname != NULL) free(dname); plist_t empty_plist = plist_new_dict(); mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, empty_plist); plist_free(empty_plist); return file_count; } void handleMoveFiles(IDeviceBackup *pThis, plist_t message, char *dlmsg, const std::string& outputPath) { /* perform a series of rename operations */ setOverallProgressFromMessage(pThis, message, dlmsg); plist_t moves = plist_array_get_item(message, 1); uint32_t cnt = plist_dict_get_size(moves); PRINT_VERBOSE(1, "Moving %d file%s\n", cnt, (cnt == 1) ? "" : "s"); plist_dict_iter iter = NULL; plist_dict_new_iter(moves, &iter); int errcode = 0; const char *errdesc = NULL; if (iter) { char *key = NULL; plist_t val = NULL; do { plist_dict_next_item(moves, iter, &key, &val); if (key && PLIST_IS_STRING(val)) { const char *str = plist_get_string_ptr(val, NULL); if (str) { std::map::iterator it = m_files.find(key); if (it != m_files.end()) { size_t fileSize = it->second; m_files.erase(it); m_files[str] = fileSize; } std::string newpath = combinePath(outputPath, str); std::string oldpath = combinePath(outputPath, key); if (existsDirectory(newpath)) fs::deleteDirectoryRecursively(newpath); else fs::deleteFile(newpath); if ((errcode = fs::moveFile(oldpath, newpath)) != 0) { printlog("Renameing '%s' to '%s' failed: %s (%d)\n", oldpath.c_str(), newpath.c_str(), strerror(errno), errno); errdesc = strerror(errcode); errcode = fs::convertErrnoToDeviceError(errcode); break; } } plist_mem_free(key); key = NULL; } } while (val); plist_mem_free(iter); } else { errcode = -1; errdesc = "Could not create dict iterator"; printlog("Could not create dict iterator\n"); } plist_t empty_dict = plist_new_dict(); mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, empty_dict); plist_free(empty_dict); if (err != MOBILEBACKUP2_E_SUCCESS) { printlog("Could not send status response, error %d\n", err); } } void handleRemoveFiles(IDeviceBackup *pThis, plist_t message, char *dlmsg, const std::string& outputPath) { setOverallProgressFromMessage(pThis, message, dlmsg); plist_t removes = plist_array_get_item(message, 1); uint32_t cnt = plist_array_get_size(removes); PRINT_VERBOSE(1, "Removing %d file%s\n", cnt, (cnt == 1) ? "" : "s"); uint32_t ii = 0; int errcode = 0; const char *errdesc = NULL; for (ii = 0; ii < cnt; ii++) { plist_t val = plist_array_get_item(removes, ii); if (plist_get_node_type(val) == PLIST_STRING) { char *str = NULL; plist_get_string_val(val, &str); if (str) { const char *checkfile = strchr(str, '/'); int suppress_warning = 0; if (checkfile) { if (strcmp(checkfile+1, "Manifest.mbdx") == 0) { suppress_warning = 1; } } std::string newpath = combinePath(outputPath, str); plist_mem_free(str); int res = 0; if (existsDirectory(newpath)) { res = fs::deleteDirectoryRecursively(newpath); } else { res = fs::deleteFile(newpath); } if (res != 0 && res != ENOENT) { if (!suppress_warning) printlog("Could not remove '%s'\n", newpath.c_str()); errcode = fs::convertErrnoToDeviceError(res); errdesc = strerror(res); } else { printlog("Removed: %s\n", newpath.c_str()); } } } } plist_t empty_dict = plist_new_dict(); mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, empty_dict); plist_free(empty_dict); if (err != MOBILEBACKUP2_E_SUCCESS) { printlog("Could not send status response, error %d\n", err); } } void handleCopyItem(IDeviceBackup *pThis, plist_t message, char *dlmsg, const std::string& outputPath) { plist_t srcpath = plist_array_get_item(message, 1); plist_t dstpath = plist_array_get_item(message, 2); int errcode = 0; const char *errdesc = NULL; if ((plist_get_node_type(srcpath) == PLIST_STRING) && (plist_get_node_type(dstpath) == PLIST_STRING)) { char *src = NULL; char *dst = NULL; plist_get_string_val(srcpath, &src); plist_get_string_val(dstpath, &dst); if (src && dst) { std::string oldpath = combinePath(outputPath, src); std::string newpath = combinePath(outputPath, dst); PRINT_VERBOSE(1, "Copying '%s' to '%s'\n", src, dst); /* check that src exists */ if (existsDirectory(oldpath)) { copyDirectory(oldpath, newpath); } else if (existsFile(oldpath)) { copyFile(oldpath, newpath); } } plist_mem_free(src); plist_mem_free(dst); } plist_t empty_dict = plist_new_dict(); mobilebackup2_error_t err = mobilebackup2_send_status_response(m_mobilebackup2, errcode, errdesc, empty_dict); plist_free(empty_dict); if (err != MOBILEBACKUP2_E_SUCCESS) { printlog("Could not send status response, error %d\n", err); } } void handleProcessMessage(plist_t message, int& operation_ok, int& result_code, const std::string outputDir) { #ifndef NDEBUG writePlistFile(message, combinePath(outputDir, "dbg", "error.plist"), PLIST_FORMAT_XML); #endif plist_t node_tmp = plist_array_get_item(message, 1); if (!PLIST_IS_DICT(node_tmp)) { printlog("Unknown message received!\n"); } plist_t nn; int error_code = -1; nn = plist_dict_get_item(node_tmp, "ErrorCode"); if (nn && PLIST_IS_UINT(nn)) { uint64_t ec = 0; plist_get_uint_val(nn, &ec); error_code = (uint32_t)ec; if (error_code == 0) { operation_ok = 1; result_code = 0; } else { result_code = -error_code; } } nn = plist_dict_get_item(node_tmp, "ErrorDescription"); const char *str = NULL; if (nn && PLIST_IS_STRING(nn)) { str = plist_get_string_ptr(nn, NULL); } if (error_code != 0) { if (str) { printlog("ErrorCode %d: %s\n", error_code, str); } else { printlog("ErrorCode %d: (Unknown)\n", error_code); } } nn = plist_dict_get_item(node_tmp, "Content"); if (nn && PLIST_IS_STRING(nn)) { str = plist_get_string_ptr(nn, NULL); PRINT_VERBOSE(1, "Content:\n"); printlog("%s", str); } } void mb2_multi_status_add_file_error(plist_t status_dict, const char *path, int error_code, const char *error_message) { if (!status_dict) return; plist_t filedict = plist_new_dict(); plist_dict_set_item(filedict, "DLFileErrorString", plist_new_string(error_message)); plist_dict_set_item(filedict, "DLFileErrorCode", plist_new_uint(error_code)); plist_dict_set_item(status_dict, path, filedict); } void mobilebackup_afc_get_file_contents(afc_client_t afc, const char *filename, char **data, uint64_t *size) { if (!afc || !data || !size) { return; } char **fileinfo = NULL; uint32_t fsize = 0; afc_get_file_info(afc, filename, &fileinfo); if (!fileinfo) { return; } int i; for (i = 0; fileinfo[i]; i+=2) { if (!strcmp(fileinfo[i], "st_size")) { fsize = atol(fileinfo[i+1]); break; } } afc_dictionary_free(fileinfo); if (fsize == 0) { return; } uint64_t f = 0; afc_file_open(afc, filename, AFC_FOPEN_RDONLY, &f); if (!f) { return; } char *buf = (char*)malloc((uint32_t)fsize); uint32_t done = 0; while (done < fsize) { uint32_t bread = 0; afc_file_read(afc, f, buf+done, 65536, &bread); if (bread > 0) { done += bread; } else { break; } } if (done == fsize) { *size = fsize; *data = buf; } else { free(buf); } afc_file_close(afc, f); } plist_t newInfoPlist(const char* udid, idevice_t device, afc_client_t afc) { /* gather data from lockdown */ plist_t value_node = NULL; plist_t root_node = NULL; plist_t itunes_settings = NULL; plist_t min_itunes_version = NULL; std::string udid_uppercase; lockdownd_client_t lockdown = NULL; if (lockdownd_client_new_with_handshake(device, &lockdown, CLIENT_ID) != LOCKDOWN_E_SUCCESS) { return NULL; } plist_t ret = plist_new_dict(); /* get basic device information in one go */ lockdownd_get_value(lockdown, NULL, NULL, &root_node); /* get iTunes settings */ lockdownd_get_value(lockdown, "com.apple.iTunes", NULL, &itunes_settings); /* get minimum iTunes version */ lockdownd_get_value(lockdown, "com.apple.mobile.iTunes", "MinITunesVersion", &min_itunes_version); lockdownd_client_free(lockdown); /* get a list of installed user applications */ plist_t app_dict = plist_new_dict(); plist_t installed_apps = plist_new_array(); instproxy_client_t ip = NULL; if (instproxy_client_start_service(device, &ip, CLIENT_ID) == INSTPROXY_E_SUCCESS) { plist_t client_opts = instproxy_client_options_new(); instproxy_client_options_add(client_opts, "ApplicationType", "User", NULL); instproxy_client_options_set_return_attributes(client_opts, "CFBundleIdentifier", "ApplicationSINF", "iTunesMetadata", NULL); plist_t apps = NULL; instproxy_browse(ip, client_opts, &apps); sbservices_client_t sbs = NULL; if (sbservices_client_start_service(device, &sbs, CLIENT_ID) != SBSERVICES_E_SUCCESS) { printlog("Couldn't establish sbservices connection. Continuing anyway.\n"); } if (apps && PLIST_IS_ARRAY(apps)) { uint32_t app_count = plist_array_get_size(apps); uint32_t i; for (i = 0; i < app_count; i++) { plist_t app_entry = plist_array_get_item(apps, i); plist_t bundle_id = plist_dict_get_item(app_entry, "CFBundleIdentifier"); if (bundle_id) { char *bundle_id_str = NULL; plist_array_append_item(installed_apps, plist_copy(bundle_id)); plist_get_string_val(bundle_id, &bundle_id_str); plist_t sinf = plist_dict_get_item(app_entry, "ApplicationSINF"); plist_t meta = plist_dict_get_item(app_entry, "iTunesMetadata"); if (sinf && meta) { plist_t adict = plist_new_dict(); plist_dict_set_item(adict, "ApplicationSINF", plist_copy(sinf)); if (sbs) { char *pngdata = NULL; uint64_t pngsize = 0; sbservices_get_icon_pngdata(sbs, bundle_id_str, &pngdata, &pngsize); if (pngdata) { plist_dict_set_item(adict, "PlaceholderIcon", plist_new_data(pngdata, pngsize)); plist_mem_free(pngdata); } } plist_dict_set_item(adict, "iTunesMetadata", plist_copy(meta)); plist_dict_set_item(app_dict, bundle_id_str, adict); } plist_mem_free(bundle_id_str); } } } plist_free(apps); if (sbs) { sbservices_client_free(sbs); } instproxy_client_options_free(client_opts); instproxy_client_free(ip); } /* Applications */ plist_dict_set_item(ret, "Applications", app_dict); /* set fields we understand */ value_node = plist_dict_get_item(root_node, "BuildVersion"); plist_dict_set_item(ret, "Build Version", plist_copy(value_node)); value_node = plist_dict_get_item(root_node, "DeviceName"); plist_dict_set_item(ret, "Device Name", plist_copy(value_node)); plist_dict_set_item(ret, "Display Name", plist_copy(value_node)); std::string uuid = makeUuid(); plist_dict_set_item(ret, "GUID", plist_new_string(uuid.c_str())); value_node = plist_dict_get_item(root_node, "IntegratedCircuitCardIdentity"); if (value_node) plist_dict_set_item(ret, "ICCID", plist_copy(value_node)); value_node = plist_dict_get_item(root_node, "InternationalMobileEquipmentIdentity"); if (value_node) plist_dict_set_item(ret, "IMEI", plist_copy(value_node)); /* Installed Applications */ plist_dict_set_item(ret, "Installed Applications", installed_apps); plist_dict_set_item(ret, "Last Backup Date", plist_new_date(time(NULL) - MAC_EPOCH, 0)); value_node = plist_dict_get_item(root_node, "MobileEquipmentIdentifier"); if (value_node) plist_dict_set_item(ret, "MEID", plist_copy(value_node)); value_node = plist_dict_get_item(root_node, "PhoneNumber"); if (value_node && (plist_get_node_type(value_node) == PLIST_STRING)) { plist_dict_set_item(ret, "Phone Number", plist_copy(value_node)); } /* FIXME Product Name */ value_node = plist_dict_get_item(root_node, "ProductType"); plist_dict_set_item(ret, "Product Type", plist_copy(value_node)); value_node = plist_dict_get_item(root_node, "ProductVersion"); plist_dict_set_item(ret, "Product Version", plist_copy(value_node)); value_node = plist_dict_get_item(root_node, "SerialNumber"); plist_dict_set_item(ret, "Serial Number", plist_copy(value_node)); /* FIXME Sync Settings? */ value_node = plist_dict_get_item(root_node, "UniqueDeviceID"); plist_dict_set_item(ret, "Target Identifier", plist_new_string(udid)); plist_dict_set_item(ret, "Target Type", plist_new_string("Device")); /* uppercase */ udid_uppercase = toUpper((char*)udid); plist_dict_set_item(ret, "Unique Identifier", plist_new_string(udid_uppercase.c_str())); char *data_buf = NULL; uint64_t data_size = 0; mobilebackup_afc_get_file_contents(afc, "/Books/iBooksData2.plist", &data_buf, &data_size); if (data_buf) { plist_dict_set_item(ret, "iBooks Data 2", plist_new_data(data_buf, data_size)); free(data_buf); } plist_t files = plist_new_dict(); const char *itunesfiles[] = { "ApertureAlbumPrefs", "IC-Info.sidb", "IC-Info.sidv", "PhotosFolderAlbums", "PhotosFolderName", "PhotosFolderPrefs", "VoiceMemos.plist", "iPhotoAlbumPrefs", "iTunesApplicationIDs", "iTunesPrefs", "iTunesPrefs.plist", NULL }; int i = 0; for (i = 0; itunesfiles[i]; i++) { data_buf = NULL; data_size = 0; char *fname = (char*)malloc(strlen("/iTunes_Control/iTunes/") + strlen(itunesfiles[i]) + 1); strcpy(fname, "/iTunes_Control/iTunes/"); strcat(fname, itunesfiles[i]); mobilebackup_afc_get_file_contents(afc, fname, &data_buf, &data_size); free(fname); if (data_buf) { plist_dict_set_item(files, itunesfiles[i], plist_new_data(data_buf, data_size)); free(data_buf); } } plist_dict_set_item(ret, "iTunes Files", files); plist_dict_set_item(ret, "iTunes Settings", itunes_settings ? plist_copy(itunes_settings) : plist_new_dict()); /* since we usually don't have iTunes, let's get the minimum required iTunes version from the device */ if (min_itunes_version) { plist_dict_set_item(ret, "iTunes Version", plist_copy(min_itunes_version)); } else { plist_dict_set_item(ret, "iTunes Version", plist_new_string("10.0.1")); } plist_free(itunes_settings); plist_free(min_itunes_version); plist_free(root_node); return ret; } bool willEncrypt() { uint8_t willEncrypt = 0; plist_t node = NULL; lockdownd_get_value(m_client, "com.apple.mobile.backup", "WillEncrypt", &node); if (node) { if (PLIST_IS_BOOLEAN(node)) { plist_get_bool_val(node, &willEncrypt); } plist_free(node); node = NULL; } return willEncrypt != 0; } int getProductVersion() { int deviceVersion = 0; plist_t node = NULL; lockdownd_get_value(m_client, NULL, "ProductVersion", &node); if (node) { const char *productVersion = NULL; if (PLIST_IS_STRING(node)) { productVersion = plist_get_string_ptr(node, NULL); } if (productVersion) { int vers[3] = { 0, 0, 0 }; if (sscanf(productVersion, "%d.%d.%d", &vers[0], &vers[1], &vers[2]) >= 2) { deviceVersion = DEVICE_VERSION(vers[0], vers[1], vers[2]); } } plist_free(node); node = NULL; } return deviceVersion; } inline mobilebackup2_error_t sendHelloMessage(double& remote_version) { /* send Hello message */ double local_versions[2] = {2.0, 2.1}; return mobilebackup2_version_exchange(m_mobilebackup2, local_versions, 2, &remote_version); } bool backup(IDeviceBackup *pThis, const std::string& outputPath) { /* start notification_proxy */ char* sourceUdid = NULL; mobilebackup2_error_t err = MOBILEBACKUP2_E_SUCCESS; int isFullBackup = 0; // uint64_t lockfile = 0; int result_code = -1; lockdownd_service_descriptor_t service = NULL; lockdownd_error_t ldret = lockdownd_start_service(m_client, NP_SERVICE_NAME, &service); if ((ldret == LOCKDOWN_E_SUCCESS) && service && service->port) { np_client_new(m_device, service, &m_np); np_set_notify_callback(m_np, notifyCallback, (void *)pThis); const char *noties[5] = { NP_SYNC_CANCEL_REQUEST, NP_SYNC_SUSPEND_REQUEST, NP_SYNC_RESUME_REQUEST, NP_BACKUP_DOMAIN_CHANGED, NULL }; np_observe_notifications(m_np, noties); } else { printlog("ERROR: Could not start service %s: %s\n", NP_SERVICE_NAME, lockdownd_strerror(ldret)); return false; } freeService(service); /* start AFC, we need this for the lock file */ ldret = lockdownd_start_service(m_client, AFC_SERVICE_NAME, &service); if ((ldret != LOCKDOWN_E_SUCCESS) || NULL == service || 0 == service->port) { freeService(service); printlog("ERROR: Could not start service %s: %s\n", AFC_SERVICE_NAME, lockdownd_strerror(ldret)); return false; } afc_client_new(m_device, service, &m_afc); freeService(service); /* start mobilebackup service and retrieve port */ ldret = lockdownd_start_service_with_escrow_bag(m_client, MOBILEBACKUP2_SERVICE_NAME, &service); freeClient(); if ((ldret != LOCKDOWN_E_SUCCESS) || NULL == service || 0 == service->port) { freeService(service); return false; } PRINT_VERBOSE(1, "Started \"%s\" service on port %d.\n", MOBILEBACKUP2_SERVICE_NAME, service->port); mobilebackup2_client_new(m_device, service, &m_mobilebackup2); freeService(service); double remote_version = 0.0; err = sendHelloMessage(remote_version); if (err != MOBILEBACKUP2_E_SUCCESS) { printlog("Could not perform backup protocol version exchange, error code %d\n", err); return false; } PRINT_VERBOSE(1, "Negotiated Protocol Version %.1f\n", remote_version); /* check abort conditions */ /* if (pThis->isCanclled()) { PRINT_VERBOSE(1, "Aborting as requested by user...\n"); skipBackup = true; goto checkpoint; } */ /* verify existing Info.plist */ std::string info_path; /* backup directory must contain an Info.plist */ info_path = combinePath(outputPath, m_udid, "Info.plist"); plist_t infoPlist = NULL; if (!info_path.empty() && existsFile(info_path)) { /// PRINT_VERBOSE(1, "Reading Info.plist from backup.\n"); readPlistFile(&infoPlist, info_path); if (!infoPlist) { printlog("Could not read Info.plist\n"); isFullBackup = 1; } } else { isFullBackup = 1; } doPostNotification(m_device, NP_SYNC_WILL_START); afc_file_open(m_afc, "/com.apple.itunes.lock_sync", AFC_FOPEN_RW, &m_lockfile); if (m_lockfile) { afc_error_t aerr; doPostNotification(m_device, NP_SYNC_LOCK_REQUEST); int i = 0; for (; i < LOCK_ATTEMPTS; i++) { aerr = afc_file_lock(m_afc, m_lockfile, AFC_LOCK_EX); if (aerr == AFC_E_SUCCESS) { doPostNotification(m_device, NP_SYNC_DID_START); break; } else if (aerr == AFC_E_OP_WOULD_BLOCK) { usleep(LOCK_WAIT); continue; } else { printlog("ERROR: could not lock file! error code: %d\n", aerr); afc_file_close(m_afc, m_lockfile); m_lockfile = 0; /// cmd = CMD_LEAVE; } } if (i == LOCK_ATTEMPTS) { printlog("ERROR: timeout while locking for sync\n"); afc_file_close(m_afc, m_lockfile); m_lockfile = 0; // skipBackup = true; return false; } } PRINT_VERBOSE(1, "Starting backup...\n"); /* make sure backup device sub-directory exists */ std::string devBackupDir = combinePath(outputPath, m_udid); fs::makeDirectory(devBackupDir, 0755); /* TODO: check domain com.apple.mobile.backup key RequiresEncrypt and WillEncrypt with lockdown */ /* TODO: verify battery on AC enough battery remaining */ /* re-create Info.plist (Device infos, IC-Info.sidb, photos, app_ids, iTunesPrefs) */ if (infoPlist) { plist_free(infoPlist); infoPlist = NULL; } infoPlist = newInfoPlist(m_udid.c_str(), m_device, m_afc); if (!infoPlist) { printlog("Failed to generate Info.plist - aborting\n"); // skipBackup = true; return false; } fs::deleteFile(info_path); writePlistFile(infoPlist, info_path.c_str(), PLIST_FORMAT_XML); plist_free(infoPlist); infoPlist = NULL; plist_t opts = NULL; if (pThis->forceFullBackup()) { /// PRINT_VERBOSE(1, "Enforcing full backup from device.\n"); opts = plist_new_dict(); plist_dict_set_item(opts, "ForceFullBackup", plist_new_bool(1)); } /* request backup from device with manifest from last backup */ /* if (willEncrypt) { /// PRINT_VERBOSE(1, "Backup will be encrypted.\n"); } else { /// PRINT_VERBOSE(1, "Backup will be unencrypted.\n"); } */ PRINT_VERBOSE(1, "Requesting backup from device...\n"); err = mobilebackup2_send_request(m_mobilebackup2, "Backup", m_udid.c_str(), sourceUdid, opts); if (opts) plist_free(opts); if (err == MOBILEBACKUP2_E_SUCCESS) { if (isFullBackup) { PRINT_VERBOSE(1, "Full backup mode.\n"); } else { PRINT_VERBOSE(1, "Incremental backup mode.\n"); } } else { if (err == MOBILEBACKUP2_E_BAD_VERSION) { printlog("ERROR: Could not start backup process: backup protocol version mismatch!\n"); } else if (err == MOBILEBACKUP2_E_REPLY_NOT_OK) { printlog("ERROR: Could not start backup process: device refused to start the backup process.\n"); } else { printlog("ERROR: Could not start backup process: unspecified error occurred\n"); } // skipBackup = true; return false; } /* reset operation success status */ plist_t message = NULL; char *dlmsg = NULL; mobilebackup2_error_t mberr = MOBILEBACKUP2_E_SUCCESS; int operation_ok = 0; int file_count = 0; int progress_finished = 0; /* process series of DLMessage* operations */ do { mberr = mobilebackup2_receive_message(m_mobilebackup2, &message, &dlmsg); if (mberr == MOBILEBACKUP2_E_RECEIVE_TIMEOUT) { PRINT_VERBOSE(2, "Device is not ready yet, retrying...\n"); // goto files_out; } else if (mberr != MOBILEBACKUP2_E_SUCCESS) { PRINT_VERBOSE(0, "ERROR: Could not receive from mobilebackup2 (%d)\n", mberr); pThis->cancel(); // goto files_out; } else { #ifndef NDEBUG static unsigned int fileIndex = 0; fileIndex++; char fileName[32] = { 0 }; sprintf(fileName, "message_%04u.plist", fileIndex); #ifdef _WIN32 std::string filePath = combinePath(outputPath, "dbg", "new", fileName); #else std::string filePath = combinePath(outputPath, "dbg", "org", fileName); #endif writePlistFile(message, filePath, PLIST_FORMAT_XML); #endif if (!strcmp(dlmsg, "DLMessageDownloadFiles")) { /* device wants to download files from the computer */ setOverallProgressFromMessage(pThis, message, dlmsg); handleSendFiles(message, outputPath.c_str()); } else if (!strcmp(dlmsg, "DLMessageUploadFiles")) { /* device wants to send files to the computer */ setOverallProgressFromMessage(pThis, message, dlmsg); file_count += handleReceiveFiles(pThis, message, outputPath.c_str()); } else if (!strcmp(dlmsg, "DLMessageGetFreeDiskSpace")) { /* device wants to know how much disk space is available on the computer */ uint64_t freespace = 0; int res = calcFreeSpace(outputPath, freespace); plist_t freespace_item = plist_new_uint(freespace); mobilebackup2_send_status_response(m_mobilebackup2, res, NULL, freespace_item); plist_free(freespace_item); } else if (!strcmp(dlmsg, "DLMessagePurgeDiskSpace")) { /* device wants to purge disk space on the host - not supported */ plist_t empty_dict = plist_new_dict(); err = mobilebackup2_send_status_response(m_mobilebackup2, -1, "Operation not supported", empty_dict); plist_free(empty_dict); } else if (!strcmp(dlmsg, "DLContentsOfDirectory")) { /* list directory contents */ handleListDirectory(message, outputPath.c_str()); } else if (!strcmp(dlmsg, "DLMessageCreateDirectory")) { /* make a directory */ handleMakeDirectory(message, outputPath.c_str()); } else if (!strcmp(dlmsg, "DLMessageMoveFiles") || !strcmp(dlmsg, "DLMessageMoveItems")) { /* perform a series of rename operations */ handleMoveFiles(pThis, message, dlmsg, outputPath); } else if (!strcmp(dlmsg, "DLMessageRemoveFiles") || !strcmp(dlmsg, "DLMessageRemoveItems")) { handleRemoveFiles(pThis, message, dlmsg, outputPath); } else if (!strcmp(dlmsg, "DLMessageCopyItem")) { handleCopyItem(pThis, message, dlmsg, outputPath); } else if (!strcmp(dlmsg, "DLMessageDisconnect")) { break; } else if (!strcmp(dlmsg, "DLMessageProcessMessage")) { handleProcessMessage(message, operation_ok, result_code, outputPath); break; } /* print status */ if ((pThis->getOverallProgress() > 0) && !progress_finished) { if (pThis->getOverallProgress() >= 100.0f) { progress_finished = 1; } // print_progress_real(pThis->getOverallProgress(), 0); PRINT_VERBOSE(1, " Finished\n"); } } plist_free(message); message = NULL; plist_mem_free(dlmsg); dlmsg = NULL; if (pThis->isCanclled()) { break; } } while (1); plist_free(message); plist_mem_free(dlmsg); /* report operation status to user */ bool res = false; PRINT_VERBOSE(1, "Received %d files from device.\n", file_count); if (operation_ok && checkSnapshotState(outputPath, m_udid, "finished")) { PRINT_VERBOSE(1, "Backup Successful.\n"); res = true; } else { if (pThis->isCanclled()) { PRINT_VERBOSE(1, "Backup Aborted.\n"); } else { PRINT_VERBOSE(1, "Backup Failed (Error Code %d).\n", -result_code); } } return res; } inline void freeService(lockdownd_service_descriptor_t& service) { if (service != NULL) { lockdownd_service_descriptor_free(service); service = NULL; } } void unlockAfc() { if (m_lockfile) { afc_file_lock(m_afc, m_lockfile, AFC_LOCK_UN); afc_file_close(m_afc, m_lockfile); m_lockfile = 0; doPostNotification(m_device, NP_SYNC_DID_FINISH); } } idevice_t getDevice() const { return m_device; } lockdownd_client_t getClient() const { return m_client; } private: idevice_t m_device; afc_client_t m_afc; uint64_t m_lockfile; np_client_t m_np; lockdownd_client_t m_client; mobilebackup2_client_t m_mobilebackup2; std::string m_udid; std::map m_files; }; IDeviceBackup::IDeviceBackup(const DeviceInfo& deviceInfo, const std::string& outputPath) : m_deviceInfo(deviceInfo), m_outputPath(outputPath), m_quitFlag(0), m_overallProgress(0) { } bool IDeviceBackup::queryDevices(std::vector &devices) { idevice_info_t *devList = NULL; int numberOfDevices = 0; if (idevice_get_device_list_extended(&devList, &numberOfDevices) < 0) { printlog("ERROR: Unable to retrieve device list!\n"); return false; } devices.clear(); for (int idx = 0; devList[idx] != NULL; idx++) { std::vector::iterator it = devices.emplace(devices.end()); it->setUdid(devList[idx]->udid); it->setUsb(devList[idx]->conn_type == CONNECTION_USBMUXD); std::string value; if (queryDeviceName(it->getUdid(), value)) { it->setName(value); } IDeviceBackupClient dbc(it->getUdid()); lockdownd_error_t err = dbc.initWithHandShake(); dbc.updateDeviceInfo(*it, err); if (LOCKDOWN_E_SUCCESS == err) { if (dbc.queryAppContainer(BUNDLEID_WECHAT, value)) { it->setWechatPath(value); } } } idevice_device_list_extended_free(devList); return true; } bool IDeviceBackup::queryDeviceName(const std::string& udid, std::string& name) { if (udid.empty()) { return false; } IDeviceBackupClient dbc(udid); if (!dbc.init()) { return false; } return dbc.queryDeviceName(name); } bool IDeviceBackup::queryWechatPath(const std::string& udid, std::string& wechatPath) { if (udid.empty()) { return false; } IDeviceBackupClient dbc(udid); if (dbc.initWithHandShake() != LOCKDOWN_E_SUCCESS) { return false; } bool res = dbc.queryAppContainer(BUNDLEID_WECHAT, wechatPath); return res; } bool IDeviceBackup::forceFullBackup() const { return false; } void IDeviceBackup::cancel() { ++m_quitFlag; } bool IDeviceBackup::isCanclled() const { return m_quitFlag > 0u; } int IDeviceBackup::getErrCode() const { return m_errCode; } const std::string IDeviceBackup::getErrMsg() const { return m_errMsg; } void IDeviceBackup::setError(int errCode, const std::string& errMsg) { m_errCode = errCode; m_errMsg = errMsg; } bool IDeviceBackup::filter(const std::string& srcPath, const std::string& destPath) const { return ((srcPath.find(m_deviceInfo.getWechatUuid()) == std::string::npos) && (srcPath.find("/Containers/Shared/AppGroup/") == std::string::npos)); } bool IDeviceBackup::backup() { // Verify if backup directory exists if (!existsDirectory(m_outputPath)) { printlog("ERROR: Backup directory does not exist: %s\n", m_outputPath.c_str()); return false; } IDeviceBackupClient dbc(m_deviceInfo.getUdid()); dbc.loadFiles(m_outputPath, m_deviceInfo.getUdid()); if (dbc.initWithHandShake() != LOCKDOWN_E_SUCCESS) { return false; } return dbc.backup(this, m_outputPath); } double IDeviceBackup::getOverallProgress() const { return (double)m_overallProgress; } void IDeviceBackup::setOverallProgress(double progress) { if (progress > 0.0) m_overallProgress = progress; } ================================================ FILE: WechatExporter/core/IDeviceBackup.h ================================================ // // IDevice.h // WechatExporter // // Created by Matthew on 2021/12/7. // Copyright © 2021 Matthew. All rights reserved. // #ifndef IDevice_h #define IDevice_h #include #include #include class DeviceInfo { private: std::string m_udid; std::string m_name; bool m_usb; bool m_locked; bool m_trustPending; bool m_trusted; std::string m_wechatPath; std::string m_wechatUuid; public: DeviceInfo() { } ~DeviceInfo() { } std::string getUdid() const { return m_udid; } void setUdid(const std::string& udid) { m_udid = udid; } std::string getName() const { return m_name; } void setName(const std::string& name) { m_name = name; } bool isUsb() const { return m_usb; } void setUsb(bool usb) { m_usb = usb; } void setLocked(bool locked) { m_locked = locked; } bool needUnlock() const { return m_locked; } void setTrustPending(bool trustPending) { m_trustPending = trustPending; } bool isTrustPending() const { return m_trustPending; } void setTrusted(bool trusted) { m_trusted = trusted; } bool needTrust() const { return !m_trusted; } std::string getWechatPath() const { return m_wechatPath; } void setWechatPath(const std::string& wechatPath) { m_wechatPath = wechatPath; parseWechatUuid(); } std::string getWechatUuid() const { return m_wechatUuid; } private: void parseWechatUuid() { m_wechatUuid.clear(); // /private/var/mobile/Containers/Data/Application/5A875B93-B816-459E-B6BA-C4A3D4162B6F const std::string startTag = "/Containers/Data/Application/"; std::string::size_type pos = m_wechatPath.find(startTag); if (pos != std::string::npos) { std::string::size_type pos2 = m_wechatPath.find("/", pos + startTag.size()); if (pos2 != std::string::npos) { m_wechatUuid = m_wechatPath.substr(pos + startTag.size(), pos2 - (pos + startTag.size())); } else { m_wechatUuid = m_wechatPath.substr(pos + startTag.size()); } } } }; #define BUNDLEID_WECHAT "com.tencent.xin" #define CLIENT_ID "wxexp" #define LOCK_ATTEMPTS 50 #define LOCK_WAIT 200000 // IDeviceBackupClient will refer libmobiledevice class IDeviceBackupClient; class IDeviceBackup { public: static bool queryDevices(std::vector& devices); static bool queryDeviceName(const std::string& udid, std::string& name); static bool queryWechatPath(const std::string& udid, std::string& wechatPath); public: IDeviceBackup(const DeviceInfo& deviceInfo, const std::string& outputPath); bool backup(); // bool backup1(); bool forceFullBackup() const; void cancel(); bool isCanclled() const; bool filter(const std::string& srcPath, const std::string& destPath) const; double getOverallProgress() const; void setOverallProgress(double progress); int getErrCode() const; const std::string getErrMsg() const; void setError(int errCode, const std::string& errMsg); private: DeviceInfo m_deviceInfo; std::string m_outputPath; unsigned int m_quitFlag; int m_errCode; std::string m_errMsg; std::atomic m_overallProgress; }; #endif /* IDevice_h */ ================================================ FILE: WechatExporter/core/ITunesParser.cpp ================================================ // // ITunesParser.cpp // WechatExporter // // Created by Matthew on 2020/9/29. // Copyright © 2020 Matthew. All rights reserved. // #include "ITunesParser.h" #include #include #include #include #include #include #include #include #include #include #ifndef NDEBUG #include #endif #include #if defined(_WIN32) // #define S_IFMT 0170000 /* [XSI] type of file mask */ // #define S_IFDIR 0040000 /* [XSI] directory */ #include #include #else #include #include #endif #include "MbdbReader.h" #include "Utils.h" #include "FileSystem.h" inline std::string getPlistStringValue(plist_t node) { std::string value; if (NULL != node) { uint64_t length = 0; const char* ptr = plist_get_string_ptr(node, &length); if (length > 0) { value.assign(ptr, length); } } return value; } inline std::string getPlistStringValue(plist_t node, const char* key) { std::string value; if (NULL != node) { plist_t subNode = plist_dict_get_item(node, key); if (NULL != subNode) { uint64_t length = 0; const char* ptr = plist_get_string_ptr(subNode, &length); if (length > 0) { value.assign(ptr, length); } } } return value; } struct __string_less { // _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX11 bool operator()(const std::string& __x, const std::string& __y) const {return __x < __y;} bool operator()(const std::pair& __x, const std::string& __y) const {return __x.first < __y;} bool operator()(const ITunesFile* __x, const std::string& __y) const {return __x->relativePath < __y;} bool operator()(const ITunesFile* __x, const ITunesFile* __y) const {return __x->relativePath < __y->relativePath;} }; class SqliteITunesFileEnumerator : public ITunesDb::ITunesFileEnumerator { public: SqliteITunesFileEnumerator(const std::string& dbPath, const std::vector& domains, bool onlyFile) : m_db(NULL), m_stmt(NULL), m_onlyFile(onlyFile) { #ifndef NDEBUG if (!existsFile(dbPath)) { return; } #endif int rc = openSqlite3Database(dbPath, &m_db); if (rc != SQLITE_OK) { // printf("Open database failed!"); closeDb(); return; } sqlite3_exec(m_db, "PRAGMA mmap_size=268435456;", NULL, NULL, NULL); // 256M:268435456 2M 2097152 sqlite3_exec(m_db, "PRAGMA synchronous=OFF;", NULL, NULL, NULL); std::string sql = "SELECT fileID,domain,relativePath,flags,file FROM Files"; if (domains.size() > 0) { sql += " WHERE "; // domain=?"; std::vector conditions(domains.size(), "domain=?"); sql += join(conditions, " OR "); } rc = sqlite3_prepare_v2(m_db, sql.c_str(), (int)(sql.size()), &m_stmt, NULL); if (rc != SQLITE_OK) { std::string error = sqlite3_errmsg(m_db); closeDb(); return; } int idx = 1; for (std::vector::const_iterator it = domains.cbegin(); it != domains.cend(); ++it, ++idx) { rc = sqlite3_bind_text(m_stmt, idx, (*it).c_str(), (int)((*it).size()), NULL); if (rc != SQLITE_OK) { finalizeStmt(); closeDb(); return; } } } virtual bool isInvalid() const { return NULL == m_db || NULL == m_stmt; } virtual bool nextFile(ITunesFile& file) { while (sqlite3_step(m_stmt) == SQLITE_ROW) { int flags = sqlite3_column_int(m_stmt, 3); if (m_onlyFile && flags != 1) { // Putting flags=1 into sql causes sqlite3 to use index of flags instead of domain and don't know why... // So filter the directory with the code continue; } const char *relativePath = reinterpret_cast(sqlite3_column_text(m_stmt, 2)); if (NULL != relativePath) { file.relativePath = relativePath; } else { file.relativePath.clear(); } const char *domain = reinterpret_cast(sqlite3_column_text(m_stmt, 1)); if (NULL != domain) { file.domain = domain; } else { file.domain.clear(); } const char *fileId = reinterpret_cast(sqlite3_column_text(m_stmt, 0)); if (NULL != fileId) { file.fileId = fileId; } else { file.fileId.clear(); } file.flags = static_cast(flags); // Files const unsigned char *blob = reinterpret_cast(sqlite3_column_blob(m_stmt, 4)); int blobBytes = sqlite3_column_bytes(m_stmt, 4); file.blob.clear(); file.size = 0; file.modifiedTime = 0; if (blobBytes > 0 && NULL != blob) { std::vector blobVector(blob, blob + blobBytes); file.blob.insert(file.blob.end(), blob, blob + blobBytes); } file.blobParsed = false; return true; // break; } return false; } virtual ~SqliteITunesFileEnumerator() { finalizeStmt(); closeDb(); } private: void closeDb() { if (NULL != m_db) { sqlite3_close(m_db); m_db = NULL; } } void finalizeStmt() { if (NULL != m_stmt) { sqlite3_finalize(m_stmt); m_stmt = NULL; } } private: sqlite3* m_db; sqlite3_stmt* m_stmt; bool m_onlyFile; }; class MbdbITunesFileEnumerator : public ITunesDb::ITunesFileEnumerator { public: MbdbITunesFileEnumerator(const std::string& dbPath, const std::vector& domains, bool onlyFile) : m_valid(false), m_domains(domains), m_onlyFile(onlyFile) { std::memset(m_fixedData, 0, 40); if (!m_reader.open(dbPath)) { return; } m_valid = true; } virtual bool isInvalid() const { return !m_valid; } virtual bool nextFile(ITunesFile& file) { std::string domainInFile; std::string path; std::string linkTarget; std::string dataHash; std::string alwaysNull; unsigned short fileMode = 0; bool isDir = false; bool skipped = false; // bool hasFilter = (bool)m_loadingFilter; while (m_reader.hasMoreData()) { if (!m_reader.read(domainInFile)) { break; } skipped = false; if (!existsDomain(domainInFile)) { skipped = true; } if (skipped) { // will skip it m_reader.skipString(); // path m_reader.skipString(); // linkTarget m_reader.skipString(); // dataHash m_reader.skipString(); // alwaysNull; m_reader.read(m_fixedData, 40); int propertyCount = m_fixedData[39]; for (int j = 0; j < propertyCount; ++j) { m_reader.skipString(); m_reader.skipString(); } } else { m_reader.read(path); m_reader.read(linkTarget); m_reader.readD(dataHash); m_reader.readD(alwaysNull); m_reader.read(m_fixedData, 40); fileMode = (m_fixedData[0] << 8) | m_fixedData[1]; isDir = S_ISDIR(fileMode); // unsigned char flags = fixedData[38]; if (m_onlyFile && isDir) { skipped = true; } unsigned int aTime = bigEndianToNative(*((uint32_t *)(m_fixedData + 18))); unsigned int bTime = bigEndianToNative(*((uint32_t *)(m_fixedData + 22))); // unsigned int cTime = bigEndianToNative(*((uint32_t *)(m_fixedData + 26))); file.size = bigEndianToNative(*((int64_t *)(m_fixedData + 30))); int propertyCount = m_fixedData[39]; for (int j = 0; j < propertyCount; ++j) { if (skipped) { m_reader.skipString(); // name m_reader.skipString(); // value } else { std::string name; std::string value; m_reader.read(name); m_reader.read(value); } } if (!skipped) { file.relativePath = path; file.domain = domainInFile; file.fileId = sha1(domainInFile + "-" + path); file.flags = isDir ? 2 : 1; file.modifiedTime = aTime != 0 ? aTime : bTime; file.blobParsed = true; // file.size = return true; } } } return false; } virtual ~MbdbITunesFileEnumerator() { } private: bool existsDomain(const std::string& domain) const { if (m_domains.empty()) { return true; } for (std::vector::const_iterator it = m_domains.cbegin(); it != m_domains.cend(); ++it) { if (domain == *it) { return true; } } return false; } private: MbdbReader m_reader; bool m_valid; std::vector m_domains; bool m_onlyFile; unsigned char m_fixedData[40]; }; ITunesDb::ITunesDb(const std::string& rootPath, const std::string& manifestFileName) : m_isMbdb(false), m_rootPath(rootPath), m_manifestFileName(manifestFileName) { std::replace(m_rootPath.begin(), m_rootPath.end(), ALT_DIR_SEP, DIR_SEP); if (!endsWith(m_rootPath, DIR_SEP)) { m_rootPath += DIR_SEP; } m_version.clear(); BackupItem manifest; if (ManifestParser::parseInfoPlist(m_rootPath, manifest, false)) { m_version = manifest.getITunesVersion(); m_iOSVersion = manifest.getIOSVersion(); } std::string dbPath = combinePath(m_rootPath, "Manifest.mbdb"); if (existsFile(dbPath)) { m_isMbdb = true; } } ITunesDb::~ITunesDb() { for (std::vector::iterator it = m_files.begin(); it != m_files.end(); ++it) { delete *it; } m_files.clear(); } bool ITunesDb::load() { return load("", false); } bool ITunesDb::load(const std::string& domain) { return load(domain, false); } bool ITunesDb::load(const std::string& domain, bool onlyFile) { std::vector domains; if (!domain.empty()) { domains.push_back(domain); } #if !defined(NDEBUG) || defined(DBG_PERF) printf("PERF: start.....%s\r\n", getTimestampString(false, true).c_str()); #endif std::unique_ptr enumerator(buildEnumerator(domains, onlyFile)); if (enumerator->isInvalid()) { return false; } bool hasFilter = (bool)m_loadingFilter; ITunesFile file; m_files.reserve(2048); while (enumerator->nextFile(file)) { if (hasFilter && !m_loadingFilter(file.relativePath.c_str(), file.flags)) { continue; } m_files.push_back(new ITunesFile(file)); } #if !defined(NDEBUG) || defined(DBG_PERF) printf("PERF: end.....%s, size=%lu\r\n", getTimestampString(false, true).c_str(), m_files.size()); #endif std::sort(m_files.begin(), m_files.end(), __string_less()); #if !defined(NDEBUG) || defined(DBG_PERF) printf("PERF: after sort.....%s\r\n", getTimestampString(false, true).c_str()); #endif return true; } ITunesDb::ITunesFileEnumerator* ITunesDb::buildEnumerator(const std::vector& domains, bool onlyFile) const { std::string dbPath = combinePath(m_rootPath, m_isMbdb ? "Manifest.mbdb" : "Manifest.db"); ITunesFileEnumerator* enumerator = m_isMbdb ? (ITunesFileEnumerator*)(new MbdbITunesFileEnumerator(dbPath, domains, onlyFile)) : (ITunesFileEnumerator*)(new SqliteITunesFileEnumerator(dbPath, domains, onlyFile)); return enumerator; } ITunesDb::ITunesFileEnumerator* ITunesDb::buildEnumerator(const std::string& dbPath, const std::vector& domains, bool onlyFile) const { ITunesFileEnumerator* enumerator = m_isMbdb ? (ITunesFileEnumerator*)(new MbdbITunesFileEnumerator(dbPath, domains, onlyFile)) : (ITunesFileEnumerator*)(new SqliteITunesFileEnumerator(dbPath, domains, onlyFile)); return enumerator; } bool ITunesDb::copy(const std::string& destPath, const std::string& backupId, std::vector& domains, std::function& func) const { std::string destBackupPath = backupId.empty() ? combinePath(destPath, "Backup") : combinePath(destPath, "Backup", backupId); if (!existsDirectory(destBackupPath)) { makeDirectory(destBackupPath); } // Copy control files const char* files[] = {"Info.plist", (m_isMbdb ? "Manifest.mbdb" : "Manifest.db"), "Manifest.plist", "Status.plist"}; for (int idx = 0; idx < sizeof(files) / sizeof(const char *); ++idx) { ::copyFile(combinePath(m_rootPath, files[idx]), combinePath(destBackupPath, files[idx])); } std::unique_ptr enumerator; if (m_isMbdb) { enumerator.reset(buildEnumerator(combinePath(destBackupPath, "Manifest.mbdb"), domains, false)); } else { std::string dbPath = combinePath(destBackupPath, "Manifest.db"); sqlite3 *db = NULL; int rc = openSqlite3Database(dbPath, &db, false); if (rc != SQLITE_OK) { // printf("Open database failed!"); sqlite3_close(db); return false; } sqlite3_exec(db, "PRAGMA mmap_size=268435456;", NULL, NULL, NULL); // 256M:268435456 2M 2097152 sqlite3_exec(db, "PRAGMA synchronous=OFF;", NULL, NULL, NULL); std::string sql = "DELETE FROM Files"; if (domains.size() > 0) { sql += " WHERE "; // domain=?"; std::vector conditions(domains.size(), "domain<>?"); sql += join(conditions, " AND "); } sqlite3_stmt* stmt = NULL; rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL); if (rc != SQLITE_OK) { std::string error = sqlite3_errmsg(db); sqlite3_close(db); return false; } int idx = 1; for (std::vector::const_iterator it = domains.cbegin(); it != domains.cend(); ++it, ++idx) { rc = sqlite3_bind_text(stmt, idx, (*it).c_str(), (int)((*it).size()), NULL); if (rc != SQLITE_OK) { sqlite3_finalize(stmt); sqlite3_close(db); return false; } } #if !defined(NDEBUG) #ifdef __APPLE__ if (__builtin_available(macOS 10.12, *)) { #endif char *expandedSql = sqlite3_expanded_sql(stmt); printf("PERF: %s sql=%s\r\n", getTimestampString(false, true).c_str(), sqlite3_expanded_sql(stmt)); #ifdef __APPLE__ } #endif #endif if ((rc = sqlite3_step(stmt)) != SQLITE_DONE) { #ifndef NDEBUG const char *errMsg = sqlite3_errmsg(db); m_lastError = std::string(errMsg); #endif sqlite3_finalize(stmt); sqlite3_close(db); return false; } sqlite3_finalize(stmt); sqlite3_exec(db, "VACUUM;", NULL, NULL, NULL); sqlite3_close(db); enumerator.reset(buildEnumerator(dbPath, std::vector(), false)); } if (enumerator->isInvalid()) { return false; } std::string subPath; std::string prefix; std::string srcFilePath; std::set subFolders; ITunesFile file; while (enumerator->nextFile(file)) { if (file.fileId.empty()) { continue; } prefix = file.fileId.substr(0, 2); srcFilePath = combinePath(m_rootPath, prefix, file.fileId); if (!existsFile(srcFilePath)) { #ifndef NDEBUG // assert(!"Source file not exists."); #endif continue; } subPath = combinePath(destBackupPath, prefix); if (subFolders.find(prefix) == subFolders.cend()) { if (!existsDirectory(subPath)) { makeDirectory(subPath); } subFolders.insert(prefix); } bool ret = ::copyFile(srcFilePath, combinePath(subPath, file.fileId)); #ifndef NDEBUG if (!ret) { std::string msg = "Failed to copy file" + combinePath(subPath, file.fileId); assert(!msg.c_str()); } #endif if (func) { if (!func(this, &file)) { break; } } } return true; } unsigned int ITunesDb::parseModifiedTime(const std::vector& data) { if (data.empty()) { return 0; } uint64_t val = 0; plist_t node = NULL; plist_from_memory(reinterpret_cast(&data[0]), static_cast(data.size()), &node); if (NULL != node) { plist_t lastModified = plist_access_path(node, 3, "$objects", 1, "LastModified"); if (NULL != lastModified) { plist_get_uint_val(lastModified, &val); } plist_free(node); } return static_cast(val); } bool ITunesDb::parseFileInfo(const ITunesFile* file) { if (NULL == file || file->blob.empty()) { return false; } if (file->blobParsed) { return true; } file->blobParsed = true; uint64_t val = 0; plist_t node = NULL; plist_from_memory(reinterpret_cast(&file->blob[0]), static_cast(file->blob.size()), &node); if (NULL != node) { plist_t lastModifiedNode = plist_access_path(node, 3, "$objects", 1, "LastModified"); if (NULL != lastModifiedNode) { plist_type pt = plist_get_node_type(lastModifiedNode); plist_get_uint_val(lastModifiedNode, &val); file->modifiedTime = (unsigned int)val; } plist_t sizeNode = plist_access_path(node, 3, "$objects", 1, "Size"); if (NULL != sizeNode) { plist_type pt = plist_get_node_type(sizeNode); val = 0; plist_get_uint_val(sizeNode, &val); file->size = val; } plist_free(node); return true; } return false; } std::string ITunesDb::findFileId(const std::string& relativePath) const { const ITunesFile* file = findITunesFile(relativePath); if (NULL == file) { return std::string(); } return file->fileId; } const ITunesFile* ITunesDb::findITunesFile(const std::string& relativePath) const { std::string formatedPath = relativePath; std::replace(formatedPath.begin(), formatedPath.end(), '\\', '/'); typename std::vector::iterator it = std::lower_bound(m_files.begin(), m_files.end(), formatedPath, __string_less()); if (it == m_files.end() || (*it)->relativePath != formatedPath) { return NULL; } return *it; } std::string ITunesDb::fileIdToRealPath(const std::string& fileId) const { if (!fileId.empty()) { return m_isMbdb ? combinePath(m_rootPath, fileId) : combinePath(m_rootPath, fileId.substr(0, 2), fileId); } return std::string(); } std::string ITunesDb::getRealPath(const ITunesFile& file) const { return fileIdToRealPath(file.fileId); } std::string ITunesDb::getRealPath(const ITunesFile* file) const { return fileIdToRealPath(file->fileId); } std::string ITunesDb::findRealPath(const std::string& relativePath) const { std::string fieldId = findFileId(relativePath); return fileIdToRealPath(fieldId); } bool ITunesDb::copyFile(const std::string& vpath, const std::string& dest, bool overwrite/* = false*/) const { std::string destPath = normalizePath(dest); if (!overwrite && existsFile(destPath)) { return true; } const ITunesFile* file = findITunesFile(vpath); if (NULL != file) { std::string srcPath = getRealPath(*file); if (!srcPath.empty()) { normalizePath(srcPath); bool result = ::copyFile(srcPath, destPath, true); if (result) { updateFileTime(dest, ITunesDb::parseModifiedTime(file->blob)); } return result; } } return false; } bool ITunesDb::copyFile(const std::string& vpath, const std::string& destPath, const std::string& destFileName, bool overwrite/* = false*/) const { std::string destFullPath = normalizePath(combinePath(destPath, destFileName)); if (!overwrite && existsFile(destFullPath)) { return true; } const ITunesFile* file = findITunesFile(vpath); if (NULL != file) { std::string srcPath = getRealPath(*file); if (!srcPath.empty()) { normalizePath(srcPath); if (!existsDirectory(destPath)) { makeDirectory(destPath); } bool result = ::copyFile(srcPath, destFullPath, true); if (result) { if (file->modifiedTime != 0) { updateFileTime(destFullPath, static_cast(file->modifiedTime)); } else if (!file->blob.empty()) { updateFileTime(destFullPath, ITunesDb::parseModifiedTime(file->blob)); } } return result; } } return false; } DecodedWechatITunesDb::DecodedWechatITunesDb(const std::string& rootPath, const std::string& manifestFileName) : ITunesDb(rootPath, manifestFileName) { } DecodedWechatITunesDb::~DecodedWechatITunesDb() { } bool DecodedWechatITunesDb::load(const std::string& domain, bool onlyFile) { return loadFiles(m_rootPath, onlyFile); } bool DecodedWechatITunesDb::loadFiles(const std::string& root, bool onlyFile) { #ifdef _WIN32 std::queue directories; directories.push(""); TCHAR szRoot[MAX_PATH] = { 0 }; _tcscpy(szRoot, CW2T(CA2W(root.c_str(), CP_UTF8))); PathAddBackslash(szRoot); TCHAR szPath[MAX_PATH] = { 0 }; TCHAR szRelativePath[MAX_PATH] = { 0 }; ULARGE_INTEGER ull; while (!directories.empty()) { const CString& dirName = directories.front(); PathCombine(szPath, szRoot, dirName); PathAddBackslash(szPath); PathAppend(szPath, TEXT("*.*")); WIN32_FIND_DATA FindFileData; HANDLE hFind = INVALID_HANDLE_VALUE; hFind = FindFirstFile((LPTSTR)szPath, &FindFileData); if (hFind == INVALID_HANDLE_VALUE) { return false; } do { if (_tcscmp(FindFileData.cFileName, TEXT(".")) == 0 || _tcscmp(FindFileData.cFileName, TEXT("..")) == 0) { continue; } bool isDir = ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0); PathCombine(szRelativePath, dirName, FindFileData.cFileName); if (!onlyFile || !isDir) { CString relativePath = szRelativePath; relativePath.Replace(DIR_SEP, ALT_DIR_SEP); CW2A pszU8(CT2W(szRelativePath), CP_UTF8); ITunesFile *file = new ITunesFile(); file->relativePath = (LPCSTR)CW2A(CT2W(relativePath), CP_UTF8);; file->fileId = (LPCSTR)pszU8; file->flags = isDir ? 2 : 1; ull.LowPart = FindFileData.ftLastWriteTime.dwLowDateTime; ull.HighPart = FindFileData.ftLastWriteTime.dwHighDateTime; file->modifiedTime = static_cast(ull.QuadPart / 10000000ULL - 11644473600ULL); m_files.push_back(file); } if (isDir) { PathAddBackslash(szRelativePath); directories.emplace(szRelativePath); } } while (::FindNextFile(hFind, &FindFileData)); FindClose(hFind); directories.pop(); } #else std::queue directories; directories.push(""); struct stat statbuf; while (!directories.empty()) { const std::string& dirName = directories.front(); std::string path = combinePath(root, dirName); struct dirent *entry = NULL; DIR *dir = opendir(path.c_str()); if (dir == NULL) { return false; } while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } if (filterFile(dirName, entry->d_name)) { continue; } bool isDir = false; std::string relativePath = dirName + entry->d_name; lstat(combinePath(path, entry->d_name).c_str(), &statbuf); isDir = S_ISDIR(statbuf.st_mode); if (!onlyFile || !isDir) { std::string fileId = relativePath; ITunesFile *file = new ITunesFile(); file->relativePath = relativePath; file->fileId = fileId; file->flags = isDir ? 2 : 1; file->modifiedTime = static_cast(statbuf.st_mtimespec.tv_sec); m_files.push_back(file); } if (isDir) { directories.push(endsWith(relativePath, "/") ? relativePath : (relativePath + "/")); } } closedir(dir); directories.pop(); } #endif std::sort(m_files.begin(), m_files.end(), __string_less()); return true; } std::string DecodedWechatITunesDb::fileIdToRealPath(const std::string& fileId) const { if (!fileId.empty()) { return combinePath(m_rootPath, fileId); } return std::string(); } bool DecodedWechatITunesDb::filterFile(const std::string& relativeDir, const std::string& fileName) { return (relativeDir.empty() && fileName.compare("Shared") == 0); } DecodedSharedWechatITunesDb::DecodedSharedWechatITunesDb(const std::string& rootPath, const std::string& manifestFileName) : DecodedWechatITunesDb(rootPath, manifestFileName) { m_rootPath = combinePath(m_rootPath, "Shared", "group.com.tencent.xin"); } DecodedSharedWechatITunesDb::~DecodedSharedWechatITunesDb() { } bool DecodedSharedWechatITunesDb::load(const std::string& domain, bool onlyFile) { return loadFiles(m_rootPath, onlyFile); } bool DecodedSharedWechatITunesDb::filterFile(const std::string& relativeDir, const std::string& fileName) { return false; } ManifestParser::ManifestParser(const std::string& manifestPath, bool incudingApps) : m_manifestPath(manifestPath), m_incudingApps(incudingApps) { } std::string ManifestParser::getLastError() const { return m_lastError; } bool ManifestParser::parse(std::vector& manifests) const { bool res = false; std::string path = normalizePath(m_manifestPath); if (endsWith(path, normalizePath("/MobileSync")) || endsWith(path, normalizePath("/MobileSync/")) || isValidMobileSync(path)) { path = combinePath(path, "Backup"); res = parseDirectory(path, manifests); } else if (isValidBackupItem(path)) { BackupItem manifest; if (parse(path, manifest) && manifest.isValid()) { manifests.push_back(manifest); res = true; } } else { // Assume the directory is ../../Backup/../ res = parseDirectory(path, manifests); } return res; } bool ManifestParser::parseDirectory(const std::string& path, std::vector& manifests) const { std::vector subDirectories; if (!listSubDirectories(path, subDirectories)) { #ifndef NDEBUG #endif m_lastError += "Failed to list subfolder in:" + path + "\r\n"; return false; } bool res = false; for (std::vector::const_iterator it = subDirectories.cbegin(); it != subDirectories.cend(); ++it) { std::string backupPath = combinePath(path, *it); if (!isValidBackupItem(backupPath)) { continue; } BackupItem manifest; manifest.setBackupId(*it); if (parse(backupPath, manifest) && manifest.isValid()) { manifests.push_back(manifest); res = true; } } if (!res) { m_lastError += "No valid backup id found in:" + path + "\r\n"; } return res; } bool ManifestParser::isValidBackupItem(const std::string& path) const { std::string fileName = combinePath(path, "Info.plist"); if (!existsFile(fileName)) { m_lastError += "Info.plist not found\r\n"; return false; } fileName = combinePath(path, "Manifest.plist"); if (!existsFile(fileName)) { m_lastError += "Manifest.plist not found\r\n"; return false; } // < iOS 10: Manifest.mbdb // >= iOS 10: Manifest.db if (!existsFile(combinePath(path, "Manifest.db")) && !existsFile(combinePath(path, "Manifest.mbdb"))) { m_lastError += "Manifest.db/Manifest.mbdb not found\r\n"; return false; } return true; } bool ManifestParser::isValidMobileSync(const std::string& path) const { std::string backupPath = combinePath(path, "Backup"); if (!existsDirectory(backupPath)) { m_lastError += "Backup folder not found\r\n"; return false; } return true; } bool ManifestParser::parse(const std::string& path, BackupItem& manifest) const { //Info.plist is a xml file if (!parseInfoPlist(path, manifest, m_incudingApps)) { m_lastError += "Failed to parse xml: Info.plist\r\n"; return false; } std::string fileName = combinePath(path, "Manifest.plist"); std::vector data; if (readFile(fileName, data)) { plist_t node = NULL; plist_from_memory(reinterpret_cast(&data[0]), static_cast(data.size()), &node); if (NULL != node) { plist_t isEncryptedNode = plist_access_path(node, 1, "IsEncrypted"); if (NULL != isEncryptedNode) { uint8_t val = 0; plist_get_bool_val(isEncryptedNode, &val); manifest.setEncrypted(val != 0); } if (manifest.getIOSVersion().empty()) { plist_t iOSVersionNode = plist_access_path(node, 2, "Lockdown", "ProductVersion"); manifest.setIOSVersion(getPlistStringValue(iOSVersionNode)); } plist_free(node); } } else { m_lastError = "Failed to read Manifest.plist\r\n"; return false; } return true; } bool ManifestParser::parseInfoPlist(const std::string& backupIdPath, BackupItem& manifest, bool includingApps) { std::string fileName = combinePath(backupIdPath, "Info.plist"); std::string contents = readFile(fileName); plist_t node = NULL; plist_from_memory(contents.c_str(), static_cast(contents.size()), &node); if (NULL == node) { return false; } manifest.setPath(backupIdPath); const char* ptr = NULL; uint64_t length = 0; std::string val; const char* ValueLastBackupDate = "Last Backup Date"; const char* ValueDisplayName = "Display Name"; const char* ValueDeviceName = "Device Name"; const char* ValueITunesVersion = "iTunes Version"; const char* ValueMacOSVersion = "macOS Version"; const char* ValueProductVersion = "Product Version"; const char* ValueInstalledApps = "Installed Applications"; const char* ValueUniqueIdentifier = "Unique Identifier"; const char* ValueTargetIdentifier = "Target Identifier"; plist_t subNode = NULL; manifest.setDeviceName(getPlistStringValue(node, ValueDeviceName)); manifest.setDisplayName(getPlistStringValue(node, ValueDisplayName)); subNode = plist_dict_get_item(node, ValueLastBackupDate); if (NULL != subNode) { int32_t sec = 0, usec = 0; plist_get_date_val(subNode, &sec, &usec); manifest.setBackupTime(fromUnixTime(sec + 978278400, false)); } manifest.setITunesVersion(getPlistStringValue(node, ValueITunesVersion)); manifest.setIOSVersion(getPlistStringValue(node, ValueProductVersion)); manifest.setMacOSVersion(getPlistStringValue(node, ValueMacOSVersion)); std::string uniqueId = getPlistStringValue(node, ValueUniqueIdentifier); std::string targetId = getPlistStringValue(node, ValueTargetIdentifier); std::string uniqueIdUpper = toUpper(uniqueId); if (toUpper(manifest.getBackupId()) != uniqueIdUpper) { if (toUpper(targetId) != uniqueIdUpper) { manifest.setBackupId(targetId); } else { manifest.setBackupId(toLower(uniqueId)); } } if (includingApps) { subNode = plist_dict_get_item(node, ValueInstalledApps); if (NULL != subNode && PLIST_IS_ARRAY(subNode)) { uint32_t arraySize = plist_array_get_size(subNode); plist_t itemNode = NULL; for (uint32_t idx = 0; idx < arraySize; ++idx) { itemNode = plist_array_get_item(subNode, idx); if (itemNode == NULL) { continue; } std::string bundleId = getPlistStringValue(itemNode); if (!bundleId.empty()) { plist_t appNode = plist_access_path(node, 2, "Applications", bundleId.c_str()); if (NULL != appNode) { plist_t appSubNode = NULL; appSubNode = plist_dict_get_item(appNode, "iTunesMetadata"); if (NULL != appSubNode) { plist_type ptype = plist_get_node_type(appSubNode); ptr = plist_get_data_ptr(appSubNode, &length); if (ptr != NULL && length > 0) { std::string metadata(ptr, length); BackupItem::AppInfo appInfo; appInfo.bundleId = bundleId; parseITunesMetadata(metadata, appInfo); manifest.addApp(appInfo); } } } } } } } plist_free(node); return true; } bool ManifestParser::parseITunesMetadata(const std::string& metadata, BackupItem::AppInfo& appInfo) { plist_t node = NULL; plist_from_memory(metadata.c_str(), static_cast(metadata.size()), &node); if (NULL == node) { return false; } /* plist_dict_iter it = NULL; char *key = NULL; plist_t plistVal = NULL; plist_dict_new_iter(node, &it); while (1) { plist_dict_next_item(node, it, &key, &plistVal); if (NULL == key) { break; } std::string keyString = key; int aa = 0; } */ appInfo.name = getPlistStringValue(node, "itemName"); appInfo.bundleShortVersion = getPlistStringValue(node, "bundleShortVersionString"); appInfo.bundleVersion = getPlistStringValue(node, "bundleVersion"); plist_free(node); return true; } DecodedManifestParser::DecodedManifestParser(const std::string& manifestPath, bool includingApps) : ManifestParser(manifestPath, includingApps) { } bool DecodedManifestParser::parse(std::vector& manifests) const { bool res = false; std::string path = normalizePath(m_manifestPath); if (isValidBackupItem(path)) { BackupItem manifest; if (parse(path, manifest) && manifest.isValid()) { manifests.push_back(manifest); res = true; } } return res; } bool DecodedManifestParser::isValidBackupItem(const std::string& path) const { std::string fileName = combinePath(path, "Documents", "LoginInfo2.dat"); if (!existsFile(fileName)) { m_lastError += "LoginInfo2.dat not found:" + fileName + "\r\n"; return false; } return true; } bool DecodedManifestParser::parse(const std::string& path, BackupItem& manifest) const { std::string fileName = combinePath(path, "Documents", "LoginInfo2.dat"); if (!existsFile(fileName)) { m_lastError += "LoginInfo2.dat not found\r\n"; return false; } manifest.setPath(path); #ifdef _WIN32 struct _stat statbuf; CA2W wpath(path.c_str(), CP_UTF8); _wstat((LPCWSTR)wpath, &statbuf); std::time_t ts = statbuf.st_mtime; #else struct stat statbuf; lstat(fileName.c_str(), &statbuf); std::time_t ts = statbuf.st_mtimespec.tv_sec; #endif std::tm * ptm = std::localtime(&ts); char buffer[32]; std::strftime(buffer, 32, "%Y-%m-%d %H:%M", ptm); manifest.setDeviceName("localhost"); manifest.setDisplayName("Wechat Backup"); manifest.setBackupTime(buffer); return true; } ================================================ FILE: WechatExporter/core/ITunesParser.h ================================================ // // ITunesParser.h // WechatExporter // // Created by Matthew on 2020/9/29. // Copyright © 2020 Matthew. All rights reserved. // #include #include #include #include #include #include #include "Utils.h" #ifndef ITunesParser_h #define ITunesParser_h struct ITunesFile { std::string domain; std::string fileId; std::string relativePath; unsigned int flags; std::vector blob; mutable unsigned int modifiedTime; mutable size_t size; mutable bool blobParsed; ITunesFile() : flags(0), modifiedTime(0), size(0), blobParsed(false) { } bool isDir() const { return flags == 2; } }; using ITunesFileVector = std::vector; using ITunesFilesIterator = typename ITunesFileVector::iterator; using ITunesFilesConstIterator = typename ITunesFileVector::const_iterator; using ITunesFileRange = std::pair; class BackupItem { public: struct AppInfo { std::string bundleId; std::string name; std::string bundleShortVersion; std::string bundleVersion; }; protected: std::string m_path; std::string m_backupId; std::string m_deviceName; std::string m_displayName; std::string m_backupTime; std::string m_iTunesVersion; std::string m_macOSVersion; std::string m_iOSVersion; bool m_encrypted; std::vector m_apps; // Installed Applications public: BackupItem() : m_encrypted(false) { } BackupItem(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) { } bool operator==(const BackupItem& rhs) const { if (this == &rhs) { return true; } return m_path == rhs.m_path; } void setPath(const std::string& path) { m_path = path; } void setBackupId(const std::string& backupId) { m_backupId = backupId; } void setDeviceName(const std::string& deviceName) { m_deviceName = deviceName; } void setDisplayName(const std::string& displayName) { m_displayName = displayName; } void setBackupTime(const std::string& backupTime) { m_backupTime = backupTime; } void setITunesVersion(const std::string& iTunesVersion) { m_iTunesVersion = iTunesVersion; } void setMacOSVersion(const std::string& macOSVersion) { m_macOSVersion = macOSVersion; } void setIOSVersion(const std::string& iOSVersion) { m_iOSVersion = iOSVersion; } std::string getIOSVersion() const { return m_iOSVersion; } bool isITunesVersionEmpty() const { return m_iTunesVersion.empty(); } void setEncrypted(bool encrypted) { m_encrypted = encrypted; } void addApp(const AppInfo& appInfo) { m_apps.push_back(appInfo); } const std::vector& getApps() const { return m_apps; } bool isEncrypted() const { return m_encrypted; } bool isValid() const { return !m_displayName.empty() && !m_backupTime.empty() && !m_deviceName.empty(); } std::string getITunesVersion() const { return m_iTunesVersion.empty() ? (m_macOSVersion.empty() ? "" : ("Embedded iTunes on MacOS " + m_macOSVersion)) : m_iTunesVersion; } std::string toString() const { return m_displayName + " [" + m_backupTime + "] (" + m_path + ")" + (m_iTunesVersion.empty() ? (" Embeded iTunes on MacOS:" + m_macOSVersion) : (" iTunes Version:" + m_iTunesVersion)); } std::string getPath() const { return m_path; } std::string getBackupId() const { return m_backupId; } }; class ITunesDb { public: class ITunesFileEnumerator { public: virtual bool isInvalid() const = 0; virtual bool nextFile(ITunesFile& file) = 0; virtual ~ITunesFileEnumerator() {} }; ITunesDb(const std::string& rootPath, const std::string& manifestFileName); virtual ~ITunesDb(); std::string getVersion() const { return m_version; } std::string getIOSVersion() const { return m_iOSVersion; } void setLoadingFilter(std::function loadingFilter) { m_loadingFilter = std::move(loadingFilter); } bool load(); bool load(const std::string& domain); virtual bool load(const std::string& domain, bool onlyFile); ITunesFileEnumerator* buildEnumerator(const std::vector& domains, bool onlyFile) const; bool copy(const std::string& destPath, const std::string& backupId, std::vector& domains, std::function& func) const; const ITunesFile* findITunesFile(const std::string& relativePath) const; std::string findFileId(const std::string& relativePath) const; std::string findRealPath(const std::string& relativePath) const; template ITunesFileVector filter(TFilter f) const; template void enumFiles(THandler handler) const; bool isMbdb() const { return m_isMbdb; } std::string getRealPath(const ITunesFile& file) const; std::string getRealPath(const ITunesFile* file) const; static unsigned int parseModifiedTime(const std::vector& data); static bool parseFileInfo(const ITunesFile* file); bool copyFile(const std::string& vpath, const std::string& dest, bool overwrite = false) const; bool copyFile(const std::string& vpath, const std::string& destPath, const std::string& destFileName, bool overwrite = false) const; #ifndef NDEBUG std::string getLastError() const { return m_lastError; } #endif protected: // bool copyMbdb(const std::string& destPath, const std::string& backupId, std::vector& domains) const; virtual std::string fileIdToRealPath(const std::string& fileId) const; ITunesFileEnumerator* buildEnumerator(const std::string& dbPath, const std::vector& domains, bool onlyFile) const; protected: bool m_isMbdb; mutable std::vector m_files; std::string m_rootPath; std::string m_manifestFileName; std::string m_version; std::string m_iOSVersion; std::function m_loadingFilter; #ifndef NDEBUG mutable std::string m_lastError; #endif }; class DecodedWechatITunesDb : public ITunesDb { public: DecodedWechatITunesDb(const std::string& rootPath, const std::string& manifestFileName); ~DecodedWechatITunesDb(); virtual bool load(const std::string& domain, bool onlyFile); protected: bool loadFiles(const std::string& root, bool onlyFile); virtual std::string fileIdToRealPath(const std::string& fileId) const; virtual bool filterFile(const std::string& relativeDir, const std::string& fileName); }; class DecodedSharedWechatITunesDb : public DecodedWechatITunesDb { public: DecodedSharedWechatITunesDb(const std::string& rootPath, const std::string& manifestFileName); ~DecodedSharedWechatITunesDb(); virtual bool load(const std::string& domain, bool onlyFile); protected: virtual bool filterFile(const std::string& relativeDir, const std::string& fileName); }; template ITunesFileVector ITunesDb::filter(TFilter f) const { ITunesFileVector files; ITunesFileRange range = std::equal_range(m_files.cbegin(), m_files.cend(), f, f); if (range.first != range.second) { for (ITunesFilesConstIterator it = range.first; it != range.second; ++it) { if (f == *it) { files.push_back(*it); } } } return files; } template void ITunesDb::enumFiles(THandler handler) const { for (ITunesFilesConstIterator it = m_files.cbegin(); it != m_files.cend(); ++it) { if (!handler(this, *it)) { break; } } } class ManifestParser { protected: std::string m_manifestPath; bool m_incudingApps; mutable std::string m_lastError; public: ManifestParser(const std::string& manifestPath, bool includingApps); virtual ~ManifestParser() {} virtual bool parse(std::vector& manifets) const; std::string getLastError() const; friend ITunesDb; protected: bool parseDirectory(const std::string& path, std::vector& manifests) const; virtual bool parse(const std::string& path, BackupItem& manifest) const; virtual bool isValidBackupItem(const std::string& path) const; virtual bool isValidMobileSync(const std::string& path) const; static bool parseInfoPlist(const std::string& backupIdPath, BackupItem& manifest, bool includingApps); static bool parseITunesMetadata(const std::string& metadata, BackupItem::AppInfo& appInfo); }; class DecodedManifestParser : public ManifestParser { public: DecodedManifestParser(const std::string& manifestPath, bool includingApps); virtual bool parse(std::vector& manifets) const; protected: virtual bool parse(const std::string& path, BackupItem& manifest) const; virtual bool isValidBackupItem(const std::string& path) const; }; #endif /* ITunesParser_h */ ================================================ FILE: WechatExporter/core/Logger.h ================================================ // // Logger.h // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include #ifndef Logger_h #define Logger_h class Logger { public: virtual void write(const std::string& log) = 0; virtual void debug(const std::string& log) = 0; virtual ~Logger() {} }; #endif /* Logger_h */ ================================================ FILE: WechatExporter/core/MMKVReader.h ================================================ // // MMKVReader.h // WechatExporter // // Created by Matthew on 2021/1/26. // Copyright © 2021 Matthew. All rights reserved. // #ifndef MMKVReader_h #define MMKVReader_h class MMKVReader { private: const unsigned char *m_ptr; size_t m_size; mutable size_t m_position; public: MMKVReader(const unsigned char *ptr, size_t size) : m_ptr(ptr), m_size(size), m_position(0) { } std::string readKey() const { std::string key; uint32_t keyLength = 0; const unsigned char* data = calcVarint32Ptr(m_ptr + m_position, m_ptr + m_size, &keyLength); m_position += data - (m_ptr + m_position); #if !defined(NDEBUG) assert(m_position <= m_size); #endif if (keyLength > 0) { auto s_size = static_cast(keyLength); if (s_size <= m_size - m_position) { key.assign((char *) (m_ptr + m_position), s_size); m_position += s_size; } else { m_position = m_size; } } #if !defined(NDEBUG) assert(m_position <= m_size); #endif return key; } void skipValue() const { uint32_t valueLength = 0; const unsigned char* data = calcVarint32Ptr(m_ptr + m_position, m_ptr + m_size, &valueLength); m_position += data - (m_ptr + m_position); if (valueLength > 0) { if ((m_position + valueLength) > m_size) { m_position = m_size; } else { m_position += valueLength; } } } std::string readStringValue() const { // MMBuffer std::string value; uint32_t valueLength = 0; const unsigned char* data = calcVarint32Ptr(m_ptr + m_position, m_ptr + m_size, &valueLength); m_position += data - (m_ptr + m_position); if (valueLength > 0) { // MMBuffer uint32_t mbbLength = 0; const unsigned char *ptr = m_ptr + m_position; ptr = calcVarint32Ptr(ptr, m_ptr + m_size, &mbbLength); #if !defined(NDEBUG) assert((m_position + valueLength) <= m_size); assert(valueLength == (ptr - (m_ptr + m_position)) + mbbLength); #endif if (mbbLength > 0) { auto s_size = static_cast(mbbLength); if (s_size <= m_size - m_position) { value.assign((char *)(ptr), s_size); m_position += valueLength; } else { m_position = m_size; } } else { m_position += valueLength; } } #if !defined(NDEBUG) assert(m_position <= m_size); #endif return value; } void seek(size_t position) const { m_position = position; } size_t getPos() const { return m_position; } bool isAtEnd() const { #if !defined(NDEBUG) assert(m_position <= m_size); #endif return m_position >= m_size; } }; #endif /* MMKVReader_h */ ================================================ FILE: WechatExporter/core/MbdbReader.h ================================================ // // MbdbReader.h // WechatExporter // // Created by Matthew on 2021/6/25. // Copyright © 2021 Matthew. All rights reserved. // #include #ifndef MbdbReader_h #define MbdbReader_h // Refer: https://www.theiphonewiki.com/wiki/ITunes_Backup#Manifest.mbdb // string Domain // string Path // string LinkTarget absolute path // string DataHash SHA.1 (some files only) // string unknown always N/A // uint16 Mode same as mbdx.Mode // uint32 unknown always 0 // uint32 unknown // uint32 UserId // uint32 GroupId mostly 501 for apps // uint32 Time1 relative to unix epoch (e.g time_t) // uint32 Time2 Time1 or Time2 is the former ModificationTime // uint32 Time3 // uint64 FileLength always 0 for link or directory // uint8 Flag 0 if special (link, directory), otherwise unknown // uint8 PropertyCount number of properties following // // Property is a couple of strings: // // string name // string value can be a string or a binary content class MbdbReader { std::ifstream m_ifs; std::vector m_buffer; #ifndef NDEBUG std::ifstream::streampos m_pos; #endif public: ~MbdbReader() { if (m_ifs.is_open()) { m_ifs.close(); } } bool open(const std::string& fileName) { m_ifs.open(fileName, std::ios_base::in | std::ios_base::binary); if (!m_ifs.is_open()) { return false; } char signature[7] = { 0 }; m_ifs.read(&signature[0], 6); if (strcmp(signature, "mbdb\5\0") != 0) { m_ifs.close(); return false; } return true; } bool hasMoreData() { return !m_ifs.eof(); } bool read(unsigned char *buffer, size_t length) { m_ifs.read(reinterpret_cast(buffer), length); return true; } bool read(std::string& str) { int b0 = m_ifs.get(); int b1 = m_ifs.get(); if ((b0 == 255 && b1 == 255) || (b0 == 0 && b1 == 0)) { str.clear(); return true; } if (b0 == std::ifstream::traits_type::eof() || b1 == std::ifstream::traits_type::eof()) { return false; } int lengthOfString = b0 * 256 + b1; m_buffer.resize(lengthOfString); m_ifs.read(&m_buffer[0], lengthOfString); str.clear(); std::copy(m_buffer.begin(), m_buffer.end(), std::back_inserter(str)); return true; } bool readD(std::string& str) { if (!read(str)) { return false; } // If only ASCII printable characters, return the string size_t i = 0, length = str.size(); for (; i < length; ++i) { if (str[i] < 32 || str[i] >= 128) { break; } } if (i == length) { return true; } // otherwise the hexadecimal dump std::string result; for (i = 0; i < length; ++i) { result.push_back(toHex(str[i] >> 4)); result.push_back(toHex(str[i] & 15)); } str.swap(result); return true; } bool skipString() { int b0 = m_ifs.get(); int b1 = m_ifs.get(); if ((b0 == 255 && b1 == 255) || (b0 == 0 && b1 == 0)) { return true; } if (b0 == std::ifstream::traits_type::eof() || b1 == std::ifstream::traits_type::eof()) { return false; } int lengthOfString = b0 * 256 + b1; m_ifs.seekg(lengthOfString, std::ios_base::cur); return true; } // static size_t skipString(const unsigned char *data, size_t length); bool skip(size_t length) { m_ifs.seekg(length, std::ios_base::cur); return true; } protected: char toHex(int value) { value &= 0xF; return (value >= 0 && value <= 9) ? (char)('0' + value) : (char)('A' + (value - 10)); } char toHexLow(int value) { value &= 0xF; return (value >= 0 && value <= 9) ? (char)('0' + value) : (char)('a' + (value - 10)); } }; #endif /* MbdbReader_h */ ================================================ FILE: WechatExporter/core/MessageParser.cpp ================================================ // // MessageParser.cpp // WechatExporter // // Created by Matthew on 2021/2/22. // Copyright © 2021 Matthew. All rights reserved. // #include "MessageParser.h" #include #include #include #include #include #include "XmlParser.h" #define ALIGNMENT_LEFT "left" #define ALIGNMENT_RIGHT "right" #define DIR_ASSETS "Files" MessageParser::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) { m_userBase = "Documents/" + m_myself.getHash(); } bool MessageParser::parse(WXMSG& msg, const Session& session, std::vector& tvs) const { TemplateValues& tv = *(tvs.emplace(tvs.end(), "msg")); tv["%%MSGID%%"] = msg.msgId; tv["%%NAME%%"] = ""; tv["%%WXNAME%%"] = ""; tv["%%TIME%%"] = fromUnixTime(msg.createTime); tv["%%MSGTYPE%%"] = std::to_string(msg.type); tv["%%MESSAGE%%"] = ""; std::string forwardedMsg; std::string forwardedMsgTitle; std::string senderId = ""; if (session.isChatroom()) { if (msg.des != 0) { std::string::size_type enter = msg.content.find(":\n"); if (enter != std::string::npos && enter + 2 < msg.content.size()) { senderId = msg.content.substr(0, enter); msg.content = msg.content.substr(enter + 2); } } } #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg_type_" + std::to_string(msg.type) + ".txt"), msg.content); writeFile(combinePath(m_outputPath, "../dbg", "msg_" + msg.msgId + ".txt"), msg.content); #endif #if !defined(NDEBUG) || defined(DBG_PERF) writeFile(combinePath(m_outputPath, "../dbg", "lastmsg.txt"), msg.content); #endif bool res = false; switch (msg.type) { case MSGTYPE_TEXT: // 1 res = parseText(msg, session, tv); break; case MSGTYPE_IMAGE: // 3 res = parseImage(msg, session, tv); break; case MSGTYPE_VOICE: // 34 res = parseVoice(msg, session, tv); break; case MSGTYPE_PUSHMAIL: // 35 res = parsePushMail(msg, session, tv); break; case MSGTYPE_VERIFYMSG: // 37 res = parseVerification(msg, session, tv); break; case MSGTYPE_POSSIBLEFRIEND: // 40 res = parsePossibleFriend(msg, session, tv); break; case MSGTYPE_SHARECARD: // 42 case MSGTYPE_IMCARD: // 66 res = parseCard(msg, session, tv); break; case MSGTYPE_VIDEO: // 43 case MSGTYPE_MICROVIDEO: // 62 res = parseVideo(msg, session, senderId, tv); break; case MSGTYPE_EMOTICON: // 47 res = parseEmotion(msg, session, tv); break; case MSGTYPE_LOCATION: // 48 res = parseLocation(msg, session, tv); break; case MSGTYPE_APP: // 49 res = parseAppMsg(msg, session, senderId, forwardedMsg, forwardedMsgTitle, tv); break; case MSGTYPE_VOIPMSG: // 50 res = parseCall(msg, session, tv); break; case MSGTYPE_VOIPNOTIFY: // 52 case MSGTYPE_VOIPINVITE: // 53 res = parseSysNotice(msg, session, tv); break; case MSGTYPE_STATUSNOTIFY: // 51 res = parseStatusNotify(msg, session, tv); break; case MSGTYPE_NOTICE: // 64 res = parseNotice(msg, session, tv); break; case MSGTYPE_SYSNOTICE: // 9999 res = parseNotice(msg, session, tv); break; case MSGTYPE_SYS: // 10000 case MSGTYPE_RECALLED: // 10002 res = parseSystem(msg, session, tv); break; default: #if !defined(NDEBUG) || defined(DBG_PERF) writeFile(combinePath(m_outputPath, "../dbg", "msg_unknwn_type_" + std::to_string(msg.type) + msg.msgId + ".txt"), msg.content); #endif res = parseText(msg, session, tv); break; } #ifndef NDEBUG if (m_resManager.hasEmojiTag(tv["%%MESSAGE%%"])) { writeFile(combinePath(m_outputPath, "../dbg", "wxemoji" + std::to_string(msg.type) + ".txt"), tv["%%MESSAGE%%"] + "\r\n\r\n"); appendFile(combinePath(m_outputPath, "../dbg", "wxemoji" + std::to_string(msg.type) + ".txt"), msg.content); } #endif std::string portraitPath = (DIR_ASSETS DIR_SEP_STR "Portrait" DIR_SEP_STR); std::string portraitUrlPath = (DIR_ASSETS "/Portrait/"); // std::string localPortrait; const Friend* protraitUser = NULL; if (session.isChatroom()) { tv["%%ALIGNMENT%%"] = (msg.des == 0) ? ALIGNMENT_RIGHT : ALIGNMENT_LEFT; if (msg.des == 0) { tv["%%NAME%%"] = m_myself.getDisplayName(); // CSS will prevent showing the name for self tv["%%WXNAME%%"] = m_myself.getWxName(); tv["%%AVATAR%%"] = portraitUrlPath + m_myself.getLocalPortrait(); // remotePortrait = m_myself.getPortrait(); protraitUser = &m_myself; } else { if (!senderId.empty()) { std::string senderHash = md5(senderId); std::string senderDisplayName = session.getMemberName(senderId); const Friend *f = m_friends.getFriend(senderHash); if (senderDisplayName.empty() && NULL != f) { senderDisplayName = f->getDisplayName(); } tv["%%NAME%%"] = senderDisplayName.empty() ? senderId : senderDisplayName; if (NULL != f) { protraitUser = f; tv["%%WXNAME%%"] = f->getWxName(); } if (NULL == f) { ensureDefaultPortraitIconExisted(combinePath(session.getOutputFileName(), portraitPath)); } tv["%%AVATAR%%"] = portraitUrlPath + ((NULL != f) ? f->getLocalPortrait() : "DefaultAvatar.png"); } else { tv["%%NAME%%"] = senderId; tv["%%AVATAR%%"] = ""; } } } else { if (msg.des == 0 || session.getUsrName() == m_myself.getUsrName()) { tv["%%ALIGNMENT%%"] = ALIGNMENT_RIGHT; tv["%%NAME%%"] = m_myself.getDisplayName(); tv["%%WXNAME%%"] = m_myself.getWxName(); tv["%%AVATAR%%"] = portraitUrlPath + m_myself.getLocalPortrait(); protraitUser = &m_myself; } else { tv["%%ALIGNMENT%%"] = ALIGNMENT_LEFT; const Friend *f = m_friends.getFriend(session.getHash()); if (NULL == f) { tv["%%NAME%%"] = session.getDisplayName(); tv["%%WXNAME%%"] = session.getWxName(); if (session.isPortraitEmpty()) { ensureDefaultPortraitIconExisted(portraitPath); } // localPortrait = combinePath(session.getOutputFileName(), portraitPath + (session.isPortraitEmpty() ? "DefaultAvatar.png" : session.getLocalPortrait())); // remotePortrait = session.getPortrait(); tv["%%AVATAR%%"] = portraitUrlPath + (session.isPortraitEmpty() ? "DefaultAvatar.png" : session.getLocalPortrait()); protraitUser = &session; } else { tv["%%NAME%%"] = f->getDisplayName(); tv["%%WXNAME%%"] = f->getWxName(); // localPortrait = portraitPath + f->getLocalPortrait(); // remotePortrait = f->getPortrait(); tv["%%AVATAR%%"] = portraitUrlPath + f->getLocalPortrait(); protraitUser = f; } } } if (!m_options.isTextMode()) { if (NULL != protraitUser) { copyPortraitIcon(&session, *protraitUser, combinePath(m_outputPath, session.getOutputFileName(), portraitPath)); // m_downloader.addTask(remotePortrait, combinePath(m_outputPath, session.getOutputFileName(), localPortrait), msg.createTime); } } if (!m_options.isTextMode()) { tv["%%NAME%%"] = safeHTML(tv["%%NAME%%"]); } if (!forwardedMsg.empty()) { // This funtion will change tvs and causes tv invalid, so we do it at last parseForwardedMsgs(session, msg, forwardedMsgTitle, forwardedMsg, tvs); } return res; } bool MessageParser::parsePortrait(const WXMSG& msg, const Session& session, const std::string& senderId, TemplateValues& tv) const { return true; } ///////////////////////////////////// bool MessageParser::parseText(const WXMSG& msg, const Session& session, TemplateValues& tv) const { if (!m_options.isTextMode()) { // std::string assetsDir = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS); // tv["%%MESSAGE%%"] = safeHTML(msg.content); // tv["%%MESSAGE%%"] = m_resManager.convertEmojis(safeHTML(msg.content), combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS); tv["%%MESSAGE%%"] = m_resManager.convertEmojis(msg.content, combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS); } else { tv["%%MESSAGE%%"] = msg.content; } return true; } bool MessageParser::parseImage(const WXMSG& msg, const Session& session, TemplateValues& tv) const { std::string vFile = combinePath(m_userBase, "Img", session.getHash(), msg.msgId); 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); } bool MessageParser::parseVoice(const WXMSG& msg, const Session& session, TemplateValues& tv) const { std::string audioSrc; int voiceLen = -1; std::string vLenStr; std::string voiceFormat; XmlParser xmlParser(msg.content); if (xmlParser.parseAttributeValue("/msg/voicemsg", "voicelength", vLenStr) && !vLenStr.empty()) { voiceLen = std::stoi(vLenStr); } if (!xmlParser.parseAttributeValue("/msg/voicemsg", "voiceformat", voiceFormat)) { voiceFormat = "4"; } const ITunesFile* audioSrcFile = NULL; if (!m_options.isTextMode()) { audioSrcFile = m_iTunesDb.findITunesFile(combinePath(m_userBase, "Audio", session.getHash(), msg.msgId + ".aud")); if (NULL != audioSrcFile) { audioSrc = m_iTunesDb.getRealPath(*audioSrcFile); } } bool result = false; if (!audioSrc.empty()) { // std::string audCopyPath = combinePath(m_outputPath, "..", "dbg"); // if (!existsDirectory(audCopyPath)) // { // makeDirectory(audCopyPath); // } // audCopyPath = combinePath(audCopyPath, msg.msgId + ".aud"); // copyFile(audioSrc, combinePath(audCopyPath, msg.msgId + ".aud")); // writeFile(combinePath(audCopyPath, msg.msgId + ".aud.xml"), msg.content); #ifdef USING_ASYNC_TASK_FOR_MP3 std::string assetsDir = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS); ensureDirectoryExisted(assetsDir); std::string mp3Path = combinePath(assetsDir, msg.msgId + ".mp3"); m_taskManager.convertAudio(&session, audioSrc, mp3Path, (voiceFormat == "0") ? TaskManager::AUDIO_FORMAT_AMR : TaskManager::AUDIO_FORMAT_SILK, ITunesDb::parseModifiedTime(audioSrcFile->blob)); result = true; #else std::string assetsDir = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS); std::string mp3Path = combinePath(assetsDir, msg.msgId + ".mp3"); ensureDirectoryExisted(assetsDir); m_pcmData.clear(); m_error.clear(); std::string err; bool isSilk = false; // SILK-v3 if (silkToPcm(audioSrc, m_pcmData, isSilk, &err) && !m_pcmData.empty()) { #ifndef NDEBUG // std::string audCopyPath = combinePath(m_outputPath, "..", "dbg"); // audCopyPath = combinePath(audCopyPath, "aud", msg.msgId + ".pcm"); // writeFile(audCopyPath, m_pcmData); #endif if (pcmToMp3(m_pcmData, mp3Path, &err)) { // copyFile(mp3Path, combinePath(audCopyPath, msg.msgId + ".slk." + voiceFormat + ".mp3")); result = true; } } else if (!isSilk) { // AMR if (amrToPcm(audioSrc, m_pcmData, &err) && !m_pcmData.empty()) { if (amrPcmToMp3(m_pcmData, mp3Path, &err)) { // copyFile(mp3Path, combinePath(audCopyPath, msg.msgId + ".amr." + (voiceFormat != "" ? voiceFormat : "empty") + ".mp3")); #ifndef NDEBUG // std::string audCopyPath = combinePath(m_outputPath, "..", "dbg"); // audCopyPath = combinePath(audCopyPath, "aud", msg.msgId + ".amr.mp3"); // copyFile(mp3Path, audCopyPath); #endif result = true; } } } if (result) { updateFileTime(mp3Path, ITunesDb::parseModifiedTime(audioSrcFile->blob)); } else { m_error += err + "\r\nvoiceFormat:*" + voiceFormat + "* " + combinePath(m_userBase, "Audio", session.getHash(), msg.msgId + ".aud"); } if (!result) { #ifndef NDEBUG std::string audCopyPath = combinePath(m_outputPath, "..", "dbg"); if (!existsDirectory(audCopyPath)) { makeDirectory(audCopyPath); } audCopyPath = combinePath(audCopyPath, msg.msgId + ".aud"); copyFile(audioSrc, audCopyPath); writeFile(audCopyPath + ".xml", msg.content); #endif } #endif } if (result) { tv.setName("audio"); tv["%%AUDIOPATH%%"] = (DIR_ASSETS "/") + msg.msgId + ".mp3"; tv["%%AUDIOTIME%%"] = getDisplayTime(voiceLen); tv["%%AUDIOLENGTH%%"] = voiceLen == -1 ? "" : std::to_string((int)(voiceLen * 300 / 60000)); // max-width: 300px <=> 60s } else { tv.setName("msg"); tv["%%MESSAGE%%"] = voiceLen == -1 ? m_resManager.getLocaleString("[Audio]") : formatString(m_resManager.getLocaleString("[Audio %s]"), getDisplayTime(voiceLen).c_str()); } return result; } bool MessageParser::parsePushMail(const WXMSG& msg, const Session& session, TemplateValues& tv) const { std::string subject; std::string digest; XmlParser xmlParser(msg.content); xmlParser.parseNodeValue("/msg/pushmail/content/subject", subject); xmlParser.parseNodeValue("/msg/pushmail/content/digest", digest); tv.setName("plainshare"); tv["%%SHARINGURL%%"] = "##"; tv["%%SHARINGTITLE%%"] = subject; tv["%%MESSAGE%%"] = digest; return true; } bool MessageParser::parseVideo(const WXMSG& msg, const Session& session, std::string& senderId, TemplateValues& tv) const { std::map attrs = { {"fromusername", ""}, {"cdnthumbwidth", ""}, {"cdnthumbheight", ""} }; XmlParser xmlParser(msg.content); if (xmlParser.parseAttributesValue("/msg/videomsg", attrs)) { } if (senderId.empty()) { senderId = attrs["fromusername"]; } std::string vfile = combinePath(m_userBase, "Video", session.getHash(), msg.msgId); 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); } bool MessageParser::parseEmotion(const WXMSG& msg, const Session& session, TemplateValues& tv) const { std::string url; if (!m_options.isTextMode()) { XmlParser xmlParser(msg.content); if (!xmlParser.parseAttributeValue("/msg/emoji", "cdnurl", url)) { url.clear(); } else { if (!startsWith(url, "http") && startsWith(url, "https")) { if (!xmlParser.parseAttributeValue("/msg/emoji", "thumburl", url)) { url.clear(); } } } } if (startsWith(url, "http") || startsWith(url, "https")) { tv.setName("emoji"); if (!m_options.isUsingRemoteEmoji()) { std::string emojiFile = url; std::smatch sm2; static std::regex pattern47_2("\\/(\\w+?)\\/\\w*$"); if (std::regex_search(emojiFile, sm2, pattern47_2)) { emojiFile = sm2[1]; } else { static int uniqueFileName = 1000000000; emojiFile = std::to_string(uniqueFileName++); } std::string emojiPath = (DIR_ASSETS DIR_SEP_STR "Emoji" DIR_SEP_STR); std::string emojiUrlPath = (DIR_ASSETS "/Emoji/"); std::string localEmojiPath = normalizePath((const std::string&)emojiPath); std::string localEmojiFile = localEmojiPath + emojiFile + ".gif"; ensureDirectoryExisted(combinePath(m_outputPath, session.getOutputFileName(), localEmojiPath)); #ifdef USING_DOWNLOADER m_downloader.addTask(url, combinePath(m_outputPath, session.getOutputFileName(), localEmojiFile), msg.createTime, "emoji"); #else m_taskManager.download(&session, url, "", combinePath(m_outputPath, session.getOutputFileName(), localEmojiFile), msg.createTime, "", "emoji"); #endif tv["%%EMOJIPATH%%"] = emojiUrlPath + emojiFile + ".gif"; tv["%%RAWEMOJIPATH%%"] = url; } else { tv["%%EMOJIPATH%%"] = url; tv["%%RAWEMOJIPATH%%"] = url; } } else { tv.setName("msg"); tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Emoji]"); } return true; } bool MessageParser::parseAppMsg(const WXMSG& msg, const Session& session, std::string& senderId, std::string& forwardedMsg, std::string& forwardedMsgTitle, TemplateValues& tv) const { WXAPPMSG appMsg = {&msg, 0, std::string(), std::string(), senderId}; XmlParser xmlParser(msg.content, true); if (senderId.empty()) { xmlParser.parseNodeValue("/msg/fromusername", senderId); } std::string appMsgTypeStr; if (!xmlParser.parseNodeValue("/msg/appmsg/type", appMsgTypeStr)) { // Failed to parse APPMSG type #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg" + std::to_string(msg.type) + "_app_invld_" + msg.msgId + ".txt"), msg.content); #endif tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Link]"); return true; } tv.setName("plainshare"); #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg" + std::to_string(msg.type) + "_app_" + appMsgTypeStr + ".txt"), msg.content); #endif appMsg.appMsgType = std::atoi(appMsgTypeStr.c_str()); xmlParser.parseAttributeValue("/msg/appmsg", "appid", appMsg.appId); if (!appMsg.appId.empty()) { xmlParser.parseNodeValue("/msg/appinfo/appname", appMsg.appName); tv["%%APPNAME%%"] = appMsg.appName; std::string vFile = combinePath(m_userBase, "appicon", appMsg.appId + ".png"); std::string portraitDir = (DIR_ASSETS DIR_SEP_STR "Portrait"); if (m_iTunesDb.copyFile(vFile, combinePath(m_outputPath, session.getOutputFileName(), portraitDir), "appicon_" + appMsg.appId + ".png")) { std::string portraitUrlDir = (DIR_ASSETS "/Portrait"); appMsg.localAppIcon = portraitUrlDir + "/appicon_" + appMsg.appId + ".png"; tv["%%APPICONPATH%%"] = appMsg.localAppIcon; } } bool res = false; switch (appMsg.appMsgType) { case APPMSGTYPE_TEXT: // 1 res = parseAppMsgText(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_IMG: // 2 res = parseAppMsgImage(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_AUDIO: // 3 res = parseAppMsgAudio(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_VIDEO: // 4 res = parseAppMsgVideo(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_URL: // 5 res = parseAppMsgUrl(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_ATTACH: // 6 res = parseAppMsgAttachment(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_OPEN: // 7 res = parseAppMsgOpen(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_EMOJI: // 8 res = parseAppMsgEmoji(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_VOICE_REMIND: // 9 res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_SCAN_GOOD: // 10 res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_GOOD: // 13 res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_EMOTION: // 15 res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_CARD_TICKET: // 16 res = parseAppMsgDefault(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_REALTIME_LOCATION: // 17 res = parseAppMsgRtLocation(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_FWD_MSG: // 19 res = parseAppMsgFwdMsg(appMsg, xmlParser, session, forwardedMsg, forwardedMsgTitle, tv); break; case APPMSGTYPE_NOTE: // 24 res = parseAppMsgNote(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_CHANNEL_CARD: // 50 res = parseAppMsgChannelCard(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_CHANNELS: // 51 res = parseAppMsgChannels(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_REFER: // 57 res = parseAppMsgRefer(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_PAT: // 62 res = parseAppMsgPat(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_TRANSFERS: // 2000 res = parseAppMsgTransfer(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_RED_ENVELOPES: // 2001 res = parseAppMsgRedPacket(appMsg, xmlParser, session, tv); break; case APPMSGTYPE_READER_TYPE: // 100001 res = parseAppMsgReaderType(appMsg, xmlParser, session, tv); break; default: res = parseAppMsgUnknownType(appMsg, xmlParser, session, tv); break; } #ifndef NDEBUG if (m_resManager.hasEmojiTag(tv["%%MESSAGE%%"])) { writeFile(combinePath(m_outputPath, "../dbg", "wxemoji_app_" + std::to_string(msg.type) + "_" + std::to_string(appMsg.appMsgType) + ".txt"), tv["%%MESSAGE%%"] + "\r\n\r\n"); appendFile(combinePath(m_outputPath, "../dbg", "wxemoji_app_" + std::to_string(msg.type) + "_" + std::to_string(appMsg.appMsgType) + ".txt"), msg.content); } #endif #ifndef NDEBUG std::string vThumbFile = m_userBase + "/OpenData/" + session.getHash() + "/" + appMsg.msg->msgId + ".pic_thum"; std::string destPath = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS, appMsg.msg->msgId + "_thum.jpg"); std::string fileId = m_iTunesDb.findFileId(vThumbFile); if (!fileId.empty() && !existsFile(destPath)) { if (appMsg.appMsgType != 19) { // assert(false); } } #endif return res; } bool MessageParser::parseCall(const WXMSG& msg, const Session& session, TemplateValues& tv) const { tv.setName("msg"); tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Video/Audio Call]"); return true; } bool MessageParser::parseLocation(const WXMSG& msg, const Session& session, TemplateValues& tv) const { std::map attrs = { {"x", ""}, {"y", ""}, {"label", ""}, {"poiname", ""} }; XmlParser xmlParser(msg.content); xmlParser.parseAttributesValue("/msg/location", attrs); std::string location = (!attrs["poiname"].empty() && !attrs["label"].empty()) ? (attrs["poiname"] + " - " + attrs["label"]) : (attrs["poiname"] + attrs["label"]); if (!location.empty()) { tv["%%MESSAGE%%"] = formatString(m_resManager.getLocaleString("[Location] %s (%s,%s)"), location.c_str(), attrs["x"].c_str(), attrs["y"].c_str()); } else { tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Location]"); } tv.setName("msg"); return true; } bool MessageParser::parseStatusNotify(const WXMSG& msg, const Session& session, TemplateValues& tv) const { #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg_" + std::to_string(msg.type) + msg.msgId + ".txt"), msg.content); #endif return parseText(msg, session, tv); } bool MessageParser::parsePossibleFriend(const WXMSG& msg, const Session& session, TemplateValues& tv) const { #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg_" + std::to_string(msg.type) + msg.msgId + ".txt"), msg.content); #endif return parseText(msg, session, tv); } bool MessageParser::parseVerification(const WXMSG& msg, const Session& session, TemplateValues& tv) const { #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg_" + std::to_string(msg.type) + msg.msgId + ".txt"), msg.content); #endif return parseText(msg, session, tv); } bool MessageParser::parseCard(const WXMSG& msg, const Session& session, TemplateValues& tv) const { std::string portraitDir = (DIR_ASSETS DIR_SEP_STR "Portrait"); std::string portraitUrlDir = (DIR_ASSETS "/Portrait"); return parseCard(session, m_outputPath, portraitDir, portraitUrlDir, msg.content, tv); } bool MessageParser::parseNotice(const WXMSG& msg, const Session& session, TemplateValues& tv) const { #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg_" + std::to_string(msg.type) + msg.msgId + ".txt"), msg.content); #endif tv.setName("notice"); Json::CharReaderBuilder builder; std::unique_ptr reader(builder.newCharReader()); Json::Value root; if (reader->parse(msg.content.c_str(), msg.content.c_str() + msg.content.size(), &root, NULL)) { tv["%%MESSAGE%%"] = root["msgContent"].asString(); } return true; } bool MessageParser::parseSysNotice(const WXMSG& msg, const Session& session, TemplateValues& tv) const { #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg_" + std::to_string(msg.type) + msg.msgId + ".txt"), msg.content); #endif tv.setName("notice"); std::string sysMsg = msg.content; removeHtmlTags(sysMsg); tv["%%MESSAGE%%"] = sysMsg; return true; } bool MessageParser::parseSystem(const WXMSG& msg, const Session& session, TemplateValues& tv) const { tv.setName("notice"); if (startsWith(msg.content, "设置拍一拍,朋友拍了拍你的头像后会出现设置的内容。 std::string plainText = msg.content; removeHtmlTags(plainText); tv["%%MESSAGE%%"] = plainText; #ifndef NDEBUG plainText.clear(); auto pos = msg.content.find(""); if (pos != std::string::npos) { auto pos2 = msg.content.find(">", 17); // length of <_wc_custom_link_ if (pos2 != std::string::npos) { plainText = msg.content.substr(pos2 + 1, pos - (pos2 + 1)); } plainText += msg.content.substr(pos + 19); // } if (plainText != tv["%%MESSAGE%%"]) { // int aa = 0; } #endif } else { #ifndef NDEBUG if (startsWith(msg.content, "<") && !startsWith(msg.content, "msgId + ".pic_thum"; std::string destPath = combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS); if (m_iTunesDb.copyFile(vThumbFile, destPath, appMsg.msg->msgId + "_thum.jpg")) { thumbUrl = (DIR_ASSETS "/") + appMsg.msg->msgId + "_thum.jpg"; } else { xmlParser.parseNodeValue("/msg/appmsg/thumburl", thumbUrl); if (thumbUrl.empty()) { thumbUrl = appMsg.localAppIcon; } } tv.setName(thumbUrl.empty() ? "plainshare" : "share"); tv["%%SHARINGIMGPATH%%"] = thumbUrl; tv["%%SHARINGTITLE%%"] = title; tv["%%SHARINGURL%%"] = url; tv["%%MESSAGE%%"] = desc; return true; } bool MessageParser::parseAppMsgAttachment(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg_" + std::to_string(appMsg.msg->type) + "_attach_" + appMsg.msg->msgId + ".txt"), appMsg.msg->content); #endif std::string title; std::string attachFileExtName; xmlParser.parseNodeValue("/msg/appmsg/title", title); xmlParser.parseNodeValue("/msg/appmsg/appattach/fileext", attachFileExtName); std::string attachFileName = m_userBase + "/OpenData/" + session.getHash() + "/" + appMsg.msg->msgId; std::string attachOutputFileName = appMsg.msg->msgId; if (!attachFileExtName.empty()) { attachFileName += "." + attachFileExtName; attachOutputFileName += "." + attachFileExtName; } return parseFile(combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS, attachFileName, attachOutputFileName, title, tv); } bool MessageParser::parseAppMsgOpen(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { return parseAppMsgDefault(appMsg, xmlParser, session, tv); } bool MessageParser::parseAppMsgEmoji(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { // Can't parse the detail info of emoji as the url is encrypted return parseAppMsgDefault(appMsg, xmlParser, session, tv); } bool MessageParser::parseAppMsgRtLocation(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Real-time Location]"); return true; } bool MessageParser::parseAppMsgFwdMsg(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, std::string& forwardedMsg, std::string& forwardedMsgTitle, TemplateValues& tv) const { std::string title; xmlParser.parseNodeValue("/msg/appmsg/title", title); xmlParser.parseNodeValue("/msg/appmsg/recorditem", forwardedMsg); #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg" + std::to_string(appMsg.msg->type) + "_app_19.txt"), forwardedMsg); #endif tv.setName("msg"); tv["%%MESSAGE%%"] = title; forwardedMsgTitle = title; return true; } bool MessageParser::parseAppMsgCard(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { return parseAppMsgDefault(appMsg, xmlParser, session, tv); } bool MessageParser::parseAppMsgChannelCard(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { // Channel Card std::map nodes = { {"username", ""}, {"avatar", ""}, {"nickname", ""}}; xmlParser.parseNodesValue("/msg/appmsg/findernamecard/*", nodes); std::string portraitDir = (DIR_ASSETS DIR_SEP_STR "Portrait"); std::string portraitUrlDir = (DIR_ASSETS "/Portrait"); return parseChannelCard(session, portraitDir, portraitUrlDir, nodes["username"], nodes["avatar"], "", nodes["nickname"], tv); } bool MessageParser::parseAppMsgChannels(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { #ifndef NDEBUG 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); #endif return parseChannels(appMsg.msg->msgId, xmlParser, NULL, "/msg/appmsg/finderFeed", session, tv); } bool MessageParser::parseAppMsgRefer(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { // Refer Message std::string title; xmlParser.parseNodeValue("/msg/appmsg/title", title); std::map nodes = { {"displayname", ""}, {"content", ""}, {"type", ""}}; if (xmlParser.parseNodesValue("/msg/appmsg/refermsg/*", nodes)) { #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg" + std::to_string(appMsg.msg->type) + "_app_" + std::to_string(APPMSGTYPE_REFER) + "_ref_" + nodes["type"] + " .txt"), nodes["content"]); #endif tv.setName("refermsg"); tv["%%MESSAGE%%"] = m_resManager.convertEmojis(title, combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS); tv["%%REFERNAME%%"] = nodes["displayname"]; if (nodes["type"] == "43") { tv["%%REFERMSG%%"] = m_resManager.getLocaleString("[Video]"); } else if (nodes["type"] == "1") { if (!m_options.isTextMode()) { tv["%%REFERMSG%%"] = m_resManager.convertEmojis(nodes["content"], combinePath(m_outputPath, session.getOutputFileName()), DIR_ASSETS, DIR_ASSETS); } else { tv["%%REFERMSG%%"] = nodes["content"]; } } else if (nodes["type"] == "3") { tv["%%REFERMSG%%"] = m_resManager.getLocaleString("[Photo]"); } else if (nodes["type"] == "49") { // APPMSG XmlParser subAppMsgXmlParser(nodes["content"], true); std::string subAppMsgTitle; subAppMsgXmlParser.parseNodeValue("/msg/appmsg/title", subAppMsgTitle); tv["%%REFERMSG%%"] = subAppMsgTitle; } else { tv["%%REFERMSG%%"] = nodes["content"]; } } else { tv.setName("msg"); tv["%%MESSAGE%%"] = title; } return true; } bool MessageParser::parseAppMsgTransfer(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { #ifndef NDEBUG writeFile(combinePath(m_outputPath, "../dbg", "msg" + std::to_string(appMsg.msg->type) + "_app_transfer_" + appMsg.msg->msgId + ".xml"), appMsg.msg->content); #endif std::map nodes = { {"title", ""}, {"type", ""}, {"des", ""}, {"url", ""}, {"thumburl", ""}, {"recorditem", ""} }; xmlParser.parseNodesValue("/msg/appmsg/*", nodes); std::map transferNode = { {"paysubtype", ""}, {"feedesc", ""}, {"pay_memo", ""}, {"receiver_username", ""}, {"payer_username", ""} }; xmlParser.parseNodesValue("/msg/appmsg/wcpayinfo/*", transferNode); // paysubtype: // // 3,4 received receive rejected // 8,9 send send rejected std::string content = transferNode["feedesc"]; const std::string& memo = transferNode["pay_memo"]; const std::string& paySubType = transferNode["paysubtype"]; std::string paySubTypeKey = (session.isChatroom() ? "Group_Transfer_Subtype_" : "Transfer_Subtype_") + paySubType; std::string paySubTypeDesc = m_resManager.getLocaleString(paySubTypeKey); if (paySubTypeDesc == paySubTypeKey) { paySubTypeDesc = ""; } else { if (session.isChatroom()) { if (paySubType == "1" || paySubType == "8" || paySubType == "9") { // Pay to std::string displayName = getMemberDisplayName(transferNode["receiver_username"], session); paySubTypeDesc = formatString(paySubTypeDesc, displayName.c_str()); } else if (paySubType == "3" || paySubType == "4" || paySubType == "5") { // 3 Accepted // 5 Accepted and waiting for ... // From std::string displayName = getMemberDisplayName(transferNode["payer_username"], session); paySubTypeDesc = formatString(paySubTypeDesc, displayName.c_str()); } } } if (!paySubTypeDesc.empty()) { content += " - "; content += paySubTypeDesc; } if (!memo.empty()) { content += "\r\n"; content += memo; } tv["%%MESSAGE%%"] = content; tv.setName("transfer"); return true; } bool MessageParser::parseAppMsgRedPacket(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Red Packet]"); return true; } bool MessageParser::parseAppMsgPat(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { // int recordNum = 0; // xmlParser.parseNodeValue("/msg/appmsg/patMsg/records/record/fromUser", fromUser); std::string fromUser; std::string temp; std::string pattedUser; xmlParser.parseNodeValue("/msg/appmsg/patMsg/records/record/fromUser", fromUser); xmlParser.parseNodeValue("/msg/appmsg/patMsg/records/record/templete", temp); xmlParser.parseNodeValue("/msg/appmsg/patMsg/records/record/pattedUser", pattedUser); std::string fromUserName; std::string pattedUserName; Friend* f = m_friends.getFriendByUid(fromUser); if (NULL != f) { fromUserName = f->getDisplayName(); } else { fromUserName = session.getMemberName(fromUser); } f = m_friends.getFriendByUid(pattedUser); if (NULL != f) { pattedUserName = f->getDisplayName(); } else { pattedUserName = session.getMemberName(pattedUser); } replaceAll(temp, "${" + fromUser + "}", fromUserName); replaceAll(temp, "${" + pattedUser + "}", pattedUserName); tv["%%MESSAGE%%"] = temp; tv.setName("system"); return true; } bool MessageParser::parseAppMsgReaderType(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { return parseAppMsgDefault(appMsg, xmlParser, session, tv); } bool MessageParser::parseAppMsgNote(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { #if !defined(NDEBUG) || defined(DBG_PERF) writeFile(combinePath(m_outputPath, "../dbg", "msg" + std::to_string(appMsg.msg->type) + "_app_24_" + appMsg.msg->msgId + ".txt"), appMsg.msg->content); #endif std::map nodes = { {"title", ""}, {"type", ""}, {"des", ""}, {"url", ""}, {"thumburl", ""}, {"recorditem", ""} }; xmlParser.parseNodesValue("/msg/appmsg/*", nodes); removeSupportUrl(nodes["url"]); if (nodes["title"].empty() && !nodes["des"].empty()) { nodes["title"].swap(nodes["des"]); } if (!nodes["recorditem"].empty()) { XmlParser xmlParser2(nodes["recorditem"], true); std::map nodes2 = { {"info", ""}, {"desc", ""}, {"edittime", ""}, {"favusername", ""} }; xmlParser2.parseNodesValue("/recordinfo/*", nodes2); if (!nodes2["info"].empty()) { nodes["title"] = nodes2["info"]; } else if (!nodes2["desc"].empty()) { nodes["title"] = nodes2["desc"]; } } if (!nodes["title"].empty() && !nodes["url"].empty()) { bool isPlainShare = nodes["thumburl"].empty(); tv.setName(isPlainShare ? "plainshare" : "share"); tv["%%SHARINGIMGPATH%%"] = nodes["thumburl"]; tv["%%SHARINGURL%%"] = nodes["url"]; tv["%%SHARINGTITLE%%"] = nodes["title"]; tv["%%MESSAGE%%"] = nodes["des"]; } else if (!nodes["title"].empty()) { tv["%%MESSAGE%%"] = nodes["title"]; } else { tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Link]"); } return true; } bool MessageParser::parseAppMsgUnknownType(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { #if !defined(NDEBUG) || defined(DBG_PERF) writeFile(combinePath(m_outputPath, "../dbg", "msg" + std::to_string(appMsg.msg->type) + "_app_unknwn_" + std::to_string(appMsg.appMsgType) + ".txt"), appMsg.msg->content); #endif return parseAppMsgDefault(appMsg, xmlParser, session, tv); } bool MessageParser::parseAppMsgDefault(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const { std::map nodes = { {"title", ""}, {"type", ""}, {"des", ""}, {"url", ""}, {"thumburl", ""}, {"recorditem", ""} }; xmlParser.parseNodesValue("/msg/appmsg/*", nodes); if (!nodes["title"].empty() && !nodes["url"].empty()) { tv.setName(nodes["thumburl"].empty() ? "plainshare" : "share"); tv["%%SHARINGIMGPATH%%"] = nodes["thumburl"]; tv["%%SHARINGURL%%"] = nodes["url"]; tv["%%SHARINGTITLE%%"] = nodes["title"]; tv["%%MESSAGE%%"] = nodes["des"]; } else if (!nodes["title"].empty()) { tv["%%MESSAGE%%"] = nodes["title"]; } else { tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Link]"); } return true; } //////////////////////////////// bool MessageParser::parseFwdMsgText(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNode *itemNode, const Session& session, TemplateValues& tv) const { std::string message; xmlParser.getChildNodeContent(itemNode, "datadesc", message); static std::vector> replaces = { {"\r\n", "
"}, {"\r", "
"}, {"\n", "
"}}; replaceAll(message, replaces); tv["%%MESSAGE%%"] = message; return true; } bool MessageParser::parseFwdMsgImage(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNode *itemNode, const Session& session, TemplateValues& tv) const { std::string fileExtName = fwdMsg.dataFormat.empty() ? "" : ("." + fwdMsg.dataFormat); std::string vfile = m_userBase + "/OpenData/" + session.getHash() + "/" + fwdMsg.msg->msgId + "/" + fwdMsg.dataId; 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); } bool MessageParser::parseFwdMsgVideo(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const { std::string fileExtName = fwdMsg.dataFormat.empty() ? "" : ("." + fwdMsg.dataFormat); std::string vfile = m_userBase + "/OpenData/" + session.getHash() + "/" + fwdMsg.msg->msgId + "/" + fwdMsg.dataId; 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); } bool MessageParser::parseFwdMsgLink(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const { std::string link; std::string title; std::string thumbUrl; std::string message; xmlNodePtr urlItemNode = xmlParser.getChildNode(itemNode, "weburlitem"); if (NULL != urlItemNode) { XmlParser::getChildNodeContent(urlItemNode, "title", title); XmlParser::getChildNodeContent(urlItemNode, "link", link); XmlParser::getChildNodeContent(urlItemNode, "thumburl", thumbUrl); XmlParser::getChildNodeContent(urlItemNode, "desc", message); } bool hasThumb = false; if (!m_options.isTextMode()) { std::string vfile = m_userBase + "/OpenData/" + session.getHash() + "/" + fwdMsg.msg->msgId + "/" + fwdMsg.dataId + ".record_thumb"; hasThumb = m_iTunesDb.copyFile(vfile, combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS, fwdMsg.msg->msgId), fwdMsg.dataId + "_thumb.jpg"); } if (!(link.empty())) { tv.setName(hasThumb ? "share" : "plainshare"); tv["%%SHARINGIMGPATH%%"] = (DIR_ASSETS "/") + fwdMsg.msg->msgId + "/" + fwdMsg.dataId + "_thumb.jpg"; tv["%%SHARINGURL%%"] = link; tv["%%SHARINGTITLE%%"] = title; tv["%%MESSAGE%%"] = message; } else { tv["%%MESSAGE%%"] = title; } return true; } bool MessageParser::parseFwdMsgLocation(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const { std::string label; std::string message; std::string lng; std::string lat; xmlNodePtr locItemNode = xmlParser.getChildNode(itemNode, "locitem"); if (NULL != locItemNode) { XmlParser::getChildNodeContent(locItemNode, "label", label); XmlParser::getChildNodeContent(locItemNode, "poiname", message); XmlParser::getChildNodeContent(locItemNode, "lat", lat); XmlParser::getChildNodeContent(locItemNode, "lng", lng); } std::string location = (!message.empty() && !label.empty()) ? (message + " - " + label) : (message + label); if (!location.empty()) { tv["%%MESSAGE%%"] = formatString(m_resManager.getLocaleString("[Location] %s (%s,%s)"), location.c_str(), lat.c_str(), lng.c_str()); } else { tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Location]"); } tv.setName("msg"); return true; } bool MessageParser::parseFwdMsgAttach(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const { std::string message; xmlParser.getChildNodeContent(itemNode, "datatitle", message); std::string fileExtName = fwdMsg.dataFormat.empty() ? "" : ("." + fwdMsg.dataFormat); std::string vfile = m_userBase + "/OpenData/" + session.getHash() + "/" + fwdMsg.msg->msgId + "/" + fwdMsg.dataId; return parseFile(combinePath(m_outputPath, session.getOutputFileName()), (DIR_ASSETS "/") + fwdMsg.msg->msgId, (DIR_ASSETS "/") + fwdMsg.msg->msgId, vfile + fileExtName, fwdMsg.dataId + fileExtName, message, tv); } bool MessageParser::parseFwdMsgCard(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const { std::string cardContent; xmlParser.getChildNodeContent(itemNode, "datadesc", cardContent); std::string portraitDir = (DIR_ASSETS DIR_SEP_STR "Portrait"); std::string portraitUrlDir = (DIR_ASSETS "/Portrait"); return parseCard(session, m_outputPath, portraitDir, portraitUrlDir, cardContent, tv); } bool MessageParser::parseFwdMsgNestedFwdMsg(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, std::string& nestedFwdMsg, std::string& nestedFwdMsgTitle, TemplateValues& tv) const { xmlParser.getChildNodeContent(itemNode, "datadesc", nestedFwdMsgTitle); xmlNodePtr nodeRecordInfo = XmlParser::getChildNode(itemNode, "recordinfo"); if (NULL != nodeRecordInfo) { nestedFwdMsg = XmlParser::getNodeOuterXml(nodeRecordInfo); } tv["%%MESSAGE%%"] = nestedFwdMsgTitle; return true; } bool MessageParser::parseFwdMsgMiniProgram(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const { std::string title; xmlParser.getChildNodeContent(itemNode, "datatitle", title); tv["%%MESSAGE%%"] = title; return true; } bool MessageParser::parseFwdMsgChannels(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const { return parseChannels(fwdMsg.msg->msgId, xmlParser, itemNode, "./finderFeed", session, tv); } bool MessageParser::parseFwdMsgChannelCard(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const { std::string usrName; std::string avatar; std::string nickName; xmlNodePtr cardItemNode = xmlParser.getChildNode(itemNode, "finderShareNameCard"); if (NULL != cardItemNode) { XmlParser::getChildNodeContent(cardItemNode, "username", usrName); XmlParser::getChildNodeContent(cardItemNode, "avatar", avatar); XmlParser::getChildNodeContent(cardItemNode, "nickname", nickName); } std::string portraitDir = (DIR_ASSETS DIR_SEP_STR "Portrait"); std::string portraitUrlDir = (DIR_ASSETS "/Portrait"); return parseChannelCard(session, portraitDir, portraitUrlDir, usrName, avatar, "", nickName, tv); } /////////////////////////////// // Implementation bool 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 { bool hasThumb = false; bool hasVideo = false; if (!m_options.isTextMode()) { std::string fullAssetsPath = combinePath(sessionPath, sessionAssetsPath); ensureDirectoryExisted(fullAssetsPath); hasThumb = m_iTunesDb.copyFile(srcThumb, combinePath(fullAssetsPath, destThumb)); if (!srcRawVideo.empty()) { hasVideo = m_iTunesDb.copyFile(srcRawVideo, combinePath(fullAssetsPath, destVideo), true); } if (!hasVideo) { hasVideo = m_iTunesDb.copyFile(srcVideo, combinePath(fullAssetsPath, destVideo)); } } if (hasVideo) { tv.setName("video"); tv["%%THUMBPATH%%"] = hasThumb ? (sessionAssetsUrlPath + "/" + destThumb) : ""; tv["%%VIDEOPATH%%"] = sessionAssetsUrlPath + "/" + destVideo; tv["%%MSGTYPE%%"] = "video"; } else if (hasThumb) { tv.setName("thumb"); tv["%%IMGTHUMBPATH%%"] = sessionAssetsUrlPath + "/" + destThumb; tv["%%MESSAGE%%"] = m_resManager.getLocaleString("(Video Missed)"); } else { tv.setName("msg"); tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Video]"); } tv["%%VIDEOWIDTH%%"] = width; tv["%%VIDEOHEIGHT%%"] = height; return true; } bool 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 { bool hasThumb = false; bool hasImage = false; if (!m_options.isTextMode()) { std::string fullAssetsPath = combinePath(sessionPath, sessionAssetsPath); hasThumb = m_iTunesDb.copyFile(srcThumb, fullAssetsPath, destThumb); if (!srcHdOrPre.empty()) { hasImage = m_iTunesDb.copyFile(srcHdOrPre, fullAssetsPath, dest); } if (!hasImage) { hasImage = m_iTunesDb.copyFile(src, fullAssetsPath, dest); } } if (hasImage) { tv.setName("image"); tv["%%IMGPATH%%"] = sessionAssetsUrlPath + "/" + dest; // If it is PDF mode, use the raw image directly for print quaility tv["%%IMGTHUMBPATH%%"] = sessionAssetsUrlPath + "/" + (((!hasThumb) || m_options.isPdfMode()) ? dest : destThumb); tv["%%MSGTYPE%%"] = "image"; tv["%%EXTRA_CLS%%"] = "raw-img"; } else if (hasThumb) { tv.setName("thumb"); tv["%%IMGTHUMBPATH%%"] = sessionAssetsUrlPath + "/" + destThumb; tv["%%MESSAGE%%"] = ""; tv["%%MSGTYPE%%"] = "image"; } else { tv.setName("msg"); tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Photo]"); } return true; } bool 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 { bool hasFile = false; if (!m_options.isTextMode()) { hasFile = m_iTunesDb.copyFile(src, combinePath(sessionPath, sessionAssetsPath), dest); } if (hasFile) { tv.setName("plainshare"); tv["%%SHARINGURL%%"] = sessionAssetsUrlPath + "/" + dest; tv["%%SHARINGTITLE%%"] = fileName; tv["%%MESSAGE%%"] = ""; tv["%%MSGTYPE%%"] = "file"; } else { tv.setName("msg"); tv["%%MESSAGE%%"] = formatString(m_resManager.getLocaleString("[File: %s]"), fileName.c_str()); } return true; } bool MessageParser::parseCard(const Session& session, const std::string& sessionPath, const std::string& portraitDir, const std::string& portraitUrlDir, const std::string& cardMessage, TemplateValues& tv) const { // static std::regex pattern42_1("nickname ?= ?\"(.+?)\""); // static std::regex pattern42_2("smallheadimgurl ?= ?\"(.+?)\""); std::map attrs; if (!m_options.isTextMode()) { attrs = { {"nickname", ""}, {"smallheadimgurl", ""}, {"bigheadimgurl", ""}, {"username", ""} }; } else { attrs = { {"nickname", ""}, {"username", ""} }; } tv["%%CARDTYPE%%"] = m_resManager.getLocaleString("[Contact Card]"); XmlParser xmlParser(cardMessage, true); if (xmlParser.parseAttributesValue("/msg", attrs) && !attrs["nickname"].empty()) { std::string portraitUrl = attrs["bigheadimgurl"].empty() ? attrs["smallheadimgurl"] : attrs["bigheadimgurl"]; bool hasPortrait = !attrs["bigheadimgurl"].empty() || !attrs["smallheadimgurl"].empty(); if (!attrs["username"].empty() && hasPortrait) { tv.setName("card"); // Some username is too long to be created on windows, have to use its md5 string std::string imgFileName = startsWith(attrs["username"], "wxid_") ? attrs["username"] : md5(attrs["username"]); tv["%%CARDNAME%%"] = attrs["nickname"]; tv["%%CARDIMGPATH%%"] = portraitUrlDir + "/" + imgFileName + ".jpg"; std::string localPortraitDir = combinePath(session.getOutputFileName(), normalizePath(portraitDir)); std::string localFile = combinePath(localPortraitDir, imgFileName + ".jpg"); ensureDirectoryExisted(combinePath(sessionPath, localPortraitDir)); std::string output = combinePath(sessionPath, localFile); #ifdef USING_DOWNLOADER m_downloader.addTask(portraitUrl, combinePath(sessionPath, localFile), 0, "card"); #else m_taskManager.download(&session, attrs["bigheadimgurl"], attrs["smallheadimgurl"], combinePath(sessionPath, localFile), 0, "", "card"); #endif } else if (!attrs["nickname"].empty()) { tv["%%MESSAGE%%"] = formatString(m_resManager.getLocaleString("[Contact Card] %s"), attrs["nickname"].c_str()); } else { tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Contact Card]"); } } else { tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Contact Card]"); } tv["%%EXTRA_CLS%%"] = "contact-card"; return true; } bool 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 { bool hasImg = false; if (!m_options.isTextMode()) { hasImg = (!usrName.empty() && !avatar.empty()); } tv["%%CARDTYPE%%"] = m_resManager.getLocaleString("[Channel Card]"); if (!name.empty()) { if (hasImg) { tv.setName("card"); tv["%%CARDNAME%%"] = name; tv["%%CARDIMGPATH%%"] = portraitUrlDir + "/" + usrName + ".jpg"; std::string localPortraitDir = normalizePath(portraitDir); std::string localFile = combinePath(localPortraitDir, usrName + ".jpg"); ensureDirectoryExisted(combinePath(m_outputPath, session.getOutputFileName(), localPortraitDir)); #ifdef USING_DOWNLOADER m_downloader.addTask(avatar, combinePath(m_outputPath, localfile), 0, "card"); #else m_taskManager.download(&session, avatar, avatarLD, combinePath(m_outputPath, localFile), 0, "", "card"); #endif } else { tv.setName("msg"); tv["%%MESSAGE%%"] = formatString(m_resManager.getLocaleString("[Channel Card] %s"), name.c_str()); } } else { tv["%%MESSAGE%%"] = m_resManager.getLocaleString("[Channel Card]"); } tv["%%EXTRA_CLS%%"] = "channel-card"; return true; } bool MessageParser::parseChannels(const std::string& msgId, const XmlParser& xmlParser, xmlNodePtr parentNode, const std::string& finderFeedXPath, const Session& session, TemplateValues& tv) const { // Channels SHI PIN HAO std::map nodes = { {"objectId", ""}, {"nickname", ""}, {"avatar", ""}, {"desc", ""}, {"mediaCount", ""}, {"feedType", ""}, {"desc", ""}, {"username", ""}}; std::map videoNodes = { {"mediaType", ""}, {"url", ""}, {"thumbUrl", ""}, {"coverUrl", ""}, {"videoPlayDuration", ""}}; if (NULL == parentNode) { xmlParser.parseNodesValue(finderFeedXPath + "/*", nodes); xmlParser.parseNodesValue(finderFeedXPath + "/mediaList/media/*", videoNodes); } else { xmlParser.parseChildNodesValue(parentNode, finderFeedXPath + "/*", nodes); xmlParser.parseChildNodesValue(parentNode, finderFeedXPath + "/mediaList/media/*", videoNodes); } #ifndef NDEBUG if (nodes["mediaCount"] == "") { int aa = 0; } #endif std::string thumbUrl; if (!m_options.isTextMode()) { thumbUrl = videoNodes["thumbUrl"].empty() ? videoNodes["coverUrl"] : videoNodes["thumbUrl"]; } const std::string portraitDir = (DIR_ASSETS DIR_SEP_STR "Portrait"); const std::string portraitUrlDir = (DIR_ASSETS "/Portrait"); tv["%%CARDNAME%%"] = nodes["nickname"]; tv["%%CHANNELS%%"] = m_resManager.getLocaleString("Channels"); tv["%%MESSAGE%%"] = nodes["desc"]; tv["%%EXTRA_CLS%%"] = "channels"; if (!thumbUrl.empty()) { tv.setName("channels"); tv["%%MSGTYPE%%"] = "channels"; std::string thumbFile = (DIR_ASSETS "/") + msgId + ".jpg"; std::string localThumbFile = (DIR_ASSETS DIR_SEP_STR) + msgId + ".jpg"; tv["%%CHANNELTHUMBPATH%%"] = thumbFile; ensureDirectoryExisted(combinePath(m_outputPath, session.getOutputFileName(), DIR_ASSETS)); #ifdef USING_DOWNLOADER m_downloader.addTask(thumbUrl, combinePath(m_outputPath, session.getOutputFileName(), localThumbFile), 0, "thumb"); #else m_taskManager.download(&session, thumbUrl, "", combinePath(m_outputPath, session.getOutputFileName(), localThumbFile), 0, "", "thumb"); #endif if (!nodes["avatar"].empty()) { std::string fileName = nodes["username"].empty() ? nodes["objectId"] : nodes["username"]; tv["%%CARDIMGPATH%%"] = portraitUrlDir + "/" + fileName + ".jpg"; std::string localPortraitDir = normalizePath(portraitDir); std::string localFile = combinePath(localPortraitDir, fileName + ".jpg"); ensureDirectoryExisted(combinePath(m_outputPath, session.getOutputFileName(), localPortraitDir)); #ifdef USING_DOWNLOADER m_downloader.addTask(nodes["avatar"], combinePath(m_outputPath, session.getOutputFileName(), localFile), 0, "card"); #else m_taskManager.download(&session, nodes["avatar"], "", combinePath(m_outputPath, session.getOutputFileName(), localFile), 0, "", "card"); #endif } tv["%%CHANNELURL%%"] = videoNodes["url"]; } return true; } bool MessageParser::parseForwardedMsgs(const Session& session, const WXMSG& msg, const std::string& title, const std::string& message, std::vector& tvs) const { std::string portraitPath = (DIR_ASSETS DIR_SEP_STR "Portrait" DIR_SEP_STR); std::string portraitUrlPath = (DIR_ASSETS "/Portrait/"); tvs.push_back(TemplateValues("notice")); TemplateValues& beginTv = tvs.back(); beginTv["%%MESSAGE%%"] = formatString(m_resManager.getLocaleString("<< %s"), title.c_str()); beginTv["%%EXTRA_CLS%%"] = "fmsgtag"; // tag for forwarded msg XmlParser xmlParser(message); XmlParser::XPathEnumerator enumerator(xmlParser, "/recordinfo/datalist/dataitem"); while (enumerator.hasNext()) { xmlNodePtr node = enumerator.nextNode(); if (NULL != node) { WXFWDMSG fmsg = { &msg }; XmlParser::getNodeAttributeValue(node, "datatype", fmsg.dataType); XmlParser::getNodeAttributeValue(node, "dataid", fmsg.dataId); XmlParser::getNodeAttributeValue(node, "subtype", fmsg.subType); XmlParser::getChildNodeContent(node, "sourcename", fmsg.displayName); XmlParser::getChildNodeContent(node, "sourcetime", fmsg.msgTime); XmlParser::getChildNodeContent(node, "srcMsgCreateTime", fmsg.srcMsgTime); XmlParser::getChildNodeContent(node, "datafmt", fmsg.dataFormat); xmlNodePtr srcNode = XmlParser::getChildNode(node, "dataitemsource"); if (NULL != srcNode) { if (!XmlParser::getChildNodeContent(srcNode, "realchatname", fmsg.usrName)) { XmlParser::getChildNodeContent(srcNode, "fromusr", fmsg.usrName); } } #if !defined(NDEBUG) || defined(DBG_PERF) fmsg.rawMessage = xmlParser.getNodeOuterXml(node); writeFile(combinePath(m_outputPath, "../dbg", "fwdmsg_" + fmsg.dataType + ".txt"), fmsg.rawMessage); #endif TemplateValues& tv = *(tvs.emplace(tvs.end(), "msg")); tv["%%ALIGNMENT%%"] = "left"; tv["%%EXTRA_CLS%%"] = "fmsg"; // forwarded msg std::string nestedFwdMsgTitle; std::string nestedFwdMsg; int dataType = fmsg.dataType.empty() ? 0 : std::atoi(fmsg.dataType.c_str()); switch (dataType) { case FWDMSG_DATATYPE_TEXT: // 1 parseFwdMsgText(fmsg, xmlParser, node, session, tv); break; case FWDMSG_DATATYPE_IMAGE: // 2 parseFwdMsgImage(fmsg, xmlParser, node, session, tv); break; case FWDMSG_DATATYPE_VIDEO: // 4 parseFwdMsgVideo(fmsg, xmlParser, node, session, tv); break; case FWDMSG_DATATYPE_LINK: // parseFwdMsgLink(fmsg, xmlParser, node, session, tv); break; case FWDMSG_DATATYPE_LOCATION: // parseFwdMsgLocation(fmsg, xmlParser, node, session, tv); break; case FWDMSG_DATATYPE_ATTACH: // parseFwdMsgAttach(fmsg, xmlParser, node, session, tv); break; case FWDMSG_DATATYPE_CARD: // parseFwdMsgCard(fmsg, xmlParser, node, session, tv); break; case FWDMSG_DATATYPE_NESTED_FWD_MSG: // parseFwdMsgNestedFwdMsg(fmsg, xmlParser, node, session, nestedFwdMsg, nestedFwdMsgTitle, tv); break; case FWDMSG_DATATYPE_MINI_PROGRAM: // 19 parseFwdMsgMiniProgram(fmsg, xmlParser, node, session, tv); break; case FWDMSG_DATATYPE_CHANNELS: // 22 parseFwdMsgChannels(fmsg, xmlParser, node, session, tv); break; case FWDMSG_DATATYPE_CHANNEL_CARD: // 26 parseFwdMsgChannelCard(fmsg, xmlParser, node, session, tv); break; default: parseFwdMsgText(fmsg, xmlParser, node, session, tv); #if !defined(NDEBUG) || defined(DBG_PERF) writeFile(combinePath(m_outputPath, "../dbg", "fwdmsg_unknwn_" + fmsg.dataType + ".txt"), fmsg.rawMessage); #endif break; } tv["%%NAME%%"] = fmsg.displayName; tv["%%MSGID%%"] = msg.msgId + "_" + fmsg.dataId; tv["%%TIME%%"] = fmsg.srcMsgTime.empty() ? fmsg.msgTime : fromUnixTime(static_cast(std::atoi(fmsg.srcMsgTime.c_str()))); // std::string localPortrait; // bool hasPortrait = false; // localPortrait = combinePath(portraitPath, fmsg.usrName + ".jpg"); if (copyPortraitIcon(&session, fmsg.usrName, fmsg.portrait, fmsg.portraitLD, combinePath(m_outputPath, session.getOutputFileName(), portraitPath))) { tv["%%AVATAR%%"] = portraitUrlPath + "/" + fmsg.usrName + ".jpg"; } else { ensureDefaultPortraitIconExisted(portraitPath); tv["%%AVATAR%%"] = portraitUrlPath + "DefaultAvatar.png"; } if ((dataType == FWDMSG_DATATYPE_NESTED_FWD_MSG) && !nestedFwdMsg.empty()) { parseForwardedMsgs(session, msg, nestedFwdMsgTitle, nestedFwdMsg, tvs); } } } tvs.push_back(TemplateValues("notice")); TemplateValues& endTv = tvs.back(); endTv["%%MESSAGE%%"] = formatString(m_resManager.getLocaleString("%s Ends >>"), title.c_str()); endTv["%%EXTRA_CLS%%"] = "fmsgtag"; // tag for forwarded msg return true; } ///////////////////////////// ///////////////////////// std::string MessageParser::getDisplayTime(int ms) const { if (ms < 1000) return "1\""; ms /= 1000; if (ms < 60) return std::to_string(ms) + "\""; int seconds = ms % 60; 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))) + "\'"); } bool MessageParser::copyPortraitIcon(const Session* session, const std::string& usrName, const std::string& portraitUrl, const std::string& portraitUrlLD, const std::string& destPath) const { return copyPortraitIcon(session, usrName, md5(usrName), portraitUrl, portraitUrlLD, destPath); } bool MessageParser::copyPortraitIcon(const Session* session, const Friend& f, const std::string& destPath) const { return copyPortraitIcon(session, f.getUsrName(), f.getHash(), f.getPortrait(), f.getSecondaryPortrait(), destPath); } bool 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 { std::string destFileName = usrName + ".jpg"; std::string destFullPath = combinePath(destPath, destFileName); if (existsFile(destFullPath)) { return true; } if (!existsDirectory(destPath)) { makeDirectory(destPath); } bool hasPortrait = false; std::string avatarPath = "share/" + m_myself.getHash() + "/session/headImg/" + usrNameHash + ".pic"; const ITunesFile* file = m_iTunesDbShare.findITunesFile(avatarPath); if (NULL != file) { ITunesDb::parseFileInfo(file); std::string srcPath = m_iTunesDbShare.getRealPath(*file); if (!srcPath.empty() && !Friend::isDefaultAvatar(file->size, srcPath)) { normalizePath(srcPath); bool result = ::copyFile(srcPath, destFullPath, true); if (result) { if (file->modifiedTime != 0) { updateFileTime(destFullPath, static_cast(file->modifiedTime)); } else if (!file->blob.empty()) { updateFileTime(destFullPath, ITunesDb::parseModifiedTime(file->blob)); } hasPortrait = true; } } } // bool hasPortrait = m_iTunesDbShare.copyFile(avatarPath, destPath, destFileName); if (!hasPortrait) { if (portraitUrl.empty() && portraitUrlLD.empty()) { const Friend *f = (m_myself.getUsrName() == usrName) ? &m_myself : m_friends.getFriend(usrNameHash); if (NULL != f) { std::string url = f->getPortrait(); std::string urlLD = f->getSecondaryPortrait(); if (!url.empty() || !urlLD.empty()) { std::string localDestPath = normalizePath(destPath); #ifdef USING_DOWNLOADER m_downloader.addTask(url, combinePath(localDestPath, destFileName), 0, "avatar"); #else m_taskManager.download(session, url, urlLD, combinePath(localDestPath, destFileName), 0, m_resManager.getDefaultAvatarPath(), "avatar"); #endif hasPortrait = true; } } } else { std::string localDestPath = normalizePath(destPath); #ifdef USING_DOWNLOADER m_downloader.addTask(portraitUrl, combinePath(localDestPath, destFileName), 0, "avatar"); #else m_taskManager.download(session, portraitUrl, portraitUrlLD, combinePath(localDestPath, destFileName), 0, m_resManager.getDefaultAvatarPath(), "avatar"); #endif hasPortrait = true; } } if (!hasPortrait) { std::string localDestPath = normalizePath(destPath); hasPortrait = ::copyFile(m_resManager.getDefaultAvatarPath(), combinePath(localDestPath, destFileName)); } return hasPortrait; } void MessageParser::ensureDefaultPortraitIconExisted(const std::string& portraitPath) const { std::string dest = combinePath(m_outputPath, portraitPath); ensureDirectoryExisted(dest); dest = combinePath(dest, "DefaultAvatar.png"); if (!existsFile(dest)) { copyFile(combinePath(m_resPath, "res", "DefaultAvatar.png"), dest, false); } } bool MessageParser::removeSupportUrl(std::string& url) { if (startsWith(url, "https://support.weixin.qq.com")) { url.clear(); return true; } return false; } std::string MessageParser::getMemberDisplayName(const std::string& usrName, const Session& session) const { std::string displayName; if (session.isChatroom()) { // Should fix the priority later: // 1 nick name of chatroom // 2 nick name of friend // 3: display name in chatroom // 4: display name of friend displayName = session.getMemberName(usrName); if (displayName.empty()) { const Friend *f = usrName == m_myself.getUsrName() ? &m_myself : m_friends.getFriend(md5(usrName)); if (NULL != f) { displayName = f->getDisplayName(); } else { displayName = usrName; } } } else { if (usrName == m_myself.getUsrName()) { displayName = m_myself.getDisplayName(); } else if (usrName == session.getUsrName()) { displayName = session.getDisplayName(); } else { const Friend *f = m_friends.getFriend(md5(usrName)); if (NULL != f) { displayName = f->getDisplayName(); } else { displayName = usrName; } } } return displayName; } ================================================ FILE: WechatExporter/core/MessageParser.h ================================================ // // MessageParser.h // WechatExporter // // Created by Matthew on 2021/2/22. // Copyright © 2021 Matthew. All rights reserved. // #ifndef MessageParser_h #define MessageParser_h #include #ifndef NDEBUG #include #endif #include "WechatObjects.h" #include "ExportOption.h" #include "ResManager.h" #include "Downloader.h" #include "TaskManager.h" #include "ITunesParser.h" #include "FileSystem.h" #include "XmlParser.h" #include "Utils.h" class TemplateValues { private: std::map m_values; std::string m_name; public: using const_iterator = std::map::const_iterator; public: TemplateValues() { } TemplateValues(const std::string& name) : m_name(name) { } std::string getName() const { return m_name; } void setName(const std::string& name) { m_name = name; } std::string& operator[](const std::string& k) { return m_values[k]; } bool hasValue(const std::string& key) const { return m_values.find(key) != m_values.cend(); } const_iterator cbegin() const { return m_values.cbegin(); } const_iterator cend() const { return m_values.cend(); } void clear() { m_values.clear(); } void clearName() { m_name.clear(); } const std::map& getValues() const { return m_values; } }; struct WechatTemplateHandler { XmlParser& m_xmlParser; std::string m_templateText; WechatTemplateHandler(XmlParser& xmlParser, const std::string& templateText) : m_xmlParser(xmlParser), m_templateText(templateText) { } std::string getText() const { return m_templateText; } bool operator() (xmlNodeSetPtr xpathNodes) { std::string linkName; std::string linkType; std::string linkHidden; for (int idx = 0; idx < xpathNodes->nodeNr; ++idx) { xmlNode *cur = xpathNodes->nodeTab[idx]; XmlParser::getNodeAttributeValue(cur, "name", linkName); XmlParser::getNodeAttributeValue(cur, "type", linkType); XmlParser::getNodeAttributeValue(cur, "hidden", linkHidden); std::string linkValue; if (linkHidden == "1") { replaceAll(m_templateText, "$" + linkName + "$", linkValue); continue; } if (linkType == "link_plain") { XmlParser::getChildNodeContent(cur, "plain", linkValue); } else if (linkType == "link_profile") { xmlNodePtr nodeMemberList = XmlParser::getChildNode(cur, "memberlist"); std::vector linkValues; if (NULL != nodeMemberList) { xmlNodePtr nodeMember = XmlParser::getChildNode(nodeMemberList, "member"); while (NULL != nodeMember) { XmlParser::getChildNodeContent(nodeMember, "nickname", linkValue); linkValues.push_back(linkValue); nodeMember = XmlParser::getNextNodeSibling(nodeMember); } } std::string linkSep; XmlParser::getChildNodeContent(cur, "separator", linkSep); linkValue = join(linkValues, linkSep.c_str()); } else if (linkType == "link_admin_explain") { XmlParser::getChildNodeContent(cur, "title", linkValue); } else if (linkType == "link_revoke_qrcode") { XmlParser::getChildNodeContent(cur, "title", linkValue); } else if (linkType == "new_link_succeed_contact") { XmlParser::getChildNodeContent(cur, "title", linkValue); } else { // Try to find value of title if (!XmlParser::getChildNodeContent(cur, "title", linkValue)) { #ifndef NDEBUG assert(false); #endif } } replaceAll(m_templateText, "$" + linkName + "$", linkValue); } return true; } }; class MessageParser { public: static const int MSGTYPE_TEXT = 1; static const int MSGTYPE_IMAGE = 3; static const int MSGTYPE_VOICE = 34; static const int MSGTYPE_PUSHMAIL = 35; static const int MSGTYPE_VERIFYMSG = 37; static const int MSGTYPE_POSSIBLEFRIEND = 40; static const int MSGTYPE_SHARECARD = 42; static const int MSGTYPE_VIDEO = 43; static const int MSGTYPE_EMOTICON = 47; static const int MSGTYPE_LOCATION = 48; static const int MSGTYPE_APP = 49; static const int MSGTYPE_VOIPMSG = 50; static const int MSGTYPE_VOIPNOTIFY = 52; static const int MSGTYPE_VOIPINVITE = 53; static const int MSGTYPE_STATUSNOTIFY = 51; static const int MSGTYPE_MICROVIDEO = 62; static const int MSGTYPE_NOTICE = 64; static const int MSGTYPE_IMCARD = 66; // Transfer = 2000, // 转账 // RedEnvelope = 2001, // 红包 // MiniProgram = 2002, // 小程序 // GroupInvite = 2003, // 群邀请 // File = 2004, // 文件消息 static const int MSGTYPE_SYSNOTICE = 9999; static const int MSGTYPE_SYS = 10000; static const int MSGTYPE_RECALLED = 10002; static const int APPMSGTYPE_TEXT = 1; static const int APPMSGTYPE_IMG = 2; static const int APPMSGTYPE_AUDIO = 3; static const int APPMSGTYPE_VIDEO = 4; static const int APPMSGTYPE_URL = 5; static const int APPMSGTYPE_ATTACH = 6; static const int APPMSGTYPE_OPEN = 7; static const int APPMSGTYPE_EMOJI = 8; static const int APPMSGTYPE_VOICE_REMIND = 9; static const int APPMSGTYPE_SCAN_GOOD = 10; static const int APPMSGTYPE_GOOD = 13; static const int APPMSGTYPE_EMOTION = 15; static const int APPMSGTYPE_CARD_TICKET = 16; static const int APPMSGTYPE_REALTIME_LOCATION = 17; static const int APPMSGTYPE_FWD_MSG = 19; static const int APPMSGTYPE_NOTE = 24; static const int APPMSGTYPE_CHANNEL_CARD = 50; static const int APPMSGTYPE_CHANNELS = 51; static const int APPMSGTYPE_REFER = 57; static const int APPMSGTYPE_PAT = 62; static const int APPMSGTYPE_TRANSFERS = 2000; static const int APPMSGTYPE_RED_ENVELOPES = 2001; static const int APPMSGTYPE_READER_TYPE = 100001; static const int FWDMSG_DATATYPE_TEXT = 1; static const int FWDMSG_DATATYPE_IMAGE = 2; static const int FWDMSG_DATATYPE_VIDEO = 4; static const int FWDMSG_DATATYPE_LINK = 5; static const int FWDMSG_DATATYPE_LOCATION = 6; static const int FWDMSG_DATATYPE_ATTACH = 8; static const int FWDMSG_DATATYPE_CARD = 16; static const int FWDMSG_DATATYPE_NESTED_FWD_MSG = 17; static const int FWDMSG_DATATYPE_MINI_PROGRAM = 19; static const int FWDMSG_DATATYPE_CHANNELS = 22; static const int FWDMSG_DATATYPE_CHANNEL_CARD = 26; 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); bool parse(WXMSG& msg, const Session& session, std::vector& tvs) const; bool copyPortraitIcon(const Session* session, const std::string& usrName, const std::string& portraitUrl, const std::string& portraitUrlLD, const std::string& destPath) const; 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; bool copyPortraitIcon(const Session* session, const Friend& f, const std::string& destPath) const; std::string getError() const { return m_error; } bool hasError() const { return !m_error.empty(); } protected: bool parsePortrait(const WXMSG& msg, const Session& session, const std::string& senderId, TemplateValues& tv) const; bool parseText(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parseImage(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parseVoice(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parsePushMail(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parseVideo(const WXMSG& msg, const Session& session, std::string& senderId, TemplateValues& tv) const; bool parseEmotion(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parseAppMsg(const WXMSG& msg, const Session& session, std::string& senderId, std::string& fwdMsg, std::string& fwdMsgTitle, TemplateValues& tv) const; bool parseCall(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parseLocation(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parseStatusNotify(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parsePossibleFriend(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parseVerification(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parseCard(const WXMSG& msg, const Session& session, TemplateValues& tv) const; bool parseNotice(const WXMSG& msg, const Session& session, TemplateValues& tv) const; // 64 bool parseSysNotice(const WXMSG& msg, const Session& session, TemplateValues& tv) const; // 9999 bool parseSystem(const WXMSG& msg, const Session& session, TemplateValues& tv) const; // APP MSG bool parseAppMsgText(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgImage(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgAudio(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgVideo(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgEmotion(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgUrl(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgAttachment(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgOpen(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgEmoji(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgRtLocation(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgFwdMsg(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, std::string& fwdMsg, std::string& fwdMsgTitle, TemplateValues& tv) const; bool parseAppMsgCard(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgChannelCard(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgChannels(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgRefer(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgTransfer(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgRedPacket(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgPat(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgReaderType(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgNote(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgUnknownType(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; bool parseAppMsgDefault(const WXAPPMSG& appMsg, const XmlParser& xmlParser, const Session& session, TemplateValues& tv) const; // FORWARDEWD MSG bool parseFwdMsgText(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; bool parseFwdMsgImage(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; bool parseFwdMsgVideo(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; bool parseFwdMsgLink(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; bool parseFwdMsgLocation(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; bool parseFwdMsgAttach(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; bool parseFwdMsgCard(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; bool parseFwdMsgNestedFwdMsg(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, std::string& nestedFwdMsg, std::string& nestedFwdMsgTitle, TemplateValues& tv) const; bool parseFwdMsgMiniProgram(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; bool parseFwdMsgChannels(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; bool parseFwdMsgChannelCard(const WXFWDMSG& fwdMsg, const XmlParser& xmlParser, xmlNodePtr itemNode, const Session& session, TemplateValues& tv) const; // Implementation 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; 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; 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; bool parseCard(const Session& session, const std::string& sessionPath, const std::string& portraitDir, const std::string& portraitUrlDir, const std::string& cardMessage, TemplateValues& tv) const; 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; bool parseChannels(const std::string& msgId, const XmlParser& xmlParser, xmlNodePtr parentNode, const std::string& finderFeedXPath, const Session& session, TemplateValues& tv) const; bool parseForwardedMsgs(const Session& session, const WXMSG& msg, const std::string& title, const std::string& message, std::vector& tvs) const; std::string getDisplayTime(int ms) const; std::string getMemberDisplayName(const std::string& usrName, const Session& session) const; void ensureDirectoryExisted(const std::string& path) const { if (!existsDirectory(path)) { makeDirectory(path); } } void ensureDefaultPortraitIconExisted(const std::string& portraitPath) const; static bool removeSupportUrl(std::string& url); protected: const ITunesDb& m_iTunesDb; const ITunesDb& m_iTunesDbShare; const ResManager& m_resManager; TaskManager& m_taskManager; Friends& m_friends; Friend m_myself; ExportOption m_options; const std::string m_resPath; const std::string m_outputPath; std::string m_userBase; mutable std::string m_error; protected: #ifndef USING_ASYNC_TASK_FOR_MP3 mutable std::vector m_pcmData; // buffer #endif }; #endif /* MessageParser_h */ ================================================ FILE: WechatExporter/core/PdfConverter.h ================================================ // // PdfConverter.h // WechatExporter // // Created by Matthew on 2021/4/14. // Copyright © 2021 Matthew. All rights reserved. // #ifndef PdfConverter_h #define PdfConverter_h class PdfConverter { public: virtual bool makeUserDirectory(const std::string& dirName) = 0; virtual bool convert(const std::string& htmlPath, const std::string& pdfPath) = 0; virtual ~PdfConverter() {} }; #endif /* PdfConverter_h */ ================================================ FILE: WechatExporter/core/RawMessage.cpp ================================================ // // RawMessage.cpp // WechatExporter // // Created by Matthew on 2020/10/13. // Copyright © 2020 Matthew. All rights reserved. // #include "RawMessage.h" #ifdef _WIN32 #include #endif bool convertUnknownField(const UnknownField &uf, std::string& value) { if (uf.type() == UnknownField::TYPE_LENGTH_DELIMITED) { value.assign(uf.length_delimited().c_str(), uf.GetLengthDelimitedSize()); return true; } else if (uf.type() == UnknownField::TYPE_VARINT) { value = std::to_string(uf.varint()); return true; } else if (uf.type() == UnknownField::TYPE_FIXED32) { value = std::to_string(uf.fixed32()); return true; } else if (uf.type() == UnknownField::TYPE_FIXED64) { value = std::to_string(uf.fixed64()); return true; } return false; } bool convertUnknownField(const UnknownField &uf, int& value) { if (uf.type() == UnknownField::TYPE_VARINT) { value = static_cast(uf.varint()); return true; } else if (uf.type() == UnknownField::TYPE_FIXED32) { value = static_cast(uf.fixed32()); return true; } else if (uf.type() == UnknownField::TYPE_FIXED64) { value = static_cast(uf.fixed64()); return true; } return false; } std::string RawMessage::toUtf8String(const std::string& str) { return UnescapeCEscapeString(str); } RawMessage::RawMessage() : m_pool(NULL), m_factory(NULL), m_message(NULL) { } RawMessage::~RawMessage() { release(); } void RawMessage::release() { if (NULL != m_message) { delete m_message; m_message = NULL; } if (NULL != m_factory) { delete m_factory; m_factory = NULL; } if (NULL != m_pool) { delete m_pool; m_pool = NULL; } } bool RawMessage::merge(const char *data, int length) { release(); m_pool = new DescriptorPool(); FileDescriptorProto file; file.set_name("empty_message.proto"); file.add_message_type()->set_name("EmptyMessage"); GOOGLE_CHECK(m_pool->BuildFile(file) != NULL); const Descriptor *descriptor = m_pool->FindMessageTypeByName("EmptyMessage"); if (NULL == descriptor) { // FormatError(outputString, lengthOfOutputString, ERROR_NO_MESSAGE_TYPE, src->messageTypeName); return false; } m_factory = new DynamicMessageFactory(m_pool); const Message *message = m_factory->GetPrototype(descriptor); if (NULL == message) { return false; } m_message = message->New(); if (NULL == m_message) { return false; } if (!m_message->ParseFromArray(reinterpret_cast(data), length)) { delete m_message; m_message = NULL; return false; } return true; } bool RawMessage::mergeFile(const std::string& path) { release(); #ifdef _WIN32 CA2W pszW(path.c_str(), CP_UTF8); std::ifstream file(pszW, std::ios::in|std::ios::binary|std::ios::ate); #else std::ifstream file(path.c_str(), std::ios::in|std::ios::binary|std::ios::ate); #endif if (file.is_open()) { std::streampos size = file.tellg(); std::vector buffer; buffer.resize(size); file.seekg (0, std::ios::beg); file.read((char *)(&buffer[0]), size); file.close(); return merge(&buffer[0], static_cast(size)); } return false; } /* bool RawMessage::parse(int fieldNumber, std::string& value) { if (NULL == m_message) { return false; } const Reflection* reflection = m_message->GetReflection(); const UnknownFieldSet& ufs = reflection->GetUnknownFields(*m_message); for (int fieldIdx = 0; fieldIdx < ufs.field_count(); ++fieldIdx) { const UnknownField uf = ufs.field(fieldIdx); if (uf.number() == fieldNumber) { value = uf.length_delimited(); return true; } } return false; } bool RawMessage::parseFile(const std::string& path, std::string& fields, std::string& value) { std::string data = readFile(path); return parse(data, fields, value); } bool RawMessage::parse(const std::string& data, std::string& fields, std::string& value) { std::queue fieldNumbers; std::string::size_type start = 0; std::string::size_type end = fields.find('.'); while (end != std::string::npos) { std::string field = fields.substr(start, end - start); if (field.empty()) { return false; } int fieldNumber = std::stoi(field); fieldNumbers.push(fieldNumber); start = end + 1; end = fields.find('.', start); } std::string field = fields.substr(start, end); if (field.empty()) { return false; } int fieldNumber = std::stoi(field); fieldNumbers.push(fieldNumber); return parse(data, fieldNumbers, value); } bool RawMessage::parse(const std::string& data, std::queue& fieldNumbers, std::string& value) { if (fieldNumbers.empty()) { return false; } UnknownFieldSet unknownFields; if (unknownFields.ParseFromString(data)) { int fieldNumber = fieldNumbers.front(); fieldNumbers.pop(); for (int fieldIdx = 0; fieldIdx < unknownFields.field_count(); ++fieldIdx) { const UnknownField uf = unknownFields.field(fieldIdx); if (uf.number() == fieldNumber) { if (fieldNumbers.empty()) { value = uf.length_delimited(); return true; } else { std::string embeded_data = uf.length_delimited(); return parse(embeded_data, fieldNumbers, value); } break; } } } return false; } */ ================================================ FILE: WechatExporter/core/RawMessage.h ================================================ // // RawMessage.h // WechatExporter // // Created by Matthew on 2020/10/13. // Copyright © 2020 Matthew. All rights reserved. // #include #include #include #include #include #include #include #include #include #include #include #include #include // #include #include #include #include "Utils.h" using namespace google::protobuf; using namespace google::protobuf::io; using namespace google::protobuf::compiler; #ifndef RawMessage_h #define RawMessage_h bool convertUnknownField(const UnknownField &uf, std::string& value); bool convertUnknownField(const UnknownField &uf, int& value); class RawMessage { public: RawMessage(); ~RawMessage(); bool merge(const char *data, int length); bool mergeFile(const std::string& path); // bool parse(int fieldNumber, std::string& value); template bool parse(const std::string& fields, T& value); // static bool parse(const std::string& data, std::string& fields, std::string& value); // static bool parse(const std::string& data, std::string& fields, int& value); // static bool parseFile(const std::string& path, std::string& fields, std::string& value); static std::string toUtf8String(const std::string& str); private: void release(); // static bool parse(const std::string& data, std::queue& fieldNumbers, std::string& value); google::protobuf::DescriptorPool* m_pool; google::protobuf::DynamicMessageFactory* m_factory; google::protobuf::Message* m_message; }; template inline bool RawMessage::parse(const std::string& fields, T& value) { if (NULL == m_message) { return false; } std::vector fieldNumbers; std::string::size_type start = 0; std::string::size_type end = fields.find('.'); while (end != std::string::npos) { std::string field = fields.substr(start, end - start); if (field.empty()) { return false; } int fieldNumber = std::stoi(field); fieldNumbers.push_back(fieldNumber); start = end + 1; end = fields.find('.', start); } std::string field = fields.substr(start, end); if (field.empty()) { return false; } int fieldNumber = std::stoi(field); fieldNumbers.push_back(fieldNumber); const Reflection* reflection = m_message->GetReflection(); const UnknownFieldSet& ufs = reflection->GetUnknownFields(*m_message); const UnknownFieldSet* pUfs = &ufs; std::vector> ufsList; for (int idx = 0; idx < fieldNumbers.size(); ++idx) { bool found = false; for (int fieldIdx = 0; fieldIdx < pUfs->field_count(); ++fieldIdx) { const UnknownField &uf = pUfs->field(fieldIdx); if (uf.number() == fieldNumbers[idx]) { found = true; if (idx == fieldNumbers.size() - 1) { return convertUnknownField(uf, value); } else { if (uf.type() == UnknownField::TYPE_GROUP) { pUfs = &(uf.group()); } else if (uf.type() == UnknownField::TYPE_LENGTH_DELIMITED) { std::unique_ptr ufs(new UnknownFieldSet()); if (ufs->ParseFromString(uf.length_delimited())) { pUfs = ufs.get(); } ufsList.push_back(std::move(ufs)); } } break; } } if (!found || NULL == pUfs) { break; } } return false; } #endif /* RawMessage_h */ ================================================ FILE: WechatExporter/core/ResManager.cpp ================================================ // // BaseResConverter.cpp // WechatExporter // // Created by Matthew on 2021/10/27. // Copyright © 2021 Matthew. All rights reserved. // #include "ResManager.h" #include #include #include "FileSystem.h" #include "Utils.h" #define TEMPLATE_NAMES {"frame", "msg", "video", "notice", "system", "audio", "image", "card", "emoji", "plainshare", "share", "thumb", "listframe", "listitem", "scripts", "filter", "refermsg", "channels", "wxemoji", "transfer"} #define TEMPLATE_FORMATS {"template", "template_txt"} ResManager::ResManager() { } ResManager::~ResManager() { } bool ResManager::initLocaleResource(const std::string& resDir, const std::string& languageCode) { m_resDir = resDir; bool res = true; if (!loadLocaleStrings(resDir, languageCode)) { res = false; } return res; } bool ResManager::initResources(const std::string& resDir, const std::string& languageCode, const std::string& templateName) { m_resDir = resDir; bool res = true; if (!loadLocaleStrings(resDir, languageCode)) { res = false; } if (!loadTemplates(resDir, templateName)) { res = false; } if (!loadEmojis(resDir)) { res = false; } return res; } std::string ResManager::getDefaultAvatarPath() const { return combinePath(m_resDir, "res", "DefaultAvatar.png"); } bool ResManager::loadLocaleStrings(const std::string& resDir, const std::string& languageCode) { m_localeStrings.clear(); std::string path = combinePath(resDir, "res", languageCode + ".txt"); if (!existsFile(path)) { return false; } std::string contents = readFile(path); if (contents.empty()) { return false; } Json::CharReaderBuilder builder; std::unique_ptr reader(builder.newCharReader()); Json::Value value; if (reader->parse(contents.c_str(), contents.c_str() + contents.size(), &value, NULL)) { int sz = value.size(); for (int idx = 0; idx < sz; ++idx) { std::string k = value[idx]["key"].asString(); std::string v = value[idx]["value"].asString(); if (m_localeStrings.find(k) != m_localeStrings.cend()) { // return false; } m_localeStrings[k] = v; } } return true; } bool ResManager::loadTemplates(const std::string& resDir, const std::string& templateName) { m_templates.clear(); const char* names[] = TEMPLATE_NAMES; std::string preTag = "%%ML:"; std::string::size_type preTagLen = preTag.size(); std::string postTag = "%%"; std::string::size_type postTagLen = postTag.size(); bool res = true; for (int idx = 0; idx < sizeof(names) / sizeof(const char*); idx++) { std::string name = names[idx]; std::string path = combinePath(resDir, "res", templateName, name + ".html"); if (!existsFile(path)) { res = false; continue; } std::string contents = readFile(path); // Localization std::string::size_type tailPos = 0; std::string::size_type pos = std::string::npos; while ((pos = contents.find(preTag, tailPos)) != std::string::npos) { std::string::size_type tailPos = contents.find(postTag, pos + preTagLen); if (tailPos == std::string::npos) { break; } std::string str = contents.substr(pos + preTagLen, tailPos - pos - preTagLen); std::string localizedStr = getLocaleString(str); contents.replace(pos, tailPos + postTagLen - pos, localizedStr); tailPos += postTagLen + localizedStr.size() - str.size() - preTagLen - postTagLen; } m_templates[name] = contents; m_newTemplates[name] = Template(contents); #ifndef NDEBUG writeFile("/Users/matthew/Documents/WechatHistory/test/templates/" + name + ".html", contents); #endif } return res; } bool ResManager::loadEmojis(const std::string& resDir) { m_emojiTags.clear(); std::string path = combinePath(resDir, "res", "emoji", "emoji.json"); if (!existsFile(path)) { return false; } std::string contents = readFile(path); if (contents.empty()) { return false; } Json::CharReaderBuilder builder; std::unique_ptrreader(builder.newCharReader()); Json::Value value; if (reader->parse(contents.c_str(), contents.c_str() + contents.size(), &value, NULL)) { if (value.isArray()) { for (Json::Value::const_iterator it = value.begin(); it != value.end(); ++it) { if (!it->isObject()) continue; Json::Value preTagValue = it->get("preTag", Json::Value::null); std::string preTag = preTagValue.isNull() ? "" : preTagValue.asCString(); if (preTag.empty()) continue; Json::Value postTagValue = it->get("postTag", Json::Value::null); std::string postTag = postTagValue.isNull() ? "" : postTagValue.asCString(); std::vector::iterator itEmojiTag = m_emojiTags.emplace(m_emojiTags.end(), preTag, postTag); Json::Value keysValue = it->get("keys", Json::Value::null); if (keysValue.isArray()) { for (Json::Value::const_iterator itKey = keysValue.begin(); itKey != keysValue.end(); ++itKey) { if (!itKey->isObject()) continue; Json::Value keyValue = itKey->get("key", Json::Value::null); std::string key = keyValue.isNull() ? "" : keyValue.asCString(); Json::Value fileValue = itKey->get("file", Json::Value::null); std::string file = fileValue.isNull() ? "" : fileValue.asCString(); if (key.empty() || file.empty()) continue; std::string fullTag = preTag + key + postTag; itEmojiTag->m_items.emplace(itEmojiTag->m_items.end(), key, fullTag, fullTag, file); } std::sort(itEmojiTag->m_items.begin(), itEmojiTag->m_items.end()); } } } int sz = value.size(); for (int idx = 0; idx < sz; ++idx) { std::string k = value[idx]["key"].asString(); std::string v = value[idx]["value"].asString(); if (m_localeStrings.find(k) != m_localeStrings.cend()) { // return false; } m_localeStrings[k] = v; } } return true; } std::string ResManager::getTemplate(const std::string& key) const { std::map::const_iterator it = m_templates.find(key); return (it == m_templates.cend()) ? "" : it->second; } const std::string& ResManager::buildFromTemplate(const std::string& key, const std::map& values) const { auto it = m_newTemplates.find(key); if (it == m_newTemplates.cend()) { return m_emptyString; } return it->second.build(values); } std::string ResManager::checkEmptyTemplates() const { std::vector keys; for (std::map::const_iterator it = m_templates.cbegin(); it != m_templates.cend(); ++it) { if (it->second.empty()) { keys.push_back(it->second); } } return join(keys, ","); } std::string ResManager::getLocaleString(const std::string& key) const { // std::string value = key; std::map::const_iterator it = m_localeStrings.find(key); return it == m_localeStrings.cend() ? key : it->second; } // Emoji bool ResManager::hasEmojiTag(const std::string& msg) const { if (msg.empty()) { return false; } bool existed = false; EmojiItemCompare comp; for (std::vector::const_iterator itTag = m_emojiTags.cbegin(); itTag != m_emojiTags.cend(); ++itTag) { std::string::size_type pos = 0; std::string::size_type tailPos = 0; while ((pos = msg.find(itTag->m_headTag, pos)) != std::string::npos) { if (itTag->hasTailTag()) { tailPos = msg.find(itTag->m_tailTag, pos + itTag->m_headTag.size()); if (tailPos == std::string::npos) { break; } std::string tag = msg.substr(pos + itTag->m_headTag.size(), tailPos - pos - itTag->m_headTag.size()); if (!tag.empty()) { std::vector::const_iterator it = std::lower_bound(itTag->m_items.cbegin(), itTag->m_items.cend(), tag, comp); if (it != itTag->m_items.cend() && it->equals(tag)) { existed = true; break; } } pos = tailPos + itTag->m_tailTag.size(); } else { pos += itTag->m_headTag.size(); for (std::vector::const_iterator it = itTag->m_items.cbegin(); it != itTag->m_items.cend(); ++it) { if (msg.compare(pos, it->getTag().size(), it->getTag()) == 0) { existed = true; break; } } if (existed) break; } } if (existed) break; } return existed; } std::string ResManager::convertEmojis(const std::string& msg, const std::string& localRootPath, const std::string& emojiPath, const std::string& emojiUrlPath) const { struct FindEmojiItem { std::string::size_type pos; std::string::size_type length; const EmojiItem* emojiItem; FindEmojiItem(std::string::size_type p, std::string::size_type l, const EmojiItem* ei) : pos(p), length(l), emojiItem(ei) { } inline bool operator<(const FindEmojiItem &rhs) const { return pos < rhs.pos; } inline bool operator>(const FindEmojiItem &rhs) const { return pos > rhs.pos; } }; // greatter to less struct FindEmojiItemCompare { inline bool operator() ( const FindEmojiItem& lhs, const FindEmojiItem& rhs) const { return lhs.pos > rhs.pos; } }; std::string emojiTemplate = getTemplate("wxemoji"); if (emojiTemplate.empty()) { return msg; } EmojiItemCompare comp; std::vector findEmojiItems; bool matched = false; for (std::vector::const_iterator itTag = m_emojiTags.cbegin(); itTag != m_emojiTags.cend(); ++itTag) { std::string::size_type pos = 0; std::string::size_type tailPos = 0; while ((pos = msg.find(itTag->m_headTag, pos)) != std::string::npos) { if (itTag->hasTailTag()) { tailPos = msg.find(itTag->m_tailTag, pos + itTag->m_headTag.size()); if (tailPos == std::string::npos) { // break while loop break; } matched = false; std::string tag = msg.substr(pos + itTag->m_headTag.size(), tailPos - pos - itTag->m_headTag.size()); if (!tag.empty()) { std::vector::const_iterator it = std::lower_bound(itTag->m_items.cbegin(), itTag->m_items.cend(), tag, comp); if (it != itTag->m_items.cend() && it->equals(tag)) { findEmojiItems.emplace(findEmojiItems.end(), pos, tailPos + itTag->m_tailTag.size() - pos, &(*it)); matched = true; } } matched ? (pos += itTag->m_headTag.size()) : (pos = tailPos + itTag->m_tailTag.size()); } else { std::string::size_type oldPos = pos; pos += itTag->m_headTag.size(); for (std::vector::const_iterator it = itTag->m_items.cbegin(); it != itTag->m_items.cend(); ++it) { if (msg.compare(pos, it->getTag().size(), it->getTag()) == 0) { findEmojiItems.emplace(findEmojiItems.end(), oldPos, itTag->m_headTag.size() + it->getTag().size(), &(*it)); pos += it->getTag().size(); break; } } } } } if (findEmojiItems.empty()) { return msg; } std::sort(findEmojiItems.begin(), findEmojiItems.end(), FindEmojiItemCompare()); std::string newMsg = msg; std::string srcEmojiPath = combinePath(m_resDir, "res", "emoji", "images"); std::string destEmojiPath = combinePath(localRootPath, emojiPath, "Emoji", "wx"); bool destEmojiPathExisted = existsDirectory(destEmojiPath); for (std::vector::const_iterator it = findEmojiItems.cbegin(); it != findEmojiItems.cend(); ++it) { std::string emojiStr = emojiTemplate; replaceAll(emojiStr, "%%EMOJI_PATH%%", emojiUrlPath + "/Emoji/wx/" + it->emojiItem->m_fileName + ".png"); replaceAll(emojiStr, "%%EMOJI_TITLE%%", it->emojiItem->getTitle()); replaceAll(emojiStr, "%%EMOJI_RAW%%", it->emojiItem->m_fullTag); newMsg.replace(it->pos, it->length, emojiStr); std::string destFileName = combinePath(destEmojiPath, it->emojiItem->m_fileName + ".png"); if (!existsFile(destFileName)) { if (!destEmojiPathExisted) { destEmojiPathExisted = makeDirectory(destEmojiPath); } copyFile(combinePath(srcEmojiPath, it->emojiItem->m_fileName + ".png"), destFileName); } } return newMsg; } bool ResManager::validateResources(const std::string& resDir, std::string& error) { const char* names[] = TEMPLATE_NAMES; const char* formats[] = TEMPLATE_FORMATS; bool res = true; for (int idx1 = 0; idx1 < sizeof(formats) / sizeof(const char*); idx1++) { std::string format = formats[idx1]; for (int idx2 = 0; idx2 < sizeof(names) / sizeof(const char*); idx2++) { std::string name = names[idx2]; std::string path = combinePath(resDir, "res", format, name + ".html"); if (!existsFile(path)) { error = "Templagte file doesn't exist: res"; error += DIR_SEP_STR + format + DIR_SEP_STR + name + ".html"; res = false; break; } if (getFileSize(path) == 0) { error = "Templagte file is empty: res"; error += DIR_SEP_STR + format + DIR_SEP_STR + name + ".html"; res = false; break; } } } return res; } ================================================ FILE: WechatExporter/core/ResManager.h ================================================ // // BaseResConverter.h // WechatExporter // // Created by Matthew on 2021/10/26. // Copyright © 2021 Matthew. All rights reserved. // #ifndef ResManager_h #define ResManager_h #include #include #include #include "Template.h" class ResManager { public: struct EmojiItem { std::string m_tag; std::string m_fullTag; std::string m_title; std::string m_fileName; EmojiItem() { } 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) { } inline bool operator<(const EmojiItem &rhs) const { return m_tag.compare(rhs.m_tag) < 0; } inline bool operator>(const EmojiItem &rhs) const { return m_tag.compare(rhs.m_tag) > 0; } const std::string& getTag() const { return m_tag; } const std::string& getTitle() const { return m_title; } const std::string& getFileName() const { return m_fileName; } bool equals(const std::string& tag) const { return m_tag.compare(tag) == 0; } }; struct EmojiItemCompare { inline bool operator() ( const EmojiItem& lhs, const std::string& rhs) const { return lhs.getTag().compare(rhs) < 0; } }; struct EmojiItemCompareN { inline bool operator() ( const EmojiItem& lhs, const std::string& rhs) const { return lhs.getTag().compare(rhs) < 0; } }; struct EmojiTag { std::string m_headTag; std::string m_tailTag; std::vector m_items; EmojiTag(const std::string& headTag, const std::string& tailTag) : m_headTag(headTag), m_tailTag(tailTag) { } bool hasTailTag() const { return !m_tailTag.empty(); } }; public: ResManager(); ~ResManager(); bool initLocaleResource(const std::string& resDir, const std::string& languageCode); bool initResources(const std::string& resDir, const std::string& languageCode, const std::string& templateName); // Default Resource std::string getDefaultAvatarPath() const; std::string getDefaultAppIconPath() const; // Localization std::string getLocaleString(const std::string& key) const; // Template std::string getTemplate(const std::string& key) const; // const Template& getNewTemplate(const std::string& key) const; std::string checkEmptyTemplates() const; const std::string& buildFromTemplate(const std::string& key, const std::map& values) const; // Emoji bool hasEmojiTag(const std::string& msg) const; std::string convertEmojis(const std::string& msg, const std::string& localRootPath, const std::string& emojiPath, const std::string& emojiUrlPath) const; static bool validateResources(const std::string& resDir, std::string& error); protected: bool loadLocaleStrings(const std::string& resDir, const std::string& languageCode); bool loadTemplates(const std::string& resDir, const std::string& templateName); bool loadEmojis(const std::string& resDir); protected: std::string m_resDir; std::map m_templates; std::map m_newTemplates; std::map m_localeStrings; std::vector m_emojiTags; std::string m_emptyString; }; #endif /* ResManager_h */ ================================================ FILE: WechatExporter/core/TaskManager.cpp ================================================ // // TaskManager.cpp // WechatExporter // // Created by Matthew on 2021/4/20. // Copyright © 2021 Matthew. All rights reserved. // #include "TaskManager.h" #include "AsyncTask.h" #include "FileSystem.h" TaskManager::TaskManager(Logger* logger) : m_logger(logger), m_downloadExecutor(NULL) #ifdef USING_ASYNC_TASK_FOR_MP3 , m_audioExecutor(NULL) #endif { m_downloadExecutor = new AsyncExecutor(2, 4, this); #ifdef USING_ASYNC_TASK_FOR_MP3 m_audioExecutor = new AsyncExecutor(1, 1, this); #endif // m_audioExecutor = m_downloadExecutor; #if !defined(NDEBUG) || defined(DBG_PERF) m_downloadExecutor->setTag("dl"); #ifdef USING_ASYNC_TASK_FOR_MP3 if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor) { m_audioExecutor->setTag("audio"); } #endif #endif } TaskManager::~TaskManager() { shutdownExecutors(); m_logger = NULL; } void TaskManager::shutdown() { #ifdef USING_ASYNC_TASK_FOR_MP3 if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor) { m_audioExecutor->shutdown(); } #endif if (NULL != m_downloadExecutor) { m_downloadExecutor->shutdown(); } } void TaskManager::shutdownExecutors() { #ifdef USING_ASYNC_TASK_FOR_MP3 if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor) { delete m_audioExecutor; m_audioExecutor = NULL; } #endif if (NULL != m_downloadExecutor) { delete m_downloadExecutor; m_downloadExecutor = NULL; } } // true: completed, false: timeout bool TaskManager::waitForCompltion(unsigned int ms) { /* bool hasMoreTasks = true; while (hasMoreTasks) { { std::unique_lock lock(m_mutex); hasMoreTasks = !(m_copyTaskQueue.empty() && m_pdfTaskQueue.empty()); } if (!hasMoreTasks) { break; } std::this_thread::sleep_for(std::chrono::milliseconds(ms == 0 ? 512 : ms)); if (ms > 0) { return false; } } */ if (!m_downloadExecutor->waitForCompltion(ms)) { return false; } #ifdef USING_ASYNC_TASK_FOR_MP3 if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor) { if (!m_audioExecutor->waitForCompltion(ms)) { return false; } } #endif return true; } void TaskManager::cancel() { std::map> copyTaskQueue; { std::unique_lock lock(m_mutex); copyTaskQueue.swap(m_copyTaskQueue); } m_downloadExecutor->cancel(); #ifdef USING_ASYNC_TASK_FOR_MP3 if (NULL != m_audioExecutor && m_audioExecutor != m_downloadExecutor) { m_audioExecutor->cancel(); } #endif for (std::map>::iterator it = copyTaskQueue.begin(); it != copyTaskQueue.end(); ++it) { for (std::set::iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { delete (*it2); } it->second.clear(); } copyTaskQueue.clear(); } size_t TaskManager::getNumberOfQueue(std::string& queueDesc) const { size_t numberOfDownloads = m_downloadExecutor->getNumberOfQueue(); #ifdef USING_ASYNC_TASK_FOR_MP3 size_t numberOfAudio = 0; if (m_audioExecutor != m_downloadExecutor) { numberOfAudio = m_audioExecutor->getNumberOfQueue(); } #endif { std::unique_lock lock(m_mutex); numberOfDownloads += m_copyTaskQueue.size(); } queueDesc = ""; if (numberOfDownloads > 0) { queueDesc += std::to_string(numberOfDownloads) + " downloads"; } #ifdef USING_ASYNC_TASK_FOR_MP3 if (numberOfAudio > 0) { if (!queueDesc.empty()) { queueDesc += ", "; } queueDesc += std::to_string(numberOfAudio) + " audios"; } #endif return numberOfDownloads #ifdef USING_ASYNC_TASK_FOR_MP3 + numberOfAudio #endif ; } void TaskManager::setUserAgent(const std::string& userAgent) { m_userAgent = userAgent; } void TaskManager::onTaskStart(const AsyncExecutor* executor, const AsyncExecutor::Task *task) { if (NULL != m_logger && task->getType() != TASK_TYPE_AUDIO) { if (task->getType() == TASK_TYPE_PDF) { m_logger->write("Task Starts: " + task->getName()); } } } void TaskManager::onTaskComplete(const AsyncExecutor* executor, const AsyncExecutor::Task *task, bool succeeded) { if (NULL != m_logger) { if (!succeeded || task->hasError()) { m_logger->write(task->getError()); } #ifndef NDEBUG else { if (task->getType() == TASK_TYPE_DOWNLOAD) { // m_logger->write("Task Ends: " + task->getName()); } if (task->getType() == TASK_TYPE_PDF) { // m_logger->write("Task Ends: " + task->getName()); } } #endif } // const Session* session = task->getUserData() == NULL ? NULL : reinterpret_cast(task->getUserData()); if (/*executor == m_downloadExecutor && */task->getType() == TASK_TYPE_DOWNLOAD) { const DownloadTask* downloadTask = dynamic_cast(task); std::unique_lock lock(m_mutex); std::map::const_iterator it = m_downloadingTasks.find(downloadTask->getUrl()); #ifndef NDEBUG assert(it != m_downloadingTasks.cend()); #endif if (it != m_downloadingTasks.cend()) { m_downloadingTasks.erase(it); } std::set copyTasks = dequeueCopyTasks(task->getTaskId()); lock.unlock(); #ifndef NDEBUG if (succeeded #ifdef FAKE_DOWNLOAD && task->getType() != TASK_TYPE_DOWNLOAD #endif ) { assert(existsFile(downloadTask->getOutput())); } #endif for (std::set::iterator it = copyTasks.begin(); it != copyTasks.end(); ++it) { m_downloadExecutor->addTask(*it); } } else if ((task->getType() == TASK_TYPE_COPY) || (task->getType() == TASK_TYPE_AUDIO)) { // check copy task } } void 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/* = ""*/) { #ifndef NDEBUG if (url == "") { assert(!"url shouldn't be empty"); } if (output.find(ALT_DIR_SEP) != std::string::npos) { assert(!"Directory is invalid"); } #endif { std::unique_lock lock(m_mutex); std::set::iterator it = m_downloadedFiles.find(output); if (it != m_downloadedFiles.end()) { return; } m_downloadedFiles.insert(output); } std::map::iterator it = m_downloadTasks.find(url); if (it != m_downloadTasks.end() && it->second == output) { // Existed and same output path, skip it return; } bool downloadFile = false; uint32_t taskId = AsyncExecutor::genNextTaskId(); AsyncExecutor::Task *task = NULL; if (it != m_downloadTasks.end()) { // Existed and different output path, copy it task = new CopyTask(it->second, output, "CP: " + url + " => " + output + " <= " + it->second); } else { DownloadTask* downloadTask = new DownloadTask(url, output, defaultFile, mtime, "DL: " + url + " => " + output); downloadTask->setUserAgent(m_userAgent); task = downloadTask; downloadFile = true; m_downloadTasks.insert(std::pair(url, output)); } task->setTaskId(taskId); task->setUserData(reinterpret_cast(session)); std::unique_lock lock(m_mutex); if (downloadFile) { m_downloadingTasks.insert(std::pair(url, taskId)); } else { std::map::const_iterator it3 = m_downloadingTasks.find(url); if (it3 != m_downloadingTasks.cend()) { // Downloading is running, add it into waiting queue std::map>::iterator it4 = m_copyTaskQueue.find(it3->second); if (it4 == m_copyTaskQueue.end()) { it4 = m_copyTaskQueue.insert(it4, std::pair>(it3->second, std::set())); } it4->second.insert(task); task = NULL; } } lock.unlock(); if (NULL != task) { m_downloadExecutor->addTask(task); } } #ifdef USING_ASYNC_TASK_FOR_MP3 void TaskManager::convertAudio(const Session* session, const std::string& pcmPath, const std::string& mp3Path, TaskManager::AUDIO_FORMAT format, unsigned int mtime) { if (NULL == session) { return; } Mp3Task *task = new Mp3Task(pcmPath, mp3Path, mtime); if (NULL != task) { task->setTaskId(AsyncExecutor::genNextTaskId()); task->setUserData(reinterpret_cast(session)); m_audioExecutor->addTask(task); } } #endif ================================================ FILE: WechatExporter/core/TaskManager.h ================================================ // // SessionTaskManager.h // WechatExporter // // Created by Matthew on 2021/4/20. // Copyright © 2021 Matthew. All rights reserved. // #ifndef TaskManager_h #define TaskManager_h #include #include #include #include "WechatObjects.h" #include "AsyncExecutor.h" #include "PdfConverter.h" #include "Logger.h" class TaskManager : public AsyncExecutor::Callback { private: Logger* m_logger; AsyncExecutor *m_downloadExecutor; #ifdef USING_ASYNC_TASK_FOR_MP3 AsyncExecutor *m_audioExecutor; #endif std::map m_downloadTasks; std::string m_userAgent; mutable std::mutex m_mutex; std::set m_downloadedFiles; std::map m_downloadingTasks; std::map> m_copyTaskQueue; #ifdef USING_ASYNC_TASK_FOR_MP3 std::queue> m_Buffers; #endif public: TaskManager(Logger* logger); ~TaskManager(); virtual void onTaskStart(const AsyncExecutor* executor, const AsyncExecutor::Task *task); virtual void onTaskComplete(const AsyncExecutor* executor, const AsyncExecutor::Task *task, bool succeeded); void setUserAgent(const std::string& userAgent); size_t getNumberOfQueue(std::string& queueDesc) const; void cancel(); void shutdown(); // true: completed, false: timeout bool waitForCompltion(unsigned int ms); 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 = ""); #ifdef USING_ASYNC_TASK_FOR_MP3 enum AUDIO_FORMAT { AUDIO_FORMAT_AMR, AUDIO_FORMAT_SILK }; void convertAudio(const Session* session, const std::string& pcmPath, const std::string& mp3Path, AUDIO_FORMAT format, unsigned int mtime); #endif private: void shutdownExecutors(); inline std::set dequeueCopyTasks(uint32_t taskId) { std::set tasks; std::map>::iterator it = m_copyTaskQueue.find(taskId); if (it != m_copyTaskQueue.end()) { tasks.swap(it->second); m_copyTaskQueue.erase(it); } return tasks; } }; #endif /* SessionTaskManager_h */ ================================================ FILE: WechatExporter/core/Template.cpp ================================================ // // Template.cpp // WechatExporter // // Created by Matthew on 2022/4/21. // Copyright © 2022 Matthew. All rights reserved. // #include "Template.h" #define TEMPLATE_TAG "%%" #define TEMPLATE_TAG_LENGTH 2 Template::Template() { } Template::Template(const std::string& tmpl) : m_template(tmpl) { size_t pos = 0; size_t posEnd = 0; while (1) { pos = m_template.find(TEMPLATE_TAG, pos); if (pos == std::string::npos) { break; } posEnd = m_template.find(TEMPLATE_TAG, pos + TEMPLATE_TAG_LENGTH); if (posEnd == std::string::npos) { break; } size_t length = posEnd - pos + TEMPLATE_TAG_LENGTH; std::string tag = m_template.substr(pos, length); m_tags.emplace_back(tag, pos, length); pos = posEnd + TEMPLATE_TAG_LENGTH; } } Template::Template(const Template& rhs) : m_template(rhs.m_template), m_tags(rhs.m_tags) { } Template& Template::operator=(const Template& rhs) { m_template = rhs.m_template; m_result = rhs.m_result; m_tags = rhs.m_tags; return *this; } const std::string& Template::build(const std::map& values) const { m_result.assign(m_template); for (auto it = m_tags.crbegin(); it != m_tags.crend(); ++it) { auto itVal = values.find(it->tag); if (itVal == values.cend()) { m_result.erase(m_result.begin() + it->pos, m_result.begin() + it->pos + it->length); } else { m_result.replace(it->pos, it->length, itVal->second); } } return m_result; } ================================================ FILE: WechatExporter/core/Template.h ================================================ // // Template.h // WechatExporter // // Created by Matthew on 2022/4/21. // Copyright © 2022 Matthew. All rights reserved. // #ifndef Template_h #define Template_h #include #include #include struct TEMPLATE_TAG { std::string tag; size_t pos; size_t length; TEMPLATE_TAG(const std::string& t, size_t p, size_t l) : tag(t), pos(p), length(l) {} }; class Template { public: Template(); Template(const std::string& tmpl); Template(const Template& rhs); Template& operator=(const Template& rhs); std::string build(const std::vector>& values) const; const std::string& build(const std::map& values) const; protected: std::string m_template; mutable std::string m_result; std::vector m_tags; }; #endif /* Template_h */ ================================================ FILE: WechatExporter/core/Updater.cpp ================================================ // // Updater.cpp // WechatExporter // // Created by Matthew on 2021/3/6. // Copyright © 2021 Matthew. All rights reserved. // #include "Updater.h" #include "Downloader.h" #include "AsyncTask.h" #include "Utils.h" Updater::Updater(const std::string& currentVersion) : m_currentVersion(currentVersion) { } Updater::~Updater() { } void Updater::setUserAgent(const std::string& userAgent) { m_userAgent = userAgent; } std::string Updater::getNewVersion() const { return m_latestVersion; } std::string Updater::getUpdateUrl() const { return m_updateUrl; } bool Updater::checkUpdate() { m_latestVersion.clear(); m_updateUrl.clear(); std::vector body; std::vector> headers; if (!m_userAgent.empty()) { headers.push_back(std::pair("User-Agent", m_userAgent)); } std::string url = "https://src.wakin.org/github/wxexp/update.conf?v=" + encodeUrl(m_currentVersion); #ifndef NDEBUG headers.push_back(std::pair("RESOLVE", "src.wakin.org:443:127.0.0.1")); url += "&dbg=1"; #endif long httpStatus = 0; #ifdef USING_DOWNLOADER if (!Downloader::httpGet(url, headers, httpStatus, body) || httpStatus != 200 || body.empty()) #else if (!DownloadTask::httpGet(url, headers, httpStatus, body) || httpStatus != 200 || body.empty()) #endif { return false; } std::string bodyStr; bodyStr.assign(reinterpret_cast(&body[0]), body.size()); replaceAll(bodyStr, "\r\n", "\n"); replaceAll(bodyStr, "\r", "\n"); std::vector parts = split(bodyStr, "\n"); if (parts.empty() || parts[0].empty()) { return false; } std::vector versionParts = split(parts[0], "."); if (versionParts.size() != 4 || !isNumber(versionParts[0]) || !isNumber(versionParts[1]) || !isNumber(versionParts[2]) || !isNumber(versionParts[3])) { return false; } m_latestVersion = parts[0]; std::vector curVersionParts = split(m_currentVersion, "."); if (curVersionParts.size() != 4 || !isNumber(curVersionParts[0]) || !isNumber(curVersionParts[1]) || !isNumber(curVersionParts[2]) || !isNumber(curVersionParts[3])) { return false; } if (parts.size() > 1 && !parts[1].empty()) { m_updateUrl = parts[1]; } for (int idx = 0; idx < 4; ++idx) { int v = std::atoi(versionParts[idx].c_str()); int cv = std::atoi(curVersionParts[idx].c_str()); if (v > cv) { return true; } } return false; } ================================================ FILE: WechatExporter/core/Updater.h ================================================ // // Updater.h // WechatExporter // // Created by Matthew on 2021/3/6. // Copyright © 2021 Matthew. All rights reserved. // #ifndef Updater_h #define Updater_h #include class Updater { public: Updater(const std::string& currentVersion); ~Updater(); void setUserAgent(const std::string& userAgent); bool checkUpdate(); std::string getNewVersion() const; std::string getUpdateUrl() const; private: std::string m_currentVersion; std::string m_latestVersion; std::string m_updateUrl; std::string m_userAgent; }; #endif /* Updater_h */ ================================================ FILE: WechatExporter/core/Utils.cpp ================================================ // // Utils.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include "Utils.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #include #include #include "Shlwapi.h" #ifndef NDEBUG #include #endif #else #include #endif #include #include #include #include #include #include "FileSystem.h" #ifdef _WIN32 #include #else #include #endif int replaceAll(std::string& input, const std::string& search, const std::string& replace) { int matched = 0; size_t pos = 0; while((pos = input.find(search, pos)) != std::string::npos) { input.replace(pos, search.length(), replace); pos += replace.length(); ++matched; } return matched; } int replaceAll(std::string& input, const std::vector>& pairs) { int matched = 0; for (std::vector>::const_iterator it = pairs.cbegin(); it != pairs.cend(); ++it) { size_t pos = 0; while((pos = input.find(it->first, pos)) != std::string::npos) { input.replace(pos, it->first.length(), it->second); pos += it->second.length(); ++matched; } } return matched; } std::string replaceAll(const std::string& input, const std::string& search, const std::string& replace) { std::string result = input; replaceAll(result, search, replace); return result; } std::string replaceAll(const std::string& input, const std::vector>& pairs) { std::string result = input; replaceAll(result, pairs); return result; } bool endsWith(const std::string& str, const std::string& suffix) { return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix); } bool endsWith(const std::string& str, std::string::value_type ch) { return !str.empty() && str[str.size() - 1] == ch; } bool startsWith(const std::string& str, const std::string& prefix, int pos/* = 0*/) { return str.size() >= prefix.size() && 0 == str.compare(pos, prefix.size(), prefix); } bool startsWith(const std::string& str, std::string::value_type ch) { return !str.empty() && str[0] == ch; } std::string toUpper(const std::string& str) { std::string res = str; std::transform(res.begin(), res.end(), res.begin(), ::toupper); return res; } std::string toLower(const std::string& str) { std::string res = str; std::transform(res.begin(), res.end(), res.begin(), ::tolower); return res; } std::vector split(const std::string& str, const std::string& delimiter) { std::vector tokens; size_t prev = 0, pos = 0; do { pos = str.find(delimiter, prev); if (pos == std::string::npos) pos = str.length(); std::string token = str.substr(prev, pos-prev); if (!token.empty()) tokens.push_back(token); prev = pos + delimiter.length(); } while (pos < str.length() && prev < str.length()); return tokens; } std::string join(const std::vector& elements, const char *const delimiter) { return join(std::cbegin(elements), std::cend(elements), delimiter); } std::string join(std::vector::const_iterator b, std::vector::const_iterator e, const char *const delimiter) { std::ostringstream os; if (b != e) { auto pe = prev(e); for (std::vector::const_iterator it = b; it != pe; ++it) { os << *it; os << delimiter; } b = pe; } if (b != e) { os << *b; } return os.str(); } std::string safeHTML(const std::string& s) { static std::vector> replaces = { {"&", "&"}, /*{" ", " "}, */{"<", "<"}, {">", ">"}, {"\r\n", "
"}, {"\r", "
"}, {"\n", "
"} }; return replaceAll(s, replaces); } void removeHtmlTags(std::string& html) { std::string::size_type startpos = 0; while ((startpos = html.find("<", startpos)) != std::string::npos) { // auto startpos = html.find("<"); auto endpos = html.find(">", startpos + 1); if (endpos == std::string::npos) { break; } html.erase(startpos, endpos - startpos + 1); } } std::string removeCdata(const std::string& str) { if (startsWith(str, "")) return str.substr(9, str.size() - 12); return str; } std::string fromUnixTime(unsigned int unixtime, bool localTime/* = true*/) { std::time_t ts = unixtime; std::tm* t1 = std::localtime(&ts); if (!localTime) { std::time_t local_secs = std::mktime(t1); struct tm *t2 = gmtime(&ts); std::time_t gmt_secs = mktime(t2); ts -= gmt_secs - local_secs; t1 = std::localtime(&ts); } char buf[30] = { 0 }; std::strftime(buf, 30, "%Y-%m-%d %H:%M:%S", t1); // std::stringstream ss; // or if you're going to print, just input directly into the output stream // ss << std::put_time(t, "%Y-%m-%d %H:%M:%S"); return std::string(buf); } uint32_t getUnixTimeStamp() { time_t rawTime = 0; time(&rawTime); struct tm *localTm = localtime(&rawTime); return mktime(localTm); } /* bool existsFile(const std::string &path) { #ifdef _WIN32 struct stat buffer; CW2A pszA(CA2W(path.c_str(), CP_UTF8)); return (stat ((LPCSTR)pszA, &buffer) == 0); #else struct stat buffer; return (stat(path.c_str(), &buffer) == 0); #endif } */ /* int makePathImpl(const std::string::value_type *path, mode_t mode) { struct stat st; int status = 0; if (stat(path, &st) != 0) { // Directory does not exist. EEXIST for race condition if (mkdir(path, mode) != 0 && errno != EEXIST) status = -1; } else if (!S_ISDIR(st.st_mode)) { // errno = ENOTDIR; status = -1; } return status; } int makePath(const std::string& path, mode_t mode) { std::vector copypath; copypath.reserve(path.size() + 1); std::copy(path.begin(), path.end(), std::back_inserter(copypath)); copypath.push_back('\0'); std::replace(copypath.begin(), copypath.end(), '\\', '/'); std::vector::iterator itStart = copypath.begin(); std::vector::iterator it; int status = 0; while (status == 0 && (it = std::find(itStart, copypath.end(), '/')) != copypath.end()) { if (it != copypath.begin()) { // Neither root nor double slash in path *it = '\0'; status = makePathImpl(©path[0], mode); *it = '/'; } itStart = it + 1; } if (status == 0) { status = makePathImpl(©path[0], mode); } return status; } */ /* bool moveFile(const std::string& src, const std::string& dest, bool overwrite) { #ifdef _WIN32 CW2T pszSrc(CA2W(src.c_str(), CP_UTF8)); CW2T pszDest(CA2W(dest.c_str(), CP_UTF8)); if (overwrite) { ::DeleteFile(pszDest); } BOOL bErrorFlag = ::MoveFile(pszSrc, pszDest); return (TRUE == bErrorFlag); #else if (overwrite) { remove(dest.c_str()); } return 0 == rename(src.c_str(), dest.c_str()); #endif } bool copyFile(const std::string& src, const std::string& dest) { #ifdef _WIN32 CW2T pszSrc(CA2W(src.c_str(), CP_UTF8)); CW2T pszDest(CA2W(dest.c_str(), CP_UTF8)); BOOL bErrorFlag = ::CopyFile(pszSrc, pszDest, FALSE); return (TRUE == bErrorFlag); #else std::ifstream ss(src, std::ios::binary); std::ofstream ds(dest, std::ios::binary); ds << ss.rdbuf(); return true; #endif } */ #ifdef _WIN32 std::string utf8ToLocalAnsi(const std::string& utf8Str) { CW2A pszA(CA2W(utf8Str.c_str(), CP_UTF8)); return std::string((LPCSTR)pszA); } #else #endif void updateFileTime(const std::string& path, time_t mtime) { #ifdef _WIN32 CW2T pszT(CA2W(path.c_str(), CP_UTF8)); struct _stat st; struct _utimbuf new_times; _tstat((LPCTSTR)pszT, &st); new_times.actime = st.st_atime; /* keep atime unchanged */ new_times.modtime = mtime; _tutime((LPCTSTR)pszT, &new_times); #else struct stat st; struct utimbuf new_times; stat(path.c_str(), &st); new_times.actime = st.st_atime; /* keep atime unchanged */ new_times.modtime = mtime; utime(path.c_str(), &new_times); #endif } /* bool deleteFile(const std::string& fileName) { return 0 == std::remove(fileName.c_str()); } */ int openSqlite3Database(const std::string& path, sqlite3 **ppDb, bool readOnly/* = true*/) { std::string encodedPath; #ifdef _WIN32 TCHAR szDriver[_MAX_DRIVE] = { 0 }; TCHAR szDir[_MAX_DIR] = { 0 }; CW2T pszT(CA2W(normalizePath(path).c_str(), CP_UTF8)); _tsplitpath(pszT, szDriver, szDir, NULL, NULL); size_t driveLen = _tcslen(szDriver); if (driveLen == 0) { // NO driver encodedPath = path; } else { CW2A pszU8(CT2W(&pszT[driveLen]), CP_UTF8); encodedPath = pszU8; } #else encodedPath = normalizePath(path); #endif std::vector parts = split(encodedPath, DIR_SEP_STR); std::vector encodedParts; encodedPath.reserve(parts.size() + 1); CURL *curl = curl_easy_init(); if (curl) { for (std::vector::const_iterator it = parts.cbegin(); it != parts.cend(); ++it) { char *ptr = curl_easy_escape(curl, it->c_str(), static_cast(it->size())); if (ptr) { encodedParts.push_back(std::string(ptr)); curl_free(ptr); } } curl_easy_cleanup(curl); encodedPath = join(encodedParts, DIR_SEP_STR); } #ifdef _WIN32 if (driveLen == 0) { if (_tcslen(szDir) > 0 && szDir[0] == DIR_SEP) { encodedPath = DIR_SEP_STR + encodedPath; } } else { CW2A pszU8(CT2W(szDriver), CP_UTF8); encodedPath = std::string((LPCSTR)pszU8) + DIR_SEP_STR + encodedPath; } #else if (startsWith(path, DIR_SEP_STR)) { encodedPath = DIR_SEP_STR + encodedPath; } #endif #ifdef _WIN32 std::string pathWithQuery = "file:///" + encodedPath; #else std::string pathWithQuery = "file://" + encodedPath; #endif // std::string pathWithQuery = "file:" + path; pathWithQuery += readOnly ? "?immutable=1&mode=ro" : "?mode=rwc"; 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); } bool isBigEndian() { short int number = 0x1; char *numPtr = (char*)&number; return (numPtr[0] != 1); } template T swapEndian(T u) { union ET { T u; unsigned char u8[sizeof(T)]; } src, dest; src.u = u; for (size_t i = 0; i < sizeof(T); ++i) dest.u8[i] = src.u8[sizeof(T) - i - 1]; return dest.u; } int GetBigEndianInteger(const unsigned char* data, int startIndex/* = 0*/) { if (isBigEndian()) { return *((int *)(data + startIndex)); } #ifndef NDEBUG int aa = (data[startIndex] << 24) | (data[startIndex + 1] << 16) | (data[startIndex + 2] << 8) | data[startIndex + 3]; int bb = swapEndian(*((int *)(data + startIndex))); if (aa == bb) { aa = bb; } #endif return swapEndian(*((int *)&data[startIndex])); } int16_t bigEndianToNative(int16_t n) { return isBigEndian() ? n : swapEndian(n); } int32_t bigEndianToNative(int32_t n) { return isBigEndian() ? n : swapEndian(n); } int64_t bigEndianToNative(int64_t n) { return isBigEndian() ? n : swapEndian(n); } uint16_t bigEndianToNative(uint16_t n) { return isBigEndian() ? n : swapEndian(n); } uint32_t bigEndianToNative(uint32_t n) { return isBigEndian() ? n : swapEndian(n); } uint64_t bigEndianToNative(uint64_t n) { return isBigEndian() ? n : swapEndian(n); } int GetLittleEndianInteger(const unsigned char* data, int startIndex/* = 0*/) { return (data[startIndex + 3] << 24) | (data[startIndex + 2] << 16) | (data[startIndex + 1] << 8) | data[startIndex]; } std::string encodeUrl(const std::string& url) { std::string encodedUrl = url; CURL *curl = curl_easy_init(); if(curl) { char *output = curl_easy_escape(curl, url.c_str(), static_cast(url.size())); if(output) { encodedUrl = output; curl_free(output); } curl_easy_cleanup(curl); } return encodedUrl; } std::string decodeUrl(const std::string& url) { std::string decodedUrl = url; CURL *curl = curl_easy_init(); if(curl) { int outlength = 0; char *output = curl_easy_unescape(curl, url.c_str(), static_cast(url.size()), &outlength); if(output) { decodedUrl = output; curl_free(output); } curl_easy_cleanup(curl); } return decodedUrl; } std::string getTimestampString(bool includingYMD/* = false*/, bool includingMs/* = false*/) { using std::chrono::system_clock; auto currentTime = std::chrono::system_clock::now(); char buffer[80]; std::time_t tt; tt = system_clock::to_time_t ( currentTime ); auto timeinfo = localtime (&tt); strftime (buffer, 80, includingYMD ? "%F %H:%M:%S" : "%H:%M:%S", timeinfo); if (includingMs) { auto transformed = currentTime.time_since_epoch().count() / 1000000; auto millis = transformed % 1000; sprintf(buffer, "%s.%03d", buffer, (int)millis); } return std::string(buffer); } bool isNumber(const std::string &s) { return !s.empty() && std::all_of(s.begin(), s.end(), ::isdigit); } std::string makeUuid() { #ifdef WIN32 UUID uuid; UuidCreate ( &uuid ); RPC_CSTR str = NULL; UuidToStringA ( &uuid, &str ); std::string s((LPCSTR)CW2A(CA2W((LPCSTR)str), CP_UTF8)); RpcStringFreeA(&str); #else uuid_t uuid; uuid_generate_random ( uuid ); char s[37]; uuid_unparse ( uuid, s ); #endif return s; } std::string toHex(unsigned char* data, size_t length) { std::stringstream stream; stream << std::setfill ('0') << std::hex; for (int idx = 0; idx < length; idx++) { stream << std::setw(2) << ((unsigned int) data[idx]); } return stream.str(); } std::string toHex(char* data, size_t length) { return toHex((unsigned char *)data, length); } ================================================ FILE: WechatExporter/core/Utils.h ================================================ // // util.h // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include #include #include #include #include #ifdef _WIN32 #include typedef int mode_t; #endif #define ENABLE_AUDIO_CONVERTION #ifndef Utils_h #define Utils_h int replaceAll(std::string& input, const std::string& search, const std::string& replace); int replaceAll(std::string& input, const std::vector>& pairs); // std::string replaceAll(const std::string& input, const std::string& search, const std::string& replace); // std::string replaceAll(const std::string& input, const std::vector>& pairs); bool endsWith(const std::string& str, const std::string& suffix); bool endsWith(const std::string& str, std::string::value_type ch); bool startsWith(const std::string& str, const std::string& prefix, int pos = 0); bool startsWith(const std::string& str, const std::string::value_type ch); std::string toUpper(const std::string& str); std::string toLower(const std::string& str); std::vector split(const std::string& str, const std::string& delim); std::string join(const std::vector& elements, const char *const delimiter); std::string join(std::vector::const_iterator b, std::vector::const_iterator e, const char *const delimiter); template std::string formatString(const std::string& format, Args && ...args) { auto size = std::snprintf(nullptr, 0, format.c_str(), std::forward(args)...); std::string output(size, '\0'); std::sprintf(&output[0], format.c_str(), std::forward(args)...); return output; } // bool existsFile(const std::string &path); // int makePath(const std::string& path, mode_t mode); std::string md5(const std::string& s); std::string sha1(const std::string& s); std::string md5File(const std::string& path); std::string safeHTML(const std::string& s); void removeHtmlTags(std::string& html); std::string removeCdata(const std::string& str); std::string fromUnixTime(unsigned int unixtime, bool localTime = true); uint32_t getUnixTimeStamp(); const char* calcVarint32Ptr(const char* p, const char* limit, uint32_t* value); const unsigned char* calcVarint32Ptr(const unsigned char* p, const unsigned char* limit, uint32_t* value); // bool moveFile(const std::string& src, const std::string& dest, bool overwrite = true); // bool copyFile(const std::string& src, const std::string& dest); #ifdef _WIN32 std::string utf8ToLocalAnsi(const std::string& utf8Str); #else #define utf8ToLocalAnsi(utf8Str) utf8Str #endif void updateFileTime(const std::string& path, time_t mtime); // bool deleteFile(const std::string& fileName); bool isBigEndian(); int GetBigEndianInteger(const unsigned char* data, int startIndex = 0); int GetLittleEndianInteger(const unsigned char* data, int startIndex = 0); int16_t bigEndianToNative(int16_t n); int32_t bigEndianToNative(int32_t n); int64_t bigEndianToNative(int64_t n); uint16_t bigEndianToNative(uint16_t n); uint32_t bigEndianToNative(uint32_t n); uint64_t bigEndianToNative(uint64_t n); struct sqlite3; int openSqlite3Database(const std::string& path, sqlite3 **ppDb, bool readOnly = true); std::string encodeUrl(const std::string& url); std::string decodeUrl(const std::string& url); // std::string utcToLocal(const std::string& utcTime); std::string getTimestampString(bool includingYMD = false, bool includingMs = false); bool amrToPcm(const std::string& silkPath, std::vector& pcmData, std::string* error = NULL); bool amrToPcm(const std::string& silkPath, const std::string& pcmPath, std::string* error = NULL); bool silkToPcm(const std::string& silkPath, std::vector& pcmData, bool& isSilk, std::string* error = NULL); bool silkToPcm(const std::string& silkPath, const std::string& pcmPath, bool& isSilk, std::string* error = NULL); bool pcmToMp3(const std::string& pcmPath, const std::string& mp3Path, std::string* error = NULL); bool pcmToMp3(const std::vector& pcmData, const std::string& mp3Path, std::string* error = NULL); bool amrPcmToMp3(const std::string& pcmPath, const std::string& mp3Path, std::string* error = NULL); bool amrPcmToMp3(const std::vector& pcmData, const std::string& mp3Path, std::string* error = NULL); void setThreadName(const char* threadName); bool isNumber(const std::string &s); std::string makeUuid(); std::string toHex(unsigned char* data, size_t length); std::string toHex(char* data, size_t length); #endif /* Utils_h */ ================================================ FILE: WechatExporter/core/Utils_audio.cpp ================================================ // // Utils_audio.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // Refer: https://www.programmersought.com/article/2635152445/ // extern "C" { #include } #include "Utils.h" #include "FileSystem.h" #ifdef _WIN32 #include #ifndef NDEBUG #include #endif #endif bool pcmToMp3(const std::string& pcmPath, const std::string& mp3Path, std::string* error/* = NULL*/) { std::vector pcmData; if (!readFile(pcmPath, pcmData)) { return false; } return pcmToMp3(pcmData, mp3Path, error); } bool pcmToMp3(const std::vector& pcmData, const std::string& mp3Path, std::string* error/* = NULL*/) { #ifndef NDEBUG assert(!pcmData.empty()); #endif #ifdef ENABLE_AUDIO_CONVERTION const int MP3_SIZE = 4096; const int num_of_channels = 1; lame_global_flags *gfp = NULL; gfp = lame_init(); if (NULL == gfp) { if (NULL != error) { *error = "lame_init failed."; } return false; } lame_set_in_samplerate(gfp, 24000); lame_set_preset(gfp, 56); lame_set_mode(gfp, MONO); // RG is enabled by default lame_set_findReplayGain(gfp, 1); // lame_set_quality(gfp, 7); //Setting Channels lame_set_num_channels(gfp, num_of_channels); unsigned long fsize = (unsigned long) (pcmData.size() / (2 * num_of_channels)); lame_set_num_samples(gfp, fsize); int samples_to_read = lame_get_framesize(gfp); int samples_of_channel = 576; samples_to_read = samples_of_channel * num_of_channels; // // int framesize = samples_to_read; // std::assert(framesize <= 1152); // int bytes_per_sample = sizeof(short int); lame_set_out_samplerate(gfp, 24000); if(lame_init_params(gfp) == -1) { //lame initialization failed lame_close(gfp); if (NULL != error) { *error = "lame_init_params failed."; } return false; } std::vector pcm_buffer; pcm_buffer.resize(samples_to_read, 0); int bytesOfPcmBuffer = static_cast(pcm_buffer.size() * sizeof(short int)); unsigned char mp3_buffer[MP3_SIZE]; int count = static_cast((pcmData.size() + bytesOfPcmBuffer - 1) / bytesOfPcmBuffer); #ifdef _WIN32 CA2W pszW(mp3Path.c_str(), CP_UTF8); // CW2A pszA(pszW); FILE *mp3 = _wfopen((LPCWSTR)pszW, L"wb" ); // FILE *mp3 = fopen(mp3Path.c_str(), "wb,ccs=UTF-8"); #else FILE *mp3 = fopen(mp3Path.c_str(), "wb"); #endif if(mp3 == NULL) { lame_close(gfp); if (NULL != error) { *error = "Failed to open file for writing: " + mp3Path; } return false; } for (int idx = 0; idx < count; ++idx) { if (idx == (count - 1) && (pcmData.size() % bytesOfPcmBuffer) != 0) { pcm_buffer.assign(pcm_buffer.size(), 0); samples_to_read = ((pcmData.size() % bytesOfPcmBuffer) + sizeof(short int) - 1) / sizeof(short int); } memcpy(reinterpret_cast(&(pcm_buffer[0])), reinterpret_cast(&(pcmData[idx * bytesOfPcmBuffer])), samples_to_read * sizeof(short int)); int write = lame_encode_buffer(gfp, &pcm_buffer[0], NULL, samples_of_channel, mp3_buffer, MP3_SIZE); fwrite(mp3_buffer, write, sizeof(char), mp3); } fclose(mp3); lame_close(gfp); #endif // ENABLE_AUDIO_CONVERTION return true; } bool amrPcmToMp3(const std::string& pcmPath, const std::string& mp3Path, std::string* error/* = NULL*/) { std::vector pcmData; if (!readFile(pcmPath, pcmData)) { return false; } return amrPcmToMp3(pcmData, mp3Path, error); } bool amrPcmToMp3(const std::vector& pcmData, const std::string& mp3Path, std::string* error/* = NULL*/) { #ifndef NDEBUG assert(!pcmData.empty()); #endif #ifdef ENABLE_AUDIO_CONVERTION const int MP3_SIZE = 4096; const int num_of_channels = 1; const int bits_per_sample = 16; lame_global_flags *gfp = NULL; gfp = lame_init(); if (NULL == gfp) { if (NULL != error) { *error = "lame_init failed."; } return false; } //Setting Channels lame_set_num_channels(gfp, num_of_channels); lame_set_in_samplerate(gfp, 8000); // lame_set_preset(gfp, 56); // lame_set_mode(gfp, MONO); // RG is enabled by default lame_set_findReplayGain(gfp, 1); // lame_set_quality(gfp, 7); /* if (lame_get_VBR(gfp) == vbr_off) { lame_set_VBR(gfp, vbr_default); } lame_set_VBR_quality(gfp, 2); */ unsigned long fsize = (unsigned long) (pcmData.size() / (num_of_channels * ((bits_per_sample + 7) / 8))); // lame_set_num_samples(gfp, fsize); lame_set_num_samples(gfp, pcmData.size() / (num_of_channels * ((bits_per_sample + 7) / 8))); int samples_to_read = lame_get_framesize(gfp); // int samples_of_channel = 576; int samples_of_channel = 320; samples_to_read = samples_of_channel * num_of_channels; // // int framesize = samples_to_read; // std::assert(framesize <= 1152); // int bytes_per_sample = sizeof(short int); lame_set_out_samplerate(gfp, 24000); if(lame_init_params(gfp) == -1) { //lame initialization failed lame_close(gfp); if (NULL != error) { *error = "lame_init_params failed."; } return false; } std::vector pcm_buffer; pcm_buffer.resize(samples_to_read, 0); int bytesOfPcmBuffer = static_cast(pcm_buffer.size() * sizeof(short int)); unsigned char mp3_buffer[MP3_SIZE]; int count = static_cast((pcmData.size() + bytesOfPcmBuffer - 1) / bytesOfPcmBuffer); #ifdef _WIN32 CA2W pszW(mp3Path.c_str(), CP_UTF8); // CW2A pszA(pszW); FILE *mp3 = _wfopen((LPCWSTR)pszW, L"wb" ); // FILE *mp3 = fopen(mp3Path.c_str(), "wb,ccs=UTF-8"); #else FILE *mp3 = fopen(mp3Path.c_str(), "wb"); #endif if(mp3 == NULL) { lame_close(gfp); if (NULL != error) { *error = "Failed to open file for writing: " + mp3Path; } return false; } for (int idx = 0; idx < count; ++idx) { if (idx == (count - 1) && (pcmData.size() % bytesOfPcmBuffer) != 0) { pcm_buffer.assign(pcm_buffer.size(), 0); samples_to_read = ((pcmData.size() % bytesOfPcmBuffer) + sizeof(short int) - 1) / sizeof(short int); } memcpy(reinterpret_cast(&(pcm_buffer[0])), reinterpret_cast(&(pcmData[idx * bytesOfPcmBuffer])), samples_to_read * sizeof(short int)); int write = lame_encode_buffer(gfp, &pcm_buffer[0], &pcm_buffer[1], samples_of_channel, mp3_buffer, MP3_SIZE); fwrite(mp3_buffer, write, sizeof(char), mp3); } fclose(mp3); lame_close(gfp); #endif // ENABLE_AUDIO_CONVERTION return true; } ================================================ FILE: WechatExporter/core/Utils_md5.cpp ================================================ // // Utils.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include #include #include #if defined(_WIN32) #include #include #define MD5_DIGEST_LENGTH 16 #define SHA_DIGEST_LENGTH 20 #elif defined(__APPLE__) #import #else #endif #include "FileSystem.h" std::string md5Impl(const void* data, size_t dataSize) { std::stringstream stream; stream << std::setfill ('0') << std::hex; #if defined(_WIN32) HCRYPTPROV hCryptProv = NULL; HCRYPTHASH hHash = NULL; BYTE bHash[0x7f] = {0}; DWORD dwHashLen= MD5_DIGEST_LENGTH; // The MD5 algorithm always returns 16 bytes. if(CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET)) { if(CryptCreateHash(hCryptProv, CALG_MD5, 0, 0, &hHash)) { if(CryptHashData(hHash, reinterpret_cast(data), static_cast(dataSize), 0)) { if(CryptGetHashParam(hHash, HP_HASHVAL, bHash, &dwHashLen, 0)) { // Make a string version of the numeric digest value for (int idx = 0; idx < 16; idx++) { stream << std::setw(2) << ((unsigned int) bHash[idx]); } } } } } CryptDestroyHash(hHash); CryptReleaseContext(hCryptProv, 0); #elif defined(__APPLE__) unsigned char digest[CC_MD5_DIGEST_LENGTH] = {0}; CC_MD5(data, (CC_LONG)dataSize, digest); // This is the md5 call for (int idx = 0; idx < CC_MD5_DIGEST_LENGTH; idx++) { stream << std::setw(2) << ((unsigned int) digest[idx]); } #else #error "Md5 Not implemented." #endif return stream.str(); } std::string md5(const std::string& s) { return md5Impl(s.c_str(), s.size()); } std::string md5File(const std::string& path) { std::vector data; if (readFile(path, data) && !data.empty()) { return md5Impl(&data[0], data.size()); } return ""; } std::string sha1(const std::string& s) { std::stringstream stream; stream << std::setfill ('0') << std::hex; #if defined(_WIN32) HCRYPTPROV hCryptProv = NULL; HCRYPTHASH hHash = NULL; BYTE bHash[0x7f] = {0}; DWORD dwHashLen= SHA_DIGEST_LENGTH ; // The SHA1 algorithm always returns 20 bytes. if(CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET)) { if(CryptCreateHash(hCryptProv, CALG_SHA1, 0, 0, &hHash)) { if(CryptHashData(hHash, reinterpret_cast(s.c_str()), static_cast(s.size()), 0)) { if(CryptGetHashParam(hHash, HP_HASHVAL, bHash, &dwHashLen, 0)) { // Make a string version of the numeric digest value for (int idx = 0; idx < dwHashLen; idx++) { stream << std::setw(2) << ((unsigned int) bHash[idx]); } } } } } CryptDestroyHash(hHash); CryptReleaseContext(hCryptProv, 0); #elif defined(__APPLE__) unsigned char digest[CC_SHA1_DIGEST_LENGTH] = {0}; CC_SHA1(s.c_str(), (CC_LONG)s.size(), digest); // This is the md5 call for (int idx = 0; idx < CC_SHA1_DIGEST_LENGTH; idx++) { stream << std::setw(2) << ((unsigned int) digest[idx]); } #else #error "SHA1 Not implemented." #endif return stream.str(); } ================================================ FILE: WechatExporter/core/Utils_protobuf.cpp ================================================ // // Utils.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include "Utils.h" #include #include #include #include #include const char* GetVarint64Ptr(const char* p, const char* limit, uint64_t* value) { uint64_t result = 0; for (uint32_t shift = 0; shift <= 63 && p < limit; shift += 7) { uint64_t byte = *(reinterpret_cast(p)); p++; if (byte & 0x80) { // More bytes are present result |= ((byte & 0x7F) << shift); } else { result |= (byte << shift); *value = result; return reinterpret_cast(p); } } return nullptr; } const char* GetVarint32PtrFallback(const char* p, const char* limit, uint32_t* value) { uint32_t result = 0; for (uint32_t shift = 0; shift <= 28 && p < limit; shift += 7) { uint32_t byte = *(reinterpret_cast(p)); p++; if (byte & 128) { // More bytes are present result |= ((byte & 127) << shift); } else { result |= (byte << shift); *value = result; return reinterpret_cast(p); } } return NULL; } const char* calcVarint32Ptr(const char* p, const char* limit, uint32_t* value) { if (p < limit) { uint32_t result = *(reinterpret_cast(p)); if ((result & 0x80) == 0) { *value = result; return p + 1; } } return GetVarint32PtrFallback(p, limit, value); } const unsigned char* calcVarint32Ptr(const unsigned char* p, const unsigned char* limit, uint32_t* value) { const char* p1 = calcVarint32Ptr(reinterpret_cast(p), reinterpret_cast(limit), value); return reinterpret_cast(p1); } /* class Protobuf2JsonErrorCollector : public google::protobuf::compiler::MultiFileErrorCollector { virtual void AddError(const std::string & filename, int line, int column, const std::string & message) { // define import error collector printf("%s, %d, %d, %s\n", filename.c_str(), line, column, message.c_str()); } }; */ /* bool parseFieldValueFromProtobuf(const unsigned char *data, size_t length, const std::string &fields, std::string& value) { std::vector fieldNumbers; std::string::size_type start = 0; std::string::size_type end = fields.find('.'); while (end != std::string::npos) { std::string field = fields.substr(start, end - start); if (field.empty()) { return false; } int fieldNumber = std::stoi(field); fieldNumbers.push_back(fieldNumber); start = end + 1; end = fields.find('.', start); } std::string field = fields.substr(start, end); if (field.empty()) { return false; } int fieldNumber = std::stoi(field); fieldNumbers.push_back(fieldNumber); DescriptorPool pool; FileDescriptorProto file; file.set_name("empty_message.proto"); file.add_message_type()->set_name("EmptyMessage"); GOOGLE_CHECK(pool.BuildFile(file) != NULL); const Descriptor *descriptor = pool.FindMessageTypeByName("EmptyMessage"); if (NULL == descriptor) { // FormatError(outputString, lengthOfOutputString, ERROR_NO_MESSAGE_TYPE, src->messageTypeName); return false; } DynamicMessageFactory factory(&pool); const Message *message = factory.GetPrototype(descriptor); std::unique_ptr msg(message->New()); // Message *msg = message->New(); if (NULL == msg) { // FormatError(outputString, lengthOfOutputString, ERROR_NEW_MESSAGE, src->messageTypeName); return false; } // ZeroCopyInputStream* in_stream = new FileInputStream(infd, 128); if (!msg->ParseFromArray(reinterpret_cast(data), static_cast(length))) { return false; } const UnknownFieldSet& ufs = msg->GetReflection()->GetUnknownFields(*msg); const UnknownFieldSet* pUfs = &ufs; for (int idx = 0; idx < fieldNumbers.size(); ++idx) { bool found = false; for (int fieldIdx = 0; fieldIdx < pUfs->field_count(); ++fieldIdx) { const UnknownField uf = pUfs->field(fieldIdx); if (uf.number() == fieldNumbers[idx]) { found = true; if (idx == fieldNumbers.size() - 1) { value = uf.length_delimited(); return true; } else { pUfs = &(uf.group()); } break; } } if (!found) { break; } } return false; } bool parseFieldValueFromProtobuf(const std::string& path, const std::string &fields, std::string& value) { std::ifstream file(path.c_str(), std::ios::in|std::ios::binary|std::ios::ate); if (file.is_open()) { std::streampos size = file.tellg(); std::vector buffer; buffer.resize(size); file.seekg (0, std::ios::beg); file.read((char *)(&buffer[0]), size); file.close(); return parseFieldValueFromProtobuf(&buffer[0], size, fields, value); } return false; } */ ================================================ FILE: WechatExporter/core/Utils_silk.cpp ================================================ #ifdef _WIN32 #define _CRT_SECURE_NO_DEPRECATE 1 #endif #include "Utils.h" #include "FileSystem.h" #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #endif #include "Utils.h" /* Define codec specific settings should be moved to h file */ #define MAX_BYTES_PER_FRAME 1024 #define MAX_INPUT_FRAMES 5 #define MAX_FRAME_LENGTH 480 #define FRAME_LENGTH_MS 20 #define MAX_API_FS_KHZ 48 #define MAX_LBRR_DELAY 2 /* From WmfDecBytesPerFrame in dec_input_format_tab.cpp */ const int amr_frame_sizes[] = { 12, 13, 15, 17, 19, 20, 26, 31, 5, 6, 5, 5, 0, 0, 0, 0 }; #ifdef _SYSTEM_IS_BIG_ENDIAN /* Function to convert a little endian int16 to a */ /* big endian int16 or vica verca */ void swap_endian( SKP_int16 vec[], SKP_int len ) { SKP_int i; SKP_int16 tmp; SKP_uint8 *p1, *p2; for( i = 0; i < len; i++ ){ tmp = vec[ i ]; p1 = (SKP_uint8 *)&vec[ i ]; p2 = (SKP_uint8 *)&tmp; p1[ 0 ] = p2[ 1 ]; p1[ 1 ] = p2[ 0 ]; } } #endif #if (defined(_WIN32) || defined(_WINCE)) #include /* timer */ #else // Linux or Mac #include #endif #ifdef _WIN32 unsigned long GetHighResolutionTime() /* O: time in usec*/ { /* Returns a time counter in microsec */ /* the resolution is platform dependent */ /* but is typically 1.62 us resolution */ LARGE_INTEGER lpPerformanceCount; LARGE_INTEGER lpFrequency; QueryPerformanceCounter(&lpPerformanceCount); QueryPerformanceFrequency(&lpFrequency); return (unsigned long)((1000000*(lpPerformanceCount.QuadPart)) / lpFrequency.QuadPart); } #else // Linux or Mac unsigned long GetHighResolutionTime() /* O: time in usec*/ { struct timeval tv; gettimeofday(&tv, 0); return((tv.tv_sec*1000000)+(tv.tv_usec)); } #endif // _WIN32 /* Seed for the random number generator, which is used for simulating packet loss */ static SKP_int32 rand_seed = 1; bool silkToPcm(const std::string& silkPath, std::vector& pcmData, bool& isSilk, std::string* error/* = NULL*/) { pcmData.clear(); isSilk = false; #ifdef ENABLE_AUDIO_CONVERTION unsigned long tottime, starttime; size_t counter; SKP_int32 totPackets, i, k; SKP_int16 ret, len, tot_len; SKP_int16 nBytes; SKP_uint8 payload[ MAX_BYTES_PER_FRAME * MAX_INPUT_FRAMES * ( MAX_LBRR_DELAY + 1 ) ]; SKP_uint8 *payloadEnd = NULL, *payloadToDec = NULL; SKP_uint8 FECpayload[ MAX_BYTES_PER_FRAME * MAX_INPUT_FRAMES ], *payloadPtr; SKP_int16 nBytesFEC; SKP_int16 nBytesPerPacket[ MAX_LBRR_DELAY + 1 ], totBytes; SKP_int16 out[ ( ( FRAME_LENGTH_MS * MAX_API_FS_KHZ ) << 1 ) * MAX_INPUT_FRAMES ], *outPtr; FILE *bitInFile; SKP_int32 packetSize_ms=0, API_Fs_Hz = 0; SKP_int32 decSizeBytes; void *psDec; SKP_float loss_prob; SKP_int32 frames, lost, quiet; SKP_SILK_SDK_DecControlStruct DecControl; /* default settings */ loss_prob = 0.0f; /* Open files */ #ifdef _WIN32 CA2W pszW(silkPath.c_str(), CP_UTF8); bitInFile = _wfopen((LPCWSTR)pszW, L"rb" ); #else bitInFile = fopen(silkPath.c_str(), "rb" ); #endif std::unique_ptr file(bitInFile, std::fclose); if( bitInFile == NULL ) { if (NULL != error) { error->assign("Failed to open file: " + silkPath); } return false; } /* Check Silk header */ { char header_buf[ 50 ]; if (fread(header_buf, sizeof(char), 1, bitInFile) != 1) { fclose(bitInFile); if (NULL != error) { error->assign("Can't read header: " + silkPath); } return false; } // header_buf[ strlen( "" ) ] = '\0'; /* Terminate with a null character */ if( header_buf[0] != 0x02 ) { counter = fread( header_buf, sizeof( char ), strlen( "!SILK_V3" ), bitInFile ); header_buf[ strlen( "!SILK_V3" ) ] = '\0'; /* Terminate with a null character */ if( strcmp( header_buf, "!SILK_V3" ) != 0 ) { /* Non-equal strings */ if (NULL != error) { error->assign("SILK Error: Wrong Header " + silkPath + ": " + toHex(header_buf, strlen( "!SILK_V3" ))); } // printf( "SILK Error: Wrong Header %s: %s\n", silkPath.c_str(), header_buf ); // exit( 0 ); fclose(bitInFile); return false; } else { isSilk = true; } } else { counter = fread( header_buf, sizeof( char ), strlen( "#!SILK_V3" ), bitInFile ); header_buf[ strlen( "#!SILK_V3" ) ] = '\0'; /* Terminate with a null character */ if( strcmp( header_buf, "#!SILK_V3" ) != 0 ) { /* Non-equal strings */ if (NULL != error) { error->assign("SILK Error: Wrong Header " + silkPath + ": " + toHex(header_buf, strlen( "!SILK_V3" ))); } // printf( "SILK Error: Wrong Header %s: %s\n", silkPath.c_str(), header_buf ); // exit( 0 ); fclose(bitInFile); return false; } else { isSilk = true; } } } // speechOutFile = fopen( speechOutFileName, "wb" ); /* Set the samplingrate that is requested for the output */ if( API_Fs_Hz == 0 ) { DecControl.API_sampleRate = 24000; } else { DecControl.API_sampleRate = API_Fs_Hz; } /* Initialize to one frame per packet, for proper concealment before first packet arrives */ DecControl.framesPerPacket = 1; /* Create decoder */ ret = SKP_Silk_SDK_Get_Decoder_Size( &decSizeBytes ); if( ret ) { // printf( "\nSKP_Silk_SDK_Get_Decoder_Size returned %d", ret ); } std::vector bufferDec; bufferDec.resize(decSizeBytes, 0); // psDec = malloc( decSizeBytes ); psDec = reinterpret_cast(&(bufferDec[0])); /* Reset decoder */ ret = SKP_Silk_SDK_InitDecoder( psDec ); if( ret ) { // printf( "\nSKP_Silk_InitDecoder returned %d", ret ); } totPackets = 0; tottime = 0; payloadEnd = payload; /* Simulate the jitter buffer holding MAX_FEC_DELAY packets */ for( i = 0; i < MAX_LBRR_DELAY; i++ ) { /* Read payload size */ counter = fread( &nBytes, sizeof( SKP_int16 ), 1, bitInFile ); #ifdef _SYSTEM_IS_BIG_ENDIAN swap_endian( &nBytes, 1 ); #endif /* Read payload */ counter = fread( payloadEnd, sizeof( SKP_uint8 ), nBytes, bitInFile ); if( ( SKP_int16 )counter < nBytes ) { break; } nBytesPerPacket[ i ] = nBytes; payloadEnd += nBytes; totPackets++; } while( 1 ) { /* Read payload size */ counter = fread( &nBytes, sizeof( SKP_int16 ), 1, bitInFile ); #ifdef _SYSTEM_IS_BIG_ENDIAN swap_endian( &nBytes, 1 ); #endif if( nBytes < 0 || counter < 1 ) { break; } /* Read payload */ counter = fread( payloadEnd, sizeof( SKP_uint8 ), nBytes, bitInFile ); if( ( SKP_int16 )counter < nBytes ) { break; } /* Simulate losses */ rand_seed = SKP_RAND( rand_seed ); if( ( ( ( float )( ( rand_seed >> 16 ) + ( 1 << 15 ) ) ) / 65535.0f >= ( loss_prob / 100.0f ) ) && ( counter > 0 ) ) { nBytesPerPacket[ MAX_LBRR_DELAY ] = nBytes; payloadEnd += nBytes; } else { nBytesPerPacket[ MAX_LBRR_DELAY ] = 0; } if( nBytesPerPacket[ 0 ] == 0 ) { /* Indicate lost packet */ lost = 1; /* Packet loss. Search after FEC in next packets. Should be done in the jitter buffer */ payloadPtr = payload; for( i = 0; i < MAX_LBRR_DELAY; i++ ) { if( nBytesPerPacket[ i + 1 ] > 0 ) { starttime = GetHighResolutionTime(); SKP_Silk_SDK_search_for_LBRR( payloadPtr, nBytesPerPacket[ i + 1 ], ( i + 1 ), FECpayload, &nBytesFEC ); tottime += GetHighResolutionTime() - starttime; if( nBytesFEC > 0 ) { payloadToDec = FECpayload; nBytes = nBytesFEC; lost = 0; break; } } payloadPtr += nBytesPerPacket[ i + 1 ]; } } else { lost = 0; nBytes = nBytesPerPacket[ 0 ]; payloadToDec = payload; } /* Silk decoder */ outPtr = out; tot_len = 0; starttime = GetHighResolutionTime(); if( lost == 0 ) { /* No Loss: Decode all frames in the packet */ frames = 0; do { /* Decode 20 ms */ ret = SKP_Silk_SDK_Decode( psDec, &DecControl, 0, payloadToDec, nBytes, outPtr, &len ); if( ret ) { // printf( "\nSKP_Silk_SDK_Decode returned %d", ret ); } frames++; outPtr += len; tot_len += len; if( frames > MAX_INPUT_FRAMES ) { /* Hack for corrupt stream that could generate too many frames */ outPtr = out; tot_len = 0; frames = 0; } /* Until last 20 ms frame of packet has been decoded */ } while( DecControl.moreInternalDecoderFrames ); } else { /* Loss: Decode enough frames to cover one packet duration */ for( i = 0; i < DecControl.framesPerPacket; i++ ) { /* Generate 20 ms */ ret = SKP_Silk_SDK_Decode( psDec, &DecControl, 1, payloadToDec, nBytes, outPtr, &len ); if( ret ) { // printf( "\nSKP_Silk_Decode returned %d", ret ); } outPtr += len; tot_len += len; } } packetSize_ms = tot_len / ( DecControl.API_sampleRate / 1000 ); tottime += GetHighResolutionTime() - starttime; totPackets++; /* Write output to file */ #ifdef _SYSTEM_IS_BIG_ENDIAN swap_endian( out, tot_len ); #endif // fwrite( out, sizeof( SKP_int16 ), tot_len, speechOutFile ); unsigned char *p = reinterpret_cast(out); std::copy(p, p + sizeof( SKP_int16 ) * tot_len, std::back_inserter(pcmData)); /* Update buffer */ totBytes = 0; for( i = 0; i < MAX_LBRR_DELAY; i++ ) { totBytes += nBytesPerPacket[ i + 1 ]; } /* Check if the received totBytes is valid */ if (totBytes < 0 || totBytes > sizeof(payload)) { if (NULL != error) { *error += "\rPackets decoded: " + std::to_string(totPackets); } // fprintf( stderr, "\rPackets decoded: %d", totPackets ); return false; } SKP_memmove( payload, &payload[ nBytesPerPacket[ 0 ] ], totBytes * sizeof( SKP_uint8 ) ); payloadEnd -= nBytesPerPacket[ 0 ]; SKP_memmove( nBytesPerPacket, &nBytesPerPacket[ 1 ], MAX_LBRR_DELAY * sizeof( SKP_int16 ) ); } /* Empty the recieve buffer */ for( k = 0; k < MAX_LBRR_DELAY; k++ ) { if( nBytesPerPacket[ 0 ] == 0 ) { /* Indicate lost packet */ lost = 1; /* Packet loss. Search after FEC in next packets. Should be done in the jitter buffer */ payloadPtr = payload; for( i = 0; i < MAX_LBRR_DELAY; i++ ) { if( nBytesPerPacket[ i + 1 ] > 0 ) { starttime = GetHighResolutionTime(); SKP_Silk_SDK_search_for_LBRR( payloadPtr, nBytesPerPacket[ i + 1 ], ( i + 1 ), FECpayload, &nBytesFEC ); tottime += GetHighResolutionTime() - starttime; if( nBytesFEC > 0 ) { payloadToDec = FECpayload; nBytes = nBytesFEC; lost = 0; break; } } payloadPtr += nBytesPerPacket[ i + 1 ]; } } else { lost = 0; nBytes = nBytesPerPacket[ 0 ]; payloadToDec = payload; } /* Silk decoder */ outPtr = out; tot_len = 0; starttime = GetHighResolutionTime(); if( lost == 0 ) { /* No loss: Decode all frames in the packet */ frames = 0; do { /* Decode 20 ms */ ret = SKP_Silk_SDK_Decode( psDec, &DecControl, 0, payloadToDec, nBytes, outPtr, &len ); if( ret ) { if (NULL != error) { *error += "\nSKP_Silk_SDK_Decode returned " + std::to_string(ret); } // printf( "\nSKP_Silk_SDK_Decode returned %d", ret ); } frames++; outPtr += len; tot_len += len; if( frames > MAX_INPUT_FRAMES ) { /* Hack for corrupt stream that could generate too many frames */ outPtr = out; tot_len = 0; frames = 0; } /* Until last 20 ms frame of packet has been decoded */ } while( DecControl.moreInternalDecoderFrames ); } else { /* Loss: Decode enough frames to cover one packet duration */ /* Generate 20 ms */ for( i = 0; i < DecControl.framesPerPacket; i++ ) { ret = SKP_Silk_SDK_Decode( psDec, &DecControl, 1, payloadToDec, nBytes, outPtr, &len ); if( ret ) { if (NULL != error) { *error += "\nSKP_Silk_Decode returned " + std::to_string(ret); } // printf( "\nSKP_Silk_Decode returned %d", ret ); } outPtr += len; tot_len += len; } } packetSize_ms = tot_len / ( DecControl.API_sampleRate / 1000 ); tottime += GetHighResolutionTime() - starttime; totPackets++; /* Write output to file */ #ifdef _SYSTEM_IS_BIG_ENDIAN swap_endian( out, tot_len ); #endif // fwrite( out, sizeof( SKP_int16 ), tot_len, speechOutFile ); unsigned char *p = reinterpret_cast(out); std::copy(p, p + sizeof( SKP_int16 ) * tot_len, std::back_inserter(pcmData)); /* Update Buffer */ totBytes = 0; for( i = 0; i < MAX_LBRR_DELAY; i++ ) { totBytes += nBytesPerPacket[ i + 1 ]; } /* Check if the received totBytes is valid */ if (totBytes < 0 || totBytes > sizeof(payload)) { if (NULL != error) { *error += "\rPackets decoded: " + std::to_string(totPackets); } // fprintf( stderr, "\rPackets decoded: %d", totPackets ); return false; } SKP_memmove( payload, &payload[ nBytesPerPacket[ 0 ] ], totBytes * sizeof( SKP_uint8 ) ); payloadEnd -= nBytesPerPacket[ 0 ]; SKP_memmove( nBytesPerPacket, &nBytesPerPacket[ 1 ], MAX_LBRR_DELAY * sizeof( SKP_int16 ) ); } /* Free decoder */ // free( psDec ); /* Close files */ // fclose( bitInFile ); // filetime = totPackets * 1e-3 * packetSize_ms; #endif // ENABLE_AUDIO_CONVERTION return true; } bool silkToPcm(const std::string& silkPath, const std::string& pcmPath, bool& isSilk, std::string* error/* = NULL*/) { std::vector pcmData; bool result = silkToPcm(silkPath, pcmData, isSilk, error); if (result) { result = writeFile(pcmPath, pcmData); } return result; } size_t skipAmrnbHeader(FILE* fp) { const char szFileHeader[] = "#!AMR\n"; size_t headerLen = strlen(szFileHeader); unsigned char cData[32]; size_t bytesRead = fread(cData, (size_t)1, headerLen, fp); if (bytesRead < headerLen || strncmp((const char *)cData, szFileHeader, bytesRead) != 0) { fseek(fp, 0, SEEK_SET); return 0; } return headerLen; } bool amrToPcm(const std::string& amrPath, std::vector& pcmData, std::string* error/* = NULL*/) { #ifdef _WIN32 CA2W pszW(amrPath.c_str(), CP_UTF8); FILE *fp = _wfopen((LPCWSTR)pszW, L"rb" ); #else FILE *fp = fopen(amrPath.c_str(), "rb" ); #endif if (NULL == fp) { return false; } skipAmrnbHeader(fp); void *amrnb_dec = Decoder_Interface_init(); if (NULL == amrnb_dec) { fclose(fp); return false; } uint8_t buffer[500], littleendian[320], *ptr; int16_t outbuffer[160]; size_t n = 0; while (1) { int size, i; /* Read the mode byte */ n = fread(buffer, 1, 1, fp); if (n <= 0) break; /* Find the packet size */ size = amr_frame_sizes[(buffer[0] >> 3) & 0x0f]; n = fread(buffer + 1, 1, size, fp); if (n != size) break; /* Decode the packet */ Decoder_Interface_Decode(amrnb_dec, buffer, outbuffer, 0); /* Convert to little endian and write to wav */ ptr = littleendian; for (i = 0; i < 160; i++) { *ptr++ = (outbuffer[i] >> 0) & 0xff; *ptr++ = (outbuffer[i] >> 8) & 0xff; } pcmData.insert(pcmData.end(), littleendian, littleendian + 320); } Decoder_Interface_exit(amrnb_dec); fclose(fp); return true; } bool amrToPcm(const std::string& amrPath, const std::string& pcmPath, std::string* error/* = NULL*/) { std::vector pcmData; bool result = amrToPcm(amrPath, pcmData, error); if (result) { result = writeFile(pcmPath, pcmData); } return result; } ================================================ FILE: WechatExporter/core/Utils_thread.cpp ================================================ // // Utils_thread.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #if 1 #include "Utils.h" #ifdef _WIN32 // 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 // https://stackoverflow.com/questions/10121560/stdthread-naming-your-thread #include const DWORD MS_VC_EXCEPTION = 0x406D1388; #pragma pack(push, 8) typedef struct tagTHREADNAME_INFO { DWORD dwType; // Must be 0x1000. LPCSTR szName; // Pointer to name (in user addr space). DWORD dwThreadID; // Thread ID (-1=caller thread). DWORD dwFlags; // Reserved for future use, must be zero. } THREADNAME_INFO; #pragma pack(pop) void setThreadName(uint32_t dwThreadID, const char* threadName) { THREADNAME_INFO info; info.dwType = 0x1000; info.szName = threadName; info.dwThreadID = dwThreadID; info.dwFlags = 0; __try { RaiseException(MS_VC_EXCEPTION, 0, sizeof(info)/sizeof(ULONG_PTR), (ULONG_PTR*)&info ); } __except(EXCEPTION_EXECUTE_HANDLER) { } } void setThreadName( const char* threadName) { setThreadName(GetCurrentThreadId(), threadName); } void setThreadName(std::thread* thread, const char* threadName) { DWORD threadId = ::GetThreadId(static_cast(thread->native_handle() ) ); setThreadName(threadId, threadName); } #elif defined(__linux__) #include void setThreadName(const char* threadName) { prctl(PR_SET_NAME, threadName, 0, 0, 0); } #else #include void setThreadName(const char* threadName) { pthread_setname_np(threadName); } #endif #endif // 0 ================================================ FILE: WechatExporter/core/Utils_xml.cpp ================================================ // // Utils.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include #include "Utils.h" #include #include #include bool getXmlNodeValue(const std::string& xml, const std::string& xpath, std::string& value) { bool result = false; value.clear(); xmlDocPtr doc = NULL; xmlXPathContextPtr xpathCtx = NULL; xmlXPathObjectPtr xpathObj = NULL; xmlNodeSetPtr xpathNodes = NULL; doc = xmlParseMemory(xml.c_str(), static_cast(xml.size())); if (doc == NULL) { goto end; } xpathCtx = xmlXPathNewContext(doc); if (xpathCtx == NULL) { goto end; } xpathObj = xmlXPathEvalExpression(BAD_CAST(xpath.c_str()), xpathCtx); if (xpathObj == NULL) { goto end; } xpathNodes = xpathObj->nodesetval; if ((xpathNodes) && (xpathNodes->nodeNr > 0)) { xmlNode *cur = xpathNodes->nodeTab[0]; xmlChar* sz = xmlNodeGetContent(cur); if (sz != NULL) { value = reinterpret_cast(sz);; xmlFree(sz); } result = true; } end: if (xpathObj) { xmlXPathFreeObject(xpathObj); } if (xpathCtx) { xmlXPathFreeContext(xpathCtx); } if (doc) { xmlFreeDoc(doc); } return result; } bool getXmlNodeAttributeValue(const std::string& xml, const std::string& xpath, const std::string& attributeName, std::string& value) { bool result = false; value.clear(); xmlDocPtr doc = NULL; xmlXPathContextPtr xpathCtx = NULL; xmlXPathObjectPtr xpathObj = NULL; xmlNodeSetPtr xpathNodes = NULL; doc = xmlParseMemory(xml.c_str(), static_cast(xml.size())); if (doc == NULL) { goto end; } xpathCtx = xmlXPathNewContext(doc); if (xpathCtx == NULL) { goto end; } xpathObj = xmlXPathEvalExpression(BAD_CAST(xpath.c_str()), xpathCtx); if (xpathObj == NULL) { goto end; } xpathNodes = xpathObj->nodesetval; if ((xpathNodes) && (xpathNodes->nodeNr > 0)) { xmlNode *cur = xpathNodes->nodeTab[0]; xmlChar* attr = xmlGetProp(cur, reinterpret_cast(attributeName.c_str())); if (NULL != attr) { value = reinterpret_cast(attr); xmlFree(attr); } result = true; } end: if (xpathObj) { xmlXPathFreeObject(xpathObj); } if (xpathCtx) { xmlXPathFreeContext(xpathCtx); } if (doc) { xmlFreeDoc(doc); } return result; } ================================================ FILE: WechatExporter/core/WechatObjects.h ================================================ // // WechatObjects.h // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include #include #include #include #include #include #ifndef NDEBUG #include #endif #include "Utils.h" #include "FileSystem.h" #ifndef WechatObjects_h #define WechatObjects_h // https://www.theiphonewiki.com/wiki/Kernel#iOS // https://en.wikipedia.org/wiki/Darwin_(operating_system) // Major * 10000 + Minor * 100 + Patch /* 6.0 13.0.0 7.0 14.0.0 9.0 15.0.0 9.3 15.4.0 9.3.2 15.5.0 9.3.3 15.6.0 10.0 16.0.0 10.1 16.1.0 10.2 16.3.0 10.3 16.5.0 10.3.2 16.6.0 10.3.3 16.7.0 11.0 17.0.0 11.1 17.2.0 11.2 17.3.0 11.2.5 17.4.0 11.3 17.5.0 11.4 17.6.0 11.4.1 17.7.0 12.0 18.0.0 12.1 18.2.0 12.2 18.5.0 12.3 18.6.0 12.4 18.7.0 13.0 19.0.0 13.3 19.2.0 13.3.1 19.3.0 13.4 19.4.0 13.4.5 19.5.0 13.5.5 19.6.0 14.0 20.0.0 14.2 20.1.0 14.3 20.2.0 14.4 20.3.0 14.5 20.4.0 */ // CFNetwork - Darwin // https://user-agents.net/applications/cfnetwork class WechatInfo { private: std::string m_version; std::string m_osVersion; std::string m_shortVersion; std::string m_cellDataVersion; struct __less { bool operator()(const std::pair& x, int y) const {return x.first < y;} }; public: void setVersion(const std::string& version) { m_version = version; m_shortVersion = version; std::vector parts = split(version, "."); if (parts.size() > 3) { parts.erase(parts.begin() + 3, parts.end()); m_shortVersion = join(parts, "."); } } void setOSVersion(const std::string& osVersion) { size_t pos = osVersion.find_first_not_of("0123456789."); if (pos == std::string::npos) { m_osVersion = osVersion; } else { m_osVersion = osVersion.substr(0, pos); } } std::string getVersion() const { return m_version; } std::string getShortVersion() const { return m_shortVersion; } void setCellDataVersion(const std::string& cellDataVersion) { m_cellDataVersion = cellDataVersion; } std::string getCellDataVersion() const { return m_cellDataVersion; } std::string buildUserAgent() const { std::vector> versionMapping = { {0, "11.0.0 485.12.7"}, {60000, "13.0.0 609.1.4"}, {70000, "14.0.0 711.3.18"}, {90000, "15.0.0 758.2.8"}, {90300, "15.4.0 758.3.15"}, {90302, "15.5.0 758.4.3"}, {90303, "15.6.0 758.5.3"}, {100000, "16.0.0 808.0.2"}, {100100, "16.1.0 808.1.4"}, {100200, "16.3.0 808.3"}, {100300, "16.5.0 811.4.18"}, {100302, "16.6.0 811.5.4"}, {100303, "16.7.0 811.5.4"}, {110000, "17.0.0 887"}, {110100, "17.2.0 889.9"}, {110200, "17.3.0 893.14.2"}, {110205, "17.4.0 894"}, {110300, "17.5.0 897.15"}, {110400, "17.6.0 901.1"}, {110401, "17.7.0 902.2"}, {120000, "18.0.0 974.2.1"}, {120100, "18.2.0 975.0.3"}, {120200, "18.5.0 978.0.7"}, {120300, "18.6.0 978.0.7"}, {120400, "18.7.0 978.0.7"}, {130000, "19.0.0 1120"}, {130300, "19.2.0 1121.2.2"}, {130301, "19.3.0 1121.2.2"}, {130400, "19.4.0 978.0.7"}, {130405, "19.5.0 1126"}, {130505, "19.6.0 1128.0.1"}, {140000, "20.0.0 1197"}, {140200, "20.1.0 1206"}, {140300, "20.2.0 1209"}, {140400, "20.3.0 1220.1"}, {140500, "20.4.0 1237"}, {140600, "20.5.0 1240.0.4"}, {140700, "20.6.0 1240.0.4"}, {150000, "21.0.0 1300.1"}, }; int osVersion = getOSVersionNumber(); std::vector>::iterator it = std::lower_bound(versionMapping.begin(), versionMapping.end(), osVersion, __less()); if (it == versionMapping.cend() || it->first != osVersion) { --it; } size_t pos = it->second.find(' '); if (pos != std::string::npos) { std::string darwinVersion = it->second.substr(0, pos); std::string cfVersion = it->second.substr(pos + 1); return "WeChat/" + (m_version.empty() ? "7.0.15.33" : m_version) + " CFNetwork/" + (cfVersion.empty() ? "978.0.7" : cfVersion) + " Darwin/" + (darwinVersion.empty() ? "18.6.0" : darwinVersion); } return "WeChat/7.0.15.33 CFNetwork/978.0.7 Darwin/18.6.0"; // default } protected: int getOSVersionNumber() const { int versionNumber = 0; if (!m_osVersion.empty()) { std::vector parts = split(m_osVersion, "."); for (int idx = 0; idx < std::min(3, static_cast(parts.size())); ++idx) { if (!parts[idx].empty()) { versionNumber += std::pow(100, (2 - idx)) * std::stoi(parts[idx]); } } } return versionNumber; } }; class Friend { #ifndef NDEBUG public: #else protected: #endif std::string m_usrName; std::string m_wxName; // WeiXin Hao std::string m_uidHash; std::string m_displayName; int m_userType; bool m_isChatroom; bool m_deleted; std::string m_portrait; std::string m_portraitHD; std::string m_outputFileName; // Use displayName first and then usrName std::string m_encodedOutputFileName; // Use displayName first and then usrName // std::map> m_members; // uidHash => std::map m_members; // uid => NickName std::vector m_memberUsrNames; // uid std::vector m_tags; public: Friend() : m_isChatroom(false), m_deleted(false) { } Friend(const std::string& uid, const std::string& hash) : m_usrName(uid), m_uidHash(hash), m_isChatroom(false), m_deleted(false) { m_isChatroom = isChatroom(uid); } static bool isSubscription(const std::string& usrName); static bool isChatroom(const std::string& usrName); static bool isDefaultAvatar(const std::string& path); static bool isDefaultAvatar(size_t fileSize, const std::string& path); inline bool isSubscription() const { return isSubscription(m_usrName); } inline std::string getUsrName() const { return m_usrName; } inline std::string getWxName() const { return m_wxName.empty() ? m_usrName : m_wxName; } inline std::string getHash() const { return m_uidHash; } void setUsrName(const std::string& usrName) { this->m_usrName = usrName; m_uidHash = md5(usrName); m_outputFileName = m_uidHash; m_isChatroom = isChatroom(usrName); } inline bool isUsrNameEmpty() const { return m_usrName.empty(); } inline bool isHashEmpty() const { return m_uidHash.empty(); } void setEmptyUsrName(const std::string& usrName) { this->m_usrName = usrName; } bool isDeleted() const { return m_deleted; } void setDeleted(bool deleted) { m_deleted = deleted; } bool containMember(const std::string& usrName) const { auto it = m_members.find(usrName); return it != m_members.cend(); } std::string getMemberName(const std::string& usrName) const { auto it = m_members.find(usrName); return it != m_members.cend() ? it->second : ""; } void addMember(const std::string& usrName, const std::string& displayName) { auto it = m_members.find(usrName); if (it != m_members.end()) { it->second = displayName; } else { m_members[usrName] = displayName; m_memberUsrNames.push_back(usrName); } } std::vector getMemberUsrNames() const { return m_memberUsrNames; } inline std::string getDisplayName() const { return m_displayName.empty() ? (m_wxName.empty() ? m_usrName : m_wxName) : m_displayName; } inline bool isDisplayNameEmpty() const { return m_displayName.empty(); } inline bool isWxNameEmpty() const { return m_wxName.empty(); } inline void setWxName(const std::string& wxName) { m_wxName = wxName; } inline void setDisplayName(const std::string& displayName) { m_displayName = displayName; } inline void setUserType(int userType) { m_userType = userType; } static bool isInvalidPortrait(const std::string& portrait); inline void setPortrait(const std::string& portrait) { #ifndef NDEBUG if (isInvalidPortrait(portrait)) { assert(false); } #endif m_portrait = portrait; } inline void setPortraitHD(const std::string& portraitHD) { m_portraitHD = portraitHD; } inline std::string getOutputFileName() const { return m_outputFileName; } inline void setOutputFileName(const std::string& outputFileName) { m_outputFileName = outputFileName; m_encodedOutputFileName = encodeUrl(outputFileName); } inline std::string getEncodedOutputFileName() const { return m_encodedOutputFileName; } inline bool isChatroom() const { return m_isChatroom; } inline bool isPortraitEmpty() const { return m_portrait.empty() && m_portraitHD.empty(); } inline std::string getPortrait() const { return m_portraitHD.empty() ? m_portrait : m_portraitHD; } inline std::string getSecondaryPortrait() const { return (m_portraitHD.empty() || m_portrait.empty()) ? "" : m_portrait; } inline std::string getLocalPortrait() const { return m_usrName + ".jpg"; } void clearTags() { m_tags.clear(); } void swapTags(std::vector tags) { m_tags.swap(tags); } std::string buildTagDesc(const std::map& tags) const { if (m_tags.empty()) { return ""; } std::vector tagDesc(m_tags.size()); for (auto it = m_tags.cbegin(); it != m_tags.cend(); ++it) { auto it2 = tags.find(std::stoull(*it)); if (it2 != tags.cend()) { tagDesc.push_back(it2->second); } } return join(tagDesc, " "); } protected: bool update(const Friend& f) { if (m_usrName.empty() && m_uidHash.empty()) { // Can't compare the object return false; } if (!m_usrName.empty()) { if (m_usrName != f.m_usrName) { return false; } } else { if (m_uidHash != f.m_uidHash) { return false; } } bool result = false; if (isUsrNameEmpty()) { setUsrName(f.getUsrName()); result = true; } if (m_wxName.empty() && !f.m_wxName.empty()) { m_wxName = f.m_wxName; result = true; } if (m_displayName.empty() && !f.m_displayName.empty()) { m_displayName = f.m_displayName; result = true; } if (m_portrait.empty() && !f.m_portrait.empty()) { m_portrait = f.m_portrait; result = true; } if (m_portraitHD.empty() && !f.m_portraitHD.empty()) { m_portraitHD = f.m_portraitHD; result = true; } if (m_members.empty()) { if (!f.m_members.empty()) { m_members = f.m_members; m_memberUsrNames = f.m_memberUsrNames; } } else { for (auto it = m_members.begin(); it != m_members.end(); ++it) { if (it->second.empty()) { auto it2 = f.m_members.find(it->first); if (it2 != f.m_members.cend()) { it->second = it2->second; result = true; } } } } return result; } }; inline bool Friend::isSubscription(const std::string& usrName) { /* 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 */ return startsWith(usrName, "gh_") || (usrName.compare("brandsessionholder") == 0) || (usrName.compare("newsapp") == 0) || (usrName.compare("weixin") == 0) || (usrName.compare("notification_messages") == 0); } inline bool Friend::isChatroom(const std::string& usrName) { return endsWith(usrName, "@chatroom") || endsWith(usrName, "@im.chatroom"); } inline bool Friend::isDefaultAvatar(const std::string& path) { return isDefaultAvatar(getFileSize(path), path); } inline bool Friend::isDefaultAvatar(size_t fileSize, const std::string& path) { return (fileSize == 5875) && (md5File(path) == "e3e807760b01d24760eba724bac616d6"); } inline bool Friend::isInvalidPortrait(const std::string& portrait) { return !portrait.empty() && (!startsWith(portrait, "http://") && !startsWith(portrait, "https://") && !startsWith(portrait, "file://")); } struct FriendDisplayNameCompare { bool operator()(const Friend& f1, const Friend& f2) const { return f1.getDisplayName().compare(f2.getDisplayName()) < 0; } bool operator()(const Friend& f1, const std::string& s2) const { return f1.getDisplayName().compare(s2) < 0; } bool operator()(const Friend* f1, const Friend* f2) const { return f1->getDisplayName().compare(f2->getDisplayName()) < 0; } bool operator()(const Friend* f1, const std::string& s2) const { return f1->getDisplayName().compare(s2) < 0; } }; class Friends { public: std::map friends; // uidHash => Friend std::map hashes; // uid => Hash /* template void handleFriend(THandler handler) { for (std::map::iterator it = friends.begin(); it != friends.end(); ++it) { handler(it->second); } } */ void toArraySortedByDisplayName(std::vector& friends) const { friends.reserve(this->friends.size()); for (auto it = this->friends.cbegin(); it != this->friends.cend(); ++it) { friends.push_back(&(it->second)); } std::sort(friends.begin(), friends.end(), FriendDisplayNameCompare()); } bool hasFriend(const std::string& hash) const { return friends.find(hash) != friends.end(); } const Friend* getFriend(const std::string& uidHash) const { std::map::const_iterator it = friends.find(uidHash); if (it == friends.cend()) { return NULL; } return &(it->second); } Friend* getFriend(const std::string& uidHash) { std::map::iterator it = friends.find(uidHash); if (it == friends.end()) { return NULL; } return &(it->second); } const Friend* getFriendByUid(const std::string& uid) const { std::map::const_iterator it = hashes.find(uid); std::string hash = it == hashes.cend() ? md5(uid) : it->second; std::map::const_iterator it2 = friends.find(hash); if (it2 == friends.cend()) { return NULL; } return &(it2->second); // return getFriend(hash); } Friend* getFriendByUid(const std::string& uid) { std::map::const_iterator it = hashes.find(uid); std::string hash = it == hashes.cend() ? md5(uid) : it->second; std::map::iterator it2 = friends.find(hash); if (it2 == friends.end()) { return NULL; } return &(it2->second); // return getFriend(hash); } Friend& addFriend(const std::string& uid) { std::map::const_iterator it = hashes.find(uid); std::string hash = it == hashes.cend() ? md5(uid) : it->second; if (it == hashes.cend()) { hashes[uid] = hash; } friends[hash] = Friend(uid, hash); return friends[hash]; } void addHash(const std::string& uid) { std::map::const_iterator it = hashes.find(uid); if (it == hashes.cend()) { hashes[uid] = md5(uid); } } }; class Session : public Friend { protected: int m_unreadCount; int m_recordCount; unsigned int m_createTime; unsigned int m_lastMessageTime; std::string m_lastMessage; std::string m_lastMessageUsrName; std::string m_lastMessageUserDisplayName; std::string m_extFileName; std::string m_dbFile; std::string m_memberIds; void *m_data; const Friend* m_owner; public: Session(const Friend* owner) : Friend(), m_unreadCount(0), m_recordCount(0), m_createTime(0), m_lastMessageTime(0), m_data(NULL), m_owner(owner) { } 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) { } inline unsigned int getCreateTime() const { return m_createTime; } inline void setCreateTime(unsigned int createTime) { m_createTime = createTime; } inline unsigned int getLastMessageTime() const { return m_lastMessageTime; } inline void setLastMessageTime(unsigned int lastMessageTime) { m_lastMessageTime = lastMessageTime; } inline std::string getLastMessage() const { return m_lastMessage; } inline void setLastMessage(const std::string& lastMessage) { m_lastMessage = lastMessage; } inline void setLastMessage(const std::string& lastMessage, const std::string& lastMessageUsrName, const Friends& friends) { if (startsWith(lastMessage, lastMessageUsrName + ":\n")) { m_lastMessage = lastMessage.substr(lastMessageUsrName.size() + 2); } else { m_lastMessage = lastMessage; } setLastMessageUsrName(lastMessageUsrName, friends); } inline std::string getLastMessageUsrName() const { return m_lastMessageUsrName; } bool isTextMessage() const { return !m_lastMessage.empty() && !startsWith(m_lastMessage, "") && !startsWith(m_lastMessage, "") && !startsWith(m_lastMessage, "{\"msgLocalID\":") && !startsWith(m_lastMessage, "> m_members; // uidHash => if (it != m_members.cend() && !it->second.empty()) // "Empty display name" means there is no specified nick name for chat group { m_lastMessageUserDisplayName = it->second; } else { const Friend* f = friends.getFriendByUid(m_lastMessageUsrName); if (NULL != f) { m_lastMessageUserDisplayName = f->getDisplayName(); } } } inline void setLastMessageUsrName(const std::string& lastMessageUsrName, const std::string& lastMessageUserDsiplayName) { m_lastMessageUsrName = lastMessageUsrName; m_lastMessageUserDisplayName = lastMessageUserDsiplayName; } inline bool hasLastMessageUserDisplayName() const { return !m_lastMessageUserDisplayName.empty(); } inline std::string getLastMessageUserDisplayName() const { return m_lastMessageUserDisplayName; } inline void setLastMessageUserDisplayName(const std::string& lastMessageUserDispalayName) { m_lastMessageUserDisplayName = lastMessageUserDispalayName; } inline bool isExtFileNameEmpty() const { return m_extFileName.empty(); } inline std::string getExtFileName() const { return m_extFileName; } inline void setExtFileName(const std::string& extFileName) { m_extFileName = extFileName; } inline bool isMemberIdsEmpty() const { return m_members.empty() && m_memberUsrNames.empty(); } inline std::string getMemberIds() const { return m_memberIds; } inline void setMemberIds(const std::string& memberIds) { m_memberIds = memberIds; } inline int getUnreadCount() const { return m_unreadCount; } inline void setUnreadCount(int unreadCount) { m_unreadCount = unreadCount; } inline int getRecordCount() const { return m_recordCount; } inline void setRecordCount(int rc) { m_recordCount = rc; } inline bool isDbFileEmpty() const { return m_dbFile.empty(); } inline std::string getDbFile() const { return m_dbFile; } inline void setDbFile(const std::string& dbFile) { m_dbFile = dbFile; } void *getData() const { return m_data; } void setData(void *data) { m_data = data; } bool update(const Friend& f) { bool result = Friend::update(f); if (!m_lastMessageUsrName.empty() && m_lastMessageUserDisplayName.empty()) { std::string memberName = f.getMemberName(m_lastMessageUsrName); if (!memberName.empty()) { m_lastMessageUserDisplayName = memberName; result = true; } } return result; } const Friend* getOwner() const { return m_owner; } }; struct SessionUsrNameCompare { bool operator()(const Session& s1, const Session& s2) const { return s1.getUsrName().compare(s2.getUsrName()) < 0; } bool operator()(const Session& s1, const std::string& s2) const { return s1.getUsrName().compare(s2) < 0; } }; struct SessionHashCompare { bool operator()(const Session& s1, const Session& s2) const { return s1.getHash().compare(s2.getHash()) < 0; } bool operator()(const Session& s1, const std::string& s2) const { return s1.getHash().compare(s2) < 0; } }; struct SessionLastMsgTimeCompare { bool operator()(const Session& s1, const Session& s2) const { return s1.getLastMessageTime() > s2.getLastMessageTime(); } }; struct WXMSG { unsigned int createTime; std::string content; int des; int type; std::string msgId; int64_t msgIdValue; uint64_t msgSvrId; int status; int tableVersion; }; struct WXAPPMSG { const WXMSG *msg; int appMsgType; std::string appId; std::string appName; std::string localAppIcon; std::string senderUsrName; }; struct WXFWDMSG { const WXMSG *msg; std::string usrName; std::string displayName; std::string portrait; std::string portraitLD; std::string dataType; std::string subType; std::string dataId; std::string dataFormat; std::string msgTime; std::string srcMsgTime; #if !defined(NDEBUG) || defined(DBG_PERF) std::string rawMessage; #endif }; #endif /* WechatObjects_h */ ================================================ FILE: WechatExporter/core/WechatParser.cpp ================================================ // // WechatParser.cpp // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #include "WechatParser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "WechatObjects.h" #include "RawMessage.h" #include "XmlParser.h" #include "MMKVReader.h" #ifdef _WIN32 #include #endif template bool parseMembers(const std::string& xml, T& f) { bool result = false; xmlDocPtr doc = NULL; xmlXPathContextPtr xpathCtx = NULL; xmlXPathObjectPtr xpathObj = NULL; xmlNodeSetPtr xpathNodes = NULL; doc = xmlParseMemory(xml.c_str(), static_cast(xml.size())); if (doc == NULL) { goto end; } xpathCtx = xmlXPathNewContext(doc); if(xpathCtx == NULL) { goto end; } xpathObj = xmlXPathEvalExpression(BAD_CAST("//RoomData/Member"), xpathCtx); if(xpathObj == NULL) { goto end; } xpathNodes = xpathObj->nodesetval; //从xpath object中得到node set if ((xpathNodes) && (xpathNodes->nodeNr > 0)) { for (int i = 0; i < xpathNodes->nodeNr; i++) { xmlNode *cur = xpathNodes->nodeTab[i]; xmlChar* uidPtr = xmlGetProp(cur, reinterpret_cast("UserName")); if (NULL == uidPtr) { continue; } std::string uid = reinterpret_cast(uidPtr); xmlFree(uidPtr); std::string displayName; cur = cur->xmlChildrenNode; while (cur != NULL) { if ((!xmlStrcmp(cur->name, reinterpret_cast("DisplayName")))) { xmlChar *dnPtr = xmlNodeGetContent(cur); if (NULL != dnPtr) { displayName = reinterpret_cast(dnPtr); xmlFree(dnPtr); } break; } cur = cur->next; } f.addMember(uid, displayName); } } result = true; end: if (xpathObj) { xmlXPathFreeObject(xpathObj); } if (xpathCtx) { xmlXPathFreeContext(xpathCtx); } if (doc) { xmlFreeDoc(doc); } return result; } LoginInfo2Parser::LoginInfo2Parser(ITunesDb *iTunesDb, Logger* logger) : m_iTunesDb(iTunesDb), m_logger(logger) { } void LoginInfo2Parser::debugLog(const std::string& log) { if (NULL != m_logger) { m_logger->debug(log); } } std::string LoginInfo2Parser::getError() const { return m_error; } bool LoginInfo2Parser::parse(std::vector& users) { std::string loginInfo2 = "Documents/LoginInfo2.dat"; std::string realPath = m_iTunesDb->findRealPath(loginInfo2); if (!realPath.empty()) { parse(realPath, users); } else { debugLog("Documents/LoginInfo2.dat not exists."); // return false; } parseUserFromFolder(users); if (users.empty()) { return false; } MMSettingInMMappedKVFilter filter; ITunesFileVector mmsettings = m_iTunesDb->filter(filter); std::map mmsettingFiles; // hash => usrName for (ITunesFilesConstIterator it = mmsettings.cbegin(); it != mmsettings.cend(); ++it) { debugLog("mmsetting: " + (*it)->relativePath + " => " + (*it)->fileId); std::string fileName = filter.parse((*it)); fileName = fileName.substr(filter.getPrefix().size()); if (fileName.empty()) { continue; } std::string usrNameHash = md5(fileName); mmsettingFiles[usrNameHash] = fileName; } for (std::vector::iterator it = users.begin(); it != users.end(); ++it) { debugLog("Parse user:" + it->getUsrName() + "(" + it->getHash() + ") => " + it->getDisplayName()); MMSettingParser mmsettingParser(m_iTunesDb); if (mmsettingParser.parse(it->getHash())) { debugLog("Succeeded to parse mmsetting:" + it->getUsrName()); it->setUsrName(mmsettingParser.getUsrName()); it->setWxName(mmsettingParser.getWxName()); if (it->isDisplayNameEmpty()) { it->setDisplayName(mmsettingParser.getDisplayName()); } it->setPortrait(mmsettingParser.getPortrait()); it->setPortraitHD(mmsettingParser.getPortraitHD()); } else { debugLog("Failed to parse mmsetting:" + it->getUsrName()); // Check mmsettings.archive in MMappedKV folde if (it->getUsrName().empty()) { std::map::const_iterator it2 = mmsettingFiles.find(it->getHash()); if (it2 != mmsettingFiles.cend()) { it->setUsrName(it2->second); } } if (!(it->getUsrName().empty())) { std::string realPath = m_iTunesDb->findRealPath("Documents/MMappedKV/mmsetting.archive." + it->getUsrName()); std::string realCrcPath = m_iTunesDb->findRealPath("Documents/MMappedKV/mmsetting.archive." + it->getUsrName() + ".crc"); if (!realPath.empty() && !realCrcPath.empty()) { MMKVParser parser(m_logger); debugLog("Parse MMKV file: Documents/MMappedKV/mmsetting.archive." + it->getUsrName() + " => " + realPath); debugLog("Parse MMKV file: Documents/MMappedKV/mmsetting.archive." + it->getUsrName() + ".crc => " + realCrcPath); if (parser.parse(realPath, realCrcPath)) { debugLog("Succeeded to parse mmkv:" + it->getUsrName()); if (it->isDisplayNameEmpty()) { it->setDisplayName(parser.getDisplayName()); } it->setPortrait(parser.getPortrait()); it->setPortraitHD(parser.getPortraitHD()); } else { debugLog("Failed to parse MMKV: Documents/MMappedKV/mmsetting.archive." + it->getUsrName()); } } else { debugLog("MMKV file not exists: Documents/MMappedKV/mmsetting.archive." + it->getUsrName()); } } } } auto it = users.begin(); while (it != users.end()) { if (it->getUsrName().empty()) { debugLog("Erase: md5=" + it->getHash() + "* dn=" + it->getDisplayName() + "*"); m_error += "Erase: md5=" + it->getHash() + "* dn=" + it->getDisplayName() + "* "; // erase() invalidates the iterator, use returned iterator it = users.erase(it); } else { ++it; } } return true; } bool LoginInfo2Parser::parse(const std::string& loginInfo2Path, std::vector& users) { RawMessage msg; if (!msg.mergeFile(loginInfo2Path)) { debugLog("Failed to parse Documents/LoginInfo2.dat(pb)."); return false; } std::string value1; if (!msg.parse("1", value1)) { debugLog("Failed to parse field 1 in Documents/LoginInfo2.dat."); return false; } users.clear(); int offset = 0; std::string::size_type length = value1.size(); debugLog("Length of field 1 in Documents/LoginInfo2.dat = " + std::to_string(length)); while (offset < length) { debugLog("Offset of field 1 in Documents/LoginInfo2.dat = " + std::to_string(offset) + "/" + std::to_string(length)); int res = parseUser(value1.c_str() + offset, static_cast(length - offset), users); if (res < 0) { break; } offset += res; } if (NULL != m_logger) { for (std::vector::iterator it = users.begin(); it != users.end(); ++it) { debugLog("User from Documents/LoginInfo2.dat:" + it->getUsrName() + "(" + it->getHash() + ") => " + it->getDisplayName()); } } return true; } int LoginInfo2Parser::parseUser(const char* data, int length, std::vector& users) { uint32_t userBufferLen = 0; const char* p = calcVarint32Ptr(data, data + length, &userBufferLen); if (NULL == p || 0 == userBufferLen) { return -1; } #ifndef NDEBUG writeFile("/Users/matthew/Documents/WxExp/LoginInfo2.user.data", (const unsigned char* )p, userBufferLen); #endif RawMessage msg; if (!msg.merge(p, userBufferLen)) { return -1; } Friend user; std::string value; if (msg.parse("1", value)) { debugLog("UsrName from Documents/LoginInfo2.dat = " + value); user.setUsrName(value); } if (msg.parse("2", value)) { user.setWxName(value); } if (msg.parse("3", value)) { debugLog("DisplayName from Documents/LoginInfo2.dat = " + value); user.setDisplayName(value); } #ifndef NDEBUG if (msg.parse("10.1.2", value)) { } if (msg.parse("10.2.2.2", value)) { } #endif users.push_back(user); m_error += "LoginInfo2.dat: *" + user.getDisplayName() + "* "; return static_cast(userBufferLen + (p - data)); } bool LoginInfo2Parser::parseUserFromFolder(std::vector& users) { debugLog("parseUserFromFolder starts..."); UserFolderFilter filter; ITunesFileVector folders = m_iTunesDb->filter(filter); for (ITunesFilesConstIterator it = folders.cbegin(); it != folders.cend(); ++it) { std::string fileName = filter.parse((*it)); m_error += "User Folder: *" + fileName + "* "; if (fileName == "00000000000000000000000000000000") { continue; } debugLog("Find User Folder:" + fileName); bool existing = false; std::vector::const_iterator it2 = users.cbegin(); for (; it2 != users.cend(); ++it2) { if (it2->getHash() == fileName) { existing = true; break; } } if (!existing) { debugLog("New User Folder:" + fileName); users.emplace(users.end(), "", fileName); m_error += "New User From Folder: *" + fileName + "* "; } } return true; } void MMSettings::clear() { m_usrName.clear(); m_displayName.clear(); m_portrait.clear(); m_portraitHD.clear(); } std::string MMSettings::getUsrName() const { return m_usrName; } std::string MMSettings::getWxName() const { return m_wxName; } std::string MMSettings::getDisplayName() const { return m_displayName; } std::string MMSettings::getPortrait() const { return m_portrait; } std::string MMSettings::getPortraitHD() const { return m_portraitHD; } MMKVParser::MMKVParser(Logger *logger) : m_logger(logger) { } void MMKVParser::debugLog(const std::string& log) { if (NULL != m_logger) { m_logger->debug(log); } } bool MMKVParser::parse(const std::string& path, const std::string& crcPath) { std::vector contents; uint32_t lastActualSize = 0; // 86: usrName // 87: wxName // 88: DisplayName if (readFile(crcPath, contents)) { if (contents.size() >= 36) { memcpy(&lastActualSize, &contents[32], 4); debugLog("MMKV lastActualSize from crc: " + std::to_string(lastActualSize) + " crc file size:" + std::to_string(contents.size())); } else { debugLog("MMKV crc file size:" + std::to_string(contents.size())); } contents.clear(); } else { debugLog("Failed to read MMKV crc file:" + crcPath); } if (!readFile(path, contents)) { debugLog("Failed to read MMKV file:" + path); return false; } debugLog("MMKV file size:" + std::to_string(contents.size())); uint32_t actualSize = 0; if (contents.size() >= 4) { memcpy(&actualSize, &contents[0], 4); } if (actualSize <= 0) { actualSize = lastActualSize; } if (actualSize <= 0) { debugLog("MMKV actualSize is less than 0: " + std::to_string(actualSize)); return false; } actualSize += 4; if (contents.size() < actualSize) { debugLog("MMKV contents size < actualSize: " + std::to_string(contents.size()) + "/" + std::to_string(actualSize)); } MMKVReader reader(&contents[0], actualSize); reader.seek(8); while (!reader.isAtEnd()) { // debugLog("MMKV offset: " + std::to_string(reader.getPos()) + "/" + std::to_string(actualSize)); const auto k = reader.readKey(); if (k.empty()) { debugLog("MMKV exception: empty key"); break; } if (k == "86") { m_usrName = reader.readStringValue(); // debugLog("MMKV usrName: " + m_usrName); } else if (k == "87") { m_wxName = reader.readStringValue(); } else if (k == "88") { m_displayName = reader.readStringValue(); // debugLog("MMKV displayName: " + m_displayName); } else if (k == "headimgurl") { m_portrait = reader.readStringValue(); } else if (k == "headhdimgurl") { m_portraitHD = reader.readStringValue(); } else { reader.skipValue(); } } return true; } MMSettingParser::MMSettingParser(ITunesDb *iTunesDb) : m_iTunesDb(iTunesDb) { } bool MMSettingParser::parse(const std::string& usrNameHash) { clear(); std::string vpath = "Documents/" + usrNameHash + "/mmsetting.archive"; std::string mmsettingPath = m_iTunesDb->findRealPath(vpath); if (mmsettingPath.empty()) { return false; } std::vector data; if (!readFile(mmsettingPath, data)) { return false; } plist_t node = NULL; plist_from_memory(reinterpret_cast(&data[0]), static_cast(data.size()), &node); if (NULL == node) { return false; } std::unique_ptr nodePtr(node, &plist_free); plist_t objectsNode = plist_access_path(node, 1, "$objects"); if (NULL == objectsNode || !PLIST_IS_ARRAY(objectsNode)) { return false; } plist_t keyedUidNodes = plist_array_get_item(objectsNode, 1); const char* keys[] = {"UsrName", "NickName", "AliasName"}; for (int idx = 0; idx < sizeof(keys) / sizeof(const char *); ++idx) { plist_t keyedUidNode = plist_dict_get_item(keyedUidNodes, keys[idx]); if (keyedUidNode != NULL && PLIST_IS_UID(keyedUidNode)) { uint64_t uid = 0; plist_get_uid_val(keyedUidNode, &uid); plist_t keyedItemNode = plist_array_get_item(objectsNode, static_cast(uid)); uint64_t valueLength = 0; const char* pValue = plist_get_string_ptr(keyedItemNode, &valueLength); if (pValue == NULL || valueLength == 0) { continue; } if (idx == 0) { m_usrName.assign(pValue, valueLength); } else if (idx == 1) { m_displayName.assign(pValue, valueLength); } else if (idx == 2) { m_wxName.assign(pValue, valueLength); } } } // "new_dicsetting" plist_t keyedUidNode = plist_dict_get_item(keyedUidNodes, "new_dicsetting"); if (keyedUidNode != NULL && PLIST_IS_UID(keyedUidNode)) { uint64_t uid = 0; plist_get_uid_val(keyedUidNode, &uid); plist_t settingNode = plist_array_get_item(objectsNode, static_cast(uid)); if (keyedUidNode != NULL && PLIST_IS_DICT(settingNode)) { plist_t settingKeysNode = plist_dict_get_item(settingNode, "NS.keys"); plist_t settingValuesNode = plist_dict_get_item(settingNode, "NS.objects"); if (settingKeysNode != NULL && PLIST_IS_ARRAY(settingKeysNode) && settingValuesNode != NULL && PLIST_IS_ARRAY(settingValuesNode)) { uint32_t settingKeysNodeSize = plist_array_get_size(settingKeysNode); uint32_t settingValuesNodeSize = plist_array_get_size(settingValuesNode); uint32_t minSize = std::min(settingKeysNodeSize, settingValuesNodeSize); for (uint32_t idx = 0; idx < minSize; ++idx) { plist_t keyedUidNode = plist_array_get_item(settingKeysNode, idx); if (keyedUidNode == NULL || !PLIST_IS_UID(keyedUidNode)) { continue; } uint64_t uid = 0; plist_get_uid_val(keyedUidNode, &uid); plist_t keyedItemNode = plist_array_get_item(objectsNode, static_cast(uid)); if (keyedItemNode == NULL || !PLIST_IS_STRING(keyedItemNode)) { continue; } uint64_t valueLength = 0; const char* pValue = plist_get_string_ptr(keyedItemNode, &valueLength); if (pValue == NULL || valueLength == 0) { continue; } std::string settingKey(pValue, valueLength); if (settingKey != "headimgurl" && settingKey != "headhdimgurl") { continue; } keyedUidNode = plist_array_get_item(settingValuesNode, idx); if (keyedUidNode == NULL || !PLIST_IS_UID(keyedUidNode)) { continue; } uid = 0; plist_get_uid_val(keyedUidNode, &uid); keyedItemNode = plist_array_get_item(objectsNode, static_cast(uid)); if (keyedItemNode == NULL || !PLIST_IS_STRING(keyedItemNode)) { continue; } valueLength = 0; pValue = plist_get_string_ptr(keyedItemNode, &valueLength); if (pValue == NULL || valueLength == 0) { continue; } if (settingKey == "headimgurl") { m_portrait.assign(pValue, valueLength); } else if (settingKey == "headhdimgurl") { m_portraitHD.assign(pValue, valueLength); } if (!m_portrait.empty() && !m_portraitHD.empty()) { break; } } } } } return true; } FriendsParser::FriendsParser(bool detailedInfo/* = true*/) : m_detailedInfo(detailedInfo) { } #ifndef NDEBUG void FriendsParser::setOutputPath(const std::string& outputPath) { m_outputPath = outputPath; } #endif bool FriendsParser::parseWcdb(const std::string& mmPath, Friends& friends) { sqlite3 *db = NULL; int rc = openSqlite3Database(mmPath, &db); if (rc != SQLITE_OK) { sqlite3_close(db); return false; } std::string sql = "SELECT userName,dbContactRemark,dbContactChatRoom,dbContactHeadImage,type FROM Friend"; sqlite3_stmt* stmt = NULL; rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL); if (rc != SQLITE_OK) { std::string error = sqlite3_errmsg(db); sqlite3_close(db); return false; } while (sqlite3_step(stmt) == SQLITE_ROW) { int userType = sqlite3_column_int(stmt, 4); const char* val = reinterpret_cast(sqlite3_column_text(stmt, 0)); if (NULL == val) { continue; } std::string uid = val; /* if (Friend::isSubscription(uid)) { continue; } */ Friend& f = friends.addFriend(uid); f.setUserType(userType); parseRemark(sqlite3_column_blob(stmt, 1), sqlite3_column_bytes(stmt, 1), f); parseChatroom(sqlite3_column_blob(stmt, 2), sqlite3_column_bytes(stmt, 2), f); if (m_detailedInfo) { parseAvatar(sqlite3_column_blob(stmt, 3), sqlite3_column_bytes(stmt, 3), f); } } sqlite3_finalize(stmt); sql = "SELECT userName,dbContactRemark,dbContactChatRoom,dbContactHeadImage,type FROM OpenIMContact"; stmt = NULL; rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL); if (rc != SQLITE_OK) { std::string error = sqlite3_errmsg(db); sqlite3_close(db); return false; } while (sqlite3_step(stmt) == SQLITE_ROW) { int userType = sqlite3_column_int(stmt, 4); const char* val = reinterpret_cast(sqlite3_column_text(stmt, 0)); if (NULL == val) { continue; } std::string uid = val; /* if (Friend::isSubscription(uid)) { continue; } */ Friend& f = friends.addFriend(uid); f.setUserType(userType); parseRemark(sqlite3_column_blob(stmt, 1), sqlite3_column_bytes(stmt, 1), f); parseChatroom(sqlite3_column_blob(stmt, 2), sqlite3_column_bytes(stmt, 2), f); if (m_detailedInfo) { parseAvatar(sqlite3_column_blob(stmt, 3), sqlite3_column_bytes(stmt, 3), f); } } sqlite3_finalize(stmt); sqlite3_close(db); #ifndef NDEBUG if (!m_outputPath.empty()) { std::string data = "usrName\tWxId\tAlias\r\n"; for (std::map::const_iterator it = friends.friends.cbegin(); it != friends.friends.cend(); ++it) { data += it->second.m_usrName + "\t" + it->second.m_wxName + "\t" + it->second.m_displayName + "\r\n"; } writeFile(combinePath(m_outputPath, "Friends.txt"), data); } #endif return true; } bool FriendsParser::parseRemark(const void *data, int length, Friend& f) { RawMessage msg; if (!msg.merge(reinterpret_cast(data), length)) { return false; } std::string value; // Remark Name if (msg.parse("3", value)) { f.setDisplayName(value); } if (f.isDisplayNameEmpty() && msg.parse("1", value)) { f.setDisplayName(value); } if (msg.parse("2", value)) { f.setWxName(value); } /* if (msg.parse("6", value)) { } */ if (m_detailedInfo) { // 8: Tags // 8: "18,42" f.clearTags(); if (msg.parse("8", value)) { std::vector tags = split(value, ","); f.swapTags(tags); } } return true; } bool FriendsParser::parseAvatar(const void *data, int length, Friend& f) { RawMessage msg; if (!msg.merge(reinterpret_cast(data), length)) { return false; } std::string value; if (msg.parse("2", value)) { if (!Friend::isInvalidPortrait(value)) { f.setPortrait(value); } } if (msg.parse("3", value)) { if (!Friend::isInvalidPortrait(value)) { f.setPortraitHD(value); } } return true; } bool FriendsParser::parseChatroom(const void *data, int length, Friend& f) { RawMessage msg; if (!msg.merge(reinterpret_cast(data), length)) { return false; } std::string value; if (msg.parse("6", value)) { parseMembers(value, f); } return true; } bool FriendsParser::parseFriendTags(ITunesDb *iTunesDb, const std::string& uidHash, std::map& tags) { std::string tagFilePath = "Documents/" + uidHash + "/contactlabel.list"; tagFilePath = iTunesDb->findRealPath(tagFilePath); if (tagFilePath.empty()) { return false; } std::vector data; if (!readFile(tagFilePath, data)) { return false; } plist_t node = NULL; plist_from_memory(reinterpret_cast(&data[0]), static_cast(data.size()), &node); if (NULL == node) { return false; } // std::unique_ptr auotNode(node, plist_free); bool res = false; std::unique_ptr nodePtr(node, &plist_free); plist_t topNode = plist_access_path(node, 1, "$top"); if (!PLIST_IS_DICT(topNode)) { return false; } plist_t rootNode = plist_dict_get_item(topNode, "root"); if (!PLIST_IS_UID(rootNode)) { return false; } uint64_t topIdx = 0; plist_get_uid_val(rootNode, &topIdx); plist_t arrayNode = plist_access_path(node, 1, "$objects"); plist_type pt = plist_get_node_type(arrayNode); if (PLIST_IS_ARRAY(arrayNode)) { plist_t keysAndObjectsNode = plist_array_get_item(arrayNode, (uint32_t)topIdx); plist_t keysNode = plist_dict_get_item(keysAndObjectsNode, "NS.keys"); plist_t objectsNode = plist_dict_get_item(keysAndObjectsNode, "NS.objects"); uint32_t numberOfKeys = plist_array_get_size(keysNode); uint32_t numberOfObjects = plist_array_get_size(objectsNode); uint32_t numberOfCount = std::min(numberOfKeys, numberOfObjects); uint64_t keyIdx = 0; uint64_t objectIdx = 0; uint64_t objValIdx = 0; uint64_t strLength = 0; uint64_t keyVal = 0; for (uint32_t idx = 0; idx < numberOfCount; ++idx) { plist_t keyIdxNode = plist_array_get_item(keysNode, idx); plist_t objectIdxNode = plist_array_get_item(objectsNode, idx); pt = plist_get_node_type(keyIdxNode); pt = plist_get_node_type(objectIdxNode); plist_get_uid_val(keyIdxNode, &keyIdx); plist_get_uid_val(objectIdxNode, &objectIdx); plist_t keyNode = plist_array_get_item(arrayNode, keyIdx); plist_t objectNode = plist_array_get_item(arrayNode, objectIdx); pt = plist_get_node_type(keyNode); pt = plist_get_node_type(objectNode); if (!PLIST_IS_UINT(keyNode)) { // continue; } if (!PLIST_IS_DICT(objectNode)) { continue; } plist_t objValIdxNode = plist_dict_get_item(objectNode, "m_nsName"); if (!PLIST_IS_UID(objValIdxNode)) { continue; } plist_get_uid_val(objValIdxNode, &objValIdx); plist_t objValNode = plist_array_get_item(arrayNode, (uint32_t)objValIdx); if (!PLIST_IS_STRING(objValNode)) { continue; } plist_get_uint_val(keyNode, &keyVal); strLength = 0; const char* ptr = plist_get_string_ptr(objValNode, &strLength); std::string val; if (NULL != ptr && strLength > 0) { val.assign(ptr, strLength); } tags[keyVal] = val; } } return res; } WechatInfoParser::WechatInfoParser(ITunesDb *iTunesDb) : m_iTunesDb(iTunesDb) { } bool WechatInfoParser::parse(WechatInfo& wechatInfo) { wechatInfo.setOSVersion(m_iTunesDb->getIOSVersion()); return parsePreferences(wechatInfo); } bool WechatInfoParser::parsePreferences(WechatInfo& wechatInfo) { std::string preferencesPath = m_iTunesDb->findRealPath("Library/Preferences/com.tencent.xin.plist"); if (preferencesPath.empty()) { return false; } std::vector data; if (!readFile(preferencesPath, data)) { return false; } plist_t node = NULL; plist_from_memory(reinterpret_cast(&data[0]), static_cast(data.size()), &node); if (NULL == node) { return false; } plist_t startupVersions = plist_access_path(node, 1, "prevStartupVersions"); if (NULL != startupVersions && PLIST_IS_ARRAY(startupVersions)) { uint32_t startupVersionsSize = plist_array_get_size(startupVersions); if (startupVersionsSize > 0) { plist_t currentVersion = plist_array_get_item(startupVersions, startupVersionsSize - 1); if (NULL != currentVersion) { uint64_t versionLength = 0; const char* pVersion = plist_get_string_ptr(currentVersion, &versionLength); if (NULL != pVersion && versionLength > 0) { std::string version; version.assign(pVersion, versionLength); wechatInfo.setVersion(version); } } } } plist_t cellDataVersion = plist_access_path(node, 1, "FrameCellDataVersion"); if (NULL != cellDataVersion) { uint64_t versionLength = 0; const char* pVersion = plist_get_string_ptr(cellDataVersion, &versionLength); if (NULL != pVersion && versionLength > 0) { std::string cellDataVersion; cellDataVersion.assign(pVersion, versionLength); wechatInfo.setCellDataVersion(cellDataVersion); } } plist_free(node); return true; } SessionsParser::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) { if (cellDataVersion.empty()) { m_cellDataVersion = "V"; } } void SessionsParser::debugLog(const std::string& log) { if (NULL != m_logger) { m_logger->debug(log); } } bool SessionsParser::parse(const Friend& user, std::vector& sessions) { std::string usrNameHash = user.getHash(); std::string userRoot = "Documents/" + usrNameHash; std::string sessionDbPath = m_iTunesDb->findRealPath(combinePath(userRoot, "session", "session.db")); if (sessionDbPath.empty()) { return false; } sqlite3 *db = NULL; int rc = openSqlite3Database(sessionDbPath, &db); if (rc != SQLITE_OK) { sqlite3_close(db); return false; } std::string sql = "SELECT ConStrRes1,CreateTime,unreadcount,UsrName FROM SessionAbstract"; sqlite3_stmt* stmt = NULL; rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL); if (rc != SQLITE_OK) { std::string error = sqlite3_errmsg(db); sqlite3_close(db); return false; } while (sqlite3_step(stmt) == SQLITE_ROW) { const char *usrName = reinterpret_cast(sqlite3_column_text(stmt, 3)); if (NULL == usrName/* || Session::isSubscription(usrName)*/) { continue; } std::vector::iterator it = sessions.emplace(sessions.cend(), &user); Session& session = (*it); session.setUsrName(usrName); session.setCreateTime(static_cast(sqlite3_column_int(stmt, 1))); const char* extFileName = reinterpret_cast(sqlite3_column_text(stmt, 0)); if (NULL != extFileName) { session.setExtFileName(extFileName); } else { // Guess ext file name // /session/data/c3/2488b928e0bf604ec1cb02b53f18a7 std::string relativePath = combinePath(userRoot, "session/data", session.getHash().substr(0, 2), session.getHash().substr(2)); const ITunesFile* file = m_iTunesDb->findITunesFile(relativePath); if (NULL != file) { session.setExtFileName(file->relativePath.substr(userRoot.size())); // file->relativePath is formatted } } session.setUnreadCount(sqlite3_column_int(stmt, 2)); const Friend* f = m_friends.getFriend(session.getHash()); if (NULL != f) { it->update(*f); } } sqlite3_finalize(stmt); sqlite3_close(db); parseUniversalSessions(user, userRoot, sessions); // if (m_detailedInfo) { parseMessageDbs(user, userRoot, sessions); } #if !defined(NDEBUG) || defined(DBG_PERF) std::set displayNames; #endif for (std::vector::iterator it = sessions.begin(); it != sessions.end(); ) { if (!it->isExtFileNameEmpty()) { parseCellData(userRoot, *it); } #if !defined(NDEBUG) || defined(DBG_PERF) if (!it->isDisplayNameEmpty()) { std::set::iterator itDN = displayNames.find(it->getDisplayName()); if (itDN == displayNames.end()) { displayNames.insert(it->getDisplayName()); } else { debugLog("Duplicate ChatGroup Name:" + it->getDisplayName() + "\t " + it->getUsrName()); } } #endif /* if (!it->isUsrNameEmpty() && it->isSubscription()) { it = sessions.erase(it); continue; } */ ++it; } // Check displayName and avatar std::string shareUserRoot = "share/" + usrNameHash; parseSessionsInGroupApp(shareUserRoot, sessions); int sessionId = 1; for (std::vector::iterator it = sessions.begin(); it != sessions.end(); ++it) { if (it->isUsrNameEmpty()) { it->setEmptyUsrName("wxid_unknwn_" + std::to_string(sessionId++)); } if (it->isDisplayNameEmpty() && !it->isMemberIdsEmpty()) { // Combine the display name from member list debugLog("ChatGroup Name of " + it->getUsrName() + " is empty. Build it from members"); parseDisplayNameFromMembers(user, *it); } } #ifndef NDEBUG // Invalidate db path int cnt = 0; for (std::vector::const_iterator it = sessions.cbegin(); it != sessions.cend(); ++it) { if (it->isDbFileEmpty()) { cnt++; } } if (cnt > 0) { // assert(false); } #endif #if !defined(NDEBUG) || defined(DBG_PERF) displayNames.clear(); int emptyDNCount = 0; int duplicateDNCount = 0; for (std::vector::iterator it = sessions.begin(); it != sessions.end(); ) { if (!it->isDisplayNameEmpty()) { std::set::iterator itDN = displayNames.find(it->getDisplayName()); if (itDN == displayNames.end()) { displayNames.insert(it->getDisplayName()); } else { duplicateDNCount++; } } else { emptyDNCount++; } ++it; } debugLog("Duplicate ChatGroup Name:" + std::to_string(duplicateDNCount) + ", Empty ChatGroup Name:" + std::to_string(emptyDNCount)); #endif return true; } bool SessionsParser::parseDisplayNameFromMembers(const Friend& user, Session& session) { std::string memberIds = session.getMemberIds(); std::vector members = memberIds.empty() ? session.getMemberUsrNames() : split(memberIds, ";"); // std::vectorgetDisplayName(); it->swap(displayName); } ++it; } session.setDisplayName(join(members, ",")); return true; } bool SessionsParser::parseUniversalSessions(const Friend& user, const std::string& userRoot, std::vector& sessions) { SessionUsrNameCompare comp; std::sort(sessions.begin(), sessions.end(), comp); std::map newSessions; std::string items[] = {"BottleSession", "SubscribeSession", "SubscribeSessionList", "WASession"}; size_t numberOfItems = sizeof(items) / sizeof(std::string); for (size_t idx = 0; idx < numberOfItems; ++idx) { std::string sessionDbPath = m_iTunesDb->findRealPath(combinePath(userRoot, "UniversalSession", items[idx], "session.db")); if (sessionDbPath.empty()) { continue; } sqlite3 *db = NULL; int rc = openSqlite3Database(sessionDbPath, &db); if (rc != SQLITE_OK) { sqlite3_close(db); continue; } std::string sql = "SELECT sessionId,lastMsgUpdateTime,unreadCount FROM SessionTable"; sqlite3_stmt* stmt = NULL; rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL); if (rc != SQLITE_OK) { std::string error = sqlite3_errmsg(db); sqlite3_close(db); continue; } while (sqlite3_step(stmt) == SQLITE_ROW) { const char *usrNamePtr = reinterpret_cast(sqlite3_column_text(stmt, 0)); if (NULL == usrNamePtr/* || Session::isSubscription(usrName)*/) { continue; } std::string usrName = usrNamePtr; if (newSessions.find(usrName) != newSessions.cend()) { continue; } std::vector::iterator it = std::lower_bound(sessions.begin(), sessions.end(), usrName, comp); if (it == sessions.end() || it->getUsrName() != usrName) { Session session(usrName, md5(usrName), &user); session.setUsrName(usrName); session.setLastMessageTime(static_cast(sqlite3_column_int(stmt, 1))); session.setUnreadCount(sqlite3_column_int(stmt, 2)); // /session/data/c3/2488b928e0bf604ec1cb02b53f18a7 std::string relativePath = combinePath(userRoot, "session/data", session.getHash().substr(0, 2), session.getHash().substr(2)); const ITunesFile* file = m_iTunesDb->findITunesFile(relativePath); if (NULL != file) { session.setExtFileName(file->relativePath.substr(userRoot.size())); // file->relativePath is formatted } session.setDeleted(true); newSessions.insert(newSessions.end(), std::pair(usrName, session)); } } sqlite3_finalize(stmt); sqlite3_close(db); } for (std::map::const_iterator it = newSessions.cbegin(); it != newSessions.cend(); ++it) { sessions.push_back(it->second); } return true; } bool SessionsParser::parseMessageDbs(const Friend& user, const std::string& userRoot, std::vector& sessions) { SessionHashCompare comp; std::sort(sessions.begin(), sessions.end(), comp); MessageDbFilter filter(userRoot); ITunesFileVector dbs = m_iTunesDb->filter(filter); const ITunesFile* file = m_iTunesDb->findITunesFile(combinePath(userRoot, "DB", "MM.sqlite")); if (NULL != file) { dbs.push_back(const_cast(file)); } std::vector deletedSessions; std::vector> sessionIds; for (ITunesFilesConstIterator it = dbs.cbegin(); it != dbs.cend(); ++it) { std::string mmPath = m_iTunesDb->getRealPath(*it); parseMessageDb(user, mmPath, sessions, deletedSessions); } // Append deletedSessions at last as parseMessageDb needs SORTED sessions for (std::vector::iterator it = deletedSessions.begin(); it != deletedSessions.end(); ++it) { // /session/data/c3/2488b928e0bf604ec1cb02b53f18a7 std::string relativePath = combinePath(userRoot, "/session/data/", it->getHash().substr(0, 2), it->getHash().substr(2)); const ITunesFile* file = m_iTunesDb->findITunesFile(relativePath); if (NULL != file) { it->setExtFileName(file->relativePath); // it->relativePath is formatted } it->setDeleted(true); sessions.push_back(*it); } return true; } bool SessionsParser::parseMessageDb(const Friend& user, const std::string& mmPath, std::vector& sessions, std::vector& deletedSessions) { sqlite3 *db = NULL; int rc = openSqlite3Database(mmPath, &db); if (rc != SQLITE_OK) { sqlite3_close(db); return false; } std::string sql = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"; sqlite3_stmt* stmt = NULL; rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL); if (rc != SQLITE_OK) { std::string error = sqlite3_errmsg(db); sqlite3_close(db); return false; } SessionHashCompare comp; while (sqlite3_step(stmt) == SQLITE_ROW) { const unsigned char* pName = sqlite3_column_text(stmt, 0); if (pName == NULL) { continue; } std::string name = reinterpret_cast(pName); // "^Chat_([0-9a-f]{32})$" if (startsWith(name, "Chat_")) { std::string chatId = name.substr(5); std::vector::iterator it = std::lower_bound(sessions.begin(), sessions.end(), chatId, comp); if (it != sessions.end() && it->getHash() == chatId) { int recordCount = 0; std::string sql2 = "SELECT COUNT(*) AS rc FROM " + name; sqlite3_stmt* stmt2 = NULL; rc = sqlite3_prepare_v2(db, sql2.c_str(), (int)(sql2.size()), &stmt2, NULL); if (rc == SQLITE_OK) { it->setDbFile(mmPath); if (sqlite3_step(stmt2) == SQLITE_ROW) { recordCount = sqlite3_column_int(stmt2, 0); it->setRecordCount(recordCount); } sqlite3_finalize(stmt2); /* uint32_t lastCreateTime = 0; std::string sql3 = "SELECT MAX(CreateTime) AS lct FROM " + name; sqlite3_stmt* stmt3 = NULL; rc = sqlite3_prepare_v2(db, sql3.c_str(), (int)(sql3.size()), &stmt3, NULL); if (rc == SQLITE_OK) { if (sqlite3_step(stmt3) == SQLITE_ROW) { lastCreateTime = sqlite3_column_int(stmt3, 0); } sqlite3_finalize(stmt3); } it = deletedSessions.emplace(deletedSessions.cend(), "", chatId, &user); it->setDbFile(mmPath); it->setLastMessageTime(lastCreateTime); it->setRecordCount(recordCount); */ } sql2 = "SELECT CreateTime,Des,Message,Type FROM " + name + " ORDER BY CreateTime DESC LIMIT 1"; rc = sqlite3_prepare_v2(db, sql2.c_str(), (int)(sql2.size()), &stmt2, NULL); if (rc == SQLITE_OK) { if (sqlite3_step(stmt2) == SQLITE_ROW) { sqlite3_int64 lastCreateTime = sqlite3_column_int64(stmt2, 0); int des = sqlite3_column_int(stmt2, 1); const unsigned char *pMsg = sqlite3_column_text(stmt2, 2); int type = sqlite3_column_int(stmt2, 3); it->setLastMessageTime((unsigned int)lastCreateTime); if (NULL != pMsg) { std::string msg = (const char *)pMsg; if (it->isChatroom()) { if (des != 0) { std::string::size_type enter = msg.find(":\n"); if (enter != std::string::npos && enter + 2 < msg.size()) { std::string senderId = msg.substr(0, enter); it->setLastMessageUsrName(senderId, m_friends); if (type == 1) { msg = msg.substr(enter + 2); it->setLastMessage(msg); } else { // it->setLastMessage(""); } } } else { // Me it->setLastMessageUsrName(user.getUsrName(), user.getDisplayName()); if (type == 1) { it->setLastMessage(msg); } else { // it->setLastMessage("-"); } } } else { if (des != 0) { it->setLastMessageUsrName(it->getUsrName(), m_friends); } else { // Me it->setLastMessageUsrName(user.getUsrName(), user.getDisplayName()); } if (type == 1) { it->setLastMessage(msg); } else { // it->setLastMessage("-"); } } } } sqlite3_finalize(stmt2); } } else { // ASSERT (false) } } } sqlite3_finalize(stmt); sqlite3_close(db); return true; } bool SessionsParser::parseCellData(const std::string& userRoot, Session& session) { std::string fileName = session.getExtFileName(); if (startsWith(fileName, DIR_SEP) || startsWith(fileName, ALT_DIR_SEP)) { fileName = fileName.substr(1); } std::string cellDataPath = combinePath(userRoot, fileName); fileName = m_iTunesDb->findRealPath(cellDataPath); if (fileName.empty()) { return false; } RawMessage msg; if (!msg.mergeFile(fileName)) { return false; } std::string value; int value2 = 0; if (msg.parse("1.1.1", value)) { if (session.isUsrNameEmpty() && (session.isHashEmpty() || md5(value) == session.getHash())) { session.setUsrName(value); } } if (session.isMemberIdsEmpty() && msg.parse("1.3", value) && !value.empty()) { session.setMemberIds(value); } if (msg.parse("1.1.6", value)) { session.setDisplayName(value); } if (msg.parse("1.1.4", value)) { if (session.isDisplayNameEmpty()) { session.setDisplayName(value); } } if (msg.parse("1.1.14", value)) { if (startsWith(value, "http://") || startsWith(value, "https://")) { session.setPortrait(value); } } if (msg.parse("1.5", value)) { parseMembers(value, session); } if (msg.parse("2.7", value2)) { // session.setLastMessageTime(static_cast(value2)); } if (msg.parse("2.2", value2)) { if (session.getRecordCount() == 0) { session.setRecordCount(value2); } } std::string lastMsg; std::string lastMsgUser; bool result1 = msg.parse("2.11", lastMsg); bool result2 = msg.parse("2.10", lastMsgUser); if (!result2 || lastMsgUser.empty()) { result2 = msg.parse("2.8", lastMsgUser); } if (result1 || result2) { #ifndef NDEBUG if (startsWith(lastMsg, "17390608691")) { msg.parse("2.9", value); msg.parse("2.10", value); } #endif // session.setLastMessage(lastMsg, lastMsgUser, friends); } // 9: "wxid_tub3kj534ntk12" // 10: "wxid_dsbf267m3a9o11" // if (msg.parse("2.9", lastMsgUser)) { // session.setLastMessageUsrName(lastMsgUser, friends); } if (session.isDisplayNameEmpty()) { SessionCellDataFilter filter(cellDataPath, m_cellDataVersion); ITunesFileVector items = m_iTunesDb->filter(filter); unsigned int lastModifiedTime = 0; for (ITunesFilesConstIterator it = items.cbegin(); it != items.cend(); ++it) { fileName = m_iTunesDb->getRealPath(*it); if (fileName.empty()) { continue; } RawMessage msg2; if (!msg2.mergeFile(fileName)) { continue; } std::string displayName; std::string msgTime; if (msg2.parse("10", value)) { msgTime = value; } if (msg2.parse("7", value)) { displayName = value; } unsigned int modifiedTime = 0; if (items.size() > 1) { modifiedTime = ITunesDb::parseModifiedTime((*it)->blob); } if (session.isDisplayNameEmpty() || (!displayName.empty() && modifiedTime > lastModifiedTime)) { session.setDisplayName(displayName); lastModifiedTime = modifiedTime; } } } return true; } bool SessionsParser::parseSessionsInGroupApp(const std::string& userRoot, std::vector& sessions) { std::map sessionMap; for (std::vector::iterator it = sessions.begin(); it != sessions.end(); ++it) { if (!it->isUsrNameEmpty()) { sessionMap.insert(sessionMap.cend(), std::make_pair<>(it->getUsrName(), &(*it))); } } SessionHashCompare comp; std::sort(sessions.begin(), sessions.end(), comp); std::string sessionDataArchivePath = m_iTunesDbShare->findRealPath(combinePath(userRoot, "session", "sessionData.archive")); if (!sessionDataArchivePath.empty()) { std::vector contents; if (readFile(sessionDataArchivePath, contents)) { RawMessage msg; if (msg.merge(reinterpret_cast(&contents[0]), static_cast(contents.size()))) { std::string value; if (msg.parse("1", value)) { uint32_t blockLen = 0; const char * data = value.c_str(); const char * data1 = NULL; while ((data1 = calcVarint32Ptr(data, value.c_str() + value.size(), &blockLen)) != NULL) { RawMessage msg2; if (msg2.merge(data1, static_cast(blockLen))) { std::string usrName; if (msg2.parse("1", usrName)) { Session* session = NULL; std::map::iterator it = sessionMap.find(usrName); if (it != sessionMap.end()) { session = it->second; } else { std::string uidHash = md5(usrName); std::vector::iterator it = std::lower_bound(sessions.begin(), sessions.end(), uidHash, comp); if (it != sessions.end() && it->getHash() == uidHash) { session = &(*it); if (it->isUsrNameEmpty()) { it->setUsrName(usrName); sessionMap.insert(sessionMap.cend(), std::make_pair<>(usrName, session)); } } } if (NULL != session && session->isDisplayNameEmpty()) { std::string nameCand1; std::string nameCand2; if (msg2.parse("2", nameCand1)) { // nameCand1 = value2; } if (!msg2.parse("3", nameCand2)) { nameCand2 = ""; } session->setDisplayName(nameCand2.empty() ? nameCand1 : nameCand2); } } } data = data1 + blockLen; } } } } } std::string extraSessionDataArchivePath = m_iTunesDbShare->findRealPath(combinePath(userRoot, "session", "extraSessionExtraSessionData.archive")); if (!extraSessionDataArchivePath.empty()) { std::vector contents; if (readFile(extraSessionDataArchivePath, contents)) { RawMessage msg; if (msg.merge(reinterpret_cast(&contents[0]), static_cast(contents.size()))) { std::string value; if (msg.parse("1", value)) { uint32_t blockLen = 0; const char * data = value.c_str(); const char * data1 = NULL; while ((data1 = calcVarint32Ptr(data, value.c_str() + value.size(), &blockLen)) != NULL) { RawMessage msg2; if (msg2.merge(data1, static_cast(blockLen))) { std::string value2; if (msg2.parse("1", value2)) { Session* session = NULL; std::map::iterator it = sessionMap.find(value2); if (it != sessionMap.end()) { session = it->second; } else { std::string uidHash = md5(value2); std::vector::iterator it = std::lower_bound(sessions.begin(), sessions.end(), uidHash, comp); if (it != sessions.end() && it->getHash() == uidHash) { session = &(*it); if (it->isUsrNameEmpty()) { it->setUsrName(value2); sessionMap.insert(sessionMap.cend(), std::make_pair<>(value2, session)); } } } if (NULL != session && session->isDisplayNameEmpty()) { std::string name; if (msg2.parse("2", value2)) { name = value2; } if (!msg2.parse("3", value2)) { value2 = ""; } session->setDisplayName(value2.empty() ? name : value2); } } } data = data1 + blockLen; } } } } } // copy avatar for (std::vector::iterator it = sessions.begin(); it != sessions.end(); ++it) { std::string avatarPath = m_iTunesDbShare->findRealPath(combinePath(userRoot, "session", "headImg", it->getHash() + ".pic")); if (!avatarPath.empty() && !Friend::isDefaultAvatar(avatarPath)) { it->setPortrait("file://" + avatarPath); } } return true; } SessionParser::SessionParser(const ExportOption& options) : m_options(options) { } SessionParser::MessageEnumerator* SessionParser::buildMsgEnumerator(const Session& session, uint64_t minId) { return new MessageEnumerator(session, m_options, minId); } uint32_t SessionParser::calcNumberOfMessages(const Session& session, uint64_t minId) { sqlite3* db = NULL; int rc = openSqlite3Database(session.getDbFile(), &db); if (rc != SQLITE_OK) { if (NULL != db) { sqlite3_close(db); db = NULL; } return 0; } std::string sql = "SELECT COUNT(MesLocalID) FROM Chat_" + session.getHash() + " WHERE MesLocalID>" + std::to_string(minId); sqlite3_stmt* stmt = NULL; rc = sqlite3_prepare_v2(db, sql.c_str(), (int)(sql.size()), &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); db = NULL; return 0; } uint32_t recordCount = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { recordCount = (uint32_t)sqlite3_column_int64(stmt, 0); } if (NULL != stmt) sqlite3_finalize(stmt); if (NULL != db) sqlite3_close(db); return recordCount; } struct MSG_ENUMERATOR_CONTEXT { sqlite3* db; sqlite3_stmt* stmt; MSG_ENUMERATOR_CONTEXT(sqlite3* d, sqlite3_stmt* s) : db(d), stmt(s) { } ~MSG_ENUMERATOR_CONTEXT() { if (NULL != stmt) sqlite3_finalize(stmt); if (NULL != db) sqlite3_close(db); } }; SessionParser::MessageEnumerator::MessageEnumerator(const Session& session, const ExportOption& options, int64_t minId) { MSG_ENUMERATOR_CONTEXT* context = new MSG_ENUMERATOR_CONTEXT(NULL, NULL); m_context = context; int rc = openSqlite3Database(session.getDbFile(), &(context->db)); if (rc != SQLITE_OK) { sqlite3_close(context->db); context->db = NULL; return; } std::string sql = "SELECT CreateTime,Des,MesLocalID,Message,MesSvrID,Status,TableVer,Type FROM Chat_" + session.getHash(); if (minId > 0) { // Incremental Exporting sql += " WHERE MesLocalID>" + std::to_string(minId); } sql += " ORDER BY CreateTime"; if (options.isDesc()) { sql += " DESC"; } rc = sqlite3_prepare_v2(context->db, sql.c_str(), (int)(sql.size()), &(context->stmt), NULL); if (rc != SQLITE_OK) { sqlite3_close(context->db); context->db = NULL; return; } } SessionParser::MessageEnumerator::~MessageEnumerator() { if (NULL != m_context) { MSG_ENUMERATOR_CONTEXT* context = reinterpret_cast(m_context); if (NULL != context) { delete context; } m_context = NULL; } } bool SessionParser::MessageEnumerator::isInvalid() const { if (NULL != m_context) { const MSG_ENUMERATOR_CONTEXT* context = reinterpret_cast(m_context); if (NULL != context) { return NULL != context->db && NULL != context->stmt; } } return false; } bool SessionParser::MessageEnumerator::nextMessage(WXMSG& msg) { if (NULL == m_context) { return NULL; } MSG_ENUMERATOR_CONTEXT* context = reinterpret_cast(m_context); if (NULL == context || NULL == context->db || NULL == context->stmt) { return false; } if (sqlite3_step(context->stmt) == SQLITE_ROW) { msg.createTime = (unsigned int)sqlite3_column_int(context->stmt, 0); msg.des = sqlite3_column_int(context->stmt, 1); msg.msgIdValue = sqlite3_column_int64(context->stmt, 2); msg.msgId = std::to_string(msg.msgIdValue); const unsigned char* pMessage = sqlite3_column_text(context->stmt, 3); if (pMessage != NULL) { msg.content = reinterpret_cast(pMessage); } else { msg.content.clear(); } msg.msgSvrId = (sqlite_uint64)sqlite3_column_int64(context->stmt, 4); msg.status = sqlite3_column_int(context->stmt, 5); msg.tableVersion = sqlite3_column_int(context->stmt, 6); msg.type = sqlite3_column_int(context->stmt, 7); return true; } return false; } ================================================ FILE: WechatExporter/core/WechatParser.h ================================================ // // WechatParser.h // WechatExporter // // Created by Matthew on 2020/9/30. // Copyright © 2020 Matthew. All rights reserved. // #ifndef WechatParser_h #define WechatParser_h #include #include #include #include #include #include #include "Utils.h" #include "Downloader.h" #include "WechatObjects.h" #include "ExportOption.h" #include "ITunesParser.h" #include "MessageParser.h" #include "Logger.h" template class FilterBase { protected: std::string m_path; std::string m_pattern; public: bool operator() (const ITunesFile* s1, const T& s2) const // less { return !startsWith(s1->relativePath, m_path) && s1->relativePath < m_path; } bool operator() (const T& s2, const ITunesFile* s1) const // greater { return !startsWith(s1->relativePath, m_path) && s1->relativePath > m_path; } bool operator==(const ITunesFile* s) const { return startsWith(s->relativePath, m_path) && (s->relativePath.find(m_pattern, m_path.size()) != std::string::npos); } std::string parse(const ITunesFile* s) const { if (*this == s) { return s->relativePath.substr(m_path.size()); } return std::string(""); } }; template class RegexFilterBase { protected: std::string m_path; std::regex m_pattern; public: bool operator() (const ITunesFile* s1, const T& s2) const // less { return !startsWith(s1->relativePath, m_path) && s1->relativePath < m_path; } bool operator() (const T& s2, const ITunesFile* s1) const // greater { return !startsWith(s1->relativePath, m_path) && s1->relativePath > m_path; } bool operator==(const ITunesFile* s) const { std::smatch sm; return startsWith(s->relativePath, m_path) && std::regex_search(s->relativePath.begin() + m_path.size(), s->relativePath.end(), sm, m_pattern); } std::string parse(const ITunesFile* s) const { std::smatch sm; if (std::regex_search(s->relativePath.begin() + m_path.size(), s->relativePath.end(), sm, m_pattern)) { return sm[1]; } return std::string(""); } }; class MessageDbFilter : public RegexFilterBase { public: MessageDbFilter(const std::string& basePath) : RegexFilterBase() { std::string vpath = basePath; std::replace(vpath.begin(), vpath.end(), '\\', '/'); if (!endsWith(vpath, "/")) { vpath += "/"; } vpath += "DB/"; m_path = vpath; m_pattern = "^(message_[0-9]{1,4}\\.sqlite)$"; } }; class UserFolderFilter : public RegexFilterBase { protected: std::string m_suffix; size_t m_pathLen; size_t m_suffixLen; public: UserFolderFilter() : RegexFilterBase() { m_path = "Documents/"; // m_pattern = "^([a-zA-Z0-9]{32})$"; m_suffix = "/DB/MM.sqlite"; m_pathLen = m_path.length(); m_suffixLen = m_suffix.length(); } bool operator==(const ITunesFile* s) const { if (/*(s->relativePath.size() != (m_path.size() + 32 + 13)) || */!startsWith(s->relativePath, m_path)|| !endsWith(s->relativePath, m_suffix)) { return false; } // if (s->relativePath.find('/', m_path.size(), 32) != std::string::npos) { // return false; } return true; } std::string parse(const ITunesFile* s) const { return s->relativePath.substr(m_pathLen, s->relativePath.length() - m_pathLen - m_suffixLen); // return s->relativePath.substr(m_path.size()) : ""; // return s->relativePath.size() > 32 ? s->relativePath.substr(m_path.size()) : ""; } }; class WechatInfoParser { private: ITunesDb *m_iTunesDb; public: WechatInfoParser(ITunesDb *iTunesDb); bool parse(WechatInfo& wechatInfo); protected: bool parsePreferences(WechatInfo& wechatInfo); }; class SessionCellDataFilter : public FilterBase { public: SessionCellDataFilter(const std::string& cellDataBasePath, const std::string& cellDataVersion) : FilterBase() { std::string vpath = cellDataBasePath; std::replace(vpath.begin(), vpath.end(), '\\', '/'); m_path = vpath; m_pattern = "celldata" + cellDataVersion; // celldataV7 } }; class LoginInfo2Parser { private: ITunesDb *m_iTunesDb; std::string m_error; Logger* m_logger; public: LoginInfo2Parser(ITunesDb *iTunesDb, Logger* logger); bool parse(std::vector& users); bool parse(const std::string& loginInfo2Path, std::vector& users); std::string getError() const; private: int parseUser(const char* data, int length, std::vector& users); bool parseUserFromFolder(std::vector& users); bool parseMMSettingsFromMMKV(std::map>& mmsettingFiles); void debugLog(const std::string& log); }; class MMSettingInMMappedKVFilter : public FilterBase { protected: std::string m_suffix; public: MMSettingInMMappedKVFilter(const std::string& uid) : FilterBase() { m_pattern = "Documents/MMappedKV/"; m_path = m_pattern + "mmsetting.archive." + uid; m_suffix = ".crc"; } MMSettingInMMappedKVFilter() : FilterBase() { m_pattern = "Documents/MMappedKV/"; m_path = m_pattern + "mmsetting.archive."; m_suffix = ".crc"; } std::string getPrefix() const { return "mmsetting.archive."; } bool operator==(const ITunesFile* s) const { return startsWith(s->relativePath, m_path) && !endsWith(s->relativePath, m_suffix); } std::string parse(const ITunesFile* s) const { if (*this == s) { return s->relativePath.substr(m_pattern.size()); } return std::string(""); } }; class MMSettings { protected: std::string m_usrName; std::string m_wxName; std::string m_displayName; std::string m_portrait; std::string m_portraitHD; public: std::string getUsrName() const; std::string getWxName() const; // WeiXin Hao std::string getDisplayName() const; std::string getPortrait() const; std::string getPortraitHD() const; protected: void clear(); }; class MMKVParser : public MMSettings { private: Logger* m_logger; public: MMKVParser(Logger* logger); bool parse(const std::string& path, const std::string& crcPath); private: void debugLog(const std::string& log); }; class MMSettingParser : public MMSettings { private: ITunesDb *m_iTunesDb; public: MMSettingParser(ITunesDb *iTunesDb); bool parse(const std::string& usrNameHash); }; class FriendsParser { public: FriendsParser(bool detailedInfo = true); bool parseWcdb(const std::string& mmPath, Friends& friends); #ifndef NDEBUG void setOutputPath(const std::string& outputPath); #endif bool parseFriendTags(ITunesDb *iTunesDb, const std::string& uidHash, std::map& tags); private: bool parseRemark(const void *data, int length, Friend& f); bool parseAvatar(const void *data, int length, Friend& f); bool parseChatroom(const void *data, int length, Friend& f); private: bool m_detailedInfo; #ifndef NDEBUG std::string m_outputPath; #endif }; class SessionsParser { private: ITunesDb *m_iTunesDb; ITunesDb *m_iTunesDbShare; std::string m_cellDataVersion; const Friends& m_friends; bool m_detailedInfo; Logger* m_logger; public: SessionsParser(ITunesDb *iTunesDb, ITunesDb *iTunesDbShare, const Friends& friends, const std::string& cellDataVersion, Logger* logger, bool detailedInfo = true); bool parse(const Friend& user, std::vector& sessions); private: bool parseUniversalSessions(const Friend& user, const std::string& userRoot, std::vector& sessions); bool parseCellData(const std::string& userRoot, Session& session); bool parseMessageDbs(const Friend& user, const std::string& userRoot, std::vector& sessions); bool parseMessageDb(const Friend& user, const std::string& mmPath, std::vector& sessions, std::vector& deletedSessions); bool parseSessionsInGroupApp(const std::string& userRoot, std::vector& sessions); bool parseDisplayNameFromMembers(const Friend& user, Session& session); void debugLog(const std::string& log); }; class SessionParser { public: class MessageEnumerator { protected: MessageEnumerator(const Session& session, const ExportOption& options, int64_t minId); friend SessionParser; public: bool isInvalid() const; bool nextMessage(WXMSG& msg); ~MessageEnumerator(); private: void* m_context; }; private: ExportOption m_options; public: SessionParser(const ExportOption& options); MessageEnumerator* buildMsgEnumerator(const Session& session, uint64_t minId); static uint32_t calcNumberOfMessages(const Session& session, uint64_t minId); }; #endif /* WechatParser_h */ ================================================ FILE: WechatExporter/core/WechatSource.h ================================================ // // WechatDataSource.h // WechatExporter // // Created by Matthew on 2021/12/23. // Copyright © 2021 Matthew. All rights reserved. // #ifndef WechatDataSource_h #define WechatDataSource_h #include "ITunesParser.h" #include "IDeviceBackup.h" class WechatSource { public: WechatSource() : m_deviceInfo(NULL) { } WechatSource(const BackupItem& backupItem) : m_backupItem(backupItem), m_deviceInfo(NULL) { } WechatSource(const DeviceInfo& deviceInfo) : m_deviceInfo(NULL) { m_deviceInfo = new DeviceInfo(deviceInfo); } ~WechatSource() { if (NULL != m_deviceInfo) { delete m_deviceInfo; } } bool operator==(const WechatSource& rhs) { if (isDevice() != rhs.isDevice()) { return false; } if (isDevice()) { return m_deviceInfo->getUdid() == rhs.m_deviceInfo->getUdid(); } else { return m_backupItem.getPath() == rhs.m_backupItem.getPath(); } } const BackupItem& getBackupItem() const { return m_backupItem; } void setBackupItem(const BackupItem& backupItem) { m_backupItem = backupItem; } const DeviceInfo* getDeviceInfo() const { return m_deviceInfo; } DeviceInfo* getDeviceInfo() { return m_deviceInfo; } bool isDevice() const { return NULL != m_deviceInfo; } std::string getDisplayName() const { if (isDevice()) { return m_deviceInfo->getName(); } return m_backupItem.toString(); } private: BackupItem m_backupItem; DeviceInfo* m_deviceInfo; }; #endif /* WechatDataSource_h */ ================================================ FILE: WechatExporter/core/XmlParser.cpp ================================================ // // XmlParser.cpp // WechatExporter // // Created by Matthew on 2020/11/12. // Copyright © 2020 Matthew. All rights reserved. // #include "XmlParser.h" struct NodeValueHandler { std::string& value; bool operator() (xmlNodeSetPtr xpathNodes) { // assert ((xpathNodes) && (xpathNodes->nodeNr > 0)) xmlNode *cur = xpathNodes->nodeTab[0]; xmlChar* sz = xmlNodeGetContent(cur); if (sz != NULL) { value = reinterpret_cast(sz); xmlFree(sz); return true; } return false; } }; struct NodesValueHandler { std::map& values; bool operator() (xmlNodeSetPtr xpathNodes) { // assert ((xpathNodes) && (xpathNodes->nodeNr > 0)) for (int idx = 0; idx < xpathNodes->nodeNr; ++idx) { xmlNode *cur = xpathNodes->nodeTab[idx]; std::map::iterator it = values.find(reinterpret_cast(cur->name)); if (it != values.end()) { xmlChar* sz = xmlNodeGetContent(cur); if (sz != NULL) { it->second = reinterpret_cast(sz); xmlFree(sz); } else { it->second.clear(); } } } return true; } }; struct AttributeHandler { const std::string& name; std::string& value; bool operator() (xmlNodeSetPtr xpathNodes) { // assert ((xpathNodes) && (xpathNodes->nodeNr > 0)) xmlNode *cur = xpathNodes->nodeTab[0]; xmlChar* attr = xmlGetProp(cur, reinterpret_cast(name.c_str())); if (NULL != attr) { value = reinterpret_cast(attr); xmlFree(attr); return true; } return false; } }; struct AttributesHandler { std::map& attributes; bool operator() (xmlNodeSetPtr xpathNodes) { // assert ((xpathNodes) && (xpathNodes->nodeNr > 0)) xmlNode *cur = xpathNodes->nodeTab[0]; for (std::map::iterator it = attributes.begin(); it != attributes.end(); ++it) { xmlChar* attr = xmlGetProp(cur, reinterpret_cast(it->first.c_str())); if (NULL != attr) { it->second = reinterpret_cast(attr); xmlFree(attr); } else { it->second.clear(); } } return true; } }; bool XmlParser::getChildNodeContent(xmlNodePtr node, const std::string& childName, std::string& value) { bool found = false; xmlNodePtr childNode = xmlFirstElementChild(node); while (NULL != childNode) { if (xmlStrcmp(childNode->name, BAD_CAST(childName.c_str())) == 0) { value = getNodeInnerText(childNode); found = true; break; } childNode = childNode->next; } return found; } xmlNodePtr XmlParser::getChildNode(xmlNodePtr node, const std::string& childName) { xmlNodePtr result = NULL; xmlNodePtr childNode = xmlFirstElementChild(node); while (NULL != childNode) { if (xmlStrcmp(childNode->name, BAD_CAST(childName.c_str())) == 0) { result = childNode; break; } childNode = childNode->next; } return result; } xmlNodePtr XmlParser::getNextNodeSibling(xmlNodePtr node) { return xmlNextElementSibling(node); } bool XmlParser::getNodeAttributeValue(xmlNodePtr node, const std::string& attributeName, std::string& value) { xmlChar* attr = xmlGetProp(node, BAD_CAST(attributeName.c_str())); if (NULL != attr) { value = reinterpret_cast(attr); xmlFree(attr); return true; } return false; } XmlParser::XmlParser(const std::string& xml, bool noError/* = false*/) : m_doc(NULL), m_xpathCtx(NULL) { int options = XML_PARSE_RECOVER; if (noError) { options |= XML_PARSE_NOERROR; } // xmlSetGenericErrorFunc(NULL, xmlGenericErrorImpl); // xmlSetStructuredErrorFunc(NULL, xmlStructuredErrorImpl); m_doc = xmlReadMemory(xml.c_str(), static_cast(xml.size()), NULL, NULL, options); if (m_doc != NULL) { m_xpathCtx = xmlXPathNewContext(m_doc); } } XmlParser::~XmlParser() { if (m_xpathCtx) { xmlXPathFreeContext(m_xpathCtx); } if (m_doc) { xmlFreeDoc(m_doc); } } xmlXPathObjectPtr XmlParser::evalXPathOnNode(xmlNodePtr node, const std::string& xpath) { return xmlXPathNodeEval(node, BAD_CAST(xpath.c_str()), m_xpathCtx); } bool XmlParser::parseNodeValue(const std::string& xpath, std::string& value) const { NodeValueHandler handler = {value}; return parseWithHandler(xpath, handler); } bool XmlParser::parseNodesValue(const std::string& xpath, std::map& values) const { NodesValueHandler handler = {values}; return parseWithHandler(xpath, handler); } bool XmlParser::parseChildNodesValue(xmlNodePtr parentNode, const std::string& xpath, std::map& values) const { XPathEnumerator enumerator(*this, parentNode, xpath); if (enumerator.isInvalid()) { return false; } while (enumerator.hasNext()) { xmlNodePtr cur = enumerator.nextNode(); std::string name = reinterpret_cast(cur->name); std::map::iterator it = values.find(name); if (it != values.end()) { it->second = getNodeInnerText(cur); } } return true; } bool XmlParser::parseAttributeValue(const std::string& xpath, const std::string& attributeName, std::string& value) const { AttributeHandler handler = {attributeName, value}; return parseWithHandler(xpath, handler); } bool XmlParser::parseAttributesValue(const std::string& xpath, std::map& attributes) const { AttributesHandler handler = {attributes}; return parseWithHandler(xpath, handler); } ================================================ FILE: WechatExporter/core/XmlParser.h ================================================ // // XmlParser.hpp // WechatExporter // // Created by Matthew on 2020/11/12. // Copyright © 2020 Matthew. All rights reserved. // #ifndef XmlParser_h #define XmlParser_h #include #include #include #include #include #include class XmlParser; class XmlParser { public: XmlParser(const std::string& xml, bool noError = false); ~XmlParser(); bool parseNodeValue(const std::string& xpath, std::string& value) const; bool parseNodesValue(const std::string& xpath, std::map& values) const; // e.g.: /path1/path2/* bool parseChildNodesValue(xmlNodePtr parentNode, const std::string& xpath, std::map& values) const; // e.g.: /path1/path2/* bool parseAttributeValue(const std::string& xpath, const std::string& attributeName, std::string& value) const; bool parseAttributesValue(const std::string& xpath, std::map& attributes) const; static xmlNodePtr getChildNode(xmlNodePtr node, const std::string& childName); static xmlNodePtr getNextNodeSibling(xmlNodePtr node); static std::string getNodeInnerText(xmlNodePtr node); static std::string getNodeInnerXml(xmlNodePtr node); static std::string getNodeOuterXml(xmlNodePtr node); static bool getChildNodeContent(xmlNodePtr node, const std::string& childName, std::string& value); static bool getNodeAttributeValue(xmlNodePtr node, const std::string& attributeName, std::string& value); class XPathEnumerator { public: XPathEnumerator(const XmlParser& xmlParser, xmlNodePtr curNode, const std::string& xpath) : m_xPathObj(NULL), m_numberOfNodes(0), m_cursor(-1) { m_xPathObj = xmlXPathNodeEval(curNode, BAD_CAST(xpath.c_str()), xmlParser.m_xpathCtx); if (NULL != m_xPathObj && NULL != m_xPathObj->nodesetval) { m_numberOfNodes = m_xPathObj->nodesetval->nodeNr; } } XPathEnumerator(const XmlParser& xmlParser, const std::string& xpath) : m_xPathObj(NULL), m_numberOfNodes(0), m_cursor(-1) { m_xPathObj = xmlXPathEvalExpression(BAD_CAST(xpath.c_str()), xmlParser.m_xpathCtx); if (NULL != m_xPathObj && NULL != m_xPathObj->nodesetval) { m_numberOfNodes = m_xPathObj->nodesetval->nodeNr; } } bool isInvalid() const { return NULL == m_xPathObj; } bool hasNext() const { return m_cursor < (m_numberOfNodes - 1); } xmlNodePtr nextNode() { return m_xPathObj->nodesetval->nodeTab[++m_cursor]; } ~XPathEnumerator() { if (NULL != m_xPathObj) { xmlXPathFreeObject(m_xPathObj); } } private: xmlXPathObjectPtr m_xPathObj; int m_numberOfNodes; mutable int m_cursor; }; public: template bool parseWithHandler(const std::string& xpath, TNodeHandler& handler) const; xmlXPathObjectPtr evalXPathOnNode(xmlNodePtr node, const std::string& xpath); private: xmlDocPtr m_doc; xmlXPathContextPtr m_xpathCtx; }; inline std::string XmlParser::getNodeInnerText(xmlNodePtr node) { if (NULL == node->children) { return ""; } const char* content = reinterpret_cast(XML_GET_CONTENT(node->children)); return NULL == content ? "" : std::string(content); } inline std::string XmlParser::getNodeInnerXml(xmlNodePtr node) { xmlChar* content = xmlNodeGetContent(node); // const char* szContent = reinterpret_cast(content); std::string xml = (NULL == content) ? "" : reinterpret_cast(content); xmlFree(content); return xml; } inline std::string XmlParser::getNodeOuterXml(xmlNodePtr node) { xmlBufferPtr buffer = xmlBufferCreate(); #ifndef NDEBUG int size = xmlNodeDump(buffer, node->doc, node, 0, 1); #else int size = xmlNodeDump(buffer, node->doc, node, 0, 0); // no format for release #endif // const char* content = reinterpret_cast(XML_GET_CONTENT(node->children)); std::string xml; if (size > 0 && NULL != buffer->content) { xml.assign(reinterpret_cast(buffer->content), size); } xmlBufferFree(buffer); return xml; } template bool XmlParser::parseWithHandler(const std::string& xpath, TNodeHandler& handler) const { bool result = false; if (m_doc == NULL || m_xpathCtx == NULL) { return false; } xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression(BAD_CAST(xpath.c_str()), m_xpathCtx); if (xpathObj != NULL) { xmlNodeSetPtr xpathNodes = xpathObj->nodesetval; if ((xpathNodes) && (xpathNodes->nodeNr > 0)) { if (handler(xpathNodes)) { result = true; } } xmlXPathFreeObject(xpathObj); } return result; } #endif /* XmlParser_h */ ================================================ FILE: WechatExporter/core/endianness.h ================================================ #ifndef ENDIANNESS_H #define ENDIANNESS_H #ifndef __LITTLE_ENDIAN #define __LITTLE_ENDIAN 1234 #endif #ifndef __BIG_ENDIAN #define __BIG_ENDIAN 4321 #endif #ifndef __BYTE_ORDER #ifdef __LITTLE_ENDIAN__ #define __BYTE_ORDER __LITTLE_ENDIAN #else #ifdef __BIG_ENDIAN__ #define __BYTE_ORDER __BIG_ENDIAN #endif #endif #endif #ifndef be16toh #if __BYTE_ORDER == __BIG_ENDIAN #define be16toh(x) (x) #else #define be16toh(x) ((((x) & 0xFF00) >> 8) | (((x) & 0x00FF) << 8)) #endif #endif #ifndef htobe16 #define htobe16 be16toh #endif #ifndef le16toh #if __BYTE_ORDER == __BIG_ENDIAN #define le16toh(x) ((((x) & 0xFF00) >> 8) | (((x) & 0x00FF) << 8)) #else #define le16toh(x) (x) #endif #endif #ifndef htole16 #define htole16 le16toh #endif #ifndef __bswap_32 #define __bswap_32(x) ((((x) & 0xFF000000) >> 24) \ | (((x) & 0x00FF0000) >> 8) \ | (((x) & 0x0000FF00) << 8) \ | (((x) & 0x000000FF) << 24)) #endif #ifndef be32toh #if __BYTE_ORDER == __BIG_ENDIAN #define be32toh(x) (x) #else #define be32toh(x) __bswap_32(x) #endif #endif #ifndef htobe32 #define htobe32 be32toh #endif #ifndef le32toh #if __BYTE_ORDER == __BIG_ENDIAN #define le32toh(x) __bswap_32(x) #else #define le32toh(x) (x) #endif #endif #ifndef htole32 #define htole32 le32toh #endif #ifndef __bswap_64 #define __bswap_64(x) ((((x) & 0xFF00000000000000ull) >> 56) \ | (((x) & 0x00FF000000000000ull) >> 40) \ | (((x) & 0x0000FF0000000000ull) >> 24) \ | (((x) & 0x000000FF00000000ull) >> 8) \ | (((x) & 0x00000000FF000000ull) << 8) \ | (((x) & 0x0000000000FF0000ull) << 24) \ | (((x) & 0x000000000000FF00ull) << 40) \ | (((x) & 0x00000000000000FFull) << 56)) #endif #ifndef htobe64 #if __BYTE_ORDER == __BIG_ENDIAN #define htobe64(x) (x) #else #define htobe64(x) __bswap_64(x) #endif #endif #ifndef be64toh #define be64toh htobe64 #endif #ifndef le64toh #if __BYTE_ORDER == __LITTLE_ENDIAN #define le64toh(x) (x) #else #define le64toh(x) __bswap_64(x) #endif #endif #ifndef htole64 #define htole64 le64toh #endif #endif /* ENDIANNESS_H */ ================================================ FILE: WechatExporter/core/md5.c ================================================ /* * This code implements the MD5 message-digest algorithm. * The algorithm is due to Ron Rivest. This code was * written by Colin Plumb in 1993, no copyright is claimed. * This code is in the public domain; do with it what you wish. * * Equivalent code is available from RSA Data Security, Inc. * This code has been tested against that, and is equivalent, * except that you don't need to include two pages of legalese * with every copy. * * To compute the message digest of a chunk of bytes, declare an * MD5Context structure, pass it to MD5Init, call MD5Update as * needed on buffers full of bytes, and then call MD5Final, which * will fill a supplied 16-byte array with the digest. */ /* This code was modified in 1997 by Jim Kingdon of Cyclic Software to not require an integer type which is exactly 32 bits. This work draws on the changes for the same purpose by Tatu Ylonen as part of SSH, but since I didn't actually use that code, there is no copyright issue. I hereby disclaim copyright in any changes I have made; this code remains in the public domain. */ // #include /* for memcpy() */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #if HAVE_STRING_H || STDC_HEADERS #include /* for memcpy() */ #endif /* Add prototype support. */ #ifndef PROTO #if defined (USE_PROTOTYPES) ? USE_PROTOTYPES : defined (__STDC__) #define PROTO(ARGS) ARGS #else #define PROTO(ARGS) () #endif #endif #include "md5.h" /* Little-endian byte-swapping routines. Note that these do not depend on the size of datatypes such as uint32, nor do they require us to detect the endianness of the machine we are running on. It is possible they should be macros for speed, but I would be surprised if they were a performance bottleneck for MD5. */ static uint32 getu32 (addr) const unsigned char *addr; { return (((((unsigned long)addr[3] << 8) | addr[2]) << 8) | addr[1]) << 8 | addr[0]; } static void putu32 (data, addr) uint32 data; unsigned char *addr; { addr[0] = (unsigned char)data; addr[1] = (unsigned char)(data >> 8); addr[2] = (unsigned char)(data >> 16); addr[3] = (unsigned char)(data >> 24); } /* * Start MD5 accumulation. Set bit count to 0 and buffer to mysterious * initialization constants. */ void MD5Init(ctx) struct MD5Context *ctx; { ctx->buf[0] = 0x67452301; ctx->buf[1] = 0xefcdab89; ctx->buf[2] = 0x98badcfe; ctx->buf[3] = 0x10325476; ctx->bits[0] = 0; ctx->bits[1] = 0; } /* * Update context to reflect the concatenation of another buffer full * of bytes. */ void MD5Update(ctx, buf, len) struct MD5Context *ctx; unsigned char const *buf; unsigned len; { uint32 t; /* Update bitcount */ t = ctx->bits[0]; if ((ctx->bits[0] = (t + ((uint32)len << 3)) & 0xffffffff) < t) ctx->bits[1]++; /* Carry from low to high */ ctx->bits[1] += len >> 29; t = (t >> 3) & 0x3f; /* Bytes already in shsInfo->data */ /* Handle any leading odd-sized chunks */ if ( t ) { unsigned char *p = ctx->in + t; t = 64-t; if (len < t) { memcpy(p, buf, len); return; } memcpy(p, buf, t); MD5Transform(ctx->buf, ctx->in); buf += t; len -= t; } /* Process data in 64-byte chunks */ while (len >= 64) { memcpy(ctx->in, buf, 64); MD5Transform(ctx->buf, ctx->in); buf += 64; len -= 64; } /* Handle any remaining bytes of data. */ memcpy(ctx->in, buf, len); } /* * Final wrapup - pad to 64-byte boundary with the bit pattern * 1 0* (64-bit count of bits processed, MSB-first) */ void MD5Final(digest, ctx) unsigned char digest[16]; struct MD5Context *ctx; { unsigned count; unsigned char *p; /* Compute number of bytes mod 64 */ count = (ctx->bits[0] >> 3) & 0x3F; /* Set the first char of padding to 0x80. This is safe since there is always at least one byte free */ p = ctx->in + count; *p++ = 0x80; /* Bytes of padding needed to make 64 bytes */ count = 64 - 1 - count; /* Pad out to 56 mod 64 */ if (count < 8) { /* Two lots of padding: Pad the first block to 64 bytes */ memset(p, 0, count); MD5Transform(ctx->buf, ctx->in); /* Now fill the next block with 56 bytes */ memset(ctx->in, 0, 56); } else { /* Pad block to 56 bytes */ memset(p, 0, count-8); } /* Append length in bits and transform */ putu32(ctx->bits[0], ctx->in + 56); putu32(ctx->bits[1], ctx->in + 60); MD5Transform(ctx->buf, ctx->in); putu32(ctx->buf[0], digest); putu32(ctx->buf[1], digest + 4); putu32(ctx->buf[2], digest + 8); putu32(ctx->buf[3], digest + 12); memset(ctx, 0, sizeof(ctx)); /* In case it's sensitive */ } #ifndef ASM_MD5 /* The four core functions - F1 is optimized somewhat */ /* #define F1(x, y, z) (x & y | ~x & z) */ #define F1(x, y, z) (z ^ (x & (y ^ z))) #define F2(x, y, z) F1(z, x, y) #define F3(x, y, z) (x ^ y ^ z) #define F4(x, y, z) (y ^ (x | ~z)) /* This is the central step in the MD5 algorithm. */ #define MD5STEP(f, w, x, y, z, data, s) \ ( w += f(x, y, z) + data, w &= 0xffffffff, w = w<>(32-s), w += x ) /* * The core of the MD5 algorithm, this alters an existing MD5 hash to * reflect the addition of 16 longwords of new data. MD5Update blocks * the data and converts bytes into longwords for this routine. */ void MD5Transform(buf, inraw) uint32 buf[4]; const unsigned char inraw[64]; { register uint32 a, b, c, d; uint32 in[16]; int i; for (i = 0; i < 16; ++i) in[i] = getu32 (inraw + 4 * i); a = buf[0]; b = buf[1]; c = buf[2]; d = buf[3]; MD5STEP(F1, a, b, c, d, in[ 0]+0xd76aa478, 7); MD5STEP(F1, d, a, b, c, in[ 1]+0xe8c7b756, 12); MD5STEP(F1, c, d, a, b, in[ 2]+0x242070db, 17); MD5STEP(F1, b, c, d, a, in[ 3]+0xc1bdceee, 22); MD5STEP(F1, a, b, c, d, in[ 4]+0xf57c0faf, 7); MD5STEP(F1, d, a, b, c, in[ 5]+0x4787c62a, 12); MD5STEP(F1, c, d, a, b, in[ 6]+0xa8304613, 17); MD5STEP(F1, b, c, d, a, in[ 7]+0xfd469501, 22); MD5STEP(F1, a, b, c, d, in[ 8]+0x698098d8, 7); MD5STEP(F1, d, a, b, c, in[ 9]+0x8b44f7af, 12); MD5STEP(F1, c, d, a, b, in[10]+0xffff5bb1, 17); MD5STEP(F1, b, c, d, a, in[11]+0x895cd7be, 22); MD5STEP(F1, a, b, c, d, in[12]+0x6b901122, 7); MD5STEP(F1, d, a, b, c, in[13]+0xfd987193, 12); MD5STEP(F1, c, d, a, b, in[14]+0xa679438e, 17); MD5STEP(F1, b, c, d, a, in[15]+0x49b40821, 22); MD5STEP(F2, a, b, c, d, in[ 1]+0xf61e2562, 5); MD5STEP(F2, d, a, b, c, in[ 6]+0xc040b340, 9); MD5STEP(F2, c, d, a, b, in[11]+0x265e5a51, 14); MD5STEP(F2, b, c, d, a, in[ 0]+0xe9b6c7aa, 20); MD5STEP(F2, a, b, c, d, in[ 5]+0xd62f105d, 5); MD5STEP(F2, d, a, b, c, in[10]+0x02441453, 9); MD5STEP(F2, c, d, a, b, in[15]+0xd8a1e681, 14); MD5STEP(F2, b, c, d, a, in[ 4]+0xe7d3fbc8, 20); MD5STEP(F2, a, b, c, d, in[ 9]+0x21e1cde6, 5); MD5STEP(F2, d, a, b, c, in[14]+0xc33707d6, 9); MD5STEP(F2, c, d, a, b, in[ 3]+0xf4d50d87, 14); MD5STEP(F2, b, c, d, a, in[ 8]+0x455a14ed, 20); MD5STEP(F2, a, b, c, d, in[13]+0xa9e3e905, 5); MD5STEP(F2, d, a, b, c, in[ 2]+0xfcefa3f8, 9); MD5STEP(F2, c, d, a, b, in[ 7]+0x676f02d9, 14); MD5STEP(F2, b, c, d, a, in[12]+0x8d2a4c8a, 20); MD5STEP(F3, a, b, c, d, in[ 5]+0xfffa3942, 4); MD5STEP(F3, d, a, b, c, in[ 8]+0x8771f681, 11); MD5STEP(F3, c, d, a, b, in[11]+0x6d9d6122, 16); MD5STEP(F3, b, c, d, a, in[14]+0xfde5380c, 23); MD5STEP(F3, a, b, c, d, in[ 1]+0xa4beea44, 4); MD5STEP(F3, d, a, b, c, in[ 4]+0x4bdecfa9, 11); MD5STEP(F3, c, d, a, b, in[ 7]+0xf6bb4b60, 16); MD5STEP(F3, b, c, d, a, in[10]+0xbebfbc70, 23); MD5STEP(F3, a, b, c, d, in[13]+0x289b7ec6, 4); MD5STEP(F3, d, a, b, c, in[ 0]+0xeaa127fa, 11); MD5STEP(F3, c, d, a, b, in[ 3]+0xd4ef3085, 16); MD5STEP(F3, b, c, d, a, in[ 6]+0x04881d05, 23); MD5STEP(F3, a, b, c, d, in[ 9]+0xd9d4d039, 4); MD5STEP(F3, d, a, b, c, in[12]+0xe6db99e5, 11); MD5STEP(F3, c, d, a, b, in[15]+0x1fa27cf8, 16); MD5STEP(F3, b, c, d, a, in[ 2]+0xc4ac5665, 23); MD5STEP(F4, a, b, c, d, in[ 0]+0xf4292244, 6); MD5STEP(F4, d, a, b, c, in[ 7]+0x432aff97, 10); MD5STEP(F4, c, d, a, b, in[14]+0xab9423a7, 15); MD5STEP(F4, b, c, d, a, in[ 5]+0xfc93a039, 21); MD5STEP(F4, a, b, c, d, in[12]+0x655b59c3, 6); MD5STEP(F4, d, a, b, c, in[ 3]+0x8f0ccc92, 10); MD5STEP(F4, c, d, a, b, in[10]+0xffeff47d, 15); MD5STEP(F4, b, c, d, a, in[ 1]+0x85845dd1, 21); MD5STEP(F4, a, b, c, d, in[ 8]+0x6fa87e4f, 6); MD5STEP(F4, d, a, b, c, in[15]+0xfe2ce6e0, 10); MD5STEP(F4, c, d, a, b, in[ 6]+0xa3014314, 15); MD5STEP(F4, b, c, d, a, in[13]+0x4e0811a1, 21); MD5STEP(F4, a, b, c, d, in[ 4]+0xf7537e82, 6); MD5STEP(F4, d, a, b, c, in[11]+0xbd3af235, 10); MD5STEP(F4, c, d, a, b, in[ 2]+0x2ad7d2bb, 15); MD5STEP(F4, b, c, d, a, in[ 9]+0xeb86d391, 21); buf[0] += a; buf[1] += b; buf[2] += c; buf[3] += d; } #endif #ifdef TEST /* Simple test program. Can use it to manually run the tests from RFC1321 for example. */ #include int main (int argc, char **argv) { struct MD5Context context; unsigned char checksum[16]; int i; int j; if (argc < 2) { fprintf (stderr, "usage: %s string-to-hash\n", argv[0]); exit (1); } for (j = 1; j < argc; ++j) { printf ("MD5 (\"%s\") = ", argv[j]); MD5Init (&context); MD5Update (&context, argv[j], strlen (argv[j])); MD5Final (checksum, &context); for (i = 0; i < 16; i++) { printf ("%02x", (unsigned int) checksum[i]); } printf ("\n"); } return 0; } #endif /* TEST */ ================================================ FILE: WechatExporter/core/md5.h ================================================ /* See md5.c for explanation and copyright information. */ // https://opensource.apple.com/source/cvs/cvs-19/cvs/lib/md5.h // https://opensource.apple.com/source/cvs/cvs-19/cvs/lib/md5.c #ifndef MD5_H #define MD5_H /* Unlike previous versions of this code, uint32 need not be exactly 32 bits, merely 32 bits or more. Choosing a data type which is 32 bits instead of 64 is not important; speed is considerably more important. ANSI guarantees that "unsigned long" will be big enough, and always using it seems to have few disadvantages. */ typedef unsigned long uint32; struct MD5Context { uint32 buf[4]; uint32 bits[2]; unsigned char in[64]; }; /* void MD5Init PROTO((struct MD5Context *context)); void MD5Update PROTO((struct MD5Context *context, unsigned char const *buf, unsigned len)); void MD5Final PROTO((unsigned char digest[16], struct MD5Context *context)); void MD5Transform PROTO((uint32 buf[4], const unsigned char in[64])); */ void MD5Init (struct MD5Context *context); void MD5Update (struct MD5Context *context, unsigned char const *buf, unsigned len); void MD5Final (unsigned char digest[16], struct MD5Context *context); void MD5Transform (uint32 buf[4], const unsigned char in[64]); /* * This is needed to make RSAREF happy on some MS-DOS compilers. */ typedef struct MD5Context MD5_CTX; #endif /* !MD5_H */ ================================================ FILE: WechatExporter/core/semaphore.h ================================================ #pragma once #include #include class semaphore { private: std::mutex mutex_; std::condition_variable condition_; unsigned long count_ = 0; // Initialized as locked. public: void notify() { std::lock_guard lock(mutex_); ++count_; condition_.notify_one(); } void wait() { std::unique_lock lock(mutex_); while (!count_) // Handle spurious wake-ups. condition_.wait(lock); --count_; } bool try_wait() { std::lock_guard lock(mutex_); if (count_) { --count_; return true; } return false; } }; ================================================ FILE: WechatExporter/en.lproj/Localizable.strings ================================================ /* Localizable.strings WechatExporter Created by Matthew on 2021/3/10. Copyright © 2021 Matthew. All rights reserved. */ "btn-yes" = "Yes"; "btn-no" = "NO"; "btn-ok" = "OK"; "btn-cancel" = "Cancel"; "err-no-output-dir" = "Please choose a output directory."; "err-output-dir-doesnt-exist" = "Output directory doesn't exist."; "err-backup-dir-doesnt-exist" = "iTunes backup directory doesn't exist."; "err-no-backup-dir" = "Please choose an iTunes backup directory."; "err-encrypted-bkp-not-supported" = "Encrypted iTunes Backup is not supported."; "err-exp-is-running" = "Export is running."; "err-failed-to-parse-backup" = "Failed to parse iTunes Backup file."; "err-wrong-param" = "Wrong parameters."; "txt-all-wechat-users" = "All WeChat Accounts"; "prompt-new-version-found" = "Found a new version: %@. Download it now?"; "session-deleted" = "(Deleted)"; "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"; "btn-grant-full-disk-access" = "Grant Full Disk Access"; "not-text-msg" = "-"; "show-logs" = "Show Logs"; "hide-logs" = "Hide Logs"; "alert-update-options" = "You may change exporting format and options in main menu:"; "no-prev-exp-found" = "There is no previous exporting in output directory and [Incremental Exporting] will be ignored."; "prev-exp-found" = "The previous exporting at %@ found, will reuse previous options."; "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."; ================================================ FILE: WechatExporter/en.lproj/Main.strings ================================================ /* Class = "NSButtonCell"; title = "..."; ObjectID = "1GT-FK-TVG"; */ "1GT-FK-TVG.title" = "..."; /* Class = "NSMenuItem"; title = "WechatExporter"; ObjectID = "1Xt-HY-uBw"; */ "1Xt-HY-uBw.title" = "WechatExporter"; /* Class = "NSMenuItem"; title = "Quit WechatExporter"; ObjectID = "4sb-4s-VLi"; */ "4sb-4s-VLi.title" = "Quit WechatExporter"; /* Class = "NSButtonCell"; title = "Check"; ObjectID = "58n-zQ-LqF"; */ "58n-zQ-LqF.title" = "Check"; /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "5Mi-xr-dCf"; */ "5Mi-xr-dCf.title" = "Table View Cell"; /* Class = "NSMenuItem"; title = "Incremental Exporting"; ObjectID = "5c2-wM-XIf"; */ "5c2-wM-XIf.title" = "Incremental Exporting"; /* Class = "NSMenuItem"; title = "Including Subscriptions"; ObjectID = "GbO-lL-BLb"; */ "GbO-lL-BLb.title" = "Including Subscriptions"; /* Class = "NSMenuItem"; title = "About WechatExporter"; ObjectID = "5kV-Vb-QxS"; */ "5kV-Vb-QxS.title" = "About WechatExporter"; /* Class = "NSTableColumn"; headerCell.title = "Name"; ObjectID = "8FB-hv-fkB"; */ "8FB-hv-fkB.headerCell.title" = "Name"; /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ "AYu-sK-qS6.title" = "Main Menu"; /* Class = "NSTextFieldCell"; title = "iTunes Backup Directory:"; ObjectID = "BqF-Du-vMt"; */ "BqF-Du-vMt.title" = "iTunes Backup Directory:"; /* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "CHb-bh-1kG"; */ "CHb-bh-1kG.title" = "Cancel"; /* Class = "NSMenuItem"; title = "From Newer To Earlier"; ObjectID = "eJH-AO-QUD"; */ "eJH-AO-QUD.title" = "Sort by Time in Descending Order"; /* Class = "NSMenuItem"; title = "Text"; ObjectID = "GeM-zb-UkW"; */ "GeM-zb-UkW.title" = "Text"; /* Class = "NSMenuItem"; title = "Show Message Filter"; ObjectID = "HWn-ip-mii"; */ "HWn-ip-mii.title" = "Show Message Filter"; /* Class = "NSWindow"; title = "Wechat Exporter"; ObjectID = "IQv-IB-iLA"; */ "IQv-IB-iLA.title" = "Wechat Exporter"; /* Class = "NSButtonCell"; title = "..."; ObjectID = "LFI-6K-mnE"; */ "LFI-6K-mnE.title" = "..."; /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "O0i-cF-8Ix"; */ "O0i-cF-8Ix.title" = "Text Cell"; /* Class = "NSMenuItem"; title = "Hide WechatExporter"; ObjectID = "Olw-nP-bQN"; */ "Olw-nP-bQN.title" = "Hide WechatExporter"; /* Class = "NSMenuItem"; title = "Check Update Automatically"; ObjectID = "Pyu-qm-bT0"; */ "Pyu-qm-bT0.title" = "Check Update Automatically"; /* Class = "NSMenuItem"; title = "Output Detailed Logs"; ObjectID = "Pyu-qm-bT0"; */ "Pyu-qm-bT0.title" = "Output Detailed Logs"; /* Class = "NSMenuItem"; title = "Open the Folder After Exporting"; ObjectID = "TWh-AV-CCF"; */ "TWh-AV-CCF.title" = "Open the Folder After Exporting"; /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "Q3f-RG-97F"; */ "Q3f-RG-97F.title" = "Text Cell"; /* Class = "NSMenuItem"; title = "PDF"; ObjectID = "T2A-kG-daZ"; */ "T2A-kG-daZ.title" = "PDF"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "UCE-Dj-bc1"; */ "UCE-Dj-bc1.title" = "File"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ "Vdr-fp-XzO.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Load Messages Asynchronously(HTML Mode)"; ObjectID = "WOF-ft-ZWq"; */ "WOF-ft-ZWq.title" = "Load Messages Asynchronously(HTML Mode)"; /* Class = "NSButtonCell"; title = "Show Logs"; ObjectID = "WmO-MK-b5O"; */ "WmO-MK-b5O.title" = "Show Logs"; /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "X6v-WO-gkU"; */ "X6v-WO-gkU.title" = "Text Cell"; /* Class = "NSTableColumn"; headerCell.title = "Number of Msgs"; ObjectID = "bsY-Rr-JHi"; */ "bsY-Rr-JHi.headerCell.title" = "Number of Msgs"; /* Class = "NSMenuItem"; title = "Load Messages On Scrolling"; ObjectID = "c4Q-qP-ppM"; */ "c4Q-qP-ppM.title" = "Load Messages On Scrolling"; /* Class = "NSMenuItem"; title = "HTML"; ObjectID = "cWo-5k-XRR"; */ "cWo-5k-XRR.title" = "HTML"; /* Class = "NSButtonCell"; title = "Close"; ObjectID = "dkl-Nk-ye1"; */ "dkl-Nk-ye1.title" = "Close"; /* Class = "NSMenuItem"; title = "Options"; ObjectID = "fod-ax-X6A"; */ "fod-ax-X6A.title" = "Options"; /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "fos-aD-v3q"; */ "fos-aD-v3q.title" = "Table View Cell"; /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "gtr-oV-Pno"; */ "gtr-oV-Pno.title" = "Text Cell"; /* Class = "NSTableColumn"; headerCell.title = "Last Message"; ObjectID = "hIY-8j-rpO"; */ "hIY-8j-rpO.headerCell.title" = "Last Message"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "k5c-yf-BQl"; */ "k5c-yf-BQl.title" = "Help"; /* Class = "NSTableColumn"; headerCell.title = "WeChat Account"; ObjectID = "mqg-xB-lDI"; */ "mqg-xB-lDI.headerCell.title" = "WeChat Account"; /* Class = "NSMenu"; title = "Format"; ObjectID = "or1-Xl-Us0"; */ "or1-Xl-Us0.title" = "Format"; /* Class = "NSMenu"; title = "Help"; ObjectID = "pOf-8G-Ji8"; */ "pOf-8G-Ji8.title" = "Help"; /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "qJ3-t0-qJh"; */ "qJ3-t0-qJh.title" = "Table View Cell"; /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "sWr-dF-dlL"; */ "sWr-dF-dlL.title" = "Table View Cell"; /* Class = "NSMenuItem"; title = "Save Avatar/Emoji in Chat Folder"; ObjectID = "tGU-e5-isp"; */ "tGU-e5-isp.title" = "Save Avatar/Emoji in Chat Folder"; /* Class = "NSMenu"; title = "Options"; ObjectID = "uG2-8c-wjP"; */ "uG2-8c-wjP.title" = "Options"; /* Class = "NSMenu"; title = "WechatExporter"; ObjectID = "uQy-DD-JDr"; */ "uQy-DD-JDr.title" = "WechatExporter"; /* Class = "NSTextFieldCell"; title = "Output Directory:"; ObjectID = "uh4-ix-NCq"; */ "uh4-ix-NCq.title" = "Output Directory:"; /* Class = "NSMenuItem"; title = "Format"; ObjectID = "v5F-yk-5ct"; */ "v5F-yk-5ct.title" = "Format"; /* Class = "NSMenuItem"; title = "WechatExporter Help"; ObjectID = "vp6-q8-ljb"; */ "vp6-q8-ljb.title" = "WechatExporter Help"; /* Class = "NSButtonCell"; title = "Export"; ObjectID = "wX7-rb-1cu"; */ "wX7-rb-1cu.title" = "Export"; /* Class = "NSMenu"; title = "File"; ObjectID = "yFn-8R-v2s"; */ "yFn-8R-v2s.title" = "File"; ================================================ FILE: WechatExporter/main.m ================================================ // // main.m // WechatExporter // // Created by Matthew on 2020/9/29. // Copyright © 2020 Matthew. All rights reserved. // #import int main(int argc, const char * argv[]) { @autoreleasepool { // Setup code that might create autoreleased objects goes here. } return NSApplicationMain(argc, argv); } ================================================ FILE: WechatExporter/res/emoji/emoji.json ================================================ [ { "preTag": "[", "postTag": "]", "keys": [ { "key": "666", "file": "Awesome" }, { "key": "Aaagh!", "file": "Aaagh!" }, { "key": "Angry", "file": "Angry" }, { "key": "Awesome", "file": "Awesome" }, { "key": "Awkward", "file": "Awkward" }, { "key": "Bah!L", "file": "Bah!L" }, { "key": "Bah!R", "file": "Bah!R" }, { "key": "Basketball", "file": "Basketball" }, { "key": "Beckon", "file": "Beckon" }, { "key": "Beer", "file": "Beer" }, { "key": "Blessing", "file": "Blessing" }, { "key": "Blowkiss", "file": "Blowkiss" }, { "key": "Blush", "file": "Blush" }, { "key": "Bomb", "file": "Bomb" }, { "key": "Boring", "file": "Boring" }, { "key": "Broken", "file": "Broken" }, { "key": "BrokenHeart", "file": "BrokenHeart" }, { "key": "Bye", "file": "Bye" }, { "key": "Cake", "file": "Cake" }, { "key": "Candle", "file": "Candle" }, { "key": "Chick", "file": "Chick" }, { "key": "Chuckle", "file": "Chuckle" }, { "key": "Clap", "file": "Clap" }, { "key": "Cleaver", "file": "Cleaver" }, { "key": "Coffee", "file": "Coffee" }, { "key": "Commando", "file": "Commando" }, { "key": "Concerned", "file": "Concerned" }, { "key": "CoolGuy", "file": "CoolGuy" }, { "key": "Cry", "file": "Cry" }, { "key": "Dagger", "file": "Dagger" }, { "key": "Determined", "file": "Determined" }, { "key": "Dizzy", "file": "Dizzy" }, { "key": "Doge", "file": "Doge" }, { "key": "Dramatic", "file": "Dramatic" }, { "key": "Drool", "file": "Drool" }, { "key": "Drowsy", "file": "Drowsy" }, { "key": "Duh", "file": "Duh" }, { "key": "Emm", "file": "Emm" }, { "key": "Emm", "file": "Emm" }, { "key": "Facepalm", "file": "Facepalm" }, { "key": "Fight", "file": "Fight" }, { "key": "Firecracker", "file": "Firecracker" }, { "key": "Fireworks", "file": "Fireworks" }, { "key": "Fist", "file": "Fist" }, { "key": "Flushed", "file": "Flushed" }, { "key": "Frown", "file": "Frown" }, { "key": "Gift", "file": "Gift" }, { "key": "GoForIt", "file": "GoForIt" }, { "key": "Grimace", "file": "Grimace" }, { "key": "Grin", "file": "Grin" }, { "key": "Hammer", "file": "Hammer" }, { "key": "Happy", "file": "Happy" }, { "key": "Heart", "file": "Heart" }, { "key": "Hey", "file": "Hey" }, { "key": "Hooray", "file": "Hooray" }, { "key": "Hug", "file": "Hug" }, { "key": "Hungry", "file": "Hungry" }, { "key": "Hurt", "file": "Hurt" }, { "key": "InLove", "file": "InLove" }, { "key": "Joyful", "file": "Joyful" }, { "key": "JumpRope", "file": "JumpRope" }, { "key": "KeepFighting", "file": "KeepFighting" }, { "key": "Kiss", "file": "Kiss" }, { "key": "Kotow", "file": "Kotow" }, { "key": "Ladybug", "file": "Ladybug" }, { "key": "Laugh", "file": "Laugh" }, { "key": "Let Down", "file": "Let Down" }, { "key": "LetMeSee", "file": "LetMeSee" }, { "key": "Lightning", "file": "Lightning" }, { "key": "Lips", "file": "Lips" }, { "key": "Lol", "file": "Lol" }, { "key": "Meditate", "file": "Meditate" }, { "key": "Moon", "file": "Moon" }, { "key": "Moue", "file": "Moue" }, { "key": "Moue", "file": "Moue" }, { "key": "MyBad", "file": "MyBad" }, { "key": "NO", "file": "Nuh-uh" }, { "key": "NO", "file": "Nuh-uh" }, { "key": "NoProb", "file": "NoProb" }, { "key": "NosePick", "file": "NosePick" }, { "key": "Nuh-uh", "file": "Nuh-uh" }, { "key": "OK", "file": "OK" }, { "key": "OK", "file": "OK" }, { "key": "OK", "file": "OK" }, { "key": "OMG", "file": "OMG" }, { "key": "Onlooker", "file": "Onlooker" }, { "key": "Packet", "file": "Packet" }, { "key": "Packet", "file": "Packet" }, { "key": "Panic", "file": "Panic" }, { "key": "Party", "file": "Party" }, { "key": "Peace", "file": "Peace" }, { "key": "Pig", "file": "Pig" }, { "key": "PingPong", "file": "PingPong" }, { "key": "Pinky", "file": "Pinky" }, { "key": "Pooh-pooh", "file": "Pooh-pooh" }, { "key": "Poop", "file": "Poop" }, { "key": "Puke", "file": "Puke" }, { "key": "Pup", "file": "Pup" }, { "key": "Respect", "file": "Respect" }, { "key": "Rice", "file": "Rice" }, { "key": "Rich", "file": "Rich" }, { "key": "RockOn", "file": "RockOn" }, { "key": "Rose", "file": "Rose" }, { "key": "Ruthless", "file": "Ruthless" }, { "key": "Salute", "file": "Fight" }, { "key": "Scold", "file": "Scold" }, { "key": "Scowl", "file": "Scowl" }, { "key": "Scream", "file": "Scream" }, { "key": "Shake", "file": "Shake" }, { "key": "Shame", "file": "Shame" }, { "key": "Shhh", "file": "Shhh" }, { "key": "Shocked", "file": "Shocked" }, { "key": "Shrunken", "file": "Shrunken" }, { "key": "Shy", "file": "Shy" }, { "key": "Sick", "file": "Sick" }, { "key": "Sigh", "file": "Sigh" }, { "key": "Silent", "file": "Silent" }, { "key": "Skull", "file": "Skull" }, { "key": "Sleep", "file": "Sleep" }, { "key": "Slight", "file": "Slight" }, { "key": "Sly", "file": "Sly" }, { "key": "Smart", "file": "Smart" }, { "key": "Smile", "file": "Smile" }, { "key": "Smirk", "file": "Smirk" }, { "key": "Smooch", "file": "Smooch" }, { "key": "Smug", "file": "Smug" }, { "key": "Sob", "file": "Sob" }, { "key": "Soccer", "file": "Soccer" }, { "key": "Speechless", "file": "Speechless" }, { "key": "Sun", "file": "Sun" }, { "key": "Surprise", "file": "Surprise" }, { "key": "Surrender", "file": "Surrender" }, { "key": "Sweat", "file": "Sweat" }, { "key": "Sweats", "file": "Sweats" }, { "key": "TaiChi L", "file": "TaiChi L" }, { "key": "TaiChi R", "file": "TaiChi R" }, { "key": "Tea", "file": "Tea" }, { "key": "TearingUp", "file": "TearingUp" }, { "key": "Terror", "file": "Terror" }, { "key": "ThumbsDown", "file": "ThumbsDown" }, { "key": "ThumbsUp", "file": "ThumbsUp" }, { "key": "Toasted", "file": "Toasted" }, { "key": "Tongue", "file": "Tongue" }, { "key": "Tormented", "file": "Tormented" }, { "key": "Tremble", "file": "Tremble" }, { "key": "Trick", "file": "Trick" }, { "key": "Twirl", "file": "Twirl" }, { "key": "Waddle", "file": "Waddle" }, { "key": "Watermelon", "file": "Watermelon" }, { "key": "Wave", "file": "Bye" }, { "key": "Whimper", "file": "Whimper" }, { "key": "Wilt", "file": "Wilt" }, { "key": "Worship", "file": "Worship" }, { "key": "Wow", "file": "Wow" }, { "key": "Wrath", "file": "Wrath" }, { "key": "Yawn", "file": "Yawn" }, { "key": "Yeah!", "file": "Yeah!" }, { "key": "[囧]", "file": "Blush" }, { "key": "一言難盡", "file": "Emm" }, { "key": "乒乓", "file": "PingPong" }, { "key": "乒乓", "file": "PingPong" }, { "key": "乱舞", "file": "Meditate" }, { "key": "亂舞", "file": "Meditate" }, { "key": "亲亲", "file": "Kiss" }, { "key": "便便", "file": "Poop" }, { "key": "便便", "file": "Poop" }, { "key": "偷笑", "file": "Chuckle" }, { "key": "偷笑", "file": "Chuckle" }, { "key": "傲慢", "file": "Smug" }, { "key": "傲慢", "file": "Smug" }, { "key": "再見", "file": "Bye" }, { "key": "再见", "file": "Bye" }, { "key": "冷汗", "file": "Blush" }, { "key": "冷汗", "file": "Blush" }, { "key": "凋谢", "file": "Wilt" }, { "key": "刀", "file": "Dagger" }, { "key": "刀", "file": "Dagger" }, { "key": "加油", "file": "GoForIt" }, { "key": "加油", "file": "GoForIt" }, { "key": "加油加油", "file": "KeepFighting" }, { "key": "加油!", "file": "KeepFighting" }, { "key": "勝利", "file": "Peace" }, { "key": "勾引", "file": "Beckon" }, { "key": "勾引", "file": "Beckon" }, { "key": "发呆", "file": "Scowl" }, { "key": "发怒", "file": "Angry" }, { "key": "发抖", "file": "Tremble" }, { "key": "可怜", "file": "Whimper" }, { "key": "可憐", "file": "Whimper" }, { "key": "右哼哼", "file": "Bah!R" }, { "key": "右哼哼", "file": "Bah!R" }, { "key": "右太极", "file": "TaiChi R" }, { "key": "右太極", "file": "TaiChi R" }, { "key": "叹气", "file": "Sigh" }, { "key": "吃瓜", "file": "Onlooker" }, { "key": "吃西瓜", "file": "Onlooker" }, { "key": "合十", "file": "Worship" }, { "key": "合十", "file": "Worship" }, { "key": "吐", "file": "Puke" }, { "key": "吐", "file": "Puke" }, { "key": "吐舌", "file": "u1F61D" }, { "key": "吓", "file": "Wrath" }, { "key": "吼嘿", "file": "Hey" }, { "key": "呲牙", "file": "Grin" }, { "key": "呲牙", "file": "Grin" }, { "key": "咒罵", "file": "Scold" }, { "key": "咒骂", "file": "Scold" }, { "key": "咖啡", "file": "Coffee" }, { "key": "咖啡", "file": "Coffee" }, { "key": "哇", "file": "Wow" }, { "key": "哈欠", "file": "Yawn" }, { "key": "哈欠", "file": "Yawn" }, { "key": "啤酒", "file": "Beer" }, { "key": "啤酒", "file": "Beer" }, { "key": "嘆息", "file": "Sigh" }, { "key": "嘘", "file": "Shhh" }, { "key": "嘴唇", "file": "Lips" }, { "key": "嘴唇", "file": "Lips" }, { "key": "嘿哈", "file": "Hey" }, { "key": "噓", "file": "Shhh" }, { "key": "噴火", "file": "Aaagh!" }, { "key": "嚇", "file": "Wrath" }, { "key": "回头", "file": "Dramatic" }, { "key": "回頭", "file": "Dramatic" }, { "key": "囧", "file": "Blush" }, { "key": "困", "file": "Drowsy" }, { "key": "坏笑", "file": "Trick" }, { "key": "壞笑", "file": "Trick" }, { "key": "大哭", "file": "Cry" }, { "key": "大哭", "file": "Cry" }, { "key": "大笑", "file": "Laugh" }, { "key": "天啊", "file": "OMG" }, { "key": "太阳", "file": "Sun" }, { "key": "太陽", "file": "Sun" }, { "key": "失敬失敬", "file": "Respect" }, { "key": "失望", "file": "Let Down" }, { "key": "失望", "file": "Let Down" }, { "key": "奋斗", "file": "Determined" }, { "key": "奮鬥", "file": "Determined" }, { "key": "奸笑", "file": "Smirk" }, { "key": "奸笑", "file": "Smirk" }, { "key": "好的", "file": "NoProb" }, { "key": "委屈", "file": "Shrunken" }, { "key": "委屈", "file": "Shrunken" }, { "key": "害羞", "file": "Shy" }, { "key": "害羞", "file": "Shy" }, { "key": "小狗", "file": "Pup" }, { "key": "小狗", "file": "Pup" }, { "key": "小雞", "file": "Chick" }, { "key": "尴尬", "file": "Awkward" }, { "key": "尷尬", "file": "Awkward" }, { "key": "崩潰", "file": "Broken" }, { "key": "左哼哼", "file": "Bah!L" }, { "key": "左哼哼", "file": "Bah!L" }, { "key": "左太极", "file": "TaiChi L" }, { "key": "左太極", "file": "TaiChi L" }, { "key": "差劲", "file": "Pinky" }, { "key": "差勁", "file": "Pinky" }, { "key": "庆祝", "file": "Party" }, { "key": "弱", "file": "ThumbsDown" }, { "key": "弱", "file": "ThumbsDown" }, { "key": "強", "file": "ThumbsUp" }, { "key": "强", "file": "ThumbsUp" }, { "key": "强壮", "file": "u1F4AA" }, { "key": "得意", "file": "CoolGuy" }, { "key": "得意", "file": "CoolGuy" }, { "key": "微笑", "file": "Smile" }, { "key": "微笑", "file": "Smile" }, { "key": "心碎", "file": "BrokenHeart" }, { "key": "心碎", "file": "BrokenHeart" }, { "key": "快哭了", "file": "TearingUp" }, { "key": "快哭了", "file": "TearingUp" }, { "key": "怄火", "file": "Aaagh!" }, { "key": "恐惧", "file": "Terror" }, { "key": "恐懼", "file": "Terror" }, { "key": "悠閑", "file": "Commando" }, { "key": "悠闲", "file": "Commando" }, { "key": "惊恐", "file": "Panic" }, { "key": "惊讶", "file": "Surprise" }, { "key": "愉快", "file": "Joyful" }, { "key": "愉快", "file": "Joyful" }, { "key": "愛你", "file": "RockOn" }, { "key": "愛心", "file": "Heart" }, { "key": "愛情", "file": "InLove" }, { "key": "慶祝", "file": "Party" }, { "key": "憨笑", "file": "Laugh" }, { "key": "打脸", "file": "MyBad" }, { "key": "打臉", "file": "MyBad" }, { "key": "抓狂", "file": "Scream" }, { "key": "抓狂", "file": "Scream" }, { "key": "投降", "file": "Surrender" }, { "key": "投降", "file": "Surrender" }, { "key": "抠鼻", "file": "NosePick" }, { "key": "抱拳", "file": "Fight" }, { "key": "抱拳", "file": "Fight" }, { "key": "拥抱", "file": "Hug" }, { "key": "拳头", "file": "Fist" }, { "key": "拳頭", "file": "Fist" }, { "key": "捂脸", "file": "Facepalm" }, { "key": "掩面", "file": "Facepalm" }, { "key": "握手", "file": "Shake" }, { "key": "握手", "file": "Shake" }, { "key": "摳鼻", "file": "NosePick" }, { "key": "撇嘴", "file": "Grimace" }, { "key": "撇嘴", "file": "Grimace" }, { "key": "擁抱", "file": "Hug" }, { "key": "擦汗", "file": "Speechless" }, { "key": "擦汗", "file": "Speechless" }, { "key": "敲打", "file": "Hammer" }, { "key": "敲打", "file": "Hammer" }, { "key": "无语", "file": "Duh" }, { "key": "旺柴", "file": "Doge" }, { "key": "旺柴", "file": "Doge" }, { "key": "晕", "file": "Dizzy" }, { "key": "暈", "file": "Dizzy" }, { "key": "月亮", "file": "Moon" }, { "key": "月亮", "file": "Moon" }, { "key": "机智", "file": "Smart" }, { "key": "枯萎", "file": "Wilt" }, { "key": "機智", "file": "Smart" }, { "key": "歐耶", "file": "Yeah!" }, { "key": "汗", "file": "Sweats" }, { "key": "流汗", "file": "Sweat" }, { "key": "流汗", "file": "Sweat" }, { "key": "流泪", "file": "Sob" }, { "key": "流淚", "file": "Sob" }, { "key": "激动", "file": "Hooray" }, { "key": "激動", "file": "Hooray" }, { "key": "炸弹", "file": "Bomb" }, { "key": "炸彈", "file": "Bomb" }, { "key": "烟花", "file": "Fireworks" }, { "key": "無語", "file": "Duh" }, { "key": "煙花", "file": "Fireworks" }, { "key": "爆竹", "file": "Firecracker" }, { "key": "爆竹", "file": "Firecracker" }, { "key": "爱你", "file": "RockOn" }, { "key": "爱心", "file": "Heart" }, { "key": "爱情", "file": "InLove" }, { "key": "猪头", "file": "Pig" }, { "key": "献吻", "file": "Smooch" }, { "key": "獻吻", "file": "Smooch" }, { "key": "玫瑰", "file": "Rose" }, { "key": "玫瑰", "file": "Rose" }, { "key": "瓢虫", "file": "Ladybug" }, { "key": "生病", "file": "Sick" }, { "key": "生病", "file": "Sick" }, { "key": "甲蟲", "file": "Ladybug" }, { "key": "疑問", "file": "Shocked" }, { "key": "疑问", "file": "Shocked" }, { "key": "疯了", "file": "Tormented" }, { "key": "瘋了", "file": "Tormented" }, { "key": "發", "file": "Rich" }, { "key": "發", "file": "Rich" }, { "key": "發呆", "file": "Scowl" }, { "key": "發怒", "file": "Angry" }, { "key": "發抖", "file": "Tremble" }, { "key": "白眼", "file": "Slight" }, { "key": "白眼", "file": "Slight" }, { "key": "皱眉", "file": "Concerned" }, { "key": "皺眉", "file": "Concerned" }, { "key": "睡", "file": "Sleep" }, { "key": "睡", "file": "Sleep" }, { "key": "破涕为笑", "file": "Lol" }, { "key": "破涕為笑", "file": "Lol" }, { "key": "磕头", "file": "Kotow" }, { "key": "磕頭", "file": "Kotow" }, { "key": "礼物", "file": "Gift" }, { "key": "社会社会", "file": "Respect" }, { "key": "福", "file": "Blessing" }, { "key": "福", "file": "Blessing" }, { "key": "禮物", "file": "Gift" }, { "key": "笑脸", "file": "Happy" }, { "key": "笑臉", "file": "Happy" }, { "key": "篮球", "file": "Basketball" }, { "key": "籃球", "file": "Basketball" }, { "key": "糗大了", "file": "Shame" }, { "key": "累", "file": "Drowsy" }, { "key": "红包", "file": "Packet" }, { "key": "羞辱", "file": "Shame" }, { "key": "翻白眼", "file": "Boring" }, { "key": "耶", "file": "Yeah!" }, { "key": "胜利", "file": "Peace" }, { "key": "脸红", "file": "Flushed" }, { "key": "臉紅", "file": "Flushed" }, { "key": "色", "file": "Drool" }, { "key": "色", "file": "Drool" }, { "key": "苦涩", "file": "Hurt" }, { "key": "茶", "file": "Tea" }, { "key": "茶", "file": "Tea" }, { "key": "菜刀", "file": "Cleaver" }, { "key": "菜刀", "file": "Cleaver" }, { "key": "蛋糕", "file": "Cake" }, { "key": "蛋糕", "file": "Cake" }, { "key": "蜡烛", "file": "Candle" }, { "key": "蠟燭", "file": "Candle" }, { "key": "衰", "file": "Toasted" }, { "key": "衰", "file": "Toasted" }, { "key": "裂开", "file": "Broken" }, { "key": "西瓜", "file": "Watermelon" }, { "key": "西瓜", "file": "Watermelon" }, { "key": "親親", "file": "Kiss" }, { "key": "調皮", "file": "Tongue" }, { "key": "讓我看看", "file": "LetMeSee" }, { "key": "让我看看", "file": "LetMeSee" }, { "key": "调皮", "file": "Tongue" }, { "key": "豬頭", "file": "Pig" }, { "key": "足球", "file": "Soccer" }, { "key": "足球", "file": "Soccer" }, { "key": "跳繩", "file": "JumpRope" }, { "key": "跳绳", "file": "JumpRope" }, { "key": "跳跳", "file": "Waddle" }, { "key": "跳跳", "file": "Waddle" }, { "key": "轉圈", "file": "Twirl" }, { "key": "转圈", "file": "Twirl" }, { "key": "鄙視", "file": "Pooh-pooh" }, { "key": "鄙视", "file": "Pooh-pooh" }, { "key": "酷", "file": "Ruthless" }, { "key": "酷", "file": "Ruthless" }, { "key": "閃電", "file": "Lightning" }, { "key": "閉嘴", "file": "Silent" }, { "key": "闪电", "file": "Lightning" }, { "key": "闭嘴", "file": "Silent" }, { "key": "阴险", "file": "Sly" }, { "key": "陰險", "file": "Sly" }, { "key": "难过", "file": "Frown" }, { "key": "難受", "file": "Hurt" }, { "key": "難過", "file": "Frown" }, { "key": "飛吻", "file": "Blowkiss" }, { "key": "飞吻", "file": "Blowkiss" }, { "key": "飯", "file": "Rice" }, { "key": "饑餓", "file": "Hungry" }, { "key": "饥饿", "file": "Hungry" }, { "key": "饭", "file": "Rice" }, { "key": "驚恐", "file": "Panic" }, { "key": "驚訝", "file": "Surprise" }, { "key": "骷髅", "file": "Skull" }, { "key": "骷髏頭", "file": "Skull" }, { "key": "鬼魂", "file": "u1F47B" }, { "key": "鸡", "file": "Chick" }, { "key": "鼓掌", "file": "Clap" }, { "key": "鼓掌", "file": "Clap" } ] }, { "preTag": "/", "postTag": "", "keys": [ { "key": "NO", "file": "Nuh-uh" }, { "key": "OK", "file": "OK" }, { "key": "乒乓", "file": "PingPong" }, { "key": "亲亲", "file": "Kiss" }, { "key": "便便", "file": "Poop" }, { "key": "偷笑", "file": "Chuckle" }, { "key": "傲慢", "file": "Smug" }, { "key": "再见", "file": "Bye" }, { "key": "冷汗", "file": "Blush" }, { "key": "凋谢", "file": "Wilt" }, { "key": "刀", "file": "Dagger" }, { "key": "勾引", "file": "Beckon" }, { "key": "发呆", "file": "Scowl" }, { "key": "发怒", "file": "Angry" }, { "key": "发抖", "file": "Tremble" }, { "key": "可怜", "file": "Whimper" }, { "key": "可爱", "file": "Joyful" }, { "key": "右哼哼", "file": "Bah!R" }, { "key": "右太极", "file": "TaiChi R" }, { "key": "吐", "file": "Puke" }, { "key": "吓", "file": "Wrath" }, { "key": "呲牙", "file": "Grin" }, { "key": "咒骂", "file": "Scold" }, { "key": "咖啡", "file": "Coffee" }, { "key": "哈欠", "file": "Yawn" }, { "key": "啤酒", "file": "Beer" }, { "key": "嘘", "file": "Shhh" }, { "key": "回头", "file": "Dramatic" }, { "key": "困", "file": "Drowsy" }, { "key": "坏笑", "file": "Trick" }, { "key": "大兵", "file": "Commando" }, { "key": "大哭", "file": "Cry" }, { "key": "太阳", "file": "Sun" }, { "key": "奋斗", "file": "Determined" }, { "key": "委屈", "file": "Shrunken" }, { "key": "害羞", "file": "Shy" }, { "key": "尴尬", "file": "Awkward" }, { "key": "左哼哼", "file": "Bah!L" }, { "key": "左太极", "file": "TaiChi L" }, { "key": "差劲", "file": "Pinky" }, { "key": "弱", "file": "ThumbsDown" }, { "key": "强", "file": "ThumbsUp" }, { "key": "得意", "file": "CoolGuy" }, { "key": "微笑", "file": "Smile" }, { "key": "心碎", "file": "BrokenHeart" }, { "key": "快哭了", "file": "TearingUp" }, { "key": "怄火", "file": "Aaagh!" }, { "key": "惊恐", "file": "Panic" }, { "key": "惊讶", "file": "Surprise" }, { "key": "憨笑", "file": "Laugh" }, { "key": "抓狂", "file": "Scream" }, { "key": "折磨", "file": "Tormented" }, { "key": "抠鼻", "file": "NosePick" }, { "key": "抱拳", "file": "Fight" }, { "key": "拥抱", "file": "Hug" }, { "key": "拳头", "file": "Fist" }, { "key": "挥手", "file": "Surrender" }, { "key": "握手", "file": "Shake" }, { "key": "撇嘴", "file": "Grimace" }, { "key": "擦汗", "file": "Speechless" }, { "key": "敲打", "file": "Hammer" }, { "key": "晕", "file": "Dizzy" }, { "key": "月亮", "file": "Moon" }, { "key": "流汗", "file": "Sweat" }, { "key": "流泪", "file": "Sob" }, { "key": "激动", "file": "Hooray" }, { "key": "炸弹", "file": "Bomb" }, { "key": "爱你", "file": "RockOn" }, { "key": "爱心", "file": "Heart" }, { "key": "爱情", "file": "InLove" }, { "key": "猪头", "file": "Pig" }, { "key": "献吻", "file": "Smooch" }, { "key": "玫瑰", "file": "Rose" }, { "key": "瓢虫", "file": "Ladybug" }, { "key": "疑问", "file": "Shocked" }, { "key": "白眼", "file": "Slight" }, { "key": "睡", "file": "Sleep" }, { "key": "磕头", "file": "Kotow" }, { "key": "示爱", "file": "Lips" }, { "key": "礼物", "file": "Gift" }, { "key": "篮球", "file": "Basketball" }, { "key": "糗大了", "file": "Shame" }, { "key": "胜利", "file": "Peace" }, { "key": "色", "file": "Drool" }, { "key": "菜刀", "file": "Cleaver" }, { "key": "蛋糕", "file": "Cake" }, { "key": "街舞", "file": "Meditate" }, { "key": "衰", "file": "Toasted" }, { "key": "西瓜", "file": "Watermelon" }, { "key": "调皮", "file": "Tongue" }, { "key": "足球", "file": "Soccer" }, { "key": "跳绳", "file": "JumpRope" }, { "key": "跳跳", "file": "Waddle" }, { "key": "转圈", "file": "Twirl" }, { "key": "鄙视", "file": "Pooh-pooh" }, { "key": "酷", "file": "Ruthless" }, { "key": "闪电", "file": "Lightning" }, { "key": "闭嘴", "file": "Silent" }, { "key": "阴险", "file": "Sly" }, { "key": "难过", "file": "Frown" }, { "key": "飞吻", "file": "Blowkiss" }, { "key": "饥饿", "file": "Hungry" }, { "key": "饭", "file": "Rice" }, { "key": "骷髅", "file": "Skull" }, { "key": "鼓掌", "file": "Clap" } ] }, { "preTag": "/:", "postTag": "", "keys": [ { "key": "!!!", "file": "Skull" }, { "key": "#-0", "file": "Hooray" }, { "key": "&-(", "file": "Shame" }, { "key": "&>", "file": "TaiChi R" }, { "key": ",@!", "file": "Toasted" }, { "key": ",@-D", "file": "Joyful" }, { "key": ",@@", "file": "Dizzy" }, { "key": ",@P", "file": "Chuckle" }, { "key": ",@f", "file": "Determined" }, { "key": ",@o", "file": "Smug" }, { "key": ",@x", "file": "Shhh" }, { "key": "--b", "file": "Blush" }, { "key": "8*", "file": "Whimper" }, { "key": "8-)", "file": "CoolGuy" }, { "key": ":!", "file": "Panic" }, { "key": ":\"(", "file": "Cry" }, { "key": ":\"|", "file": "TearingUp" }, { "key": ":$", "file": "Shy" }, { "key": ":(", "file": "Frown" }, { "key": ":)", "file": "Smile" }, { "key": ":*", "file": "Kiss" }, { "key": ":+", "file": "Ruthless" }, { "key": ":,@", "file": "Commando" }, { "key": ":-O", "file": "Yawn" }, { "key": ":-S", "file": "Scold" }, { "key": ":-|", "file": "Awkward" }, { "key": ":8", "file": "Tormented" }, { "key": ":<", "file": "Sob" }, { "key": ":>", "file": "Laugh" }, { "key": ":@", "file": "Angry" }, { "key": ":B", "file": "Drool" }, { "key": ":D", "file": "Grin" }, { "key": ":L", "file": "Sweat" }, { "key": ":O", "file": "Surprise" }, { "key": ":P", "file": "Tongue" }, { "key": ":Q", "file": "Scream" }, { "key": ":T", "file": "Puke" }, { "key": ":X", "file": "Silent" }, { "key": ":Z", "file": "Sleep" }, { "key": ":d", "file": "Slight" }, { "key": ":g", "file": "Hungry" }, { "key": ":|", "file": "Scowl" }, { "key": ":~", "file": "Grimace" }, { "key": "<&", "file": "TaiChi L" }, { "key": "<@", "file": "Bah!L" }, { "key": "", "file": "Blowkiss" }, { "key": "", "file": "Aaagh!" }, { "key": "", "file": "Watermelon" }, { "key": ">-|", "file": "Pooh-pooh" }, { "key": "?", "file": "Shocked" }, { "key": "@)", "file": "Fight" }, { "key": "@>", "file": "Bah!R" }, { "key": "@@", "file": "Fist" }, { "key": "@x", "file": "Wrath" }, { "key": "B-)", "file": "Trick" }, { "key": "P-(", "file": "Shrunken" }, { "key": "X-)", "file": "Sly" }, { "key": "bad", "file": "Pinky" }, { "key": "basketb", "file": "Basketball" }, { "key": "beer", "file": "Beer" }, { "key": "bome", "file": "Bomb" }, { "key": "break", "file": "BrokenHeart" }, { "key": "bye", "file": "Bye" }, { "key": "cake", "file": "Cake" }, { "key": "circle", "file": "Twirl" }, { "key": "coffee", "file": "Coffee" }, { "key": "dig", "file": "NosePick" }, { "key": "eat", "file": "Rice" }, { "key": "fade", "file": "Wilt" }, { "key": "footb", "file": "Soccer" }, { "key": "gift", "file": "Gift" }, { "key": "handclap", "file": "Clap" }, { "key": "heart", "file": "Heart" }, { "key": "hiphot", "file": "Meditate" }, { "key": "hug", "file": "Hug" }, { "key": "jj", "file": "Beckon" }, { "key": "jump", "file": "Waddle" }, { "key": "kiss", "file": "Smooch" }, { "key": "kn", "file": "Dagger" }, { "key": "kotow", "file": "Kotow" }, { "key": "ladybug", "file": "Ladybug" }, { "key": "li", "file": "Lightning" }, { "key": "love", "file": "InLove" }, { "key": "lvu", "file": "RockOn" }, { "key": "moon", "file": "Moon" }, { "key": "no", "file": "Nuh-uh" }, { "key": "oY", "file": "Surrender" }, { "key": "ok", "file": "OK" }, { "key": "oo", "file": "PingPong" }, { "key": "pd", "file": "Cleaver" }, { "key": "pig", "file": "Pig" }, { "key": "rose", "file": "Rose" }, { "key": "shake", "file": "Tremble" }, { "key": "share", "file": "Shake" }, { "key": "shit", "file": "Poop" }, { "key": "showlove", "file": "Lips" }, { "key": "skip", "file": "JumpRope" }, { "key": "strong", "file": "ThumbsUp" }, { "key": "sun", "file": "Sun" }, { "key": "turn", "file": "Dramatic" }, { "key": "v", "file": "Peace" }, { "key": "weak", "file": "ThumbsDown" }, { "key": "wipe", "file": "Speechless" }, { "key": "xx", "file": "Hammer" }, { "key": "|-)", "file": "Drowsy" } ] } ] ================================================ FILE: WechatExporter/res/en.txt ================================================ [ { "key": "Transfer_Subtype_1", "value": "Transfer" }, { "key": "Transfer_Subtype_3", "value": "Accepted " }, { "key": "Transfer_Subtype_5", "value": "Accepted " }, { "key": "Transfer_Subtype_4", "value": "Rejected" }, { "key": "Transfer_Subtype_8", "value": "Accepted" }, { "key": "Transfer_Subtype_9", "value": "Rejected" }, { "key": "Transfer_Subtype_10", "value": "Expired" }, { "key": "Transfer_Subtype_1", "value": "Pay to %s" }, { "key": "Group_Transfer_Subtype_3", "value": "From %s - Accepted " }, { "key": "Group_Transfer_Subtype_5", "value": "From %s - Accepted " }, { "key": "Group_Transfer_Subtype_4", "value": "From %s - Rejected" }, { "key": "Group_Transfer_Subtype_8", "value": "Pay to %s - Accepted" }, { "key": "Group_Transfer_Subtype_9", "value": "Pay to %s - Rejected" }, { "key": "Group_Transfer_Subtype_10", "value": "Expired" }, { "key": "", "value": "" }, { "key": "", "value": "" } ] ================================================ FILE: WechatExporter/res/templates/audio.html ================================================
%%NAME%% %%TIME%%
%%AUDIOTIME%%
================================================ FILE: WechatExporter/res/templates/card.html ================================================
%%NAME%% %%TIME%%
%%CARDNAME%%
%%CARDTYPE%%
================================================ FILE: WechatExporter/res/templates/channels.html ================================================
%%NAME%% %%TIME%%
%%CARDNAME%%
%%MESSAGE%%
%%CHANNELS%%
================================================ FILE: WechatExporter/res/templates/emoji.html ================================================
%%NAME%% %%TIME%%
%%EMOJI_TITLE%%
================================================ FILE: WechatExporter/res/templates/filter.html ================================================   %%ML:Photos%% %%ML:Videos%% %%ML:All%% ================================================ FILE: WechatExporter/res/templates/frame.html ================================================ %%DISPLAYNAME%% - %%ML:WeChat Chat History%%
%%DISPLAYNAME%%
%%HEADER_FILTER%%
%%BODY%%

================================================ FILE: WechatExporter/res/templates/frame_filter.html ================================================ %%DISPLAYNAME%% - 微信聊天记录
%%DISPLAYNAME%%
显示所有消息   只显示照片   只显示视频
%%BODY%%
%%LOADING_SCRIPTS%% ================================================ FILE: WechatExporter/res/templates/image.html ================================================
%%NAME%% %%TIME%%
================================================ FILE: WechatExporter/res/templates/listframe.html ================================================ %%ML:WeChat Chat History%% %%USERNAME%% %%TBODY%%
================================================ FILE: WechatExporter/res/templates/listitem.html ================================================ %%ITEMTEXT%% ================================================ FILE: WechatExporter/res/templates/member.html ================================================
%%NAME%% %%TIME%%
================================================ FILE: WechatExporter/res/templates/members.html ================================================
%%NAME%% %%TIME%%
================================================ FILE: WechatExporter/res/templates/msg.html ================================================
%%NAME%% %%TIME%%
%%MESSAGE%%
================================================ FILE: WechatExporter/res/templates/notice.html ================================================
%%MESSAGE%%
================================================ FILE: WechatExporter/res/templates/plainshare.html ================================================
%%NAME%% %%TIME%%
%%MESSAGE%%
%%APPNAME%%
================================================ FILE: WechatExporter/res/templates/refermsg.html ================================================
%%NAME%% %%TIME%%
「%%REFERNAME%%:%%REFERMSG%%」

- - - - - - - - - - - - - - -
%%MESSAGE%%
================================================ FILE: WechatExporter/res/templates/scripts.html ================================================ (function() { var msgArray = %%JSON_DATA%%; for (var idx = 0; idx < msgArray.length; idx++) { window.moreWechatMsgs.push(msgArray[idx]); } msgArray = null; loadMsgsForNextPage(); })(); ================================================ FILE: WechatExporter/res/templates/share.html ================================================
%%NAME%% %%TIME%%
%%MESSAGE%%
%%APPNAME%%
================================================ FILE: WechatExporter/res/templates/system.html ================================================
%%ML:System Message:%% %%MESSAGE%%
================================================ FILE: WechatExporter/res/templates/thumb.html ================================================
%%NAME%% %%TIME%%
%%MESSAGE%%
================================================ FILE: WechatExporter/res/templates/transfer.html ================================================
%%NAME%% %%TIME%%
%%MESSAGE%%
%%ML:Weixin Transfer%%
================================================ FILE: WechatExporter/res/templates/video.html ================================================
%%NAME%% %%TIME%%
================================================ FILE: WechatExporter/res/templates/videonew.html ================================================
%%NAME%% %%TIME%%
================================================ FILE: WechatExporter/res/templates/wxemoji.html ================================================ ================================================ FILE: WechatExporter/res/templates_txt/audio.html ================================================ %%NAME%% (%%TIME%%):%%MESSAGE%% ================================================ FILE: WechatExporter/res/templates_txt/card.html ================================================ %%NAME%% (%%TIME%%):%%CARDTYPE%% %%CARDNAME%% ================================================ FILE: WechatExporter/res/templates_txt/channels.html ================================================ %%NAME%% (%%TIME%%):%%MESSAGE%% - %%CHANNELS%% %%CARDNAME%% ================================================ FILE: WechatExporter/res/templates_txt/emoji.html ================================================ %%NAME%% (%%TIME%%):%%ML:[Emoji]%% ================================================ FILE: WechatExporter/res/templates_txt/frame.html ================================================ %%DISPLAYNAME%% - %%ML:WeChat Chat History%% %%BODY%% ================================================ FILE: WechatExporter/res/templates_txt/image.html ================================================ %%NAME%% (%%TIME%%):%%ML:[Photo]%% ================================================ FILE: WechatExporter/res/templates_txt/listframe.html ================================================ %%ML:WeChat Chat History%% %%USERNAME%% %%TBODY%% ================================================ FILE: WechatExporter/res/templates_txt/listitem.html ================================================ %%ITEMLINK%% ================================================ FILE: WechatExporter/res/templates_txt/msg.html ================================================ %%NAME%% (%%TIME%%):%%MESSAGE%% ================================================ FILE: WechatExporter/res/templates_txt/notice.html ================================================ %%MESSAGE%% ================================================ FILE: WechatExporter/res/templates_txt/plainshare.html ================================================ %%NAME%% (%%TIME%%):%%SHARINGTITLE%% ================================================ FILE: WechatExporter/res/templates_txt/refermsg.html ================================================ 「%%REFERNAME%%:%%REFERMSG%%」 - - - - - - - - - - - - - - - %%MESSAGE%% ================================================ FILE: WechatExporter/res/templates_txt/scripts.html ================================================ ================================================ FILE: WechatExporter/res/templates_txt/share.html ================================================ %%NAME%% (%%TIME%%):%%SHARINGTITLE%% - %%APPNAME%% ================================================ FILE: WechatExporter/res/templates_txt/system.html ================================================ %%ML:System Message:%% %%MESSAGE%% ================================================ FILE: WechatExporter/res/templates_txt/thumb.html ================================================ %%NAME%% (%%TIME%%):%%MESSAGE%% ================================================ FILE: WechatExporter/res/templates_txt/transfer.html ================================================ %%NAME%% (%%TIME%%):%%ML:Weixin Transfer%% %%MESSAGE%% ================================================ FILE: WechatExporter/res/templates_txt/video.html ================================================ %%NAME%% (%%TIME%%):%%ML:[Video]%% ================================================ FILE: WechatExporter/res/templates_txt/wxemoji.html ================================================ [%%EMOJI_RAW%%] ================================================ FILE: WechatExporter/res/zh-Hans.txt ================================================ [ { "key": "[Video]", "value": "[视频]" }, { "key": "(Video Missed)", "value": "(视频丢失)" }, { "key": "[Emoji]", "value": "[表情]" }, { "key": "[Audio]", "value": "[语音]" }, { "key": "[Audio %s]", "value": "[语音 %s]" }, { "key": "[Video/Audio Call]", "value": "[视频/语音通话]" }, { "key": "[Photo]", "value": "[图片]" }, { "key": "[Location]", "value": "[位置]" }, { "key": "[Location] %s (%s,%s)", "value": "[位置] %s (%s,%s)" }, { "key": "[Red Packet]", "value": "[红包]" }, { "key": "[Transfer]", "value": "[转账]" }, { "key": "[Real-time Location]", "value": "[实时位置共享]" }, { "key": "[File]", "value": "[文件]" }, { "key": "[Link]", "value": "[链接]" }, { "key": "[Contact Card]", "value": "[个人名片]" }, { "key": "[Channel Card]", "value": "[视频号名片]" }, { "key": "[Contact Card] %s", "value": "[个人名片] %s" }, { "key": "[Channel Card] %s", "value": "[视频号名片] %s" }, { "key": "Channels", "value": "视频号" }, { "key": "iTunes: %s", "value": "iTunes: %s" }, { "key": "Failed to parse the backup data of iTunes in the directory: %s", "value": "解析iTunes备份目录失败: %s" }, { "key": "Previous task has not completed.", "value": "前一个导出任务还未结束。" }, { "key": "Can't access output directory: %s", "value": "不能访问输出目录: %s" }, { "key": "Finding WeChat accounts...", "value": "查找微信登录账户..." }, { "key": "Failed to find WeChat account.", "value": "查找微信登录账户失败。" }, { "key": "%d WeChat account(s) found.", "value": "找到 %d个账号的消息记录。" }, { "key": "Handling account: %s, WeChat Id: %s", "value": "开始处理微信账户: %s,微信号:%s" }, { "key": "Reading account info.", "value": "读取账号信息" }, { "key": "Reading chat info", "value": "读取对话信息" }, { "key": "%d chats found.", "value": "找到%d条对话。" }, { "key": "%d/%d: Handling the chat with %s", "value": "%d/%d: 处理与 %s 的对话" }, { "key": "Skip subscription: %s", "value": "跳过订阅号:%s" }, { "key": "Succeeded handling %d messages.", "value": "成功处理%d条消息" }, { "key": "Completed in %s.", "value": "导出完成,耗时:%s" }, { "key": "Cancelled in %s.", "value": "导出被取消,用时:%s" }, { "key": "Waiting for images(%d) downloading.", "value": "等待%d张图片下载。" }, { "key": "iTunes Version: %s, iOS Version: %s, WeChat Version: %s", "value": "iTunes版本:%s, iOS版本:%s, 微信版本:%s" }, { "key": "iTunes Backup: %s", "value": "iTunes备份目录: %s" }, { "key": "<< %s", "value": "<< %s" }, { "key": "%s Ends >>", "value": "%s 结束 >>" }, { "key": "[File: %s]", "value": "[文件: %s]" }, { "key": "WeChat Chat History", "value": "微信聊天记录" }, { "key": "Play", "value": "播放" }, { "key": "Press [Enter] key after inputting keywords", "value": "输入关键词后按回车键" }, { "key": "Show Photos Only", "value": "仅显示照片" }, { "key": "Show Videos Only", "value": "仅显示视频" }, { "key": "Show All Messages", "value": "显示所有消息" }, { "key": "Photos", "value": "照片" }, { "key": "Videos", "value": "视频" }, { "key": "All", "value": "所有" }, { "key": " (WeChat ID: %s)", "value": " (微信号:%s)" }, { "key": "Weixin Transfer", "value": "微信转账" }, { "key": "Transfer_Subtype_1", "value": "转账" }, { "key": "Transfer_Subtype_3", "value": "已收款" }, { "key": "Transfer_Subtype_5", "value": "已接收" }, { "key": "Transfer_Subtype_4", "value": "已退还" }, { "key": "Transfer_Subtype_8", "value": "已被接收" }, { "key": "Transfer_Subtype_9", "value": "已被退还" }, { "key": "Transfer_Subtype_10", "value": "已过期" }, { "key": "Group_Transfer_Subtype_1", "value": "向%s转账" }, { "key": "Group_Transfer_Subtype_3", "value": "来自%s的转账 - 已收款" }, { "key": "Group_Transfer_Subtype_5", "value": "来自%s的转账 - 已收款" }, { "key": "Group_Transfer_Subtype_4", "value": "来自%s的转账 - 已退还" }, { "key": "Group_Transfer_Subtype_8", "value": "向%s转账 - 已被接收" }, { "key": "Group_Transfer_Subtype_9", "value": "向%s转账 - 已被退还" }, { "key": "Group_Transfer_Subtype_10", "value": "已过期" }, { "key": "", "value": "" }, { "key": "", "value": "" } ] ================================================ FILE: WechatExporter/zh-Hans.lproj/Localizable.strings ================================================ /* Localizable.strings WechatExporter Created by Matthew on 2021/3/10. Copyright © 2021 Matthew. All rights reserved. */ "btn-yes" = "是"; "btn-no" = "否"; "btn-ok" = "确定"; "btn-cancel" = "取消"; "err-no-output-dir" = "请选择输出目录。"; "err-output-dir-doesnt-exist" = "输出目录不存在。"; "err-backup-dir-doesnt-exist" = "iTunes备份目录不存在。"; "err-no-backup-dir" = "请选择iTunes备份目录。"; "err-encrypted-bkp-not-supported" = "不支持加密的iTunes备份。"; "err-exp-is-running" = "导出已经在执行。"; "err-failed-to-parse-backup" = "解析iTunes Backup文件失败。"; "err-wrong-param" = "参数错误。"; "txt-all-wechat-users" = "所有微信账户"; "prompt-new-version-found" = "发现新版本:%@,是否下载?"; "session-deleted" = "(已删除)"; "grant-full-disk-access" = "因为WechatExporter程序需要读取iTunes备份数据,请先授权完全磁盘访问权限:苹果菜单 › 系统偏好设置... › 安全心与隐私 › 隐私 › 完全磁盘访问权限"; "btn-grant-full-disk-access" = "授权完全磁盘访问权限"; "not-text-msg" = "-"; "show-logs" = "显示日志"; "hide-logs" = "隐藏日志"; "alert-update-options" = "你可以在主菜单中修改导出格式和选项:"; "no-prev-exp-found" = "输出目录下不存在前一次导出的信息,【增量导出】将被忽略。"; "prev-exp-found" = "输出目录下发现了%s的导出数据,本次导出将采用“增量导出”模式,并复用前一次的设置。"; "invld-inc-exp-for-multi-users" = "由于设计错误,对于多个微信账号的增量备份复原可能产生错误,请更换新的目录导出。"; ================================================ FILE: WechatExporter/zh-Hans.lproj/Main.strings ================================================ /* Class = "NSButtonCell"; title = "..."; ObjectID = "1GT-FK-TVG"; */ "1GT-FK-TVG.title" = "..."; /* Class = "NSMenuItem"; title = "WechatExporter"; ObjectID = "1Xt-HY-uBw"; */ "1Xt-HY-uBw.title" = "微信聊天记录导出程序"; /* Class = "NSMenuItem"; title = "Quit WechatExporter"; ObjectID = "4sb-4s-VLi"; */ "4sb-4s-VLi.title" = "退出WechatExporter"; /* Class = "NSButtonCell"; title = "Check"; ObjectID = "58n-zQ-LqF"; */ "58n-zQ-LqF.title" = "Check"; /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "5Mi-xr-dCf"; */ "5Mi-xr-dCf.title" = "Table View Cell"; /* Class = "NSMenuItem"; title = "Incremental Exporting"; ObjectID = "5c2-wM-XIf"; */ "5c2-wM-XIf.title" = "增量导出"; /* Class = "NSMenuItem"; title = "Including Subscriptions"; ObjectID = "GbO-lL-BLb"; */ "GbO-lL-BLb.title" = "保留公众号"; /* Class = "NSMenuItem"; title = "About WechatExporter"; ObjectID = "5kV-Vb-QxS"; */ "5kV-Vb-QxS.title" = "关于WechatExporter"; /* Class = "NSTableColumn"; headerCell.title = "Name"; ObjectID = "8FB-hv-fkB"; */ "8FB-hv-fkB.headerCell.title" = "名称"; /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ "AYu-sK-qS6.title" = "Main Menu"; /* Class = "NSTextFieldCell"; title = "iTunes Backup Directory:"; ObjectID = "BqF-Du-vMt"; */ "BqF-Du-vMt.title" = "iTunes备份目录:"; /* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "CHb-bh-1kG"; */ "CHb-bh-1kG.title" = "取消"; /* Class = "NSMenuItem"; title = "From Earlier To Newer"; ObjectID = "eJH-AO-QUD"; */ "eJH-AO-QUD.title" = "按消息时间倒序导出"; /* Class = "NSMenuItem"; title = "Text"; ObjectID = "GeM-zb-UkW"; */ "GeM-zb-UkW.title" = "文本模式"; /* Class = "NSMenuItem"; title = "Show Message Filter"; ObjectID = "HWn-ip-mii"; */ "HWn-ip-mii.title" = "显示聊天记录过滤"; /* Class = "NSWindow"; title = "Wechat Exporter"; ObjectID = "IQv-IB-iLA"; */ "IQv-IB-iLA.title" = "Wechat Exporter"; /* Class = "NSButtonCell"; title = "..."; ObjectID = "LFI-6K-mnE"; */ "LFI-6K-mnE.title" = "..."; /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "O0i-cF-8Ix"; */ "O0i-cF-8Ix.title" = "Text Cell"; /* Class = "NSMenuItem"; title = "Hide WechatExporter"; ObjectID = "Olw-nP-bQN"; */ "Olw-nP-bQN.title" = "隐藏WechatExporter"; /* Class = "NSMenuItem"; title = "Check Update Automatically"; ObjectID = "Pyu-qm-bT0"; */ "Pyu-qm-bT0.title" = "自动检查更新"; /* Class = "NSMenuItem"; title = "Output Detailed Logs"; ObjectID = "Pyu-qm-bT0"; */ "Pyu-qm-bT0.title" = "输出更多日志"; /* Class = "NSMenuItem"; title = "Open the Folder After Exporting"; ObjectID = "TWh-AV-CCF"; */ "TWh-AV-CCF.title" = "导出后打开目录"; /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "Q3f-RG-97F"; */ "Q3f-RG-97F.title" = "Text Cell"; /* Class = "NSMenuItem"; title = "PDF"; ObjectID = "T2A-kG-daZ"; */ "T2A-kG-daZ.title" = "PDF"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "UCE-Dj-bc1"; */ "UCE-Dj-bc1.title" = "文件"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ "Vdr-fp-XzO.title" = "隐藏其他"; /* Class = "NSMenuItem"; title = "Load Messages Asynchronously(HTML Mode)"; ObjectID = "WOF-ft-ZWq"; */ "WOF-ft-ZWq.title" = "异步加载聊天记录"; /* Class = "NSButtonCell"; title = "Show Logs"; ObjectID = "WmO-MK-b5O"; */ "WmO-MK-b5O.title" = "显示日志"; /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "X6v-WO-gkU"; */ "X6v-WO-gkU.title" = "Text Cell"; /* Class = "NSTableColumn"; headerCell.title = "Number of Msgs"; ObjectID = "bsY-Rr-JHi"; */ "bsY-Rr-JHi.headerCell.title" = "聊天记录数"; /* Class = "NSMenuItem"; title = "Load Messages On Scrolling"; ObjectID = "c4Q-qP-ppM"; */ "c4Q-qP-ppM.title" = "上滑滚动时加载更多聊天记录"; /* Class = "NSMenuItem"; title = "HTML"; ObjectID = "cWo-5k-XRR"; */ "cWo-5k-XRR.title" = "HTML"; /* Class = "NSButtonCell"; title = "Close"; ObjectID = "dkl-Nk-ye1"; */ "dkl-Nk-ye1.title" = "关闭"; /* Class = "NSMenuItem"; title = "Options"; ObjectID = "fod-ax-X6A"; */ "fod-ax-X6A.title" = "选项"; /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "fos-aD-v3q"; */ "fos-aD-v3q.title" = "Table View Cell"; /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "gtr-oV-Pno"; */ "gtr-oV-Pno.title" = "Text Cell"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "k5c-yf-BQl"; */ "k5c-yf-BQl.title" = "帮助"; /* Class = "NSTableColumn"; headerCell.title = "Wechat Account"; ObjectID = "mqg-xB-lDI"; */ "mqg-xB-lDI.headerCell.title" = "微信账号"; /* Class = "NSMenu"; title = "Format"; ObjectID = "or1-Xl-Us0"; */ "or1-Xl-Us0.title" = "格式"; /* Class = "NSTableColumn"; headerCell.title = "Last Message"; ObjectID = "hIY-8j-rpO"; */ "hIY-8j-rpO.headerCell.title" = "最新消息"; /* Class = "NSMenu"; title = "Help"; ObjectID = "pOf-8G-Ji8"; */ "pOf-8G-Ji8.title" = "帮助"; /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "qJ3-t0-qJh"; */ "qJ3-t0-qJh.title" = "Table View Cell"; /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "sWr-dF-dlL"; */ "sWr-dF-dlL.title" = "Table View Cell"; /* Class = "NSMenuItem"; title = "Save Avatar/Emoji in Chat Folder"; ObjectID = "tGU-e5-isp"; */ "tGU-e5-isp.title" = "头像和表情存放到聊天记录子目录"; /* Class = "NSMenu"; title = "Options"; ObjectID = "uG2-8c-wjP"; */ "uG2-8c-wjP.title" = "选项"; /* Class = "NSMenu"; title = "WechatExporter"; ObjectID = "uQy-DD-JDr"; */ "uQy-DD-JDr.title" = "WechatExporter"; /* Class = "NSTextFieldCell"; title = "Output Directory:"; ObjectID = "uh4-ix-NCq"; */ "uh4-ix-NCq.title" = "输出目录:"; /* Class = "NSMenuItem"; title = "Format"; ObjectID = "v5F-yk-5ct"; */ "v5F-yk-5ct.title" = "格式"; /* Class = "NSMenuItem"; title = "WechatExporter Help"; ObjectID = "vp6-q8-ljb"; */ "vp6-q8-ljb.title" = "WechatExporter帮助"; /* Class = "NSButtonCell"; title = "Export"; ObjectID = "wX7-rb-1cu"; */ "wX7-rb-1cu.title" = "导出"; /* Class = "NSMenu"; title = "File"; ObjectID = "yFn-8R-v2s"; */ "yFn-8R-v2s.title" = "文件"; ================================================ FILE: WechatExporter.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 341A5B2F253828F300914BE3 /* res in Resources */ = {isa = PBXBuildFile; fileRef = 341A5B2E253828F300914BE3 /* res */; }; 342B0349281125C7009FBD5E /* Template.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342B0347281125C7009FBD5E /* Template.cpp */; }; 342EDAFC25241D91006A295A /* WechatParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342EDAFA25241D91006A295A /* WechatParser.cpp */; }; 342EDB00252450EB006A295A /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 342EDAFF252450EB006A295A /* libcurl.tbd */; }; 342EDB0325245206006A295A /* Downloader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342EDB0125245206006A295A /* Downloader.cpp */; }; 342EDB062524700A006A295A /* Exporter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342EDB042524700A006A295A /* Exporter.cpp */; }; 342EDB0925247852006A295A /* Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 342EDB0825247852006A295A /* Utils.cpp */; }; 342EDB0B252495BD006A295A /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 342EDB0A252495BD006A295A /* libxml2.tbd */; }; 343F6117252322D500FFE085 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 343F6116252322D500FFE085 /* AppDelegate.mm */; }; 343F611A252322D500FFE085 /* ViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 343F6119252322D500FFE085 /* ViewController.mm */; }; 343F611C252322D600FFE085 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 343F611B252322D600FFE085 /* Assets.xcassets */; }; 343F611F252322D600FFE085 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 343F611D252322D600FFE085 /* Main.storyboard */; }; 343F6122252322D600FFE085 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 343F6121252322D600FFE085 /* main.m */; }; 343F612D25234BD300FFE085 /* ITunesParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 343F612C25234BD300FFE085 /* ITunesParser.cpp */; }; 343F613025234BFC00FFE085 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 343F612F25234BFC00FFE085 /* libsqlite3.tbd */; }; 344AF3A827844A6D00ED7586 /* LICENSES in Resources */ = {isa = PBXBuildFile; fileRef = 344AF3A727844A6D00ED7586 /* LICENSES */; }; 344AF3AB2784551900ED7586 /* libmp3lame.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 344AF3AA2784551900ED7586 /* libmp3lame.0.dylib */; }; 344AF3AC2784551900ED7586 /* libmp3lame.0.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 344AF3AA2784551900ED7586 /* libmp3lame.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 345CE65825F8A456003DDD0F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 345CE65A25F8A456003DDD0F /* Localizable.strings */; }; 3471A77A25EB5A9A007D186B /* FileSystem.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34AB9A1325B8908D006D3617 /* FileSystem.cpp */; }; 347E601525C7E55100B33BAB /* SessionDataSource.mm in Sources */ = {isa = PBXBuildFile; fileRef = 347E601425C7E55100B33BAB /* SessionDataSource.mm */; }; 3489DE46262A843C00F51416 /* AsyncExecutor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3489DE44262A843C00F51416 /* AsyncExecutor.cpp */; }; 3489DE50262E74BF00F51416 /* AsyncTask.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3489DE4E262E74BE00F51416 /* AsyncTask.cpp */; }; 3489DE55262EB03000F51416 /* TaskManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3489DE53262EB03000F51416 /* TaskManager.cpp */; }; 3497342625F384D100CAC6CD /* Updater.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3497342425F384D100CAC6CD /* Updater.cpp */; }; 3497342B25F75D4300CAC6CD /* HttpHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3497342A25F75D4300CAC6CD /* HttpHelper.mm */; }; 3497891B26037783001D1F8F /* AppConfiguration.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3497891A26037783001D1F8F /* AppConfiguration.mm */; }; 349DAD2C255D3BB800BFE204 /* XmlParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 349DAD2A255D3BB800BFE204 /* XmlParser.cpp */; }; 349DAD32255E6A0700BFE204 /* Utils_thread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 349DAD31255E6A0600BFE204 /* Utils_thread.cpp */; }; 34A0336125E34B0300E06CC5 /* MessageParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34A0335F25E34B0300E06CC5 /* MessageParser.cpp */; }; 34C0E1CC277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1C6277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib */; }; 34C0E1CD277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1C6277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 34C0E1CE277FDAA800CD4ADE /* libplist-2.0.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1C7277FDAA800CD4ADE /* libplist-2.0.3.dylib */; }; 34C0E1CF277FDAA800CD4ADE /* libplist-2.0.3.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1C7277FDAA800CD4ADE /* libplist-2.0.3.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 34C0E1D0277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1C8277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib */; }; 34C0E1D1277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1C8277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 34C0E1D2277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1C9277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib */; }; 34C0E1D3277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1C9277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 34C0E1D4277FDAA800CD4ADE /* libcrypto.1.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1CA277FDAA800CD4ADE /* libcrypto.1.1.dylib */; }; 34C0E1D5277FDAA800CD4ADE /* libcrypto.1.1.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1CA277FDAA800CD4ADE /* libcrypto.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 34C0E1D6277FDAA800CD4ADE /* libssl.1.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C0E1CB277FDAA800CD4ADE /* libssl.1.1.dylib */; }; 34C0E1D7277FDAA800CD4ADE /* libssl.1.1.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 34C0E1CB277FDAA800CD4ADE /* libssl.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 34CC43BC275F001000ABC2BB /* IDeviceBackup.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34CC43BB275F001000ABC2BB /* IDeviceBackup.cpp */; }; 34E3E90A2531BD8E0093042D /* Utils_md5.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E9092531BD8E0093042D /* Utils_md5.cpp */; }; 34E3E90C2531BE200093042D /* Utils_protobuf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E90B2531BE1F0093042D /* Utils_protobuf.cpp */; }; 34E3E9242535555F0093042D /* RawMessage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E9232535555F0093042D /* RawMessage.cpp */; }; 34EC42B1272AEB6F0013570B /* ResManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34EC42B0272AEB6F0013570B /* ResManager.cpp */; }; 34ED31E825528A1800C42698 /* Utils_audio.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34ED31E725528A1800C42698 /* Utils_audio.cpp */; }; 34ED32082552A98600C42698 /* Utils_silk.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34ED32072552A98600C42698 /* Utils_silk.cpp */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 34C0E1D8277FDAA800CD4ADE /* Embed Libraries */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 34C0E1D1277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib in Embed Libraries */, 34C0E1D3277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib in Embed Libraries */, 34C0E1CD277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib in Embed Libraries */, 34C0E1D5277FDAA800CD4ADE /* libcrypto.1.1.dylib in Embed Libraries */, 34C0E1CF277FDAA800CD4ADE /* libplist-2.0.3.dylib in Embed Libraries */, 344AF3AC2784551900ED7586 /* libmp3lame.0.dylib in Embed Libraries */, 34C0E1D7277FDAA800CD4ADE /* libssl.1.1.dylib in Embed Libraries */, ); name = "Embed Libraries"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 3407BE1427748AE2008D0F9E /* WechatSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WechatSource.h; sourceTree = ""; }; 34140A12254988C4003CE75A /* ExportNotifierImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExportNotifierImpl.h; sourceTree = ""; }; 341A5B2E253828F300914BE3 /* res */ = {isa = PBXFileReference; lastKnownFileType = folder; path = res; sourceTree = ""; }; 342B03462811255E009FBD5E /* Template.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Template.h; sourceTree = ""; }; 342B0347281125C7009FBD5E /* Template.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Template.cpp; sourceTree = ""; }; 342EDAFA25241D91006A295A /* WechatParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = WechatParser.cpp; sourceTree = ""; }; 342EDAFB25241D91006A295A /* WechatParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WechatParser.h; sourceTree = ""; }; 342EDAFD25241E64006A295A /* WechatObjects.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WechatObjects.h; sourceTree = ""; }; 342EDAFE2524485C006A295A /* Utils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Utils.h; sourceTree = ""; }; 342EDAFF252450EB006A295A /* libcurl.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcurl.tbd; path = usr/lib/libcurl.tbd; sourceTree = SDKROOT; }; 342EDB0125245206006A295A /* Downloader.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Downloader.cpp; sourceTree = ""; }; 342EDB0225245206006A295A /* Downloader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Downloader.h; sourceTree = ""; }; 342EDB042524700A006A295A /* Exporter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Exporter.cpp; sourceTree = ""; }; 342EDB052524700A006A295A /* Exporter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Exporter.h; sourceTree = ""; }; 342EDB07252471D6006A295A /* Logger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Logger.h; sourceTree = ""; }; 342EDB0825247852006A295A /* Utils.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Utils.cpp; sourceTree = ""; }; 342EDB0A252495BD006A295A /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; }; 343F6112252322D500FFE085 /* WechatExporter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WechatExporter.app; sourceTree = BUILT_PRODUCTS_DIR; }; 343F6115252322D500FFE085 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 343F6116252322D500FFE085 /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = ""; }; 343F6118252322D500FFE085 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 343F6119252322D500FFE085 /* ViewController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ViewController.mm; sourceTree = ""; }; 343F611B252322D600FFE085 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 343F611E252322D600FFE085 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 343F6120252322D600FFE085 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 343F6121252322D600FFE085 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 343F6123252322D600FFE085 /* WechatExporter.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WechatExporter.entitlements; sourceTree = ""; }; 343F612B25234BD300FFE085 /* ITunesParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITunesParser.h; sourceTree = ""; }; 343F612C25234BD300FFE085 /* ITunesParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ITunesParser.cpp; sourceTree = ""; }; 343F612F25234BFC00FFE085 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 344AF3A727844A6D00ED7586 /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = folder; name = LICENSES; path = WechatExporter/LICENSES; sourceTree = ""; }; 344AF3AA2784551900ED7586 /* 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 = ""; }; 3455A16A27E5C528006B0797 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; 345C8D4D2543F5E30036368C /* ExportNotifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExportNotifier.h; sourceTree = ""; }; 345C8D4E2543F5E30036368C /* semaphore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = semaphore.h; sourceTree = ""; }; 345CE65925F8A456003DDD0F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 345CE66125F8A717003DDD0F /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 347A144E2685A77300E794ED /* MbdbReader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MbdbReader.h; sourceTree = ""; }; 347BE8D12626B37D0004EBE4 /* PdfConverter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PdfConverter.h; sourceTree = ""; }; 347E600D25C00A4100B33BAB /* MMKVReader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMKVReader.h; sourceTree = ""; }; 347E601325C7E53500B33BAB /* SessionDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionDataSource.h; sourceTree = ""; }; 347E601425C7E55100B33BAB /* SessionDataSource.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SessionDataSource.mm; sourceTree = ""; }; 3481B1E2287A4FFA00E515E4 /* ExportOption.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExportOption.h; sourceTree = ""; }; 3489DE44262A843C00F51416 /* AsyncExecutor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AsyncExecutor.cpp; sourceTree = ""; }; 3489DE45262A843C00F51416 /* AsyncExecutor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncExecutor.h; sourceTree = ""; }; 3489DE4E262E74BE00F51416 /* AsyncTask.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AsyncTask.cpp; sourceTree = ""; }; 3489DE4F262E74BE00F51416 /* AsyncTask.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncTask.h; sourceTree = ""; }; 3489DE53262EB03000F51416 /* TaskManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = TaskManager.cpp; sourceTree = ""; }; 3489DE54262EB03000F51416 /* TaskManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TaskManager.h; sourceTree = ""; }; 3489DE5A26316ECB00F51416 /* PdfConverterImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PdfConverterImpl.h; sourceTree = ""; }; 348C373626A696B200FDBBDB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 3497342425F384D100CAC6CD /* Updater.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Updater.cpp; sourceTree = ""; }; 3497342525F384D100CAC6CD /* Updater.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Updater.h; sourceTree = ""; }; 3497342A25F75D4300CAC6CD /* HttpHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = HttpHelper.mm; sourceTree = ""; }; 3497342E25F75D5A00CAC6CD /* HttpHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HttpHelper.h; sourceTree = ""; }; 3497891926037783001D1F8F /* AppConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppConfiguration.h; sourceTree = ""; }; 3497891A26037783001D1F8F /* AppConfiguration.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AppConfiguration.mm; sourceTree = ""; }; 349DAD2A255D3BB800BFE204 /* XmlParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = XmlParser.cpp; sourceTree = ""; }; 349DAD2B255D3BB800BFE204 /* XmlParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XmlParser.h; sourceTree = ""; }; 349DAD31255E6A0600BFE204 /* Utils_thread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_thread.cpp; sourceTree = ""; }; 34A0335F25E34B0300E06CC5 /* MessageParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MessageParser.cpp; sourceTree = ""; }; 34A0336025E34B0300E06CC5 /* MessageParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MessageParser.h; sourceTree = ""; }; 34AB9A1225B89075006D3617 /* FileSystem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileSystem.h; sourceTree = ""; }; 34AB9A1325B8908D006D3617 /* FileSystem.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = FileSystem.cpp; sourceTree = ""; }; 34C0E1C6277FDAA800CD4ADE /* 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 = ""; }; 34C0E1C7277FDAA800CD4ADE /* 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 = ""; }; 34C0E1C8277FDAA800CD4ADE /* 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 = ""; }; 34C0E1C9277FDAA800CD4ADE /* 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 = ""; }; 34C0E1CA277FDAA800CD4ADE /* 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 = ""; }; 34C0E1CB277FDAA800CD4ADE /* 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 = ""; }; 34CA9B0F269FE6FB00C530C2 /* ExportContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExportContext.h; sourceTree = ""; }; 34CC43BA275EFFF400ABC2BB /* IDeviceBackup.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IDeviceBackup.h; sourceTree = ""; }; 34CC43BB275F001000ABC2BB /* IDeviceBackup.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = IDeviceBackup.cpp; sourceTree = ""; }; 34E3E9032525C2640093042D /* LoggerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoggerImpl.h; sourceTree = ""; }; 34E3E9092531BD8E0093042D /* Utils_md5.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_md5.cpp; sourceTree = ""; }; 34E3E90B2531BE1F0093042D /* Utils_protobuf.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_protobuf.cpp; sourceTree = ""; }; 34E3E922253555470093042D /* RawMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RawMessage.h; sourceTree = ""; }; 34E3E9232535555F0093042D /* RawMessage.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RawMessage.cpp; sourceTree = ""; }; 34EC42AF272AEB6F0013570B /* ResManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResManager.h; sourceTree = ""; }; 34EC42B0272AEB6F0013570B /* ResManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ResManager.cpp; sourceTree = ""; }; 34ED31E725528A1800C42698 /* Utils_audio.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_audio.cpp; sourceTree = ""; }; 34ED31EC25528CF500C42698 /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = System/Library/Frameworks/CoreAudio.framework; sourceTree = SDKROOT; }; 34ED31EF255294A000C42698 /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; }; 34ED31F1255294A500C42698 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; 34ED31F3255294BB00C42698 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 34ED31F5255294D100C42698 /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; }; 34ED31F8255294E100C42698 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; 34ED31FB255294E500C42698 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 34ED31FE2552950100C42698 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; 34ED32072552A98600C42698 /* Utils_silk.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Utils_silk.cpp; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 343F610F252322D500FFE085 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 34C0E1D2277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib in Frameworks */, 34C0E1D0277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib in Frameworks */, 344AF3AB2784551900ED7586 /* libmp3lame.0.dylib in Frameworks */, 342EDB00252450EB006A295A /* libcurl.tbd in Frameworks */, 34C0E1D4277FDAA800CD4ADE /* libcrypto.1.1.dylib in Frameworks */, 343F613025234BFC00FFE085 /* libsqlite3.tbd in Frameworks */, 34C0E1D6277FDAA800CD4ADE /* libssl.1.1.dylib in Frameworks */, 34C0E1CC277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib in Frameworks */, 34C0E1CE277FDAA800CD4ADE /* libplist-2.0.3.dylib in Frameworks */, 342EDB0B252495BD006A295A /* libxml2.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 343F6109252322D500FFE085 = { isa = PBXGroup; children = ( 344AF3A727844A6D00ED7586 /* LICENSES */, 343F6114252322D500FFE085 /* WechatExporter */, 343F6113252322D500FFE085 /* Products */, 343F612E25234BFC00FFE085 /* Frameworks */, ); sourceTree = ""; }; 343F6113252322D500FFE085 /* Products */ = { isa = PBXGroup; children = ( 343F6112252322D500FFE085 /* WechatExporter.app */, ); name = Products; sourceTree = ""; }; 343F6114252322D500FFE085 /* WechatExporter */ = { isa = PBXGroup; children = ( 3497891926037783001D1F8F /* AppConfiguration.h */, 3497891A26037783001D1F8F /* AppConfiguration.mm */, 343F6115252322D500FFE085 /* AppDelegate.h */, 343F6116252322D500FFE085 /* AppDelegate.mm */, 343F611B252322D600FFE085 /* Assets.xcassets */, 343F612A25234BBE00FFE085 /* core */, 34140A12254988C4003CE75A /* ExportNotifierImpl.h */, 343F6120252322D600FFE085 /* Info.plist */, 34E3E9032525C2640093042D /* LoggerImpl.h */, 343F6121252322D600FFE085 /* main.m */, 343F611D252322D600FFE085 /* Main.storyboard */, 341A5B2E253828F300914BE3 /* res */, 343F6118252322D500FFE085 /* ViewController.h */, 343F6119252322D500FFE085 /* ViewController.mm */, 343F6123252322D600FFE085 /* WechatExporter.entitlements */, 347E601325C7E53500B33BAB /* SessionDataSource.h */, 347E601425C7E55100B33BAB /* SessionDataSource.mm */, 3497342A25F75D4300CAC6CD /* HttpHelper.mm */, 3497342E25F75D5A00CAC6CD /* HttpHelper.h */, 345CE65A25F8A456003DDD0F /* Localizable.strings */, 3489DE5A26316ECB00F51416 /* PdfConverterImpl.h */, ); path = WechatExporter; sourceTree = ""; }; 343F612A25234BBE00FFE085 /* core */ = { isa = PBXGroup; children = ( 3489DE44262A843C00F51416 /* AsyncExecutor.cpp */, 3489DE45262A843C00F51416 /* AsyncExecutor.h */, 3489DE4E262E74BE00F51416 /* AsyncTask.cpp */, 3489DE4F262E74BE00F51416 /* AsyncTask.h */, 342EDB0125245206006A295A /* Downloader.cpp */, 342EDB0225245206006A295A /* Downloader.h */, 34CA9B0F269FE6FB00C530C2 /* ExportContext.h */, 342EDB042524700A006A295A /* Exporter.cpp */, 342EDB052524700A006A295A /* Exporter.h */, 345C8D4D2543F5E30036368C /* ExportNotifier.h */, 34AB9A1325B8908D006D3617 /* FileSystem.cpp */, 34AB9A1225B89075006D3617 /* FileSystem.h */, 34CC43BB275F001000ABC2BB /* IDeviceBackup.cpp */, 34CC43BA275EFFF400ABC2BB /* IDeviceBackup.h */, 343F612C25234BD300FFE085 /* ITunesParser.cpp */, 343F612B25234BD300FFE085 /* ITunesParser.h */, 342EDB07252471D6006A295A /* Logger.h */, 347A144E2685A77300E794ED /* MbdbReader.h */, 34A0335F25E34B0300E06CC5 /* MessageParser.cpp */, 34A0336025E34B0300E06CC5 /* MessageParser.h */, 347E600D25C00A4100B33BAB /* MMKVReader.h */, 347BE8D12626B37D0004EBE4 /* PdfConverter.h */, 34E3E9232535555F0093042D /* RawMessage.cpp */, 34E3E922253555470093042D /* RawMessage.h */, 34EC42B0272AEB6F0013570B /* ResManager.cpp */, 34EC42AF272AEB6F0013570B /* ResManager.h */, 345C8D4E2543F5E30036368C /* semaphore.h */, 3489DE53262EB03000F51416 /* TaskManager.cpp */, 3489DE54262EB03000F51416 /* TaskManager.h */, 3497342425F384D100CAC6CD /* Updater.cpp */, 3497342525F384D100CAC6CD /* Updater.h */, 34ED31E725528A1800C42698 /* Utils_audio.cpp */, 34E3E9092531BD8E0093042D /* Utils_md5.cpp */, 34E3E90B2531BE1F0093042D /* Utils_protobuf.cpp */, 34ED32072552A98600C42698 /* Utils_silk.cpp */, 349DAD31255E6A0600BFE204 /* Utils_thread.cpp */, 342EDB0825247852006A295A /* Utils.cpp */, 342EDAFE2524485C006A295A /* Utils.h */, 342EDAFD25241E64006A295A /* WechatObjects.h */, 342EDAFA25241D91006A295A /* WechatParser.cpp */, 342EDAFB25241D91006A295A /* WechatParser.h */, 3407BE1427748AE2008D0F9E /* WechatSource.h */, 349DAD2A255D3BB800BFE204 /* XmlParser.cpp */, 349DAD2B255D3BB800BFE204 /* XmlParser.h */, 342B03462811255E009FBD5E /* Template.h */, 342B0347281125C7009FBD5E /* Template.cpp */, 3481B1E2287A4FFA00E515E4 /* ExportOption.h */, ); path = core; sourceTree = ""; }; 343F612E25234BFC00FFE085 /* Frameworks */ = { isa = PBXGroup; children = ( 344AF3AA2784551900ED7586 /* libmp3lame.0.dylib */, 34C0E1CA277FDAA800CD4ADE /* libcrypto.1.1.dylib */, 34C0E1C6277FDAA800CD4ADE /* libimobiledevice-1.0.6.dylib */, 34C0E1C9277FDAA800CD4ADE /* libimobiledevice-glue-1.0.0.dylib */, 34C0E1C7277FDAA800CD4ADE /* libplist-2.0.3.dylib */, 34C0E1CB277FDAA800CD4ADE /* libssl.1.1.dylib */, 34C0E1C8277FDAA800CD4ADE /* libusbmuxd-2.0.6.dylib */, 34ED31FE2552950100C42698 /* CoreMedia.framework */, 34ED31FB255294E500C42698 /* libz.tbd */, 34ED31F8255294E100C42698 /* libiconv.tbd */, 34ED31F5255294D100C42698 /* libbz2.tbd */, 34ED31F3255294BB00C42698 /* Security.framework */, 34ED31F1255294A500C42698 /* AudioToolbox.framework */, 34ED31EF255294A000C42698 /* VideoToolbox.framework */, 34ED31EC25528CF500C42698 /* CoreAudio.framework */, 342EDB0A252495BD006A295A /* libxml2.tbd */, 342EDAFF252450EB006A295A /* libcurl.tbd */, 343F612F25234BFC00FFE085 /* libsqlite3.tbd */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 343F6111252322D500FFE085 /* WechatExporter */ = { isa = PBXNativeTarget; buildConfigurationList = 343F6126252322D600FFE085 /* Build configuration list for PBXNativeTarget "WechatExporter" */; buildPhases = ( 343F610E252322D500FFE085 /* Sources */, 343F610F252322D500FFE085 /* Frameworks */, 343F6110252322D500FFE085 /* Resources */, 34C0E1D8277FDAA800CD4ADE /* Embed Libraries */, ); buildRules = ( ); dependencies = ( ); name = WechatExporter; productName = WechatExporter; productReference = 343F6112252322D500FFE085 /* WechatExporter.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 343F610A252322D500FFE085 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1200; ORGANIZATIONNAME = Matthew; TargetAttributes = { 343F6111252322D500FFE085 = { CreatedOnToolsVersion = 11.3.1; }; }; }; buildConfigurationList = 343F610D252322D500FFE085 /* Build configuration list for PBXProject "WechatExporter" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, "zh-Hans", ); mainGroup = 343F6109252322D500FFE085; productRefGroup = 343F6113252322D500FFE085 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 343F6111252322D500FFE085 /* WechatExporter */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 343F6110252322D500FFE085 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 341A5B2F253828F300914BE3 /* res in Resources */, 345CE65825F8A456003DDD0F /* Localizable.strings in Resources */, 343F611C252322D600FFE085 /* Assets.xcassets in Resources */, 344AF3A827844A6D00ED7586 /* LICENSES in Resources */, 343F611F252322D600FFE085 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 343F610E252322D500FFE085 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 3497342625F384D100CAC6CD /* Updater.cpp in Sources */, 34CC43BC275F001000ABC2BB /* IDeviceBackup.cpp in Sources */, 3471A77A25EB5A9A007D186B /* FileSystem.cpp in Sources */, 34E3E9242535555F0093042D /* RawMessage.cpp in Sources */, 34E3E90C2531BE200093042D /* Utils_protobuf.cpp in Sources */, 34A0336125E34B0300E06CC5 /* MessageParser.cpp in Sources */, 343F611A252322D500FFE085 /* ViewController.mm in Sources */, 3497891B26037783001D1F8F /* AppConfiguration.mm in Sources */, 3489DE46262A843C00F51416 /* AsyncExecutor.cpp in Sources */, 34ED31E825528A1800C42698 /* Utils_audio.cpp in Sources */, 342EDB0325245206006A295A /* Downloader.cpp in Sources */, 34E3E90A2531BD8E0093042D /* Utils_md5.cpp in Sources */, 3489DE50262E74BF00F51416 /* AsyncTask.cpp in Sources */, 342B0349281125C7009FBD5E /* Template.cpp in Sources */, 343F6122252322D600FFE085 /* main.m in Sources */, 342EDAFC25241D91006A295A /* WechatParser.cpp in Sources */, 3497342B25F75D4300CAC6CD /* HttpHelper.mm in Sources */, 34EC42B1272AEB6F0013570B /* ResManager.cpp in Sources */, 342EDB0925247852006A295A /* Utils.cpp in Sources */, 349DAD32255E6A0700BFE204 /* Utils_thread.cpp in Sources */, 3489DE55262EB03000F51416 /* TaskManager.cpp in Sources */, 343F6117252322D500FFE085 /* AppDelegate.mm in Sources */, 349DAD2C255D3BB800BFE204 /* XmlParser.cpp in Sources */, 342EDB062524700A006A295A /* Exporter.cpp in Sources */, 347E601525C7E55100B33BAB /* SessionDataSource.mm in Sources */, 34ED32082552A98600C42698 /* Utils_silk.cpp in Sources */, 343F612D25234BD300FFE085 /* ITunesParser.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 343F611D252322D600FFE085 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 343F611E252322D600FFE085 /* Base */, 348C373626A696B200FDBBDB /* zh-Hans */, 3455A16A27E5C528006B0797 /* en */, ); name = Main.storyboard; sourceTree = ""; }; 345CE65A25F8A456003DDD0F /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( 345CE65925F8A456003DDD0F /* en */, 345CE66125F8A717003DDD0F /* zh-Hans */, ); name = Localizable.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 343F6124252322D600FFE085 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = /usr/local/include/; LIBRARY_SEARCH_PATHS = /usr/local/lib; MACOSX_DEPLOYMENT_TARGET = 10.10; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; }; name = Debug; }; 343F6125252322D600FFE085 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = /usr/local/include/; LIBRARY_SEARCH_PATHS = /usr/local/lib; MACOSX_DEPLOYMENT_TARGET = 10.10; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; }; name = Release; }; 343F6127252322D600FFE085 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = WechatExporter/WechatExporter.entitlements; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = L848BW5698; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); HEADER_SEARCH_PATHS = ( /usr/local/include/, "${SDKROOT}/usr/include/libxml2/", "releases/windows-libs/include", ); INFOPLIST_FILE = WechatExporter/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/releases/windows-libs/x64/rel/bin", ); MACOSX_DEPLOYMENT_TARGET = 10.10; MARKETING_VERSION = 1.9.6; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "-L/usr/local/lib", "-lprotobuf", "-ljsoncpp", "-lSKP_SILK_SDK", "-lopencore-amrnb", ); PRODUCT_BUNDLE_IDENTIFIER = org.wakin.WechatExporter; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 343F6128252322D600FFE085 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = WechatExporter/WechatExporter.entitlements; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = L848BW5698; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "NDEBUG=1"; HEADER_SEARCH_PATHS = ( /usr/local/include/, "${SDKROOT}/usr/include/libxml2/", "releases/windows-libs/include", ); INFOPLIST_FILE = WechatExporter/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/releases/windows-libs/x64/rel/bin", ); MACOSX_DEPLOYMENT_TARGET = 10.10; MARKETING_VERSION = 1.9.6; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "-L/usr/local/lib", "-lprotobuf", "-ljsoncpp", "-lSKP_SILK_SDK", "-lopencore-amrnb", ); PRODUCT_BUNDLE_IDENTIFIER = org.wakin.WechatExporter; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 343F610D252322D500FFE085 /* Build configuration list for PBXProject "WechatExporter" */ = { isa = XCConfigurationList; buildConfigurations = ( 343F6124252322D600FFE085 /* Debug */, 343F6125252322D600FFE085 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 343F6126252322D600FFE085 /* Build configuration list for PBXNativeTarget "WechatExporter" */ = { isa = XCConfigurationList; buildConfigurations = ( 343F6127252322D600FFE085 /* Debug */, 343F6128252322D600FFE085 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 343F610A252322D500FFE085 /* Project object */; } ================================================ FILE: WechatExporter.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: WechatExporter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: WechatExporter.xcodeproj/xcshareddata/xcschemes/WechatExporter.xcscheme ================================================ ================================================ FILE: WechatExporter.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: WechatExporter.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: WechatExporterCmd/WechatExporter.cpp ================================================ // // main.cpp // WechatExporterCmd // // Created by Matthew on 2022/3/4. // #include #include #include #include #ifdef _WIN32 #elif defined(__APPLE__) #include #include #include "Utils.h" #else #endif #include "WechatExporterCmd.h" std::string getCurrentLanguageCode(); std::string getExecutablePath(); std::string getExecutableDir(); class LoggerImpl : public Logger { public: void write(const std::string& log) { std::cout << log << std::endl; } void debug(const std::string& log) { std::cout << "DBG:: " << log << std::endl; } }; int main(int argc, const char * argv[]) { const char * fullPath = argv[0]; const char *executableName = basename((char *)argv[0]); int outputFormat = OUTPUT_FORMAT_HTML; int asyncLoading = HTML_OPTION_ONSCROLL; bool outputFilter = false; std::string backupDir; std::string outputDir; std::string account; std::vector sessions; for (int idx = 1; idx < argc; idx++) { if (strcmp("--help", argv[idx]) == 0) { printHelp(executableName); return 0; } const char* equals_pos = strchr(argv[idx], '='); if (equals_pos == NULL) { continue; } std::string name = std::string(argv[idx], equals_pos - argv[idx]); if (name == "--backup") { backupDir = parseArgumentwithQuato(equals_pos + 1); } else if (name == "--output") { outputDir = parseArgumentwithQuato(equals_pos + 1); } else if (name == "--account") { account = parseArgumentwithQuato(equals_pos + 1); } else if (name == "--session") { sessions.push_back(parseArgumentwithQuato(equals_pos + 1)); } else if (name == "--asyncloading") { if (strcmp("sync", equals_pos + 1) == 0) { asyncLoading = HTML_OPTION_SYNC; } else if (strcmp("oninit", equals_pos + 1) == 0) { asyncLoading = HTML_OPTION_ONINIT; } } else if (name == "--filter") { if (strcmp("yes", equals_pos + 1) == 0) { outputFilter = true; } } } if (backupDir.empty() || !existsDirectory(backupDir)) { std::cout << "Please input valid iTunes backup directory." << std::endl; return 1; } if (outputDir.empty() || !existsDirectory(outputDir)) { std::cout << "Please input valid output directory." << std::endl; return 1; } if (account.empty()) { std::cout << "Please input account name." << std::endl; return 1; } std::string workDir(argv[0], 0, strlen(argv[0]) - strlen(executableName)); workDir = getExecutableDir(); if (!endsWith(workDir, "/")) { workDir += "/"; } std::string languageCode = getCurrentLanguageCode(); LoggerImpl logger; return exportSessions(languageCode, &logger, workDir, backupDir, outputDir, account, sessions, outputFormat, asyncLoading, outputFilter); } std::string getExecutablePath() { char rawPathName[PATH_MAX]; char realPathName[PATH_MAX]; uint32_t rawPathSize = (uint32_t)sizeof(rawPathName); if(!_NSGetExecutablePath(rawPathName, &rawPathSize)) { realpath(rawPathName, realPathName); } return std::string(realPathName); } std::string getExecutableDir() { std::string executablePath = getExecutablePath(); char *executablePathStr = new char[executablePath.length() + 1]; strcpy(executablePathStr, executablePath.c_str()); char* executableDir = dirname(executablePathStr); delete [] executablePathStr; return std::string(executableDir); } std::string getCurrentLanguageCode() { std::locale loc = std::locale(""); std::string name = loc.name(); return loc.name(); // NSString *preferredLanguage = [[NSLocale preferredLanguages] objectAtIndex:0]; // if ([preferredLanguage hasPrefix:@"zh-Hans"]) { // preferredLanguage = @"zh-Hans"; } // return preferredLanguage; } ================================================ FILE: WechatExporterCmd/WechatExporterCmd.h ================================================ // // main.cpp // WechatExporterCmd // // Created by Matthew on 2022/3/4. // #include #include #include #include #ifdef _WIN32 #elif defined(__APPLE__) #include "Utils.h" #include "Exporter.h" #include "IDeviceBackup.h" #include "WechatSource.h" #else #endif #ifndef WechatExporterCmd_h #define WechatExporterCmd_h #define OUTPUT_FORMAT_HTML 0 #define OUTPUT_FORMAT_TEXT 1 #define HTML_OPTION_SYNC 0 #define HTML_OPTION_ONSCROLL 1 #define HTML_OPTION_ONINIT 2 class Logger; void printHelp(const char *executableName) { std::cout << "Usage: " << executableName << " [OPTION]\n" "Export Wechat chat history based on the options given:\n" " --backup=PATH Specify the directory of iTunes Backup\n" " --output=PATH Specify the directory in that Wechat chat history will be exported.\n" " --format=FORMAT FORMAT may be one of 'html', 'text'. 'html' is default.\n" " --account=ACCOUNT Specify the WeChat account which will be exported.\n" " --session=SESSION Friend name or chat group name which will be exported. May be specified multiple times\n" " If no session is specified, all sessions of the account will be exported.\n" " --asyncloading=[HTML LOADING OPTION]\n" " [HTML LOADING OPTION] may be one of 'sync', 'onscroll', 'oninit'. 'onscroll' is default.\n" " --filter=FILTER FILTER may be one of 'no', 'yes'. 'no' is default.\n" " --help Show this help.\n" << std::endl; } std::string parseArgumentwithQuato(const char *path) { std::string parsedPath; if (NULL == path) { return parsedPath; } size_t start = 0; size_t length = strlen(path); if (length > 1 && path[length - 1] == '"') { length--; } if (path[0] == '"') { start = 1; length--; } parsedPath = std::string(path, start, length); return parsedPath; } int 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& sessions, int outputFormat, int asyncLoading, bool outputFilter) { // const std::string& workDir, const std::string& backup, const std::string& output, Logger* logger, PdfConverter* pdfConverter Exporter exp(workDir, backupDir, outputDir, logger, NULL); exp.setLanguageCode(languageCode); ExportOption options; if (outputFormat == OUTPUT_FORMAT_TEXT) { options.setTextMode(); exp.setExtName("txt"); exp.setTemplatesName("templates_txt"); } else { exp.setExtName("html"); exp.setTemplatesName("templates"); if (asyncLoading == HTML_OPTION_SYNC) { options.setTextMode(); // exp.setSyncLoading(); } else { if (asyncLoading != HTML_OPTION_ONINIT) { options.setLoadingDataOnScroll(); } } } if (outputFilter) { options.supportsFilter(); } options.filterByName(); exp.setOptions(options); std::map> usersAndSessions; std::map mapSessions; for (auto it = sessions.cbegin(); it != sessions.cend(); ++it) { mapSessions[*it] = NULL; } usersAndSessions[account] = mapSessions; exp.filterUsersAndSessions(usersAndSessions); exp.run(); exp.waitForComplition(); return 0; } #endif // WechatExporterCmd_h ================================================ FILE: WechatExporterCmd.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 340E16BA2823B83600ECB4CD /* Template.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 340E16B82823B83600ECB4CD /* Template.cpp */; }; 3410714127D1AF0600CAC805 /* WechatExporter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410714027D1AF0600CAC805 /* WechatExporter.cpp */; }; 3410717E27D1AFD900CAC805 /* WechatParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410714B27D1AFD700CAC805 /* WechatParser.cpp */; }; 3410717F27D1AFD900CAC805 /* Utils_md5.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410714D27D1AFD700CAC805 /* Utils_md5.cpp */; }; 3410718027D1AFD900CAC805 /* Utils_protobuf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410714F27D1AFD700CAC805 /* Utils_protobuf.cpp */; }; 3410718127D1AFD900CAC805 /* Updater.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715027D1AFD700CAC805 /* Updater.cpp */; }; 3410718327D1AFD900CAC805 /* AsyncTask.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715227D1AFD800CAC805 /* AsyncTask.cpp */; }; 3410718427D1AFD900CAC805 /* RawMessage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715527D1AFD800CAC805 /* RawMessage.cpp */; }; 3410718627D1AFD900CAC805 /* MessageParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715727D1AFD800CAC805 /* MessageParser.cpp */; }; 3410718727D1AFD900CAC805 /* TaskManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715927D1AFD800CAC805 /* TaskManager.cpp */; }; 3410718827D1AFD900CAC805 /* XmlParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410715F27D1AFD800CAC805 /* XmlParser.cpp */; }; 3410718927D1AFD900CAC805 /* AsyncExecutor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716127D1AFD800CAC805 /* AsyncExecutor.cpp */; }; 3410718A27D1AFD900CAC805 /* IDeviceBackup.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716827D1AFD800CAC805 /* IDeviceBackup.cpp */; }; 3410718B27D1AFD900CAC805 /* Downloader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716927D1AFD900CAC805 /* Downloader.cpp */; }; 3410718C27D1AFD900CAC805 /* Utils_audio.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716A27D1AFD900CAC805 /* Utils_audio.cpp */; }; 3410718D27D1AFD900CAC805 /* FileSystem.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716B27D1AFD900CAC805 /* FileSystem.cpp */; }; 3410718E27D1AFD900CAC805 /* Exporter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716E27D1AFD900CAC805 /* Exporter.cpp */; }; 3410718F27D1AFD900CAC805 /* Utils_silk.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410716F27D1AFD900CAC805 /* Utils_silk.cpp */; }; 3410719027D1AFD900CAC805 /* Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410717127D1AFD900CAC805 /* Utils.cpp */; }; 3410719127D1AFD900CAC805 /* ResManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410717527D1AFD900CAC805 /* ResManager.cpp */; }; 3410719327D1AFD900CAC805 /* ITunesParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410717A27D1AFD900CAC805 /* ITunesParser.cpp */; }; 3410719427D1AFD900CAC805 /* Utils_thread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3410717D27D1AFD900CAC805 /* Utils_thread.cpp */; }; 3410719927D1B04300CAC805 /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3410719727D1B01700CAC805 /* libcurl.tbd */; }; 3410719C27D1B05500CAC805 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3410719B27D1B04A00CAC805 /* libxml2.tbd */; }; 341071A527D1B18F00CAC805 /* libmp3lame.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3410719E27D1B18F00CAC805 /* libmp3lame.0.dylib */; }; 341071A627D1B18F00CAC805 /* libmp3lame.0.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 3410719E27D1B18F00CAC805 /* libmp3lame.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 341071A727D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3410719F27D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib */; }; 341071A827D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 3410719F27D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 341071A927D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A027D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib */; }; 341071AA27D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A027D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 341071AB27D1B19000CAC805 /* libplist-2.0.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A127D1B18F00CAC805 /* libplist-2.0.3.dylib */; }; 341071AC27D1B19000CAC805 /* libplist-2.0.3.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A127D1B18F00CAC805 /* libplist-2.0.3.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 341071AD27D1B19000CAC805 /* libssl.1.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A227D1B18F00CAC805 /* libssl.1.1.dylib */; }; 341071AE27D1B19000CAC805 /* libssl.1.1.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A227D1B18F00CAC805 /* libssl.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 341071AF27D1B19000CAC805 /* libimobiledevice-glue-1.0.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A327D1B18F00CAC805 /* libimobiledevice-glue-1.0.0.dylib */; }; 341071B027D1B19000CAC805 /* libimobiledevice-glue-1.0.0.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A327D1B18F00CAC805 /* libimobiledevice-glue-1.0.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 341071B127D1B19000CAC805 /* libcrypto.1.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071A427D1B18F00CAC805 /* libcrypto.1.1.dylib */; }; 341071B227D1B19000CAC805 /* libcrypto.1.1.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 341071A427D1B18F00CAC805 /* libcrypto.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 341071B627D1B1AE00CAC805 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071B527D1B1A300CAC805 /* libiconv.tbd */; }; 341071B727D1B1B600CAC805 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071B427D1B19600CAC805 /* libz.tbd */; }; 341071C427D1B5A000CAC805 /* res in CopyFiles */ = {isa = PBXBuildFile; fileRef = 341071C327D1B58800CAC805 /* res */; }; 341071C527D1B5A500CAC805 /* LICENSES in CopyFiles */ = {isa = PBXBuildFile; fileRef = 341071C227D1B56800CAC805 /* LICENSES */; }; 341071C727D1B5D500CAC805 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 341071C627D1B5CB00CAC805 /* libsqlite3.tbd */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 3410713B27D1AF0600CAC805 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 12; dstPath = ""; dstSubfolderSpec = 16; files = ( 341071C527D1B5A500CAC805 /* LICENSES in CopyFiles */, 341071C427D1B5A000CAC805 /* res in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; 341071B327D1B19000CAC805 /* Embed Libraries */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Frameworks; dstSubfolderSpec = 16; files = ( 341071AA27D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib in Embed Libraries */, 341071A827D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib in Embed Libraries */, 341071B227D1B19000CAC805 /* libcrypto.1.1.dylib in Embed Libraries */, 341071A627D1B18F00CAC805 /* libmp3lame.0.dylib in Embed Libraries */, 341071B027D1B19000CAC805 /* libimobiledevice-glue-1.0.0.dylib in Embed Libraries */, 341071AC27D1B19000CAC805 /* libplist-2.0.3.dylib in Embed Libraries */, 341071AE27D1B19000CAC805 /* libssl.1.1.dylib in Embed Libraries */, ); name = "Embed Libraries"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 340E16B82823B83600ECB4CD /* Template.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Template.cpp; path = WechatExporter/core/Template.cpp; sourceTree = SOURCE_ROOT; }; 340E16B92823B83600ECB4CD /* Template.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Template.h; path = WechatExporter/core/Template.h; sourceTree = SOURCE_ROOT; }; 3410713D27D1AF0600CAC805 /* WechatExporterCmd */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = WechatExporterCmd; sourceTree = BUILT_PRODUCTS_DIR; }; 3410714027D1AF0600CAC805 /* WechatExporter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = WechatExporter.cpp; sourceTree = ""; }; 3410714B27D1AFD700CAC805 /* WechatParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = WechatParser.cpp; path = WechatExporter/core/WechatParser.cpp; sourceTree = SOURCE_ROOT; }; 3410714C27D1AFD700CAC805 /* WechatObjects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WechatObjects.h; path = WechatExporter/core/WechatObjects.h; sourceTree = SOURCE_ROOT; }; 3410714D27D1AFD700CAC805 /* Utils_md5.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_md5.cpp; path = WechatExporter/core/Utils_md5.cpp; sourceTree = SOURCE_ROOT; }; 3410714E27D1AFD700CAC805 /* ITunesParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ITunesParser.h; path = WechatExporter/core/ITunesParser.h; sourceTree = SOURCE_ROOT; }; 3410714F27D1AFD700CAC805 /* Utils_protobuf.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_protobuf.cpp; path = WechatExporter/core/Utils_protobuf.cpp; sourceTree = SOURCE_ROOT; }; 3410715027D1AFD700CAC805 /* Updater.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Updater.cpp; path = WechatExporter/core/Updater.cpp; sourceTree = SOURCE_ROOT; }; 3410715227D1AFD800CAC805 /* AsyncTask.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = AsyncTask.cpp; path = WechatExporter/core/AsyncTask.cpp; sourceTree = SOURCE_ROOT; }; 3410715327D1AFD800CAC805 /* WechatSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WechatSource.h; path = WechatExporter/core/WechatSource.h; sourceTree = SOURCE_ROOT; }; 3410715427D1AFD800CAC805 /* Downloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Downloader.h; path = WechatExporter/core/Downloader.h; sourceTree = SOURCE_ROOT; }; 3410715527D1AFD800CAC805 /* RawMessage.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = RawMessage.cpp; path = WechatExporter/core/RawMessage.cpp; sourceTree = SOURCE_ROOT; }; 3410715727D1AFD800CAC805 /* MessageParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = MessageParser.cpp; path = WechatExporter/core/MessageParser.cpp; sourceTree = SOURCE_ROOT; }; 3410715827D1AFD800CAC805 /* MbdbReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MbdbReader.h; path = WechatExporter/core/MbdbReader.h; sourceTree = SOURCE_ROOT; }; 3410715927D1AFD800CAC805 /* TaskManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = TaskManager.cpp; path = WechatExporter/core/TaskManager.cpp; sourceTree = SOURCE_ROOT; }; 3410715B27D1AFD800CAC805 /* FileSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FileSystem.h; path = WechatExporter/core/FileSystem.h; sourceTree = SOURCE_ROOT; }; 3410715C27D1AFD800CAC805 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = WechatExporter/core/Utils.h; sourceTree = SOURCE_ROOT; }; 3410715D27D1AFD800CAC805 /* MMKVReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MMKVReader.h; path = WechatExporter/core/MMKVReader.h; sourceTree = SOURCE_ROOT; }; 3410715F27D1AFD800CAC805 /* XmlParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = XmlParser.cpp; path = WechatExporter/core/XmlParser.cpp; sourceTree = SOURCE_ROOT; }; 3410716027D1AFD800CAC805 /* WechatParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WechatParser.h; path = WechatExporter/core/WechatParser.h; sourceTree = SOURCE_ROOT; }; 3410716127D1AFD800CAC805 /* AsyncExecutor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = AsyncExecutor.cpp; path = WechatExporter/core/AsyncExecutor.cpp; sourceTree = SOURCE_ROOT; }; 3410716227D1AFD800CAC805 /* Exporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Exporter.h; path = WechatExporter/core/Exporter.h; sourceTree = SOURCE_ROOT; }; 3410716327D1AFD800CAC805 /* MessageParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MessageParser.h; path = WechatExporter/core/MessageParser.h; sourceTree = SOURCE_ROOT; }; 3410716427D1AFD800CAC805 /* AsyncTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AsyncTask.h; path = WechatExporter/core/AsyncTask.h; sourceTree = SOURCE_ROOT; }; 3410716527D1AFD800CAC805 /* XmlParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = XmlParser.h; path = WechatExporter/core/XmlParser.h; sourceTree = SOURCE_ROOT; }; 3410716627D1AFD800CAC805 /* TaskManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TaskManager.h; path = WechatExporter/core/TaskManager.h; sourceTree = SOURCE_ROOT; }; 3410716827D1AFD800CAC805 /* IDeviceBackup.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = IDeviceBackup.cpp; path = WechatExporter/core/IDeviceBackup.cpp; sourceTree = SOURCE_ROOT; }; 3410716927D1AFD900CAC805 /* Downloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Downloader.cpp; path = WechatExporter/core/Downloader.cpp; sourceTree = SOURCE_ROOT; }; 3410716A27D1AFD900CAC805 /* Utils_audio.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_audio.cpp; path = WechatExporter/core/Utils_audio.cpp; sourceTree = SOURCE_ROOT; }; 3410716B27D1AFD900CAC805 /* FileSystem.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = FileSystem.cpp; path = WechatExporter/core/FileSystem.cpp; sourceTree = SOURCE_ROOT; }; 3410716C27D1AFD900CAC805 /* ExportContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ExportContext.h; path = WechatExporter/core/ExportContext.h; sourceTree = SOURCE_ROOT; }; 3410716D27D1AFD900CAC805 /* Logger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logger.h; path = WechatExporter/core/Logger.h; sourceTree = SOURCE_ROOT; }; 3410716E27D1AFD900CAC805 /* Exporter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Exporter.cpp; path = WechatExporter/core/Exporter.cpp; sourceTree = SOURCE_ROOT; }; 3410716F27D1AFD900CAC805 /* Utils_silk.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_silk.cpp; path = WechatExporter/core/Utils_silk.cpp; sourceTree = SOURCE_ROOT; }; 3410717027D1AFD900CAC805 /* ExportNotifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ExportNotifier.h; path = WechatExporter/core/ExportNotifier.h; sourceTree = SOURCE_ROOT; }; 3410717127D1AFD900CAC805 /* Utils.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils.cpp; path = WechatExporter/core/Utils.cpp; sourceTree = SOURCE_ROOT; }; 3410717227D1AFD900CAC805 /* ResManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResManager.h; path = WechatExporter/core/ResManager.h; sourceTree = SOURCE_ROOT; }; 3410717327D1AFD900CAC805 /* RawMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RawMessage.h; path = WechatExporter/core/RawMessage.h; sourceTree = SOURCE_ROOT; }; 3410717427D1AFD900CAC805 /* Updater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Updater.h; path = WechatExporter/core/Updater.h; sourceTree = SOURCE_ROOT; }; 3410717527D1AFD900CAC805 /* ResManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ResManager.cpp; path = WechatExporter/core/ResManager.cpp; sourceTree = SOURCE_ROOT; }; 3410717827D1AFD900CAC805 /* AsyncExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AsyncExecutor.h; path = WechatExporter/core/AsyncExecutor.h; sourceTree = SOURCE_ROOT; }; 3410717927D1AFD900CAC805 /* PdfConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PdfConverter.h; path = WechatExporter/core/PdfConverter.h; sourceTree = SOURCE_ROOT; }; 3410717A27D1AFD900CAC805 /* ITunesParser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ITunesParser.cpp; path = WechatExporter/core/ITunesParser.cpp; sourceTree = SOURCE_ROOT; }; 3410717B27D1AFD900CAC805 /* IDeviceBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = IDeviceBackup.h; path = WechatExporter/core/IDeviceBackup.h; sourceTree = SOURCE_ROOT; }; 3410717C27D1AFD900CAC805 /* semaphore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = semaphore.h; path = WechatExporter/core/semaphore.h; sourceTree = SOURCE_ROOT; }; 3410717D27D1AFD900CAC805 /* Utils_thread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Utils_thread.cpp; path = WechatExporter/core/Utils_thread.cpp; sourceTree = SOURCE_ROOT; }; 3410719727D1B01700CAC805 /* libcurl.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcurl.tbd; path = usr/lib/libcurl.tbd; sourceTree = SDKROOT; }; 3410719B27D1B04A00CAC805 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; }; 3410719E27D1B18F00CAC805 /* 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 = ""; }; 3410719F27D1B18F00CAC805 /* 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 = ""; }; 341071A027D1B18F00CAC805 /* 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 = ""; }; 341071A127D1B18F00CAC805 /* 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 = ""; }; 341071A227D1B18F00CAC805 /* 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 = ""; }; 341071A327D1B18F00CAC805 /* 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 = ""; }; 341071A427D1B18F00CAC805 /* 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 = ""; }; 341071B427D1B19600CAC805 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 341071B527D1B1A300CAC805 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; 341071C227D1B56800CAC805 /* LICENSES */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENSES; path = WechatExporter/LICENSES; sourceTree = SOURCE_ROOT; }; 341071C327D1B58800CAC805 /* res */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = res; path = WechatExporter/res; sourceTree = SOURCE_ROOT; }; 341071C627D1B5CB00CAC805 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 3455A17027E949BF006B0797 /* WechatExporterCmd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WechatExporterCmd.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 3410713A27D1AF0600CAC805 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 341071A727D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib in Frameworks */, 341071B727D1B1B600CAC805 /* libz.tbd in Frameworks */, 341071A927D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib in Frameworks */, 341071A527D1B18F00CAC805 /* libmp3lame.0.dylib in Frameworks */, 3410719927D1B04300CAC805 /* libcurl.tbd in Frameworks */, 341071B627D1B1AE00CAC805 /* libiconv.tbd in Frameworks */, 341071C727D1B5D500CAC805 /* libsqlite3.tbd in Frameworks */, 341071AD27D1B19000CAC805 /* libssl.1.1.dylib in Frameworks */, 341071AF27D1B19000CAC805 /* libimobiledevice-glue-1.0.0.dylib in Frameworks */, 3410719C27D1B05500CAC805 /* libxml2.tbd in Frameworks */, 341071AB27D1B19000CAC805 /* libplist-2.0.3.dylib in Frameworks */, 341071B127D1B19000CAC805 /* libcrypto.1.1.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 3410713427D1AF0600CAC805 = { isa = PBXGroup; children = ( 3410713F27D1AF0600CAC805 /* WechatExporterCmd */, 3410713E27D1AF0600CAC805 /* Products */, 3410719627D1B01700CAC805 /* Frameworks */, ); sourceTree = ""; }; 3410713E27D1AF0600CAC805 /* Products */ = { isa = PBXGroup; children = ( 3410713D27D1AF0600CAC805 /* WechatExporterCmd */, ); name = Products; sourceTree = ""; }; 3410713F27D1AF0600CAC805 /* WechatExporterCmd */ = { isa = PBXGroup; children = ( 3455A17027E949BF006B0797 /* WechatExporterCmd.h */, 341071C327D1B58800CAC805 /* res */, 341071C227D1B56800CAC805 /* LICENSES */, 3410714927D1AFC600CAC805 /* core */, 3410714027D1AF0600CAC805 /* WechatExporter.cpp */, ); path = WechatExporterCmd; sourceTree = ""; }; 3410714927D1AFC600CAC805 /* core */ = { isa = PBXGroup; children = ( 340E16B82823B83600ECB4CD /* Template.cpp */, 340E16B92823B83600ECB4CD /* Template.h */, 3410716127D1AFD800CAC805 /* AsyncExecutor.cpp */, 3410717827D1AFD900CAC805 /* AsyncExecutor.h */, 3410715227D1AFD800CAC805 /* AsyncTask.cpp */, 3410716427D1AFD800CAC805 /* AsyncTask.h */, 3410716927D1AFD900CAC805 /* Downloader.cpp */, 3410715427D1AFD800CAC805 /* Downloader.h */, 3410716C27D1AFD900CAC805 /* ExportContext.h */, 3410716E27D1AFD900CAC805 /* Exporter.cpp */, 3410716227D1AFD800CAC805 /* Exporter.h */, 3410717027D1AFD900CAC805 /* ExportNotifier.h */, 3410716B27D1AFD900CAC805 /* FileSystem.cpp */, 3410715B27D1AFD800CAC805 /* FileSystem.h */, 3410716827D1AFD800CAC805 /* IDeviceBackup.cpp */, 3410717B27D1AFD900CAC805 /* IDeviceBackup.h */, 3410717A27D1AFD900CAC805 /* ITunesParser.cpp */, 3410714E27D1AFD700CAC805 /* ITunesParser.h */, 3410716D27D1AFD900CAC805 /* Logger.h */, 3410715827D1AFD800CAC805 /* MbdbReader.h */, 3410715727D1AFD800CAC805 /* MessageParser.cpp */, 3410716327D1AFD800CAC805 /* MessageParser.h */, 3410715D27D1AFD800CAC805 /* MMKVReader.h */, 3410717927D1AFD900CAC805 /* PdfConverter.h */, 3410715527D1AFD800CAC805 /* RawMessage.cpp */, 3410717327D1AFD900CAC805 /* RawMessage.h */, 3410717527D1AFD900CAC805 /* ResManager.cpp */, 3410717227D1AFD900CAC805 /* ResManager.h */, 3410717C27D1AFD900CAC805 /* semaphore.h */, 3410715927D1AFD800CAC805 /* TaskManager.cpp */, 3410716627D1AFD800CAC805 /* TaskManager.h */, 3410715027D1AFD700CAC805 /* Updater.cpp */, 3410717427D1AFD900CAC805 /* Updater.h */, 3410716A27D1AFD900CAC805 /* Utils_audio.cpp */, 3410714D27D1AFD700CAC805 /* Utils_md5.cpp */, 3410714F27D1AFD700CAC805 /* Utils_protobuf.cpp */, 3410716F27D1AFD900CAC805 /* Utils_silk.cpp */, 3410717D27D1AFD900CAC805 /* Utils_thread.cpp */, 3410717127D1AFD900CAC805 /* Utils.cpp */, 3410715C27D1AFD800CAC805 /* Utils.h */, 3410714C27D1AFD700CAC805 /* WechatObjects.h */, 3410714B27D1AFD700CAC805 /* WechatParser.cpp */, 3410716027D1AFD800CAC805 /* WechatParser.h */, 3410715327D1AFD800CAC805 /* WechatSource.h */, 3410715F27D1AFD800CAC805 /* XmlParser.cpp */, 3410716527D1AFD800CAC805 /* XmlParser.h */, ); name = core; sourceTree = ""; }; 3410719627D1B01700CAC805 /* Frameworks */ = { isa = PBXGroup; children = ( 341071C627D1B5CB00CAC805 /* libsqlite3.tbd */, 341071B527D1B1A300CAC805 /* libiconv.tbd */, 341071B427D1B19600CAC805 /* libz.tbd */, 341071A427D1B18F00CAC805 /* libcrypto.1.1.dylib */, 341071A027D1B18F00CAC805 /* libimobiledevice-1.0.6.dylib */, 341071A327D1B18F00CAC805 /* libimobiledevice-glue-1.0.0.dylib */, 3410719E27D1B18F00CAC805 /* libmp3lame.0.dylib */, 341071A127D1B18F00CAC805 /* libplist-2.0.3.dylib */, 341071A227D1B18F00CAC805 /* libssl.1.1.dylib */, 3410719F27D1B18F00CAC805 /* libusbmuxd-2.0.6.dylib */, 3410719B27D1B04A00CAC805 /* libxml2.tbd */, 3410719727D1B01700CAC805 /* libcurl.tbd */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 3410713C27D1AF0600CAC805 /* WechatExporterCmd */ = { isa = PBXNativeTarget; buildConfigurationList = 3410714427D1AF0600CAC805 /* Build configuration list for PBXNativeTarget "WechatExporterCmd" */; buildPhases = ( 3410713927D1AF0600CAC805 /* Sources */, 3410713A27D1AF0600CAC805 /* Frameworks */, 3410713B27D1AF0600CAC805 /* CopyFiles */, 341071B327D1B19000CAC805 /* Embed Libraries */, ); buildRules = ( ); dependencies = ( ); name = WechatExporterCmd; productName = WechatExporterCmd; productReference = 3410713D27D1AF0600CAC805 /* WechatExporterCmd */; productType = "com.apple.product-type.tool"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 3410713527D1AF0600CAC805 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1200; TargetAttributes = { 3410713C27D1AF0600CAC805 = { CreatedOnToolsVersion = 12.0.1; }; }; }; buildConfigurationList = 3410713827D1AF0600CAC805 /* Build configuration list for PBXProject "WechatExporterCmd" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 3410713427D1AF0600CAC805; productRefGroup = 3410713E27D1AF0600CAC805 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 3410713C27D1AF0600CAC805 /* WechatExporterCmd */, ); }; /* End PBXProject section */ /* Begin PBXSourcesBuildPhase section */ 3410713927D1AF0600CAC805 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 3410718E27D1AFD900CAC805 /* Exporter.cpp in Sources */, 3410719427D1AFD900CAC805 /* Utils_thread.cpp in Sources */, 3410718D27D1AFD900CAC805 /* FileSystem.cpp in Sources */, 3410718627D1AFD900CAC805 /* MessageParser.cpp in Sources */, 3410719027D1AFD900CAC805 /* Utils.cpp in Sources */, 3410718727D1AFD900CAC805 /* TaskManager.cpp in Sources */, 3410719127D1AFD900CAC805 /* ResManager.cpp in Sources */, 3410718C27D1AFD900CAC805 /* Utils_audio.cpp in Sources */, 3410718927D1AFD900CAC805 /* AsyncExecutor.cpp in Sources */, 3410718327D1AFD900CAC805 /* AsyncTask.cpp in Sources */, 3410718127D1AFD900CAC805 /* Updater.cpp in Sources */, 3410718A27D1AFD900CAC805 /* IDeviceBackup.cpp in Sources */, 3410718827D1AFD900CAC805 /* XmlParser.cpp in Sources */, 3410718F27D1AFD900CAC805 /* Utils_silk.cpp in Sources */, 3410718027D1AFD900CAC805 /* Utils_protobuf.cpp in Sources */, 3410718B27D1AFD900CAC805 /* Downloader.cpp in Sources */, 3410717E27D1AFD900CAC805 /* WechatParser.cpp in Sources */, 3410717F27D1AFD900CAC805 /* Utils_md5.cpp in Sources */, 3410718427D1AFD900CAC805 /* RawMessage.cpp in Sources */, 340E16BA2823B83600ECB4CD /* Template.cpp in Sources */, 3410719327D1AFD900CAC805 /* ITunesParser.cpp in Sources */, 3410714127D1AF0600CAC805 /* WechatExporter.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 3410714227D1AF0600CAC805 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.10; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; }; name = Debug; }; 3410714327D1AF0600CAC805 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.10; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; }; name = Release; }; 3410714527D1AF0600CAC805 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = L848BW5698; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); HEADER_SEARCH_PATHS = ( /usr/local/include/, "${SDKROOT}/usr/include/libxml2/", "releases/windows-libs/include", ); LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/releases/windows-libs/x64/rel/cmd", ); MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_LDFLAGS = ( "-L/usr/local/lib", "-lprotobufd", "-ljsoncpp", "-lSKP_SILK_SDK", "-lopencore-amrnb", ); PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Debug; }; 3410714627D1AF0600CAC805 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = L848BW5698; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "NDEBUG=1"; HEADER_SEARCH_PATHS = ( /usr/local/include/, "${SDKROOT}/usr/include/libxml2/", "releases/windows-libs/include", ); LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/releases/windows-libs/x64/rel/cmd", ); MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "-L/usr/local/lib", "-lprotobuf", "-ljsoncpp", "-lSKP_SILK_SDK", "-lopencore-amrnb", ); PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 3410713827D1AF0600CAC805 /* Build configuration list for PBXProject "WechatExporterCmd" */ = { isa = XCConfigurationList; buildConfigurations = ( 3410714227D1AF0600CAC805 /* Debug */, 3410714327D1AF0600CAC805 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 3410714427D1AF0600CAC805 /* Build configuration list for PBXNativeTarget "WechatExporterCmd" */ = { isa = XCConfigurationList; buildConfigurations = ( 3410714527D1AF0600CAC805 /* Debug */, 3410714627D1AF0600CAC805 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 3410713527D1AF0600CAC805 /* Project object */; } ================================================ FILE: WechatExporterCmd.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: WechatExporterCmd.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: WechatExporterCmd.xcodeproj/xcshareddata/xcschemes/WechatExporterCmd.xcscheme ================================================ ================================================ FILE: docs/index.html ================================================ 微信聊天记录导出程序 Wechat Exporter

微信聊天记录导出程序 Wechat Exporter (for Windows/MacOS)

C++源码:https://github.com/BlueMatthew/WechatExporter

操作步骤

  1. 通过iTunes将手机备份到电脑上(建议备份前杀掉微信),Windows操作系统一般位于目录:C:\用户[用户名]\AppData\Roaming\Apple Computer\MobileSync\Backup\。Android手机可以找一个iPad/iPhone设备,把聊天记录迁移到iPad/iPhone设备上,然后通过iTunes备份到电脑上。

  2. 下载本代码的执行文件:Windows x64版本 或者 MacOS x64版本,然后解压压缩文件

  3. 执行解压出来的WechatExport.exe/WechatExporter (Windows下如果运行报缺少必须的dll文件,请安装Visual C++ 2017 redist后再尝试运行)

  4. 按界面提示进行操作。
    Windows界面截屏
    MacOS界面截屏

模版修改

解压目录下的res(MacOS版本位于Contents)子目录里存放了输出聊天记录的html页面模版,其中通过两个%包含起来的字符串,譬如,%%NAME%%,不要修改之外,其它页面内容和格式都可以自行调整。

系统依赖

Windows版本:Windows 7+, Visual C++ 2017 redist at The latest supported Visual C++ downloads
MacOS版本:MacOS 10.10(Yosemite)+

程序编译

程序依赖如下第三方库:
- libxml2: http://www.xmlsoft.org/
- libcurl: https://curl.se/libcurl/
- libsqlite3: https://www.sqlite.org/index.html
- libprotobuf: https://github.com/protocolbuffers/protobuf
- libjsoncpp: https://github.com/open-source-parsers/jsoncpp
- lame: http://lame.sourceforge.net/
- silk: https://github.com/collects/silk (也参考了: https://github.com/kn007/silk-v3-decoder)
- libplist: https://github.com/libimobiledevice/libplist https://github.com/libimobiledevice-win32/libplist
- libiconv(windows only): https://www.gnu.org/software/libiconv/
- openssl(windows only):https://github.com/openssl/openssl
- WTL (windows only):https://sourceforge.net/projects/wtl/

MacOS下,libxml2,libcurl,libsqlite3直接使用了Xcode自带的库,其它第三方库需自行编译。
libmp3lame需手动删除文件include/libmp3lame.sym中的行:lame_init_old

Windows环境下,silk自带Visual Studio工程文件,可以直接利用Visual Studio编译,其余除了libplist之外,都通过vcpkg可以编译。libplist在vcpkg中也存在,但是在编译x64-windows-static target的时候报了错,于是直接通过Visual Studio建了工程进行编译。可以直接下载预编译好的静态库文件

 

 

================================================ FILE: docs/update.conf ================================================ 1.8.0.8 https://src.wakin.org/github/wxexp/ https://src.wakin.org/github/wxexp/ ================================================ FILE: libplist.README.md ================================================ the libplist is for win32 from vcpkg as vcpkg install fails on x64-windows-static it should be from: https://github.com/libimobiledevice-win32/libplist For Mac OS, please use the code at: https://github.com/libimobiledevice/libplist ================================================ FILE: release ================================================ releases ================================================ FILE: vcproject/AboutDlg.h ================================================ // aboutdlg.h : interface of the CAboutDlg class // ///////////////////////////////////////////////////////////////////////////// #pragma once #include #include "VersionDetector.h" class CAboutDlg : public CDialogImpl { public: enum { IDD = IDD_ABOUTBOX }; BEGIN_MSG_MAP(CAboutDlg) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) COMMAND_ID_HANDLER(IDOK, OnCloseCmd) COMMAND_ID_HANDLER(IDCANCEL, OnCloseCmd) END_MSG_MAP() // Handler prototypes (uncomment arguments if needed): // LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) // LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) // LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/) LRESULT OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { m_homePageLinkCtrl.SubclassWindow(GetDlgItem(IDC_VERSION)); // Replace to current version // If the version is set in aboud dlg resource directly, // The code below won't bring error. // CStatic lblVersion = GetDlgItem(IDC_VERSION); CString version; m_homePageLinkCtrl.GetWindowText(version); VersionDetector vd; CString newVersion = vd.GetProductVersion(); newVersion = TEXT("v") + newVersion; version.Replace(TEXT("v1.0"), newVersion); m_homePageLinkCtrl.SetLabel(version); m_homePageLinkCtrl.SetHyperLink(TEXT("https://github.com/BlueMatthew/WechatExporter")); CenterWindow(GetParent()); return TRUE; } LRESULT OnCloseCmd(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { EndDialog(wID); return 0; } protected: CHyperLink m_homePageLinkCtrl; }; ================================================ FILE: vcproject/AppConfiguration.cpp ================================================ #include "stdafx.h" #include "AppConfiguration.h" #include "PdfConverterImpl.h" #include "Core.h" #include #include "Utils.h" #define APP_ROOT_PATH TEXT("Software\\WechatExporter") void AppConfiguration::SetDescOrder(BOOL descOrder) { SetDwordProperty(TEXT("DescOrder"), descOrder); } BOOL AppConfiguration::GetDescOrder() { BOOL descOrder = FALSE; DWORD value = 0; if (GetDwordProperty(TEXT("DescOrder"), value)) { descOrder = (value != 0) ? TRUE : FALSE; } return descOrder; } void AppConfiguration::SetUsingRemoteEmoji(BOOL usingRemoteEmoji) { SetDwordProperty(TEXT("UsingRemoteEmoji"), usingRemoteEmoji); } BOOL AppConfiguration::GetUsingRemoteEmoji() { DWORD value = 0; GetDwordProperty(TEXT("UsingRemoteEmoji"), value); return (value != 0) ? TRUE : FALSE; } void AppConfiguration::SetIncrementalExporting(BOOL incrementalExporting) { SetDwordProperty(TEXT("IncrementalExp"), incrementalExporting); } BOOL AppConfiguration::GetIncrementalExporting() { DWORD value = 0; // default is 0 GetDwordProperty(TEXT("IncrementalExp"), value); return (value != 0) ? TRUE : FALSE; } UINT AppConfiguration::GetOutputFormat() { UINT outputFormat = OUTPUT_FORMAT_HTML; DWORD dwValue = 0; if (GetDwordProperty(TEXT("OutputFormat"), dwValue)) { if (dwValue >= OUTPUT_FORMAT_HTML && dwValue < OUTPUT_FORMAT_LAST) { outputFormat = dwValue; } } return outputFormat; } void AppConfiguration::SetOutputFormat(UINT outputFormat) { if (outputFormat < OUTPUT_FORMAT_HTML || outputFormat >= OUTPUT_FORMAT_LAST) { outputFormat = OUTPUT_FORMAT_HTML; } SetDwordProperty(TEXT("OutputFormat"), outputFormat); } void AppConfiguration::SetSavingInSession(BOOL savingInSession) { SetDwordProperty(TEXT("SaveFilesInSF"), savingInSession); } BOOL AppConfiguration::GetSavingInSession() { DWORD value = 1; if (GetDwordProperty(TEXT("SaveFilesInSF"), value)) { return value != 0; } return TRUE; } void AppConfiguration::SetLastOutputDir(LPCTSTR szOutputDir) { SetStringProperty(TEXT("OutputDir"), szOutputDir); } CString AppConfiguration::GetLastOrDefaultOutputDir() { CString outputDir; if (GetStringProperty(TEXT("OutputDir"), outputDir)) { return outputDir; } return GetDefaultOutputDir(); } CString AppConfiguration::GetDefaultOutputDir() { TCHAR szOutput[MAX_PATH] = { 0 }; HRESULT hr = SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, szOutput); return CString(SUCCEEDED(hr) ? szOutput : TEXT("")); } void AppConfiguration::SetLastBackupDir(LPCTSTR szBackupDir) { SetStringProperty(TEXT("BackupDir"), szBackupDir); } CString AppConfiguration::GetLastBackupDir() { CString backupDir; GetStringProperty(TEXT("BackupDir"), backupDir); return backupDir; } CString AppConfiguration::GetDefaultBackupDir(BOOL bCheckExistence/* = TRUE*/) { CString backupDir; TCHAR szPath[2][MAX_PATH] = { { 0 }, { 0 } }; // Check iTunes Folder HRESULT hr = SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, szPath[0]); _tcscat(szPath[0], TEXT("\\Apple Computer\\MobileSync\\Backup")); // iTunes App from MS Store hr = SHGetFolderPath(NULL, CSIDL_PROFILE, NULL, SHGFP_TYPE_CURRENT, szPath[1]); _tcscat(szPath[1], TEXT("\\Apple\\MobileSync\\Backup")); for (int idx = 0; idx < 2; ++idx) { DWORD dwAttrib = ::GetFileAttributes(szPath[idx]); if (dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY)) { backupDir = szPath[idx]; break; } } if (!bCheckExistence && backupDir.IsEmpty()) { backupDir = szPath[0]; } return backupDir; } DWORD AppConfiguration::GetLastCheckUpdateTime() { DWORD dwValue = 0; GetDwordProperty(TEXT("LastChkUpdateTime"), dwValue); return dwValue; } void AppConfiguration::SetLastCheckUpdateTime(DWORD lastCheckUpdateTime/* = 0*/) { if (0 == lastCheckUpdateTime) { lastCheckUpdateTime = (DWORD)getUnixTimeStamp(); } SetDwordProperty(TEXT("LastChkUpdateTime"), lastCheckUpdateTime); } void AppConfiguration::SetCheckingUpdateDisabled(BOOL disabled) { SetDwordProperty(TEXT("ChkUpdateDisabled"), disabled); } BOOL AppConfiguration::GetCheckingUpdateDisabled() { DWORD dwValue = 0; GetDwordProperty(TEXT("ChkUpdateDisabled"), dwValue); return dwValue != 0; } void AppConfiguration::SetSyncLoading() { SetDwordProperty(TEXT("AsyncLoadingState"), ASYNC_NONE); } BOOL AppConfiguration::GetSyncLoading() { return GetDwordValue(TEXT("AsyncLoadingState"), ASYNC_ONSCROLL) == ASYNC_NONE; } DWORD AppConfiguration::GetAsyncLoading() { return GetDwordValue(TEXT("AsyncLoadingState"), ASYNC_ONSCROLL); } void AppConfiguration::SetAsyncLoading(DWORD asyncLoading) { SetDwordProperty(TEXT("AsyncLoadingState"), asyncLoading); } void AppConfiguration::SetLoadingDataOnScroll() { SetDwordProperty(TEXT("AsyncLoadingState"), ASYNC_ONSCROLL); } BOOL AppConfiguration::GetLoadingDataOnScroll() { return GetDwordValue(TEXT("AsyncLoadingState"), ASYNC_ONSCROLL) == ASYNC_ONSCROLL; } void AppConfiguration::SetNormalPagination() { SetDwordProperty(TEXT("AsyncLoadingState"), ASYNC_PAGER_NORMAL); } BOOL AppConfiguration::GetNormalPagination() { return GetDwordValue(TEXT("AsyncLoadingState"), ASYNC_ONSCROLL) == ASYNC_PAGER_NORMAL; } void AppConfiguration::SetPaginationOnYear() { SetDwordProperty(TEXT("AsyncLoadingState"), ASYNC_PAGER_ON_YEAR); } BOOL AppConfiguration::GetPaginationOnYear() { return GetDwordValue(TEXT("AsyncLoadingState"), ASYNC_ONSCROLL) == ASYNC_PAGER_ON_YEAR; } void AppConfiguration::SetPaginationOnMonth() { SetDwordProperty(TEXT("AsyncLoadingState"), ASYNC_PAGER_ON_MONTH); } BOOL AppConfiguration::GetPaginationOnMonth() { return GetDwordValue(TEXT("AsyncLoadingState"), ASYNC_ONSCROLL) == ASYNC_PAGER_ON_MONTH; } void AppConfiguration::SetSupportingFilter(BOOL supportingFilter) { SetDwordProperty(TEXT("Filter"), supportingFilter); } BOOL AppConfiguration::GetSupportingFilter() { DWORD dwValue = 0; // FALSE GetDwordProperty(TEXT("Filter"), dwValue); return dwValue != 0; } void AppConfiguration::SetOutputDebugLogs(BOOL dbgLogs) { SetDwordProperty(TEXT("DebugLogs"), dbgLogs); } BOOL AppConfiguration::OutputDebugLogs() { DWORD dwValue = 0; // FALSE GetDwordProperty(TEXT("DebugLogs"), dwValue); return dwValue != 0; } void AppConfiguration::SetIncludingSubscriptions(BOOL includingSubscriptions) { SetDwordProperty(TEXT("IncludingSubscriptions"), includingSubscriptions); } BOOL AppConfiguration::IncludeSubscriptions() { DWORD dwValue = 0; // FALSE GetDwordProperty(TEXT("IncludingSubscriptions"), dwValue); return dwValue != 0; } void AppConfiguration::SetOpenningFolderAfterExp(BOOL openningFolderAfterExp) { SetDwordProperty(TEXT("OpenningFolderAfterExp"), openningFolderAfterExp); } BOOL AppConfiguration::GetOpenningFolderAfterExp() { DWORD dwValue = 1; // TRUE GetDwordProperty(TEXT("OpenningFolderAfterExp"), dwValue); return dwValue != 0; } BOOL AppConfiguration::IsPdfSupported() { PdfConverterImpl converter(NULL); return converter.isPdfSupported() ? TRUE : FALSE; } void AppConfiguration::upgrade() { CRegKey rk; if (rk.Create(HKEY_CURRENT_USER, APP_ROOT_PATH, REG_NONE, REG_OPTION_NON_VOLATILE, KEY_READ | KEY_WRITE) != ERROR_SUCCESS) { return; } DWORD value = 0; HRESULT hr = rk.QueryDWORDValue(TEXT("AsyncLoading"), value); if (ERROR_SUCCESS == hr) { rk.DeleteValue(TEXT("AsyncLoading")); if (value == 0) { SetSyncLoading(); } } hr = rk.QueryDWORDValue(TEXT("LoadingDataOnScroll"), value); if (ERROR_SUCCESS == hr) { rk.DeleteValue(TEXT("LoadingDataOnScroll")); if (value == 1) { SetLoadingDataOnScroll(); } } rk.Close(); } uint64_t AppConfiguration::BuildOptions() { ExportOption options; if (GetOutputFormat() == OUTPUT_FORMAT_TEXT) { options.setTextMode(); } if (GetOutputFormat() == OUTPUT_FORMAT_PDF) { options.setPdfMode(); } options.setOrder(!GetDescOrder()); // getSavingInSession if (GetSyncLoading()) { options.setSyncLoading(); } else { options.setLoadingDataOnScroll(GetLoadingDataOnScroll()); } options.setIncrementalExporting(GetIncrementalExporting()); options.supportsFilter(GetSupportingFilter()); options.outputDebugLogs(OutputDebugLogs()); if (IncludeSubscriptions()) { options.includesSubscription(); } return (uint64_t)options; } BOOL AppConfiguration::GetStringProperty(LPCTSTR name, CString& value) { CRegKey rk; if (rk.Open(HKEY_CURRENT_USER, APP_ROOT_PATH, KEY_READ) == ERROR_SUCCESS) { ULONG chars = 0; HRESULT hr = rk.QueryStringValue(name, NULL, &chars); if (ERROR_SUCCESS == hr) { hr = rk.QueryStringValue(name, value.GetBufferSetLength(chars), &chars); value.ReleaseBuffer(); return ERROR_SUCCESS == hr; } rk.Close(); } return FALSE; } BOOL AppConfiguration::SetStringProperty(LPCTSTR name, LPCTSTR value) { CRegKey rk; if (rk.Create(HKEY_CURRENT_USER, APP_ROOT_PATH, REG_NONE, REG_OPTION_NON_VOLATILE, KEY_READ | KEY_WRITE) == ERROR_SUCCESS) { HRESULT hr = rk.SetStringValue(name, value); rk.Close(); return ERROR_SUCCESS == hr; } return FALSE; } BOOL AppConfiguration::GetDwordProperty(LPCTSTR name, DWORD& value) { CRegKey rk; if (rk.Open(HKEY_CURRENT_USER, APP_ROOT_PATH, KEY_READ) == ERROR_SUCCESS) { HRESULT hr = rk.QueryDWORDValue(name, value); rk.Close(); return ERROR_SUCCESS == hr; } return FALSE; } DWORD AppConfiguration::GetDwordValue(LPCTSTR name, DWORD defaultValue) { DWORD value = defaultValue; if (GetDwordProperty(name, value)) { return value; } return defaultValue; } BOOL AppConfiguration::SetDwordProperty(LPCTSTR name, DWORD value) { CRegKey rk; if (rk.Create(HKEY_CURRENT_USER, APP_ROOT_PATH, REG_NONE, REG_OPTION_NON_VOLATILE, KEY_READ | KEY_WRITE) == ERROR_SUCCESS) { HRESULT hr = rk.SetDWORDValue(name, value); rk.Close(); return ERROR_SUCCESS == hr; } return FALSE; } ================================================ FILE: vcproject/AppConfiguration.h ================================================ #pragma once #include #define ASYNC_NONE 0 #define ASYNC_ONSCROLL 1 #define ASYNC_PAGER_NORMAL 2 #define ASYNC_PAGER_ON_YEAR 3 #define ASYNC_PAGER_ON_MONTH 4 class AppConfiguration { public: enum { OUTPUT_FORMAT_HTML = 0, OUTPUT_FORMAT_TEXT, OUTPUT_FORMAT_PDF, OUTPUT_FORMAT_LAST }; static void SetDescOrder(BOOL descOrder); static BOOL GetDescOrder(); static UINT GetOutputFormat(); static void SetOutputFormat(UINT outputFormat); static void SetSavingInSession(BOOL savingInSession); static BOOL GetSavingInSession(); static void SetUsingRemoteEmoji(BOOL usingRemoteEmoji); static BOOL GetUsingRemoteEmoji(); static void SetIncrementalExporting(BOOL incrementalExporting); static BOOL GetIncrementalExporting(); static void SetLastOutputDir(LPCTSTR szOutputDir); static CString GetLastOrDefaultOutputDir(); static CString GetDefaultOutputDir(); static void SetLastBackupDir(LPCTSTR szBackupDir); static CString GetLastBackupDir(); static CString GetDefaultBackupDir(BOOL bCheckExistence = TRUE); static DWORD GetLastCheckUpdateTime(); static void SetLastCheckUpdateTime(DWORD lastCheckUpdateTime = 0); static void SetCheckingUpdateDisabled(BOOL disabled); static BOOL GetCheckingUpdateDisabled(); static void SetLoadingDataOnScroll(); static BOOL GetLoadingDataOnScroll(); static void SetSyncLoading(); static BOOL GetSyncLoading(); static DWORD GetAsyncLoading(); static void SetAsyncLoading(DWORD asyncLoading); static void SetNormalPagination(); static BOOL GetNormalPagination(); static void SetPaginationOnYear(); static BOOL GetPaginationOnYear(); static void SetPaginationOnMonth(); static BOOL GetPaginationOnMonth(); static void SetSupportingFilter(BOOL supportingFilter); static BOOL GetSupportingFilter(); static void SetOutputDebugLogs(BOOL dbgLogs); static BOOL OutputDebugLogs(); static void SetIncludingSubscriptions(BOOL includingSubscriptions); static BOOL IncludeSubscriptions(); static void SetOpenningFolderAfterExp(BOOL openningFolderAfterExp); static BOOL GetOpenningFolderAfterExp(); static BOOL IsPdfSupported(); static void upgrade(); static uint64_t BuildOptions(); protected: static BOOL GetStringProperty(LPCTSTR name, CString& value); static BOOL SetStringProperty(LPCTSTR name, LPCTSTR value); static BOOL GetDwordProperty(LPCTSTR name, DWORD& value); static BOOL SetDwordProperty(LPCTSTR name, DWORD value); static DWORD GetDwordValue(LPCTSTR name, DWORD defaultValue); static BOOL IsAppInstalled(LPCTSTR name, BOOL lmOrCU); }; ================================================ FILE: vcproject/BackupDlg.h ================================================ // aboutdlg.h : interface of the CAboutDlg class // ///////////////////////////////////////////////////////////////////////////// #pragma once #include class CBackupDlg : public CDialogImpl { public: enum { IDD = IDD_BACKUP_DLG }; BEGIN_MSG_MAP(CBackupDlg) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) MESSAGE_HANDLER(WM_TIMER, OnTimer) // COMMAND_ID_HANDLER(IDOK, OnCloseCmd) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) COMMAND_ID_HANDLER(IDCANCEL, OnCloseCmd) END_MSG_MAP() CBackupDlg(const DeviceInfo& deviceInfo, const CString& outputDir) : m_deviceInfo(deviceInfo), m_outputDir(outputDir), m_backup(NULL), m_eventId(0), m_result(false) { } // Handler prototypes (uncomment arguments if needed): // LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) // LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) // LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/) LRESULT OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { CenterWindow(GetParent()); CProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS); // progressCtrl.SetWindowText(TEXT("")); progressCtrl.ModifyStyle(PBS_MARQUEE, 0); progressCtrl.SetRange32(1, 100); progressCtrl.SetStep(1); progressCtrl.SetPos(0); CW2A outputDir(CT2W((LPCTSTR)m_outputDir), CP_UTF8); m_backup = new IDeviceBackup(m_deviceInfo, (LPCSTR)outputDir); m_task = std::async(std::launch::async, &IDeviceBackup::backup, m_backup); m_eventId = SetTimer(1, 200); return TRUE; } LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { if (0 != m_eventId) { KillTimer(m_eventId); } CenterWindow(GetParent()); return 0; } LRESULT OnTimer(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { if (NULL != m_backup) { double progress = m_backup->getOverallProgress(); int pos = (int)(progress * 100); if (pos > 100) { pos = 100; } else if (pos < 0) { pos = 0; } CProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS); if (pos != progressCtrl.GetPos()) { progressCtrl.SetPos(pos); } } std::future_status status = m_task.wait_for(std::chrono::seconds(0)); if (status == std::future_status::ready) { KillTimer(m_eventId); m_eventId = 0; m_result = m_task.get(); EndDialog(IDOK); } return 0; } LRESULT OnCloseCmd(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { EndDialog(wID); return 0; } bool getBackupResult() const { return m_result; } protected: // CHyperLink m_homePageLinkCtrl; const DeviceInfo& m_deviceInfo; CString m_outputDir; IDeviceBackup* m_backup; std::future m_task; UINT m_eventId; bool m_result; }; ================================================ FILE: vcproject/ColoredControls.h ================================================ #if !defined(AFX_COLOREDCONTROLS_H__20020226_0D16_1B29_088A_0080AD509054__INCLUDED_) #define AFX_COLOREDCONTROLS_H__20020226_0D16_1B29_088A_0080AD509054__INCLUDED_ #pragma once ///////////////////////////////////////////////////////////////////////////// // Colored Control - Windows Controls with colours // // Written by Bjarke Viksoe (bjarke@viksoe.dk) // Copyright (c) 2002 Bjarke Viksoe. // // This file include the following controls: // CColoredDialog // CColoredStaticCtrl // CColoredEditCtrl // CColoredButtonCtrl // CColoredComboBoxCtrl // CColoredTabCtrl // CColoredListViewCtrl // CColoredTreeViewCtrl // // Add the following macro to the parent's message map: // REFLECT_NOTIFICATIONS() // // This code may be used in compiled form in any way you desire. This // source file may be redistributed by any means PROVIDING it is // not sold for profit without the authors written consent, and // providing that this notice and the authors name is included. // // This file is provided "as is" with no expressed or implied warranty. // The author accepts no liability if it causes any damage to you or your // computer whatsoever. It's free, so don't hassle me about it. // // Beware of bugs. // #ifndef __cplusplus #error WTL requires C++ compilation (use a .cpp suffix) #endif #ifndef __ATLCTRLS_H__ #error ColoredControls.h requires atlctrls.h to be included first #endif ///////////////////////////////////////////////////////////////////////////// // Dialog with new colour or bitmap background // To use this class: Derive from CColoredDialog and then // chain the message map CHAIN_MSG_MAP(CColoredDialog)... template< class T > class CColoredDialog { public: CBrush m_brBack; // Operations BOOL SetBitmap(UINT nRes) { // Load the bitmap and create a new brush CBitmap bmBack; bmBack.LoadBitmap(nRes); if( !m_brBack.IsNull() ) m_brBack.DeleteObject(); m_brBack.CreatePatternBrush(bmBack); // Repaint T* pT = static_cast(this); pT->Invalidate(); } void SetColor(COLORREF clr) { // Create a new brush from the color if( !m_brBack.IsNull() ) m_brBack.DeleteObject(); m_brBack.CreateSolidBrush(clr); // Repaint T* pT = static_cast(this); pT->Invalidate(); } // Message map and handlers BEGIN_MSG_MAP(CColoredDialog) MESSAGE_HANDLER(WM_CTLCOLORDLG, OnCtlColorDlg) END_MSG_MAP() LRESULT OnCtlColorDlg(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { T* pT = static_cast(this); if( m_brBack.IsNull() ) return pT->DefWindowProc(); return (LRESULT) (HBRUSH) m_brBack; } }; ///////////////////////////////////////////////////////////////////////////// // CColoredStaticCtrl - A Static control with new colours template< class T, class TBase = CStatic, class TWinTraits = CControlWinTraits > class ATL_NO_VTABLE CColoredStaticImpl : public CWindowImpl< T, TBase, TWinTraits > { public: DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName()) // Color codes for Static control COLORREF m_clrBackground; COLORREF m_clrNormalText; COLORREF m_clrNormalBk; COLORREF m_clrDisabledText; COLORREF m_clrDisabledBk; // User-defined colours needs a brush CBrush m_brBackground; CBrush m_brNormalBk; CBrush m_brDisabledBk; // We use a brush-handle when no custom colours have been assigned // because system-color brushes must not be destroyed... CBrushHandle m_hbrNormalBk; CBrushHandle m_hbrDisabledBk; // Operations BOOL SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd==NULL); ATLASSERT(::IsWindow(hWnd)); #ifdef _DEBUG // Check class TCHAR szBuffer[16]; if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) { ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0); } #endif BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd); if( bRet ) _Init(); return bRet; } void SetNormalColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT); m_clrNormalText = clrText; // Background if( !m_brNormalBk.IsNull() ) m_brNormalBk.DeleteObject(); if( clrBack == CLR_INVALID ) { m_clrNormalBk = ::GetSysColor(COLOR_BTNFACE); m_hbrNormalBk = ::GetSysColorBrush(COLOR_BTNFACE); } else { m_clrNormalBk = clrBack; m_brNormalBk.CreateSolidBrush(clrBack); m_hbrNormalBk = m_brNormalBk; } // Repaint Invalidate(); } void SetDisabledColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_GRAYTEXT); m_clrDisabledText = clrText; // Background if( !m_brDisabledBk.IsNull() ) m_brDisabledBk.DeleteObject(); if( clrBack == CLR_INVALID ) { m_clrDisabledBk = ::GetSysColor(COLOR_BTNFACE); m_hbrDisabledBk = ::GetSysColorBrush(COLOR_BTNFACE); } else { m_clrDisabledBk = clrBack; m_brDisabledBk.CreateSolidBrush(clrBack); m_hbrDisabledBk = m_brDisabledBk; } // Repaint Invalidate(); } void SetBkColor(COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Background if( !m_brBackground.IsNull() ) m_brBackground.DeleteObject(); m_clrBackground = clrBack; m_brBackground.CreateSolidBrush(clrBack); // Repaint Invalidate(); } // Implementation void _Init() { ATLASSERT(::IsWindow(m_hWnd)); COLORREF clrNone = CLR_INVALID; SetNormalColors( clrNone, clrNone ); SetDisabledColors( clrNone, clrNone ); } // Message map and handlers BEGIN_MSG_MAP(CColoredStaticImpl) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(OCM_CTLCOLORSTATIC, OnCtlColorStatic) MESSAGE_HANDLER(OCM_CTLCOLORDLG, OnCtlColorDlg) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { LRESULT lRes = DefWindowProc(uMsg, wParam, lParam); _Init(); return lRes; } LRESULT OnCtlColorStatic(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) { BOOL bEnabled = IsWindowEnabled(); CDCHandle dc( (HDC) wParam ); dc.SetBkMode(TRANSPARENT); dc.SetTextColor( bEnabled ? m_clrNormalText : m_clrDisabledText ); dc.SetBkColor( bEnabled ? m_clrNormalBk : m_clrDisabledBk ); return (LRESULT) (HBRUSH) ( bEnabled ? m_hbrNormalBk : m_hbrDisabledBk ); } LRESULT OnCtlColorDlg(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { T* pT = static_cast(this); if( m_brBackground.IsNull() ) return pT->DefWindowProc(); return (LRESULT) (HBRUSH) m_brBackground; } }; class CColoredStaticCtrl : public CColoredStaticImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ColoredStatic"), GetWndClassName()) }; class CColoredCheckboxCtrl : public CColoredStaticImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ColoredCheckbox"), GetWndClassName()) }; class CColoredOptionCtrl : public CColoredStaticImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ColoredOption"), GetWndClassName()) }; ///////////////////////////////////////////////////////////////////////////// // CColoredEditCtrl - An Edit control with new colours template< class T, class TBase = CEdit, class TWinTraits = CControlWinTraits > class ATL_NO_VTABLE CColoredEditImpl : public CWindowImpl< T, TBase, TWinTraits > { public: DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName()) // Color codes for Edit control COLORREF m_clrNormalText; COLORREF m_clrNormalBk; COLORREF m_clrDisabledText; COLORREF m_clrDisabledBk; COLORREF m_clrReadOnlyText; COLORREF m_clrReadOnlyBk; // User-defined colours needs a brush CBrush m_brNormalBk; CBrush m_brDisabledBk; CBrush m_brReadOnlyBk; // We use a brush-handle when no custom colours have been assigned // because system-color brushes must not be destroyed... CBrushHandle m_hbrNormalBk; CBrushHandle m_hbrDisabledBk; CBrushHandle m_hbrReadOnlyBk; // Operations BOOL SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd==NULL); ATLASSERT(::IsWindow(hWnd)); #ifdef _DEBUG // Check class TCHAR szBuffer[16]; if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) { ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0); } #endif BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd); if( bRet ) _Init(); return bRet; } void SetNormalColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_WINDOWTEXT); m_clrNormalText = clrText; // Background if( !m_brNormalBk.IsNull() ) m_brNormalBk.DeleteObject(); if( clrBack == CLR_INVALID ) { m_clrNormalBk = ::GetSysColor(COLOR_WINDOW); m_hbrNormalBk = ::GetSysColorBrush(COLOR_WINDOW); } else { m_clrNormalBk = clrBack; m_brNormalBk.CreateSolidBrush(clrBack); m_hbrNormalBk = m_brNormalBk; } // Repaint Invalidate(); } void SetDisabledColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_GRAYTEXT); m_clrDisabledText = clrText; // Background if( !m_brDisabledBk.IsNull() ) m_brDisabledBk.DeleteObject(); if( clrBack == CLR_INVALID ) { m_clrDisabledBk = ::GetSysColor(COLOR_BTNFACE); m_hbrDisabledBk = ::GetSysColorBrush(COLOR_BTNFACE); } else { m_clrDisabledBk = clrBack; m_brDisabledBk.CreateSolidBrush(clrBack); m_hbrDisabledBk = m_brDisabledBk; } // Repaint Invalidate(); } void SetReadOnlyColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT); m_clrReadOnlyText = clrText; // Background if( !m_brReadOnlyBk.IsNull() ) m_brReadOnlyBk.DeleteObject(); if( clrBack == CLR_INVALID ) { m_clrReadOnlyBk = ::GetSysColor(COLOR_BTNFACE); m_hbrReadOnlyBk = ::GetSysColorBrush(COLOR_BTNFACE); } else { m_clrReadOnlyBk = clrBack; m_brReadOnlyBk.CreateSolidBrush(clrBack); m_hbrReadOnlyBk = m_brReadOnlyBk; } // Repaint Invalidate(); } // Implementation void _Init() { ATLASSERT(::IsWindow(m_hWnd)); COLORREF clrNone = CLR_INVALID; SetNormalColors( clrNone, clrNone ); SetDisabledColors( clrNone, clrNone ); SetReadOnlyColors( clrNone, clrNone ); } // Message map and handlers BEGIN_MSG_MAP(CColoredEditImpl) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(OCM_CTLCOLOREDIT, OnCtlColorEdit) MESSAGE_HANDLER(OCM_CTLCOLORMSGBOX, OnCtlColorEdit) MESSAGE_HANDLER(OCM_CTLCOLORSTATIC, OnCtlColorStatic) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { LRESULT lRes = DefWindowProc(uMsg, wParam, lParam); _Init(); return lRes; } LRESULT OnCtlColorEdit(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) { CDCHandle dc( (HDC) wParam ); dc.SetBkMode(TRANSPARENT); dc.SetTextColor( m_clrNormalText ); dc.SetBkColor( m_clrNormalBk ); return (LRESULT) (HBRUSH) m_hbrNormalBk; } LRESULT OnCtlColorStatic(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) { // Microsoft Q130952 explains why we also need this CDCHandle dc( (HDC) wParam ); dc.SetBkMode(TRANSPARENT); if( IsWindowEnabled() ) { ATLASSERT(GetStyle() & ES_READONLY); dc.SetTextColor( m_clrReadOnlyText ); dc.SetBkColor( m_clrReadOnlyBk ); return (LRESULT) (HBRUSH) m_hbrReadOnlyBk; } else { dc.SetTextColor( m_clrDisabledText ); dc.SetBkColor( m_clrDisabledBk ); return (LRESULT) (HBRUSH) m_hbrDisabledBk; } } }; class CColoredEditCtrl : public CColoredEditImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ColoredEdit"), GetWndClassName()) }; ///////////////////////////////////////////////////////////////////////////// // CColoredButtonCtrl - The Button control with custom colours template< class T, class TBase = CButton, class TWinTraits = CControlWinTraits > class ATL_NO_VTABLE CColoredButtonImpl : public CWindowImpl< T, TBase, TWinTraits > { public: DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName()) // Color codes for Button control COLORREF m_clrText; COLORREF m_clrBackUp; COLORREF m_clrBackDown; COLORREF m_clrBackDisabled; // Image support CImageList m_ImageList; UINT m_nImage; UINT m_nSelImage; // Operations BOOL SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd==NULL); ATLASSERT(::IsWindow(hWnd)); #ifdef _DEBUG // Check class TCHAR szBuffer[20]; if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) { ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0); } #endif BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd); if( bRet ) _Init(); return bRet; } void SetTextColor(COLORREF clrText) { ATLASSERT(::IsWindow(m_hWnd)); if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT); m_clrText = clrText; // Repaint Invalidate(); } void SetBkColor(COLORREF clrUp, COLORREF clrDown=-1) { ATLASSERT(::IsWindow(m_hWnd)); if( clrUp == CLR_INVALID ) clrUp = ::GetSysColor(COLOR_BTNFACE); if( clrDown == CLR_INVALID ) clrDown = clrUp; m_clrBackUp = clrUp; m_clrBackDown = clrDown; // Repaint Invalidate(); } void SetDisabledColor(COLORREF clrDisabled) { ATLASSERT(::IsWindow(m_hWnd)); if( clrDisabled == CLR_INVALID ) clrDisabled = ::GetSysColor(COLOR_BTNFACE); m_clrBackDisabled = clrDisabled; // Repaint Invalidate(); } void SetImageList(HIMAGELIST hImageList) { ATLASSERT(::IsWindow(m_hWnd)); m_ImageList = hImageList; } void SetImage(UINT nImage) { ATLASSERT(::IsWindow(m_hWnd)); ATLASSERT(!m_ImageList.IsNull()); m_nImage = nImage; } void SetSelImage(UINT nImage) { ATLASSERT(::IsWindow(m_hWnd)); ATLASSERT(!m_ImageList.IsNull()); m_nSelImage = nImage; } // Implementation void _Init() { ATLASSERT(::IsWindow(m_hWnd)); // We need this style to prevent Windows from painting the button ModifyStyle(0, BS_OWNERDRAW); COLORREF clrNone = CLR_INVALID; SetTextColor( clrNone ); SetBkColor( clrNone, clrNone ); SetDisabledColor( clrNone ); m_nImage = m_nSelImage = (UINT)-1; } // Message map and handlers BEGIN_MSG_MAP(CColoredButtonImpl) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground) MESSAGE_HANDLER(WM_LBUTTONDBLCLK, OnLButtonDblClick) MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem) END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { LRESULT lRes = DefWindowProc(uMsg, wParam, lParam); _Init(); return lRes; } LRESULT OnEraseBackground(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { return 1; // no background needed } LRESULT OnLButtonDblClick(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { return DefWindowProc(WM_LBUTTONDOWN, wParam, lParam); } LRESULT OnDrawItem(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) { LPDRAWITEMSTRUCT lpDIS = (LPDRAWITEMSTRUCT) lParam; ATLASSERT(lpDIS->CtlType==ODT_BUTTON); CDCHandle dc = lpDIS->hDC; RECT rc = lpDIS->rcItem; DWORD dwStyle = GetStyle(); bool bSelected = (lpDIS->itemState & ODS_SELECTED) != 0; bool bDisabled = (lpDIS->itemState & (ODS_DISABLED|ODS_GRAYED)) != 0; COLORREF clrBack = bSelected ? m_clrBackDown : m_clrBackUp; if( bDisabled ) clrBack = m_clrBackDisabled; dc.FillSolidRect(&rc, clrBack); // Draw edge if( dwStyle & BS_FLAT ) { dc.DrawEdge(&rc, bSelected ? BDR_SUNKENOUTER : BDR_RAISEDINNER, BF_RECT); } else { dc.DrawEdge(&rc, bSelected ? EDGE_SUNKEN : EDGE_RAISED, BF_RECT); } ::InflateRect(&rc, -2 * ::GetSystemMetrics(SM_CXEDGE), -2 * ::GetSystemMetrics(SM_CYEDGE)); // Draw focus rectangle if( lpDIS->itemState & ODS_FOCUS ) dc.DrawFocusRect(&rc); ::InflateRect(&rc, -1, -1); // Offset when button is selected if( bSelected ) ::OffsetRect(&rc, 1, 1); // Draw image UINT nImage = m_nImage; if( bSelected && m_nSelImage != -1 ) nImage = m_nSelImage; if( !m_ImageList.IsNull() && nImage != -1 ) { POINT pt = { rc.left, rc.top-1 }; SIZE sizeIcon; m_ImageList.GetIconSize(sizeIcon); if( bDisabled ) { //m_ImageList.DrawEx(nImage, dc, pt.x, pt.y, sizeIcon.cx, sizeIcon.cy, CLR_NONE, ::GetSysColor(COLOR_GRAYTEXT), ILD_BLEND50 | ILD_TRANSPARENT); HICON hIcon = m_ImageList.GetIcon(nImage); dc.DrawState(pt, sizeIcon, hIcon, DST_ICON | DSS_DISABLED); ::DestroyIcon(hIcon); } else { m_ImageList.Draw(dc, nImage, pt, bDisabled ? ILD_BLEND25 : ILD_TRANSPARENT); } rc.left += sizeIcon.cx + 3; } // Draw text UINT nLen = GetWindowTextLength(); LPTSTR pstr = (LPTSTR) _alloca( (nLen+1)*sizeof(TCHAR) ); GetWindowText(pstr, nLen+1); UINT uFlags = 0; if( dwStyle & BS_LEFT ) uFlags |= DT_LEFT; else if( dwStyle & BS_RIGHT ) uFlags |= DT_RIGHT; else if( dwStyle & BS_CENTER ) uFlags |= DT_CENTER; else uFlags |= DT_CENTER; if( dwStyle & BS_TOP ) uFlags |= DT_TOP; else if( dwStyle & BS_BOTTOM ) uFlags |= DT_BOTTOM; else if( dwStyle & BS_VCENTER ) uFlags |= DT_VCENTER; else uFlags |= DT_VCENTER; if( (dwStyle & BS_MULTILINE) == 0 ) uFlags |= DT_SINGLELINE; dc.SetBkMode(TRANSPARENT); dc.SetTextColor(bDisabled ? ::GetSysColor(COLOR_GRAYTEXT) : m_clrText); dc.DrawText(pstr, nLen, &rc, uFlags); return TRUE; } }; class CColoredButtonCtrl : public CColoredButtonImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ColoredButton"), GetWndClassName()) }; ///////////////////////////////////////////////////////////////////////////// // CColoredComboBoxCtrl - The ComboBox control with custom colours template< class T, class TBase = CComboBox, class TWinTraits = CControlWinTraits > class ATL_NO_VTABLE CColoredComboBoxImpl : public CWindowImpl< T, TBase, TWinTraits > { public: DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName()) // Color codes for ComboBox control COLORREF m_clrListText; COLORREF m_clrListBk; COLORREF m_clrSelectedListText; COLORREF m_clrSelectedListBk; COLORREF m_clrDisabledText; COLORREF m_clrDisabledBk; COLORREF m_clrEditText; COLORREF m_clrEditBk; // User-defined colours needs a brush CBrush m_brListBk; CBrush m_brSelectedListBk; CBrush m_brDisabledBk; CBrush m_brEditBk; // We use a brush-handle when no custom colours have been assigned // because system-color brushes must not be destroyed... CBrushHandle m_hbrListBk; CBrushHandle m_hbrSelectedListBk; CBrushHandle m_hbrDisabledBk; CBrushHandle m_hbrEditBk; // Operations BOOL SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd==NULL); ATLASSERT(::IsWindow(hWnd)); #ifdef _DEBUG // Check class TCHAR szBuffer[16]; if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) { ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0); } #endif BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd); if( bRet ) _Init(); return bRet; } void SetListColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_WINDOWTEXT); m_clrListText = clrText; // Background if( !m_brListBk.IsNull() ) m_brListBk.DeleteObject(); if( clrBack == CLR_INVALID ) { m_clrListBk = ::GetSysColor(COLOR_WINDOW); m_hbrListBk = ::GetSysColorBrush(COLOR_WINDOW); } else { m_clrListBk = clrBack; m_brListBk.CreateSolidBrush(clrBack); m_hbrListBk = m_brListBk; } // Repaint Invalidate(); } void SetDisabledColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_GRAYTEXT); m_clrDisabledText = clrText; // Background if( !m_brDisabledBk.IsNull() ) m_brDisabledBk.DeleteObject(); if( clrBack == CLR_INVALID ) { m_clrDisabledBk = ::GetSysColor(COLOR_BTNFACE); m_hbrDisabledBk = ::GetSysColorBrush(COLOR_BTNFACE); } else { m_clrDisabledBk = clrBack; m_brDisabledBk.CreateSolidBrush(clrBack); m_hbrDisabledBk = m_brDisabledBk; } // Repaint Invalidate(); } void SetEditColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT); m_clrEditText = clrText; // Background if( !m_brEditBk.IsNull() ) m_brEditBk.DeleteObject(); if( clrBack == CLR_INVALID ) { m_clrEditBk = ::GetSysColor(COLOR_BTNFACE); m_hbrEditBk = ::GetSysColorBrush(COLOR_BTNFACE); } else { m_clrEditBk = clrBack; m_brEditBk.CreateSolidBrush(clrBack); m_hbrEditBk = m_brEditBk; } // Repaint Invalidate(); } // Implementation void _Init() { ATLASSERT(::IsWindow(m_hWnd)); COLORREF clrNone = CLR_INVALID; SetListColors( clrNone, clrNone ); SetDisabledColors( clrNone, clrNone ); SetEditColors( clrNone, clrNone ); } // Message map and handlers BEGIN_MSG_MAP(CColoredComboBoxImpl) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(WM_CTLCOLOREDIT, OnCtlColorEdit) MESSAGE_HANDLER(OCM_CTLCOLOREDIT, OnCtlColorEdit) MESSAGE_HANDLER(WM_CTLCOLORMSGBOX, OnCtlColorEdit) MESSAGE_HANDLER(WM_CTLCOLORLISTBOX, OnCtlColorListBox) MESSAGE_HANDLER(OCM_CTLCOLORSTATIC, OnCtlColorStatic) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { LRESULT lRes = DefWindowProc(uMsg, wParam, lParam); _Init(); return lRes; } LRESULT OnCtlColorEdit(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) { CDCHandle dc( (HDC) wParam ); dc.SetBkMode(TRANSPARENT); dc.SetTextColor( m_clrEditText ); dc.SetBkColor( m_clrEditBk ); return (LRESULT) (HBRUSH) m_hbrEditBk; } LRESULT OnCtlColorListBox(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) { CDCHandle dc( (HDC) wParam ); dc.SetBkMode(TRANSPARENT); dc.SetTextColor( m_clrListText ); dc.SetBkColor( m_clrListBk ); return (LRESULT) (HBRUSH) m_hbrListBk; } LRESULT OnCtlColorStatic(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) { CDCHandle dc( (HDC) wParam ); dc.SetBkMode(TRANSPARENT); if( IsWindowEnabled() ) { dc.SetTextColor( m_clrEditText ); dc.SetBkColor( m_clrEditBk ); return (LRESULT) (HBRUSH) m_hbrEditBk; } else { dc.SetTextColor( m_clrDisabledText ); dc.SetBkColor( m_clrDisabledBk ); return (LRESULT) (HBRUSH) m_hbrDisabledBk; } } }; class CColoredComboBoxCtrl : public CColoredComboBoxImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ColoredComboBox"), GetWndClassName()) }; ///////////////////////////////////////////////////////////////////////////// // CColoredTabCtrl - A Tab control with tab colours template< class T, class TBase = CTabCtrl, class TWinTraits = CControlWinTraits > class ATL_NO_VTABLE CColoredTabImpl : public CWindowImpl< T, TBase, TWinTraits > { public: DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName()) // Color codes for Tab control COLORREF m_clrBackground; COLORREF m_clrNormalText; COLORREF m_clrNormalBk; COLORREF m_clrDisabledText; COLORREF m_clrDisabledBk; COLORREF m_clrInactiveText; COLORREF m_clrInactiveBk; COLORREF m_clrHighlightText; COLORREF m_clrHighlightBk; HFONT m_hInactiveFont; // Operations BOOL SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd==NULL); ATLASSERT(::IsWindow(hWnd)); #ifdef _DEBUG // Check class TCHAR szBuffer[20]; if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) { ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0); } #endif BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd); if( bRet ) _Init(); return bRet; } void SetBackgroundColor(COLORREF clrBack) { if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE); m_clrBackground = clrBack; // Repaint Invalidate(); } void SetNormalColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_WINDOWTEXT); m_clrNormalText = clrText; // Background if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE); m_clrNormalBk = clrBack; // Repaint Invalidate(); } void SetDisabledColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_GRAYTEXT); m_clrDisabledText = clrText; // Background if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE); m_clrDisabledBk = clrBack; // Repaint Invalidate(); } void SetInactiveColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_BTNTEXT); m_clrInactiveText = clrText; // Background if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE); m_clrInactiveBk = clrBack; // Repaint Invalidate(); } void SetHighlightColors(COLORREF clrText, COLORREF clrBack) { ATLASSERT(::IsWindow(m_hWnd)); // Text #if(WINVER >= 0x0500) if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_HOTLIGHT); #else if( clrText == CLR_INVALID ) clrText = ::GetSysColor(COLOR_HIGHLIGHT); #endif m_clrHighlightText = clrText; // Background if( clrBack == CLR_INVALID ) clrBack = ::GetSysColor(COLOR_BTNFACE); m_clrHighlightBk = clrBack; // Repaint Invalidate(); } void SetInactiveFont(HFONT hFont) { m_hInactiveFont = hFont; // Repaint Invalidate(); } // Implementation void _Init() { ATLASSERT(::IsWindow(m_hWnd)); ATLASSERT((GetStyle() & (TCS_BUTTONS|TCS_VERTICAL))==0); // We're drawing the text... ModifyStyle(0, TCS_OWNERDRAWFIXED); m_hInactiveFont = NULL; COLORREF clrNone = CLR_INVALID; SetBackgroundColor( clrNone ); SetNormalColors( clrNone, clrNone ); SetDisabledColors( clrNone, clrNone ); SetInactiveColors( clrNone, clrNone ); SetHighlightColors( clrNone, clrNone ); } // Message map and handlers BEGIN_MSG_MAP(CColoredTabImpl) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(WM_PAINT, OnPaint) MESSAGE_HANDLER(WM_PRINTCLIENT, OnPaint) MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { LRESULT lRes = DefWindowProc(uMsg, wParam, lParam); _Init(); return lRes; } LRESULT OnDrawItem(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) { LPDRAWITEMSTRUCT lpDIS = (LPDRAWITEMSTRUCT) lParam; ATLASSERT(lpDIS->CtlType==ODT_TAB); ATLASSERT(lpDIS->itemAction & ODA_DRAWENTIRE); CDCHandle dc = lpDIS->hDC; RECT rc = lpDIS->rcItem; DWORD dwStyle = GetStyle(); HFONT hFont = GetFont(); TCHAR szText[128]; TCITEM itm = { 0 }; itm.mask = TCIF_TEXT | TCIF_IMAGE | TCIF_STATE; itm.pszText = szText; itm.cchTextMax = sizeof(szText)/sizeof(TCHAR); itm.dwStateMask = (DWORD) -1; GetItem(lpDIS->itemID, &itm); COLORREF clrText; COLORREF clrBack; int cyOffset = 0; if( lpDIS->itemState & ODS_DISABLED ) { clrText = m_clrDisabledText; clrBack = m_clrDisabledBk; } else if( itm.dwState & TCIS_HIGHLIGHTED ) { clrText = m_clrHighlightText; clrBack = m_clrHighlightBk; } else if( lpDIS->itemState & ODS_SELECTED ) { clrText = m_clrNormalText; clrBack = m_clrNormalBk; cyOffset = 1; } else { clrText = m_clrInactiveText; clrBack = m_clrInactiveBk; if( m_hInactiveFont != NULL ) hFont = m_hInactiveFont; } dc.FillSolidRect(&rc, clrBack); if( itm.iImage != -1 ) { CImageList iml = GetImageList(); int cxImage, cyImage; iml.GetIconSize(cxImage, cyImage); POINT pt = { rc.left + 5, rc.top + 1 + ((rc.bottom-rc.top)/2-(cyImage)/2) }; iml.Draw(dc, itm.iImage, pt, ILD_TRANSPARENT ); rc.left += cxImage; } HFONT hOldFont = dc.SelectFont(hFont); dc.SetBkColor(clrBack), dc.SetTextColor(clrText); dc.SetBkMode(TRANSPARENT); UINT dwFlags = DT_VCENTER | DT_SINGLELINE | DT_NOCLIP; if( dwStyle & TCS_FORCELABELLEFT ) { dwFlags |= DT_LEFT; rc.left += 3; } else { dwFlags |= DT_CENTER; } dc.DrawText(itm.pszText, -1, &rc, dwFlags); dc.SelectFont(hOldFont); return TRUE; } LRESULT OnPaint(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) { T* pT = static_cast(this); if( wParam != NULL ) { RECT rc; GetClientRect(&rc); pT->DoPaint( (HDC) wParam ); // Let the tab-control paint itself bHandled = FALSE; } else { PAINTSTRUCT m_ps; HDC hDC = ::BeginPaint(m_hWnd, &m_ps); pT->DoPaint(hDC); // Let the tab-control paint itself DefWindowProc(WM_PRINTCLIENT, (WPARAM) hDC, PRF_CLIENT); ::EndPaint(m_hWnd, &m_ps); } return 0; } void DoPaint(CDCHandle dc) { // Calculate the upper box, so we can paint a // different background colour. We need to do the calculation // because of a possible multi-line tab-control... RECT rcWin; GetWindowRect(&rcWin); RECT rcClient = rcWin; AdjustRect(FALSE, &rcClient); RECT rcTop = { 0, 0, rcWin.right-rcWin.left, rcClient.top-rcWin.top }; dc.FillSolidRect(&rcTop, m_clrBackground); // TODO: If you want to paint the tab client-area a different // colour, this is the place to do it... } }; class CColoredTabCtrl : public CColoredTabImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ColoredTab"), GetWndClassName()) }; ///////////////////////////////////////////////////////////////////////////// // CColoredListViewCtrl - A ListView control with individual item colours template< class T, class TBase = CListViewCtrl, class TWinTraits = CControlWinTraits > class ATL_NO_VTABLE CColoredListViewImpl : public CWindowImpl< T, TBase, TWinTraits >, public CCustomDraw< T > { public: DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName()) typedef struct { COLORREF clrText; COLORREF clrBackground; HFONT hFont; LPARAM lParam; } LVCOLORPARAM; int m_nColumns; // Operations BOOL SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd==NULL); ATLASSERT(::IsWindow(hWnd)); #ifdef _DEBUG // Check class TCHAR szBuffer[16]; if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) { ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0); } #endif BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd); if( bRet ) _Init(); return bRet; } int InsertItem(UINT nMask, int nItem, LPCTSTR lpszItem, UINT nState, UINT nStateMask, int nImage=-1, LPARAM lParam=0) { ATLASSERT(::IsWindow(m_hWnd)); // You must have initialized columns before calling this! // And you are not allowed to add columns once the list is populated! CHeaderCtrl ctrlHeader = GetHeader(); if( m_nColumns == 0 ) m_nColumns = ctrlHeader.GetItemCount(); ATLASSERT(m_nColumns > 0); ATLASSERT(ctrlHeader.GetItemCount()==m_nColumns); // Create a place-holder of property controls for each subitem... LVCOLORPARAM* pParams; ATLTRY( pParams = new LVCOLORPARAM[m_nColumns] ); ATLASSERT(pParams); if( pParams == NULL ) return -1; ::FillMemory(pParams, sizeof(LVCOLORPARAM) * m_nColumns, -1); // Finally create the listview item itself... if( nItem == -1 ) nItem = GetItemCount(); nMask |= LVIF_PARAM; pParams[0].lParam = lParam; return TBase::InsertItem(nMask, nItem, lpszItem, nState, nStateMask, nImage, (LPARAM) pParams); } int InsertItem(int nItem, LPCTSTR lpszItem, int nImage) { ATLASSERT(::IsWindow(m_hWnd)); return InsertItem(LVIF_TEXT|LVIF_IMAGE, nItem, lpszItem, 0, 0, nImage, 0); } DWORD_PTR GetItemData(int nItem) const { ATLASSERT(::IsWindow(m_hWnd)); LVITEM lvi = { 0 }; lvi.iItem = nItem; lvi.mask = LVIF_PARAM; if( GetItem(&lvi) == -1 ) return 0; LVCOLORPARAM* pParams = (LVCOLORPARAM*) lvi.lParam; return (DWORD_PTR) pParams[0].lParam; } BOOL SetItemColors(int nItem, int nSubItem, COLORREF clrText, COLORREF clrBk) { ATLASSERT(::IsWindow(m_hWnd)); LVITEM lvi = { 0 }; lvi.iItem = nItem; lvi.mask = LVIF_PARAM; if( GetItem(&lvi) == -1 ) return FALSE; LVCOLORPARAM* pParams = (LVCOLORPARAM*) lvi.lParam; ATLASSERT(nSubItem>=0 && nSubItem, 1 ) // Because WTL 3.1 does not support subitem custom drawing. // Should be forward-compatible with new versions... REFLECTED_NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnNotifyCustomDraw) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { LRESULT lRes = DefWindowProc(uMsg, wParam, lParam); _Init(); return lRes; } LRESULT OnDeleteItem(int /*idCtrl*/, LPNMHDR pnmh, BOOL& /*bHandled*/) { ATLASSERT(m_nColumns>0); LPNMLISTVIEW pnmlv = (LPNMLISTVIEW) pnmh; ATLASSERT(pnmlv->lParam); LVCOLORPARAM* pParams = reinterpret_cast(pnmlv->lParam); delete [] pParams; return 0; } LRESULT OnNotifyCustomDraw(int idCtrl, LPNMHDR pnmh, BOOL& bHandled) { T* pT = static_cast(this); pT->SetMsgHandled(FALSE); LPNMCUSTOMDRAW lpNMCustomDraw = (LPNMCUSTOMDRAW) pnmh; DWORD dwRet = 0; switch( lpNMCustomDraw->dwDrawStage ) { case CDDS_ITEMPREPAINT | CDDS_SUBITEM: dwRet = pT->OnSubItemPrePaint(idCtrl, lpNMCustomDraw); pT->SetMsgHandled(TRUE); return dwRet; } bHandled = FALSE; return dwRet; } // Custom painting DWORD OnPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/) { return CDRF_NOTIFYITEMDRAW; // We need per-item notifications } DWORD OnItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/) { return CDRF_NOTIFYSUBITEMDRAW; // We need per-subitem notifications } DWORD OnSubItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW lpNMCustomDraw) { LPNMLVCUSTOMDRAW lpNMLVCD = (LPNMLVCUSTOMDRAW) lpNMCustomDraw; ATLASSERT(lpNMLVCD->nmcd.lItemlParam); LVCOLORPARAM* pParams = reinterpret_cast(lpNMLVCD->nmcd.lItemlParam); const LVCOLORPARAM& param = pParams[ lpNMLVCD->iSubItem ]; if( param.clrText != CLR_INVALID ) lpNMLVCD->clrText = param.clrText; if( param.clrBackground != CLR_INVALID ) lpNMLVCD->clrTextBk = param.clrBackground; return CDRF_NEWFONT; // We changed the colors } }; class CColoredListViewCtrl : public CColoredListViewImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ColoredListView"), GetWndClassName()) }; ///////////////////////////////////////////////////////////////////////////// // CColoredTreeViewCtrl - A TreeView with item colours template< class T, class TBase = CTreeViewCtrl, class TWinTraits = CControlWinTraits > class ATL_NO_VTABLE CColoredTreeViewImpl : public CWindowImpl< T, TBase, TWinTraits >, public CCustomDraw< T > { public: DECLARE_WND_SUPERCLASS(NULL, TBase::GetWndClassName()) typedef struct tagTVCOLOR { COLORREF clrText; COLORREF clrBackground; COLORREF clrSelText; COLORREF clrSelBackground; HFONT hFont; } TVCOLOR; CSimpleMap< HTREEITEM, TVCOLOR > m_mapColors; // Operations BOOL SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd==NULL); ATLASSERT(::IsWindow(hWnd)); #ifdef _DEBUG // Check class TCHAR szBuffer[20]; if( ::GetClassName(hWnd, szBuffer, (sizeof(szBuffer)/sizeof(TCHAR))-1) ) { ATLASSERT(::lstrcmpi(szBuffer, TBase::GetWndClassName())==0); } #endif BOOL bRet = CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd); if( bRet ) _Init(); return bRet; } // Use syntax: // CColoredTreeViewCtrl::TVCOLOR col; // col.clrText = RGB(120,120,120); // col.clrBackground = -1; // col.clrSelText = -1; // col.clrSelBackground = -1; // col.hFont = NULL; // ctrl.SetItemColors(hItem, &col); // void SetItemColors(HTREEITEM hItem, TVCOLOR* color) { ATLASSERT(::IsWindow(m_hWnd)); m_mapColors.Add(hItem, *color); } // Implementation void _Init() { ATLASSERT(::IsWindow(m_hWnd)); } // Message map and handlers BEGIN_MSG_MAP(CColoredTreeViewImpl) MESSAGE_HANDLER(WM_CREATE, OnCreate) REFLECTED_NOTIFY_CODE_HANDLER(TVN_DELETEITEM, OnDeleteItem) CHAIN_MSG_MAP_ALT( CCustomDraw< T >, 1 ) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { LRESULT lRes = DefWindowProc(uMsg, wParam, lParam); _Init(); return lRes; } LRESULT OnDeleteItem(int /*idCtrl*/, LPNMHDR pnmh, BOOL& bHandled) { LPNMTREEVIEW pnmtv = (LPNMTREEVIEW) pnmh; m_mapColors.Remove( pnmtv->itemOld.hItem ); bHandled = FALSE; return 0; } // Custom painting DWORD OnPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/) { return CDRF_NOTIFYITEMDRAW; // We need per-item notifications } DWORD OnItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW lpNMCustomDraw) { LPNMTVCUSTOMDRAW lpNMTVCD = (LPNMTVCUSTOMDRAW) lpNMCustomDraw; HTREEITEM hItem = (HTREEITEM) lpNMTVCD->nmcd.dwItemSpec; int iIndex = m_mapColors.FindKey( hItem ); if( iIndex == -1 ) return CDRF_DODEFAULT; // The TreeView allows us to change colours for // the selected item also (text only). const TVCOLOR& col = m_mapColors.GetValueAt(iIndex); if( lpNMTVCD->nmcd.uItemState & CDIS_SELECTED ) { if( col.clrSelText != CLR_INVALID ) lpNMTVCD->clrText = col.clrSelText; // Selection background colour cannot be set with current // versions of the TreeView control. if( col.clrSelBackground != CLR_INVALID ) lpNMTVCD->clrTextBk = col.clrSelBackground; } else { if( col.clrText != CLR_INVALID ) lpNMTVCD->clrText = col.clrText; if( col.clrBackground != CLR_INVALID ) lpNMTVCD->clrTextBk = col.clrBackground; } if( col.hFont != NULL ) ::SelectObject(lpNMTVCD->nmcd.hdc, col.hFont); return CDRF_NEWFONT; // We changed the colors/font } }; class CColoredTreeViewCtrl : public CColoredTreeViewImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ColoredTreeView"), GetWndClassName()) }; #endif // !defined(AFX_COLOREDCONTROLS_H__20020226_0D16_1B29_088A_0080AD509054__INCLUDED_) ================================================ FILE: vcproject/Core.h ================================================ #pragma once #include "..\WechatExporter\core\FileSystem.h" #include "..\WechatExporter\core\Logger.h" #include "..\WechatExporter\core\PdfConverter.h" #include "..\WechatExporter\core\ExportOption.h" #include "..\WechatExporter\core\Exporter.h" #include "..\WechatExporter\core\Updater.h" #include "..\WechatExporter\core\IDeviceBackup.h" #include "..\WechatExporter\core\WechatSource.h" ================================================ FILE: vcproject/ExportNotifierImpl.h ================================================ #pragma once #include "stdafx.h" #include "Core.h" class ExportNotifierImpl : public ExportNotifier { protected: HWND m_hWnd; public: static const UINT WM_START = WM_USER + 10; static const UINT WM_COMPLETE = WM_USER + 11; static const UINT WM_PROGRESS = WM_USER + 12; static const UINT WM_USR_SESS_START = WM_USER + 13; static const UINT WM_USR_SESS_COMPLETE = WM_USER + 14; static const UINT WM_SESSION_START = WM_USER + 16; static const UINT WM_SESSION_COMPLETE = WM_USER + 17; static const UINT WM_SESSION_PROGRESS = WM_USER + 18; static const UINT WM_TASKS_START = WM_USER + 19; static const UINT WM_TASKS_COMPLETE = WM_USER + 20; static const UINT WM_TASKS_PROGRESS = WM_USER + 21; static const UINT WM_EN_END = WM_TASKS_PROGRESS; public: ExportNotifierImpl(HWND hWnd) : m_hWnd(hWnd) { } ~ExportNotifierImpl() { m_hWnd = NULL; } void onStart() const { ::PostMessage(m_hWnd, WM_START, (WPARAM)0, (LPARAM)1/*cancellable*/); } void onProgress(uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const { ::PostMessage(m_hWnd, WM_PROGRESS, (WPARAM)numberOfMessages, (LPARAM)numberOfTotalMessages); } void onComplete(bool cancelled) const { ::PostMessage(m_hWnd, WM_COMPLETE, (WPARAM)0, (LPARAM)(cancelled ? 1 : 0)); } void onUserSessionStart(const std::string& usrName, uint32_t numberOfSessions) const { } void onUserSessionComplete(const std::string& usrName) const { } void onSessionStart(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfTotalMessages) const { ::PostMessage(m_hWnd, WM_SESSION_START, reinterpret_cast(sessionData), (LPARAM)numberOfTotalMessages); } void onSessionProgress(const std::string& sessionUsrName, void * sessionData, uint32_t numberOfMessages, uint32_t numberOfTotalMessages) const { ::PostMessage(m_hWnd, WM_SESSION_PROGRESS, reinterpret_cast(sessionData), (LPARAM)numberOfMessages); } void onSessionComplete(const std::string& sessionUsrName, void * sessionData, bool cancelled) const { ::PostMessage(m_hWnd, WM_SESSION_COMPLETE, reinterpret_cast(sessionData), (LPARAM)(cancelled ? 1 : 0)); } void onTasksStart(const std::string& usrName, uint32_t numberOfTotalTasks) const { ::PostMessage(m_hWnd, WM_TASKS_START, (WPARAM)numberOfTotalTasks, 0); } void onTasksProgress(const std::string& usrName, uint32_t numberOfCompletedTasks, uint32_t numberOfTotalMessages) const { ::PostMessage(m_hWnd, WM_TASKS_PROGRESS, numberOfTotalMessages, (LPARAM)numberOfCompletedTasks); } void onTasksComplete(const std::string& usrName, bool cancelled) const { ::PostMessage(m_hWnd, WM_TASKS_COMPLETE, 0, (LPARAM)(cancelled ? 1 : 0)); } }; ================================================ FILE: vcproject/ITunesDetector.h ================================================ #pragma once #include "stdafx.h" #include "VersionDetector.h" class ITunesDetector { public: ITunesDetector() : m_installed(false) { detectStandaloneITunes(); // detectAppWithWmi(); if (!m_installed) { // Check if there is iTunes installed in MS Store // detectITunesApp(); } if (!m_installed) { // Check if there is iTunes installed in MS Store from registry detectITunesAppFromRegistry2(); } if (!m_installed) { // Check if there is iTunes installed in MS Store from registry CString rootKeyPath = TEXT("Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppModel\\Repository\\Packages"); detectITunesAppFromRegistry(HKEY_CURRENT_USER, rootKeyPath); if (!m_installed) { rootKeyPath = TEXT("SOFTWARE\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppModel\\PackageRepository\\Packages"); detectITunesAppFromRegistry(HKEY_LOCAL_MACHINE, rootKeyPath); } } } ~ITunesDetector() { } bool isInstalled() const { return m_installed; } CString getVersion() const { return m_version; } CString getInstallPath() const { return m_path; } protected: void detectStandaloneITunes() { TCHAR value[MAX_PATH] = { 0 }; CRegKey rkITunes; if (rkITunes.Open(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\Apple Computer, Inc.\\iTunes"), KEY_READ) == ERROR_SUCCESS) { ULONG chars = MAX_PATH; if (rkITunes.QueryStringValue(TEXT("Version"), value, &chars) == ERROR_SUCCESS) { m_installed = true; m_version = value; } if (rkITunes.QueryStringValue(TEXT("InstallDir"), value, &chars) == ERROR_SUCCESS) { m_path = value; } rkITunes.Close(); } } void detectITunesAppFromRegistry(HKEY rootKey, const CString& rootKeyPath) { // Check if there is iTunes installed in MS Store CRegKey rkITunes; LRESULT res = rkITunes.Open(rootKey, rootKeyPath, KEY_READ); if (res == ERROR_SUCCESS) { DWORD dwIndex = 0; ULONG chars = MAX_PATH; TCHAR subkeyName[MAX_PATH] = { 0 }; while ((res = rkITunes.EnumKey(dwIndex, subkeyName, &chars)) != ERROR_NO_MORE_ITEMS) { if (res == ERROR_SUCCESS) { CRegKey rk; res = rk.Open(rkITunes.m_hKey, subkeyName, KEY_READ); if (res == ERROR_SUCCESS) { chars = MAX_PATH; TCHAR value[MAX_PATH] = { 0 }; if (rk.QueryStringValue(TEXT("PackageID"), value, &chars) == ERROR_SUCCESS) { if (_tcsstr(value, TEXT("AppleInc.iTunes_")) != NULL) { #ifndef NDEBUG MessageBox(NULL, value, TEXT("Debug"), MB_OK); #endif m_installed = true; m_version = parseVersionFromPackageId(value); chars = MAX_PATH; if (rk.QueryStringValue(TEXT("PackageRootFolder"), value, &chars) == ERROR_SUCCESS) { m_path = value; } } } rk.Close(); if (m_installed) { break; } } } chars = MAX_PATH; dwIndex++; } rkITunes.Close(); #ifndef NDEBUG #endif } #ifndef NDEBUG else { MessageBox(NULL, TEXT("Failed to Open Registry Key: Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppModel\\Repository\\Packages"), TEXT("Debug"), MB_OK); } #endif } void detectITunesAppFromRegistry2() { // Check if there is iTunes installed in MS Store CRegKey rkITunes; CString rootKeyPath = TEXT("Software\\Classes"); LRESULT res = rkITunes.Open(HKEY_CURRENT_USER, rootKeyPath, KEY_READ); if (res == ERROR_SUCCESS) { DWORD dwIndex = 0; ULONG chars = MAX_PATH; TCHAR subkeyName[MAX_PATH] = { 0 }; while ((res = rkITunes.EnumKey(dwIndex, subkeyName, &chars)) != ERROR_NO_MORE_ITEMS) { if (res == ERROR_SUCCESS) { if (regValueEqualsTo(rkITunes.m_hKey, subkeyName, NULL, TEXT("iTunes"))) { TCHAR shellPath[MAX_PATH] = { 0 }; _tcscpy(shellPath, subkeyName); _tcscat(shellPath, TEXT("\\")); _tcscat(shellPath, TEXT("Shell\\open")); if (regValueEqualsTo(rkITunes.m_hKey, shellPath, TEXT("AppUserModelID"), TEXT("AppleInc.iTunes_nzyj5cx40ttqa!iTunes"))) { chars = MAX_PATH; TCHAR value[MAX_PATH] = { 0 }; if (queryRegValue(rkITunes.m_hKey, shellPath, TEXT("PackageId"), value, chars)) { m_installed = true; m_version = parseVersionFromPackageId(value); m_path = TEXT(""); } } } } if (m_installed) { break; } chars = MAX_PATH; dwIndex++; } rkITunes.Close(); #ifndef NDEBUG #endif } #ifndef NDEBUG else { MessageBox(NULL, TEXT("Failed to Open Registry Key: Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppModel\\Repository\\Packages"), TEXT("Debug"), MB_OK); } #endif } bool regValueEqualsTo(HKEY rootKey, const CString& keyPath, LPCTSTR szValueName, LPCTSTR szExpectedValue) { bool result = false; ULONG chars = MAX_PATH; TCHAR value[MAX_PATH] = { 0 }; if (queryRegValue(rootKey, keyPath, szValueName, value, chars)) { if (_tcsstr(value, szExpectedValue) != NULL) { result = true; } } return result; } bool queryRegValue(HKEY rootKey, const CString& keyPath, LPCTSTR szValueName, LPTSTR szValue, ULONG nChars) { bool result = false; CRegKey rk; LRESULT res = rk.Open(rootKey, keyPath, KEY_READ); if (res == ERROR_SUCCESS) { if (rk.QueryStringValue(szValueName, szValue, &nChars) == ERROR_SUCCESS) { result = true; } rk.Close(); } return result; } void detectITunesApp() { // Check if there is iTunes installed in MS Store HRESULT result = S_OK; TCHAR szPath[MAX_PATH] = { 0 }; result = SHGetFolderPath(NULL, CSIDL_PROGRAM_FILES, NULL, SHGFP_TYPE_CURRENT, szPath); if (FAILED(result)) { return; } _tcscat(szPath, TEXT("\\WindowsApps\\")); if (!PathFileExists(szPath)) { return; } WIN32_FIND_DATA FindFileData; HANDLE hFind = INVALID_HANDLE_VALUE; TCHAR szPath2[MAX_PATH] = { 0 }; _tcscpy(szPath2, szPath); _tcscat(szPath2, TEXT("*.*")); hFind = FindFirstFile(szPath2, &FindFileData); if (hFind == INVALID_HANDLE_VALUE) { return; } do { if (_tcscmp(FindFileData.cFileName, TEXT(".")) == 0 || _tcscmp(FindFileData.cFileName, TEXT("..")) == 0) { continue; } if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { continue; } // AppleInc.iTunes_12110.26.53016.0_x64__nzyj5cx40ttqa if (_tcsncmp(FindFileData.cFileName, TEXT("AppleInc.iTunes_"), 16) == 0) { TCHAR szITunes[MAX_PATH] = { 0 }; _tcscpy(szITunes, szPath); _tcscat(szITunes, FindFileData.cFileName); TCHAR szITunesExe[MAX_PATH] = { 0 }; _tcscpy(szITunesExe, szITunes); _tcscat(szITunes, TEXT("\\iTunes.exe")); if (!PathFileExists(szITunes)) { continue; } m_path = szITunes; VersionDetector versionDetector; m_version = versionDetector.GetProductVersion(szITunesExe); if (m_version.IsEmpty()) { m_version = parseVersionFromPackageId(FindFileData.cFileName); } m_installed = true; break; } // CW2A pszU8(CT2W(FindFileData.cFileName), CP_UTF8); // subDirectories.push_back((LPCSTR)pszU8); } while (::FindNextFile(hFind, &FindFileData)); FindClose(hFind); } CString parseVersionFromPackageId(const CString packageId) { CString version = packageId; // AppleInc.iTunes_12110.26.53016.0_x64__nzyj5cx40ttqa int pos = version.Find('_'); if (pos != -1) { version = version.Mid(pos + 1); pos = version.Find('_'); if (pos != -1) { version = version.Left(pos); return version; } } return TEXT(""); } private: bool m_installed; CString m_path; CString m_version; }; ================================================ FILE: vcproject/LogListBox.h ================================================ #if !defined(AFX_LOGLISTBOX_H_INCLUDED_) #define AFX_LOGLISTBOX_H_INCLUDED_ #include #include #pragma once #ifndef __cplusplus #error WTL requires C++ compilation (use a .cpp suffix) #endif #ifndef __ATLAPP_H__ #error LogListBox.h requires atlapp.h to be included first #endif #ifndef __ATLCTRLS_H__ #error LogListBox.h requires atlctrls.h to be included first #endif template class ATL_NO_VTABLE CLogListBoxImpl : public ATL::CWindowImpl< T, TBase, TWinTraits > { public: // DECLARE_WND_SUPERCLASS(_T("WTL_LogListBox"), GetWndClassName()) DECLARE_WND_SUPERCLASS2(NULL, T, TBase::GetWndClassName()) // DECLARE_WND_SUPERCLASS2(NULL, CLogListBox, CListBox::GetWndClassName()) // DECLARE_WND_SUPERCLASS(_T("WTL_LogListBox"), GetWndClassName()) // Message map BEGIN_MSG_MAP(CLogListBoxImpl) MESSAGE_HANDLER(WM_KEYDOWN, OnKeyDown) END_MSG_MAP() // Message handlers LRESULT OnKeyDown(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { if ((::GetKeyState(VK_CONTROL) & 0x8000) != 0) { if (wParam == 'A' || wParam == 'a') { SetSel(-1); } else if (wParam == 'C' || wParam == 'c') { int selCount = GetSelCount(); if (selCount > 0) { std::vector rgIndex(selCount, 0); GetSelItems(selCount, &rgIndex[0]); CString contents; CString line; for (int idx = 0; idx < selCount; ++idx) { GetText(rgIndex[idx], line); contents.Append(line); contents.Append(TEXT("\r\n")); } int cch = contents.GetLength(); if (cch > 0 && ::OpenClipboard(NULL)) { ::EmptyClipboard(); HGLOBAL hglbCopy = ::GlobalAlloc(GMEM_MOVEABLE, (cch + 1) * sizeof(TCHAR)); if (NULL != hglbCopy) { // Lock the handle and copy the text to the buffer. LPTSTR lptstrCopy = (LPTSTR)::GlobalLock(hglbCopy); LPTSTR lptstrBuffer = contents.LockBuffer(); memcpy(lptstrCopy, lptstrBuffer, cch * sizeof(TCHAR)); contents.UnlockBuffer(); lptstrCopy[cch] = (TCHAR)0; // null character ::GlobalUnlock(hglbCopy); // Place the handle on the clipboard. #ifdef _UNICODE ::SetClipboardData(CF_UNICODETEXT, hglbCopy); #else ::SetClipboardData(CF_TEXT, hglbCopy); #endif } ::CloseClipboard(); if (NULL != hglbCopy) ::GlobalFree(hglbCopy); } } } } return 0; } }; class CLogListBox : public CLogListBoxImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_LogListBox"), GetWndClassName()) }; #endif // !defined(AFX_LOGLISTBOX_H_INCLUDED_) ================================================ FILE: vcproject/LoggerImpl.h ================================================ #pragma once #include "stdafx.h" #include "Core.h" #include #include class LoggerImpl : public Logger { protected: #if !defined(NDEBUG) || defined(DBG_PERF) CRITICAL_SECTION m_cs; TCHAR m_szLogFile[MAX_PATH]; #endif HWND m_hWndLog; public: LoggerImpl(HWND hWndLog) : m_hWndLog(hWndLog) { #if !defined(NDEBUG) || defined(DBG_PERF) InitializeCriticalSection(&m_cs); m_szLogFile[0] = 0; #endif } ~LoggerImpl() { m_hWndLog = NULL; #if !defined(NDEBUG) || defined(DBG_PERF) DeleteCriticalSection(&m_cs); #endif } void setLogPath(LPCTSTR lpszLogPath) { #if !defined(NDEBUG) || defined(DBG_PERF) EnterCriticalSection(&m_cs); _tcscpy(m_szLogFile, lpszLogPath); PathAppend(m_szLogFile, TEXT("log.txt")); HANDLE hFile = CreateFile(m_szLogFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE != hFile) { CloseHandle(hFile); } LeaveCriticalSection(&m_cs); #endif } void outputLog(LPCTSTR pszLog) { ::SendMessage(m_hWndLog, LB_ADDSTRING, 0, (LPARAM)pszLog); LRESULT count = ::SendMessage(m_hWndLog, LB_GETCOUNT, 0, 0L); ::SendMessage(m_hWndLog, LB_SETTOPINDEX, count - 1, 0L); } void write(const std::string& log) { #if !defined(NDEBUG) || defined(DBG_PERF) std::string timeString = getTimestampString(false, true) + ": "; #else std::string timeString = getTimestampString() + ": "; #endif CA2T szTime(timeString.c_str()); #if !defined(NDEBUG) || defined(DBG_PERF) EnterCriticalSection(&m_cs); if (_tcslen(m_szLogFile) > 0) { HANDLE hFile = ::CreateFile(m_szLogFile, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE != hFile) { ::SetFilePointer(hFile, 0, 0, FILE_END); DWORD dwBytesToWrite = static_cast(log.size()); DWORD dwBytesWritten = 0; BOOL bErrorFlag = WriteFile(hFile, timeString.c_str(), timeString.size(), &dwBytesWritten, NULL); bErrorFlag = WriteFile(hFile, log.c_str(), dwBytesToWrite, &dwBytesWritten, NULL); if (bErrorFlag) { WriteFile(hFile, "\r\n", 2, &dwBytesWritten, NULL); } CloseHandle(hFile); } } LeaveCriticalSection(&m_cs); #endif CW2T pszT(CA2W(log.c_str(), CP_UTF8)); std::vector szLog; szLog.resize(_tcslen(pszT) + _tcslen(szTime) + 1, 0); _tcscpy(&szLog[0], (LPCTSTR)szTime); _tcscat(&szLog[0], (LPCTSTR)pszT); outputLog(&szLog[0]); } void debug(const std::string& log) { // #if !defined(NDEBUG) || defined(DBG_PERF) write("[DBG] " + log); // #endif } }; ================================================ FILE: vcproject/MainFrm.h ================================================ // MainFrm.h : interface of the CMainFrame class // ///////////////////////////////////////////////////////////////////////////// #pragma once class CMainFrame : public CFrameWindowImpl, public CUpdateUI, public CMessageFilter, public CIdleHandler { public: DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME) BOOL m_pdfSupported; CView m_view; virtual BOOL PreTranslateMessage(MSG* pMsg) { if(CFrameWindowImpl::PreTranslateMessage(pMsg)) return TRUE; return m_view.PreTranslateMessage(pMsg); } virtual BOOL OnIdle() { return FALSE; } BEGIN_UPDATE_UI_MAP(CMainFrame) END_UPDATE_UI_MAP() BEGIN_MSG_MAP(CMainFrame) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) MESSAGE_HANDLER(WM_INITMENU, OnInitMenu) COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit) COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout) COMMAND_ID_HANDLER(ID_HELP_HOMEPAGE, OnHelpHomePage) COMMAND_ID_HANDLER(ID_FILE_CHK_UPDATE, OnCheckUpdate) COMMAND_ID_HANDLER(ID_FILE_EXP_ITUNES, OnExportITunes) COMMAND_ID_HANDLER(ID_FILE_DBG_LOGS, OnOutputDbgLogs) COMMAND_ID_HANDLER(ID_FILE_OPEN_FOLDER, OnOpenFolder) COMMAND_RANGE_HANDLER(ID_FORMAT_HTML, ID_FORMAT_PDF, OnOutputFormat) COMMAND_ID_HANDLER(ID_OPT_DESC_ORDER, OnDescOrder) COMMAND_ID_HANDLER(ID_OPT_DL_EMOJI, OnDownloadingEmoji) COMMAND_ID_HANDLER(ID_OPT_LM_ONSCROLL, OnAsyncLoading) COMMAND_ID_HANDLER(ID_OPT_NORMALPAGINATION, OnAsyncLoading) COMMAND_ID_HANDLER(ID_OPT_PAGINATION_YEAR, OnAsyncLoading) COMMAND_ID_HANDLER(ID_OPT_PAGINATION_MONTH, OnAsyncLoading) COMMAND_ID_HANDLER(ID_OPT_FILTER, OnFilter) COMMAND_ID_HANDLER(ID_OPT_INCREMENTALEXP, OnIncrementalExporting) COMMAND_ID_HANDLER(ID_OPT_SUBSCRIPTIONS, OnIncludeSubscriptions) CHAIN_MSG_MAP(CUpdateUI) CHAIN_MSG_MAP(CFrameWindowImpl) END_MSG_MAP() // Handler prototypes (uncomment arguments if needed): // LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) // LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) // LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/) LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { m_pdfSupported = AppConfiguration::IsPdfSupported(); CenterWindow(m_hWnd); m_hWndClient = m_view.Create(m_hWnd); // register object for message filtering and idle updates CMessageLoop* pLoop = _Module.GetMessageLoop(); ATLASSERT(pLoop != NULL); pLoop->AddMessageFilter(this); pLoop->AddIdleHandler(this); AppConfiguration::upgrade(); CMenuHandle menu = GetMenu(); CMenuHandle subMenuFile = menu.GetSubMenu(0); subMenuFile.CheckMenuItem(ID_FILE_CHK_UPDATE, MF_BYCOMMAND | (AppConfiguration::GetCheckingUpdateDisabled() ? MF_UNCHECKED : MF_CHECKED)); subMenuFile.CheckMenuItem(ID_FILE_DBG_LOGS, MF_BYCOMMAND | (AppConfiguration::OutputDebugLogs() ? MF_CHECKED : MF_UNCHECKED)); subMenuFile.CheckMenuItem(ID_FILE_OPEN_FOLDER, MF_BYCOMMAND | (AppConfiguration::GetOpenningFolderAfterExp() ? MF_CHECKED : MF_UNCHECKED)); AppConfiguration::upgrade(); CMenuHandle subMenuFormat = menu.GetSubMenu(1); UINT outputFormat = AppConfiguration::GetOutputFormat(); subMenuFormat.CheckMenuRadioItem(ID_FORMAT_HTML, ID_FORMAT_PDF, ID_FORMAT_HTML + outputFormat, MF_BYCOMMAND | MFT_RADIOCHECK); CMenuHandle subMenuOptions = menu.GetSubMenu(2); subMenuOptions.CheckMenuItem(ID_OPT_DESC_ORDER, MF_BYCOMMAND | (AppConfiguration::GetDescOrder() ? MF_CHECKED : MF_UNCHECKED)); // subMenuOptions.CheckMenuItem(ID_OPT_SAVING_IN_SESSION, MF_BYCOMMAND | (AppConfiguration::GetSavingInSession() ? MF_CHECKED : MF_UNCHECKED)); subMenuOptions.CheckMenuItem(ID_OPT_DL_EMOJI, MF_BYCOMMAND | (AppConfiguration::GetUsingRemoteEmoji() ? MF_UNCHECKED : MF_CHECKED)); subMenuOptions.CheckMenuItem(ID_OPT_FILTER, MF_BYCOMMAND | (AppConfiguration::GetSupportingFilter() ? MF_CHECKED : MF_UNCHECKED)); InitAsyncLoadingMenu(subMenuOptions, TRUE); subMenuOptions.CheckMenuItem(ID_OPT_INCREMENTALEXP, MF_BYCOMMAND | (AppConfiguration::GetIncrementalExporting() ? MF_CHECKED : MF_UNCHECKED)); subMenuOptions.CheckMenuItem(ID_OPT_SUBSCRIPTIONS, MF_BYCOMMAND | (AppConfiguration::IncludeSubscriptions() ? MF_CHECKED : MF_UNCHECKED)); subMenuOptions.RemoveMenu(ID_OPT_SUBSCRIPTIONS, MF_BYCOMMAND); return 0; } LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { // unregister message filtering and idle updates CMessageLoop* pLoop = _Module.GetMessageLoop(); ATLASSERT(pLoop != NULL); pLoop->RemoveMessageFilter(this); pLoop->RemoveIdleHandler(this); bHandled = FALSE; return 1; } LRESULT OnInitMenu(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { BOOL enabled = m_view.IsViewIdle(); CMenuHandle menu = GetMenu(); CMenuHandle subMenuFile = menu.GetSubMenu(0); subMenuFile.EnableMenuItem(ID_FILE_EXP_ITUNES, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED)); subMenuFile.EnableMenuItem(ID_FILE_DBG_LOGS, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED)); subMenuFile.EnableMenuItem(ID_FILE_OPEN_FOLDER, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED)); CMenuHandle subMenuFormat = menu.GetSubMenu(1); subMenuFormat.EnableMenuItem(ID_FORMAT_HTML, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED)); subMenuFormat.EnableMenuItem(ID_FORMAT_TEXT, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED)); subMenuFormat.EnableMenuItem(ID_FORMAT_PDF, MF_BYCOMMAND | ((enabled && m_pdfSupported) ? MF_ENABLED : MF_DISABLED)); CMenuHandle subMenuOptions = menu.GetSubMenu(2); subMenuOptions.EnableMenuItem(ID_OPT_DESC_ORDER, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED)); // subMenuOptions.EnableMenuItem(ID_OPT_SAVING_IN_SESSION, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED)); subMenuOptions.EnableMenuItem(ID_OPT_DL_EMOJI, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED)); subMenuOptions.EnableMenuItem(ID_OPT_FILTER, MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED)); InitAsyncLoadingMenu(subMenuOptions, enabled); subMenuOptions.CheckMenuItem(ID_OPT_INCREMENTALEXP, MF_BYCOMMAND | (AppConfiguration::GetIncrementalExporting() ? MF_CHECKED : MF_UNCHECKED)); subMenuOptions.CheckMenuItem(ID_OPT_SUBSCRIPTIONS, MF_BYCOMMAND | (AppConfiguration::IncludeSubscriptions() ? MF_CHECKED : MF_UNCHECKED)); return 1; } void InitAsyncLoadingMenu(CMenuHandle& subMenuOptions, BOOL isIdle) { UINT outputFormat = AppConfiguration::GetOutputFormat(); BOOL asyncLoadingEnabled = (outputFormat == AppConfiguration::OUTPUT_FORMAT_HTML); if (asyncLoadingEnabled) { asyncLoadingEnabled = isIdle; } DWORD asyncLoading = AppConfiguration::GetAsyncLoading(); UINT ids[] = { ID_OPT_LM_ONSCROLL, ID_OPT_NORMALPAGINATION, ID_OPT_PAGINATION_YEAR, ID_OPT_PAGINATION_MONTH }; UINT asyncLoadingStates[] = { ASYNC_ONSCROLL, ASYNC_PAGER_NORMAL, ASYNC_PAGER_ON_YEAR, ASYNC_PAGER_ON_MONTH }; for (int idx = 0; idx < (sizeof(ids) / sizeof(UINT)); ++idx) { subMenuOptions.EnableMenuItem(ids[idx], MF_BYCOMMAND | (asyncLoadingEnabled ? MF_ENABLED : MF_DISABLED)); subMenuOptions.CheckMenuItem(ids[idx], MF_BYCOMMAND | ((asyncLoading == asyncLoadingStates[idx]) ? MF_CHECKED : MF_UNCHECKED)); } } LRESULT OnFilter(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/) { CMenuHandle menu = GetMenu(); CMenuHandle subMenu = menu.GetSubMenu(2); UINT menuState = subMenu.GetMenuState(ID_OPT_FILTER, MF_BYCOMMAND); BOOL supportingFilter = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE; subMenu.CheckMenuItem(ID_OPT_FILTER, MF_BYCOMMAND | (supportingFilter ? MF_UNCHECKED : MF_CHECKED)); AppConfiguration::SetSupportingFilter(!supportingFilter); return 0; } LRESULT OnDescOrder(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/) { CMenuHandle menu = GetMenu(); CMenuHandle subMenu = menu.GetSubMenu(2); UINT menuState = subMenu.GetMenuState(ID_OPT_DESC_ORDER, MF_BYCOMMAND); BOOL curDescOrder = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE; subMenu.CheckMenuItem(ID_OPT_DESC_ORDER, MF_BYCOMMAND | (curDescOrder ? MF_UNCHECKED : MF_CHECKED)); AppConfiguration::SetDescOrder(!curDescOrder); return 0; } LRESULT OnDownloadingEmoji(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/) { CMenuHandle menu = GetMenu(); CMenuHandle subMenu = menu.GetSubMenu(2); UINT menuState = subMenu.GetMenuState(ID_OPT_DL_EMOJI, MF_BYCOMMAND); BOOL curState = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE; subMenu.CheckMenuItem(ID_OPT_DL_EMOJI, MF_BYCOMMAND | (curState ? MF_UNCHECKED : MF_CHECKED)); AppConfiguration::SetUsingRemoteEmoji(curState); return 0; } LRESULT OnOutputFormat(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/) { CMenuHandle menu = GetMenu(); CMenuHandle subMenuFormat = menu.GetSubMenu(1); subMenuFormat.CheckMenuRadioItem(ID_FORMAT_HTML, ID_FORMAT_PDF, wID, MF_BYCOMMAND | MFT_RADIOCHECK); UINT outputFormat = wID - ID_FORMAT_HTML; AppConfiguration::SetOutputFormat(outputFormat); // subMenuFormat.EnableMenuItem(ID_OPT_ASYNC_LOADING, MF_BYCOMMAND | ((outputFormat != AppConfiguration::OUTPUT_FORMAT_HTML) ? MF_DISABLED : MF_ENABLED)); return 0; } LRESULT OnIncrementalExporting(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CMenuHandle menu = GetMenu(); CMenuHandle subMenu = menu.GetSubMenu(2); UINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND); BOOL incrementalExporting = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE; subMenu.CheckMenuItem(wID, MF_BYCOMMAND | (incrementalExporting ? MF_UNCHECKED : MF_CHECKED)); AppConfiguration::SetIncrementalExporting(!incrementalExporting); return 0; } LRESULT OnIncludeSubscriptions(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CMenuHandle menu = GetMenu(); CMenuHandle subMenu = menu.GetSubMenu(2); UINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND); BOOL stateValue = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE; subMenu.CheckMenuItem(wID, MF_BYCOMMAND | (stateValue ? MF_UNCHECKED : MF_CHECKED)); AppConfiguration::SetIncludingSubscriptions(!stateValue); return 0; } LRESULT OnAsyncLoading(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& /*bHandled*/) { UINT ids[] = { ID_OPT_LM_ONSCROLL, ID_OPT_NORMALPAGINATION, ID_OPT_PAGINATION_YEAR, ID_OPT_PAGINATION_MONTH}; UINT asyncLoading[] = { ASYNC_ONSCROLL, ASYNC_PAGER_NORMAL, ASYNC_PAGER_ON_YEAR, ASYNC_PAGER_ON_MONTH }; CMenuHandle menu = GetMenu(); CMenuHandle subMenu = menu.GetSubMenu(2); UINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND); BOOL asyncState = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE; int selectedIdx = 0; for (int idx = 0; idx < sizeof(ids) / sizeof(UINT); ++idx) { if (ids[idx] != wID) { subMenu.CheckMenuItem(ids[idx], MF_BYCOMMAND | MF_UNCHECKED); } else { selectedIdx = idx; subMenu.CheckMenuItem(ids[idx], MF_BYCOMMAND | (asyncState ? MF_UNCHECKED : MF_CHECKED)); } } if (asyncState) { AppConfiguration::SetSyncLoading(); } else { AppConfiguration::SetAsyncLoading(asyncLoading[selectedIdx]); } return 0; } LRESULT OnCheckUpdate(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CMenuHandle menu = GetMenu(); CMenuHandle subMenu = menu.GetSubMenu(0); UINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND); BOOL stateValue = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE; subMenu.CheckMenuItem(wID, MF_BYCOMMAND | (stateValue ? MF_UNCHECKED : MF_CHECKED)); AppConfiguration::SetCheckingUpdateDisabled(stateValue); return 0; } LRESULT OnExportITunes(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { m_view.ExportITunesBackup(); return 0; } LRESULT OnOpenFolder(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CMenuHandle menu = GetMenu(); CMenuHandle subMenu = menu.GetSubMenu(0); UINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND); BOOL stateValue = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE; subMenu.CheckMenuItem(wID, MF_BYCOMMAND | (stateValue ? MF_UNCHECKED : MF_CHECKED)); AppConfiguration::SetOpenningFolderAfterExp(!stateValue); return 0; } LRESULT OnOutputDbgLogs(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CMenuHandle menu = GetMenu(); CMenuHandle subMenu = menu.GetSubMenu(0); UINT menuState = subMenu.GetMenuState(wID, MF_BYCOMMAND); BOOL stateValue = (menuState != 0xFFFFFFFF) && ((menuState & MF_CHECKED) == MF_CHECKED) ? TRUE : FALSE; subMenu.CheckMenuItem(wID, MF_BYCOMMAND | (stateValue ? MF_UNCHECKED : MF_CHECKED)); AppConfiguration::SetOutputDebugLogs(!stateValue); return 0; } LRESULT OnHelpHomePage(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { DWORD_PTR dwRet = (DWORD_PTR)::ShellExecute(0, _T("open"), TEXT("https://github.com/BlueMatthew/WechatExporter"), 0, 0, SW_SHOWNORMAL); return 0; } LRESULT OnAppAbout(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CAboutDlg dlg; dlg.DoModal(); return 0; } LRESULT OnFileExit(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { PostMessage(WM_CLOSE); return 0; } }; ================================================ FILE: vcproject/PdfConverterImpl.h ================================================ #pragma once #include "stdafx.h" #include "Core.h" #include class PdfConverterImpl : public PdfConverter { public: PdfConverterImpl(LPCTSTR outputDir) : m_pdfSupported(false) { if (isChromeInstalled(m_assemblyPath)) { m_pdfSupported = true; m_param = TEXT("--headless --disable-extensions --disable-gpu --print-to-pdf=\"%%DEST%%\" --print-to-pdf-no-header \"file://%%SRC%%\""); } else if (isEdgeInstalled(m_assemblyPath)) { m_pdfSupported = true; m_param = TEXT("--headless --disable-extensions --disable-gpu --print-to-pdf=\"%%DEST%%\" --print-to-pdf-no-header \"file://%%SRC%%\""); } if (NULL != outputDir) { initShellFile(outputDir); } } bool isPdfSupported() const { return m_pdfSupported; } ~PdfConverterImpl() { } void executeCommand() { if (!::PathFileExists(m_shellPath)) { return; } const char *pauseCmd = "pause\r\n"; appendFile(m_shellPath, reinterpret_cast(pauseCmd), strlen(pauseCmd)); ShellExecute(NULL, TEXT("open"), m_shellPath, NULL, NULL, SW_SHOW); } bool makeUserDirectory(const std::string& dirName) { CW2T pszDir(CA2W(dirName.c_str(), CP_UTF8)); TCHAR pdfOutputDir[MAX_PATH] = { 0 }; PathCombine(pdfOutputDir, m_output, TEXT("pdf")); TCHAR userOutputDir[MAX_PATH] = { 0 }; PathCombine(userOutputDir, pdfOutputDir, pszDir); CString command = TEXT("\r\nIF NOT EXIST \""); command += userOutputDir; command += TEXT("\" MKDIR \""); command += userOutputDir; command += TEXT("\"\r\n"); CW2A pszU(CT2W(command), TARGET_CODE_PAGE); appendFile(m_shellPath, reinterpret_cast((LPCSTR)pszU), strlen(pszU)); return true; } bool convert(const std::string& htmlPath, const std::string& pdfPath) { if (!m_pdfSupported) { return false; } CW2T pszHtmlPath(CA2W(htmlPath.c_str(), CP_UTF8)); TCHAR url[MAX_PATH * 4] = { 0 }; DWORD cchUrl = MAX_PATH * 4; // max posible buffer size HRESULT res = UrlCreateFromPath(pszHtmlPath, url, &cchUrl, NULL); if (FAILED(res)) { return false; } CW2T pszPdfPath(CA2W(pdfPath.c_str(), CP_UTF8)); CString command(TEXT("\"")); command += m_assemblyPath; command += TEXT("\" "); command += TEXT("--headless --disable-extensions --disable-gpu --print-to-pdf-no-header --print-to-pdf=\""); command += (LPCTSTR)pszPdfPath; command += TEXT("\" "); command += url; command += TEXT("\r\n"); CT2A content(command, TARGET_CODE_PAGE); appendFile((LPCTSTR)m_shellPath, reinterpret_cast((LPCSTR)content), strlen(content)); // appendFile((LPCTSTR)m_shellPath, reinterpret_cast((LPCTSTR)command), _tcslen(command) * sizeof(TCHAR)); return true; } bool convert2(const std::string& htmlPath, const std::string& pdfPath) { CString param = m_param; CW2T pszSrc(CA2W(htmlPath.c_str(), CP_UTF8)); CW2T pszDest(CA2W(pdfPath.c_str(), CP_UTF8)); param.Replace(TEXT("%%SRC%%"), pszSrc); param.Replace(TEXT("%%DEST%%"), pszDest); SHELLEXECUTEINFO ShExecInfo = { 0 }; ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS; ShExecInfo.hwnd = NULL; ShExecInfo.lpVerb = TEXT("open"); ShExecInfo.lpFile = (LPCTSTR)m_assemblyPath; ShExecInfo.lpParameters = (LPCTSTR)param; ShExecInfo.lpDirectory = NULL; ShExecInfo.nShow = SW_HIDE; ShExecInfo.hInstApp = NULL; ShellExecuteEx(&ShExecInfo); WaitForSingleObject(ShExecInfo.hProcess, INFINITE); CloseHandle(ShExecInfo.hProcess); // ShellExecute(NULL, TEXT("open"), m_assemblyPath, param, NULL, SW_HIDE); return true; } protected: bool isChromeInstalled(CString& assemblyPath) { return isAppInstalled(TEXT("chrome.exe"), true, assemblyPath) || isAppInstalled(TEXT("chrome.exe"), false, assemblyPath); } bool isEdgeInstalled(CString& assemblyPath) { return isAppInstalled(TEXT("msedge.exe"), true, assemblyPath) || isAppInstalled(TEXT("msedge.exe"), false, assemblyPath); } bool isAppInstalled(LPCTSTR name, bool lmOrCU, CString& assemblyPath) { BOOL installed = false; CRegKey rk; CString path = lmOrCU ? TEXT("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\") : TEXT("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\"); path += name; if (rk.Open(lmOrCU ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, (LPCTSTR)path, KEY_READ) == ERROR_SUCCESS) { ULONG chars = 0; HRESULT hr = rk.QueryStringValue(NULL, NULL, &chars); if (SUCCEEDED(hr)) { CString appPath; hr = rk.QueryStringValue(NULL, appPath.GetBufferSetLength(chars), &chars); appPath.ReleaseBuffer(); if (PathFileExists(appPath)) { assemblyPath = appPath; installed = true; } } rk.Close(); } return installed; } void initShellFile(LPCTSTR outputDir) { m_output = outputDir; TCHAR shellFile[MAX_PATH] = { 0 }; PathCombine(shellFile, outputDir, TEXT("pdf.bat")); m_shellPath = shellFile; ::DeleteFile(shellFile); CString command; if (TARGET_CODE_PAGE == CP_UTF8) { command += TEXT("CHCP 65001\r\n"); } TCHAR pdfPath[MAX_PATH] = { 0 }; PathCombine(pdfPath, outputDir, TEXT("pdf")); command += TEXT("IF NOT EXIST \""); command += pdfPath; command += TEXT("\" MKDIR \""); command += pdfPath; command += TEXT("\"\r\n"); CW2A pszU(CT2W(command), TARGET_CODE_PAGE); appendFile((LPCTSTR)m_shellPath, reinterpret_cast((LPCSTR)pszU), strlen(pszU)); } bool appendFile(LPCTSTR path, const unsigned char* data, size_t dataLength) { HANDLE hFile = ::CreateFile(path, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { return false; } ::SetFilePointer(hFile, 0, 0, FILE_END); DWORD dwBytesToWrite = static_cast(dataLength); DWORD dwBytesWritten = 0; BOOL bErrorFlag = WriteFile(hFile, data, dwBytesToWrite, &dwBytesWritten, NULL); ::CloseHandle(hFile); return (TRUE == bErrorFlag); } private: bool m_pdfSupported; CString m_assemblyPath; CString m_output; CString m_shellPath; CString m_param; // const UINT TARGET_CODE_PAGE = CP_ACP; const UINT TARGET_CODE_PAGE = CP_UTF8; }; ================================================ FILE: vcproject/ProgressListViewCtrl.h ================================================ #ifndef _PROGRESSLISTVIEWCTRL_H #define _PROGRESSLISTVIEWCTRL_H class CProgressListViewCtrl : public CWindowImpl, public CCustomDraw { public: BEGIN_MSG_MAP(CProgressListViewCtrl) MESSAGE_HANDLER(WM_CREATE, OnCreate) // REFLECTED_NOTIFY_CODE_HANDLER(LVN_DELETEITEM, OnDeleteItem) CHAIN_MSG_MAP_ALT(CCustomDraw, 1) // Because WTL 3.1 does not support subitem custom drawing. // Should be forward-compatible with new versions... // REFLECTED_NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnNotifyCustomDraw) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() CProgressListViewCtrl() : CWindowImpl() { m_itemForProgressBar = -1; m_subItemForProgressBar = -1; m_progressUpper = 0; m_progressPos = 0; } BOOL SubclassWindow(HWND hWnd) { return CWindowImpl::SubclassWindow(hWnd) ? _Init() : FALSE; } LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { bHandled = FALSE; return _Init() ? 0 : -1; } LRESULT OnNotifyCustomDraw(int idCtrl, LPNMHDR pnmh, BOOL& bHandled) { SetMsgHandled(FALSE); LPNMCUSTOMDRAW lpNMCustomDraw = (LPNMCUSTOMDRAW)pnmh; DWORD dwRet = CDRF_DODEFAULT; switch (lpNMCustomDraw->dwDrawStage) { case CDDS_ITEMPREPAINT | CDDS_SUBITEM: dwRet = OnSubItemPrePaint(idCtrl, lpNMCustomDraw); SetMsgHandled(TRUE); return dwRet; } bHandled = FALSE; return dwRet; } DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW /*lpNMCustomDraw*/) { return CDRF_NOTIFYITEMDRAW; } DWORD OnItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/) { return CDRF_NOTIFYSUBITEMDRAW; // We need per-subitem notifications } DWORD OnSubItemPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW lpNMCustomDraw) { NMLVCUSTOMDRAW* pLVCD = reinterpret_cast(lpNMCustomDraw); if (pLVCD->nmcd.dwItemSpec == m_itemForProgressBar && pLVCD->iSubItem == m_subItemForProgressBar) { CDCHandle dcPaint(pLVCD->nmcd.hdc); int nContextState = dcPaint.SaveDC(); int State = ListView_GetItemState(m_hWnd, pLVCD->nmcd.dwItemSpec, LVIS_CUT | LVIS_SELECTED | LVIS_FOCUSED); CString text; GetItemText((int)pLVCD->nmcd.dwItemSpec, pLVCD->iSubItem, text); int colorIndex = (State & LVIS_SELECTED) ? 1 : 0; //3D border spacer (not exactly what we need either) int nSpacerW = GetSystemMetrics(SM_CXEDGE); //Get horizontal DPI setting int dpiX = dcPaint.GetDeviceCaps(LOGPIXELSX); int nSpacePx = (int)(nSpacerW * 2.5 * dpiX / 96.0); RECT rc = pLVCD->nmcd.rc; // ATLTRACE(TEXT("SubItem=%d Bounds: left=%d right=%d\r\n"), pLVCD->iSubItem, rc.left, rc.right); rc.left += 2; rc.right -= 2; rc.top += 1; rc.bottom -= 2; RECT rcText = rc; rcText.left += nSpacePx; rcText.right -= nSpacePx; dcPaint.FillSolidRect(&pLVCD->nmcd.rc, m_clrBkgnd[colorIndex]); dcPaint.SetTextColor(m_clrText[colorIndex]); dcPaint.DrawText(text, -1, &rcText, DT_VCENTER | DT_SINGLELINE); rc.right = rc.left + (LONG)(m_percent * (rc.right - rc.left)); if (rcText.right > rc.right) { rcText.right = rc.right; } dcPaint.FillSolidRect(&rc, m_clrPercent[colorIndex]); dcPaint.SetTextColor(m_clrPercentText[colorIndex]); dcPaint.DrawText(text, -1, &rcText, DT_VCENTER | DT_SINGLELINE); dcPaint.RestoreDC(nContextState); return CDRF_SKIPDEFAULT; } else if (pLVCD->nmcd.dwItemSpec != m_itemForProgressBar && pLVCD->iSubItem == m_subItemForProgressBar) { pLVCD->clrText = RGB(0x0, 0xFF, 0x0); } return CDRF_DODEFAULT; } void SetProgressBarInfo(int item, int subItem, int progressUpper, int progressPos = 0) { int nOrgItem = m_itemForProgressBar; int nOrgSubItem = m_subItemForProgressBar; m_itemForProgressBar = item; m_subItemForProgressBar = subItem; m_progressUpper = progressUpper; if (nOrgItem != -1 && nOrgSubItem != -1) { RECT rc; GetSubItemRect(nOrgItem, nOrgSubItem, LVIR_BOUNDS, &rc); InvalidateRect(&rc); } SetProgressPos(progressPos); } void SetProgressPos(int progressPos) { m_progressPos = progressPos; m_percent = (m_progressUpper > 0) ? (((float)progressPos) / ((float)m_progressUpper)) : 0.0f; RECT rc; GetSubItemRect(m_itemForProgressBar, m_subItemForProgressBar, LVIR_BOUNDS, &rc); InvalidateRect(&rc); } void ClearProgressBar() { int nOrgItem = m_itemForProgressBar; int nOrgSubItem = m_subItemForProgressBar; m_itemForProgressBar = -1; // m_subItemForProgressBar = -1; m_progressUpper = 0; if (nOrgItem != -1 && nOrgSubItem != -1) { RECT rc; GetSubItemRect(nOrgItem, nOrgSubItem, LVIR_BOUNDS, &rc); InvalidateRect(&rc); } } protected: BOOL _Init() { m_clrText[0] = ::GetSysColor(COLOR_WINDOWTEXT); m_clrBkgnd[0] = ::GetSysColor(COLOR_WINDOW); m_clrPercentText[0] = ::GetSysColor(COLOR_HIGHLIGHTTEXT); m_clrPercent[0] = ::GetSysColor(COLOR_HIGHLIGHT); m_clrText[1] = ::GetSysColor(COLOR_HIGHLIGHTTEXT); m_clrBkgnd[1] = ::GetSysColor(COLOR_HIGHLIGHT); m_clrPercentText[1] = ::GetSysColor(COLOR_HOTLIGHT); m_clrPercent[1] = ::GetSysColor(COLOR_WINDOW); return TRUE; } int m_itemForProgressBar; int m_subItemForProgressBar; int m_progressUpper; int m_progressPos; float m_percent; COLORREF m_clrText[2]; COLORREF m_clrBkgnd[2]; COLORREF m_clrPercent[2]; COLORREF m_clrPercentText[2]; }; #endif // _PROGRESSLISTVIEWCTRL_H ================================================ FILE: vcproject/TextProgressBarCtrl.h ================================================ #pragma once #include #include #include template class ATL_NO_VTABLE CTextProgressBarCtrlT : public CWindowImpl, public CThemeImpl { public: CTextProgressBarCtrlT() { SetThemeClassList(L"PROGRESS"); } BOOL SubclassWindow(HWND hWnd) { BOOL result = CWindowImpl::SubclassWindow(hWnd); if (result) { if (GetThemeClassList() != NULL) OpenThemeData(); } return result; } BEGIN_MSG_MAP(CTextProgressBarCtrl) CHAIN_MSG_MAP(CThemeImpl) MESSAGE_HANDLER(WM_PAINT, OnPaint) MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() // message handlers LRESULT OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { RECT rc; GetClientRect(&rc); CPaintDC dc(m_hWnd); if (rc.right > rc.left && rc.bottom > rc.top) { CMemoryDC dcMem(dc.m_hDC, rc); if (IsAppThemed()) DrawThemedProgressBar(dcMem, rc); else DrawClassicProgressBar(dcMem, rc); DrawText(dcMem, rc); } bHandled = TRUE; return 1; } LRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { return 1; // we painted the background } protected: void CalcFillRect(CRect& rc) { int nLower = 0, nUpper = 0; GetRange(nLower, nUpper); if (nLower >= nUpper) nLower = nUpper - 1; int nPos = GetPos(); if (nPos < nLower) nPos = nLower; if (nPos > nUpper) nPos = nUpper; DWORD dwStyle = GetStyle(); BOOL bVertical = (dwStyle & PBS_VERTICAL) == PBS_VERTICAL; if (bVertical) rc.top = rc.bottom - (rc.bottom - rc.top) * (nPos - nLower) / (nUpper - nLower); else rc.right = rc.left + (rc.right - rc.left) * (nPos - nLower) / (nUpper - nLower); } void DrawThemedProgressBar(CMemoryDC& dc, const CRect& rc) { DrawThemeBackground(dc, PP_BAR, 0, &rc, NULL); CRect rcFill = rc; CalcFillRect(rcFill); DrawThemeBackground(dc, PP_FILL, PBFS_NORMAL, &rcFill, NULL); } void DrawClassicProgressBar(CMemoryDC& dc, const CRect& rc) { CRect rcEdge = rc; dc.DrawEdge(&rcEdge, EDGE_ETCHED, BF_RECT); CRect rc2 = rc; rc2.DeflateRect(1, 1, 1, 1); dc.FillSolidRect(&rc2, ::GetSysColor(COLOR_BTNFACE)); CRect rcFill = rc2; if (rcFill.right > rcFill.left && rcFill.bottom > rcFill.top) { CalcFillRect(rcFill); dc.FillSolidRect(&rcFill, ::GetSysColor(COLOR_HIGHLIGHT)); } } // draw text on the bar void DrawText(CMemoryDC& dc, const CRect& rc) { CString text; if (GetWindowText(text) > 0) { int oldBkMode = dc.SetBkMode(TRANSPARENT); HFONT hFont = GetFont(); if (NULL == hFont) { hFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT); } HFONT oldFont = dc.SelectFont(hFont); CRect rcText = rc; dc.DrawText((LPCTSTR)text, -1, &rcText, DT_CENTER | DT_VCENTER | DT_SINGLELINE); dc.SelectFont(oldFont); dc.SetBkMode(oldBkMode); } } }; class CTextProgressBarCtrl : public CTextProgressBarCtrlT { public: DECLARE_WND_SUPERCLASS(_T("WTL_progressbar"), GetWndClassName()) }; ================================================ FILE: vcproject/ToolTipButton.h ================================================ #pragma once template class ATL_NO_VTABLE CToolTipButtonImpl : public ATL::CWindowImpl< T, TBase, TWinTraits > { public: DECLARE_WND_SUPERCLASS2(NULL, T, TBase::GetWndClassName()) enum { _nImageNormal = 0, _nImagePushed, _nImageFocusOrHover, _nImageDisabled, _nImageCount = 4, }; enum { ID_TIMER_FIRST = 1000, ID_TIMER_REPEAT = 1001 }; CToolTipCtrl m_tip; LPTSTR m_lpstrToolTipText; // Constructor/Destructor CToolTipButtonImpl() : m_lpstrToolTipText(NULL) { } ~CToolTipButtonImpl() { delete[] m_lpstrToolTipText; } // overridden to provide proper initialization BOOL SubclassWindow(HWND hWnd) { BOOL bRet = ATL::CWindowImpl< T, TBase, TWinTraits >::SubclassWindow(hWnd); if (bRet != FALSE) { T* pT = static_cast(this); pT->Init(); } return bRet; } // Attributes int GetToolTipTextLength() const { return (m_lpstrToolTipText == NULL) ? -1 : lstrlen(m_lpstrToolTipText); } bool GetToolTipText(LPTSTR lpstrText, int nLength) const { ATLASSERT(lpstrText != NULL); if (m_lpstrToolTipText == NULL) return false; errno_t nRet = ATL::Checked::tcsncpy_s(lpstrText, nLength, m_lpstrToolTipText, _TRUNCATE); return ((nRet == 0) || (nRet == STRUNCATE)); } bool SetToolTipText(LPCTSTR lpstrText) { if (m_lpstrToolTipText != NULL) { delete[] m_lpstrToolTipText; m_lpstrToolTipText = NULL; } if (lpstrText == NULL) { if (m_tip.IsWindow()) m_tip.Activate(FALSE); return true; } int cchLen = lstrlen(lpstrText) + 1; ATLTRY(m_lpstrToolTipText = new TCHAR[cchLen]); if (m_lpstrToolTipText == NULL) return false; ATL::Checked::tcscpy_s(m_lpstrToolTipText, cchLen, lpstrText); if (m_tip.IsWindow()) { m_tip.Activate(TRUE); m_tip.AddTool(this->m_hWnd, m_lpstrToolTipText); } return true; } // Overrideables // Message map and handlers BEGIN_MSG_MAP(CBitmapButtonImpl) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) MESSAGE_RANGE_HANDLER(WM_MOUSEFIRST, WM_MOUSELAST, OnMouseMessage) // MESSAGE_HANDLER(WM_MOUSEMOVE, OnMouseMove) // MESSAGE_HANDLER(WM_MOUSELEAVE, OnMouseLeave) END_MSG_MAP() LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { T* pT = static_cast(this); pT->Init(); bHandled = FALSE; return 1; } LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { if (m_tip.IsWindow()) { m_tip.DestroyWindow(); m_tip.m_hWnd = NULL; } bHandled = FALSE; return 1; } LRESULT OnMouseMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { MSG msg = { this->m_hWnd, uMsg, wParam, lParam }; if (m_tip.IsWindow()) m_tip.RelayEvent(&msg); bHandled = FALSE; return 1; } // Implementation void Init() { // create a tool tip m_tip.Create(this->m_hWnd); ATLASSERT(m_tip.IsWindow()); if (m_tip.IsWindow() && (m_lpstrToolTipText != NULL)) { m_tip.Activate(TRUE); m_tip.AddTool(this->m_hWnd, m_lpstrToolTipText); } } BOOL StartTrackMouseLeave() { TRACKMOUSEEVENT tme = {}; tme.cbSize = sizeof(tme); tme.dwFlags = TME_LEAVE; tme.hwndTrack = this->m_hWnd; return ::TrackMouseEvent(&tme); } }; class CToolTipButton : public CToolTipButtonImpl { public: DECLARE_WND_SUPERCLASS(_T("WTL_ToolTipButton"), GetWndClassName()) CToolTipButton() : CToolTipButtonImpl() { } }; ================================================ FILE: vcproject/VersionDetector.h ================================================ #pragma once #include "stdafx.h" class VersionDetector { public: CString GetProductVersion() { TCHAR szPath[MAX_PATH] = { 0 }; GetModuleFileName(NULL, szPath, MAX_PATH); return GetProductVersion(szPath); } CString GetProductVersion(LPCTSTR szPath) { CString strResult; DWORD dwHandle; DWORD dwSize = GetFileVersionInfoSize(szPath, &dwHandle); if (dwSize > 0) { BYTE* pbBuf = new BYTE[dwSize]; if (GetFileVersionInfo(szPath, dwHandle, dwSize, pbBuf)) { UINT uiSize; BYTE* lpb; if (VerQueryValue(pbBuf, TEXT("\\VarFileInfo\\Translation"), (void**)&lpb, &uiSize)) { WORD* lpw = (WORD*)lpb; CString strQuery; strQuery.Format(TEXT("\\StringFileInfo\\%04x%04x\\ProductVersion"), lpw[0], lpw[1]); if (VerQueryValue(pbBuf, const_cast((LPCTSTR)strQuery), (void**)&lpb, &uiSize) && uiSize > 0) { strResult = (LPCTSTR)lpb; } } } delete[] pbBuf; } return strResult; } }; ================================================ FILE: vcproject/View.h ================================================ // View.h : interface of the CView class // ///////////////////////////////////////////////////////////////////////////// #pragma once #include #include #include #include "LoggerImpl.h" #include "PdfConverterImpl.h" #include "ExportNotifierImpl.h" // #include "ColoredControls.h" #include "LogListBox.h" #include "ProgressListViewCtrl.h" #include "TextProgressBarCtrl.h" #include "AppConfiguration.h" #include "VersionDetector.h" #include "ViewHelper.h" #define SAFE_DELETE(ptr) { delete ptr; ptr = NULL; } class CView : public CDialogImpl, public CDialogResize { private: enum VIEW_STATE { VS_IDLE = 0, VS_LOADING, VS_EXPORTING }; static const int SUBITEM_PROGRESS = 5; static const UINT_PTR EVENT_ID_PROGRESS = 1; // CColoredComboBoxCtrl m_cbmBoxBackups; // CColoredComboBoxCtrl m_cbmBoxUsers; CImageList m_sourceTypeIcons; CLogListBox m_logListBox; CSortListViewCtrl m_sessionsListCtrl; CProgressListViewCtrl m_progressListCtrl; CStatic m_progressTextCtrl; CTextProgressBarCtrl m_progressBarCtrl; CToolTipButton m_btnExpITunes; LoggerImpl* m_logger; PdfConverterImpl* m_pdfConverter; ExportNotifierImpl* m_notifier; Exporter* m_exporter; std::vector m_wechatSources; // std::vector m_devices; std::vector m_manifests; std::vector>> m_usersAndSessions; int m_itemClicked; VIEW_STATE m_viewState; UINT_PTR m_eventIdProgress; class CLoadingHandler { protected: HWND m_hWnd; std::future m_task; Exporter m_exp; CWaitCursor m_waitCursor; bool runTask() { if (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)) { m_exp.setLanguageCode("zh-Hans"); } bool ret = m_exp.loadUsersAndSessions(); ::PostMessage(m_hWnd, WM_LOADDATA, (ret ? 1 : 0), reinterpret_cast(this)); return ret; } public: CLoadingHandler(HWND hWnd, const std::string& resDir, const std::string& backupDir, Logger* logger) : m_hWnd(hWnd), m_exp(resDir, backupDir, "", logger, NULL) { ExportOption options; options.outputDebugLogs(AppConfiguration::OutputDebugLogs()); m_exp.setOptions(options); } ~CLoadingHandler() { } void startTask() { m_task = std::async(std::launch::async, &CLoadingHandler::runTask, this); } void waitForCompletion() { m_task.wait(); } void getUsersAndSessions(std::vector>>& usersAndSessions) { m_exp.swapUsersAndSessions(usersAndSessions); } CString getVersions() const { CW2T pszV1(CA2W(m_exp.getITunesVersion().c_str(), CP_UTF8)); CW2T pszV2(CA2W(m_exp.getIOSVersion().c_str(), CP_UTF8)); CW2T pszV3(CA2W(m_exp.getWechatVersion().c_str(), CP_UTF8)); CString versions; versions.Format(IDS_VERSIONS, (LPCTSTR)pszV1, (LPCTSTR)pszV2, (LPCTSTR)pszV3); return versions; } }; class CUpdateHandler : public CIdleHandler { protected: HWND m_hWnd; std::future m_task; Updater m_updater; public: CUpdateHandler(HWND hWnd, const std::string& currentVersion, const std::string& userAgent) : m_hWnd(hWnd), m_updater(currentVersion) { m_updater.setUserAgent(userAgent); } ~CUpdateHandler() { } void startTask() { m_task = std::async(std::launch::async, &Updater::checkUpdate, &m_updater); } virtual BOOL OnIdle() { std::future_status status = m_task.wait_for(std::chrono::seconds(0)); if (status == std::future_status::ready) { ::PostMessage(m_hWnd, WM_CHKUPDATE, 1, reinterpret_cast(this)); return FALSE; } return TRUE; } bool hasNewVersion() { return m_task.get(); } CString getNewVersion() const { CW2T pszT(CA2W(m_updater.getNewVersion().c_str(), CP_UTF8)); return CString(pszT); } CString getUpdateUrl() const { CW2T pszT(CA2W(m_updater.getUpdateUrl().c_str(), CP_UTF8)); return CString(pszT); } }; public: enum { IDD = IDD_WECHATEXPORTER_FORM }; static const UINT WM_START = ExportNotifierImpl::WM_START; static const UINT WM_COMPLETE = ExportNotifierImpl::WM_COMPLETE; static const UINT WM_PROGRESS = ExportNotifierImpl::WM_PROGRESS; static const UINT WM_USR_SESS_START = ExportNotifierImpl::WM_USR_SESS_START; static const UINT WM_USR_SESS_COMPLETE = ExportNotifierImpl::WM_USR_SESS_COMPLETE; static const UINT WM_SESSION_START = ExportNotifierImpl::WM_SESSION_START; static const UINT WM_SESSION_COMPLETE = ExportNotifierImpl::WM_SESSION_COMPLETE; static const UINT WM_SESSION_PROGRESS = ExportNotifierImpl::WM_SESSION_PROGRESS; static const UINT WM_TASKS_START = ExportNotifierImpl::WM_TASKS_START; static const UINT WM_TASKS_COMPLETE = ExportNotifierImpl::WM_TASKS_COMPLETE; static const UINT WM_TASKS_PROGRESS = ExportNotifierImpl::WM_TASKS_PROGRESS; static const UINT WM_MSG_START = ExportNotifierImpl::WM_EN_END; static const UINT WM_UPD_VIEWSTATE = WM_MSG_START + 1; static const UINT WM_LOADDATA = WM_MSG_START + 2; static const UINT WM_CHKUPDATE = WM_MSG_START + 3; CView() : CDialogImpl(), CDialogResize(), m_viewState(VS_IDLE), m_eventIdProgress(0) { } LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { m_sourceTypeIcons.Create(16, 16, ILC_COLOR32 | ILC_MASK, 2, 2); int res = m_sourceTypeIcons.AddIcon(AtlLoadIcon(IDI_IPHONE)); res = m_sourceTypeIcons.AddIcon(AtlLoadIcon(IDI_ITUNES)); CComboBoxEx cmb = GetDlgItem(IDC_BACKUP); cmb.SetImageList(m_sourceTypeIcons); m_progressTextCtrl = GetDlgItem(IDC_PROGRESS_TEXT); m_logListBox.SubclassWindow(GetDlgItem(IDC_LOGS)); m_progressBarCtrl.SubclassWindow(GetDlgItem(IDC_PROGRESS)); m_btnExpITunes.SubclassWindow(GetDlgItem(IDC_EXP_ITNS)); // m_cbmBoxBackups.SetEditColors(CLR_INVALID, ::GetSysColor(COLOR_WINDOWTEXT)); // m_cbmBoxUsers.SetEditColors(CLR_INVALID, ::GetSysColor(COLOR_WINDOWTEXT)); CString toolTip; toolTip.LoadString(IDS_EXP_ITNS); m_btnExpITunes.SetToolTipText(toolTip); m_logger = NULL; m_pdfConverter = NULL; m_notifier = NULL; m_exporter = NULL; m_itemClicked = -2; // Init the CDialogResize code DlgResize_Init(); InitializeSessionList(); InitializeSessionProgressList(); m_notifier = new ExportNotifierImpl(m_hWnd); m_logger = new LoggerImpl(GetDlgItem(IDC_LOGS)); CString backupDir = AppConfiguration::GetDefaultBackupDir(FALSE); CString text; text.Format(IDS_STATIC_BACKUP, (LPCTSTR)backupDir); CStatic label = GetDlgItem(IDC_STATIC_BACKUP); label.SetWindowText(text); SetDlgItemText(IDC_OUTPUT, AppConfiguration::GetLastOrDefaultOutputDir()); std::vector wechatSources; std::vector devices; IDeviceBackup::queryDevices(devices); for (std::vector::const_iterator it = devices.cbegin(); it != devices.cend(); ++it) { WechatSource* source = new WechatSource(*it); wechatSources.push_back(source); } backupDir = AppConfiguration::GetDefaultBackupDir(); #ifndef NDEBUG CString lastBackupDir = AppConfiguration::GetLastBackupDir(); #endif std::vector backupItems; if (!backupDir.IsEmpty()) { CW2A backupDirU8(CT2W(backupDir), CP_UTF8); #ifndef USING_DECODED_ITUNESBACKUP std::unique_ptr parser(new ManifestParser((LPCSTR)backupDirU8, false)); #else std::unique_ptr parser(new DecodedManifestParser((LPCSTR)backupDirU8, false)); #endif // ManifestParser parser((LPCSTR)backupDirU8, false); parser->parse(backupItems); } #ifndef NDEBUG if (!lastBackupDir.IsEmpty() && lastBackupDir != backupDir) { CW2A backupDirU8(CT2W(lastBackupDir), CP_UTF8); // ManifestParser parser((LPCSTR)backupDirU8, false); #ifndef USING_DECODED_ITUNESBACKUP std::unique_ptr parser(new ManifestParser((LPCSTR)backupDirU8, false)); #else std::unique_ptr parser(new DecodedManifestParser((LPCSTR)backupDirU8, false)); #endif parser->parse(backupItems); } #endif for (std::vector::const_iterator it = backupItems.cbegin(); it != backupItems.cend(); ++it) { WechatSource* source = new WechatSource(*it); wechatSources.push_back(source); } if (!wechatSources.empty()) { UpdateBackups(wechatSources); } #if defined(NDEBUG) && !defined(DBG_PERF) if (!AppConfiguration::GetCheckingUpdateDisabled() && (getUnixTimeStamp() - AppConfiguration::GetLastCheckUpdateTime()) > 86400u) { ::PostMessage(m_hWnd, WM_CHKUPDATE, 0, 0); } #endif return TRUE; } void OnFinalMessage(HWND hWnd) { if (NULL != m_exporter) { m_exporter->cancel(); m_exporter->waitForComplition(); SAFE_DELETE(m_exporter); m_exporter = NULL; } SAFE_DELETE(m_notifier); SAFE_DELETE(m_pdfConverter); SAFE_DELETE(m_logger); // override to do something, if needed } BOOL PreTranslateMessage(MSG* pMsg) { return CWindow::IsDialogMessage(pMsg); } BEGIN_MSG_MAP(CView) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) CHAIN_MSG_MAP(CDialogResize) MESSAGE_HANDLER(WM_TIMER, OnTimer) COMMAND_HANDLER(IDC_BACKUP, CBN_SELCHANGE, OnBackupSelChange) COMMAND_HANDLER(IDC_CHOOSE_BKP, BN_CLICKED, OnBnClickedChooseBkp) COMMAND_HANDLER(IDC_CHOOSE_OUTPUT, BN_CLICKED, OnBnClickedChooseOutput) COMMAND_HANDLER(IDC_USERS, CBN_SELCHANGE, OnUserSelChange) COMMAND_HANDLER(IDC_SHOW_LOGS, BN_CLICKED, OnBnClickedShowLogs) COMMAND_HANDLER(IDC_EXP_ITNS, BN_CLICKED, OnBnClickedExpITunes) COMMAND_HANDLER(IDC_EXPORT, BN_CLICKED, OnBnClickedExport) COMMAND_HANDLER(IDC_CANCEL, BN_CLICKED, OnBnClickedCancel) COMMAND_HANDLER(IDC_CLOSE, BN_CLICKED, OnBnClickedClose) MESSAGE_HANDLER(WM_START, OnStart) MESSAGE_HANDLER(WM_COMPLETE, OnComplete) MESSAGE_HANDLER(WM_PROGRESS, OnProgress) MESSAGE_HANDLER(WM_USR_SESS_START, OnUserSessionStart) MESSAGE_HANDLER(WM_USR_SESS_COMPLETE, OnUserSessionComplete) MESSAGE_HANDLER(WM_SESSION_START, OnSessionStart) MESSAGE_HANDLER(WM_SESSION_COMPLETE, OnSessionComplete) MESSAGE_HANDLER(WM_SESSION_PROGRESS, OnSessionProgress) MESSAGE_HANDLER(WM_TASKS_START, OnTasksStart) MESSAGE_HANDLER(WM_TASKS_COMPLETE, OnTasksComplete) MESSAGE_HANDLER(WM_TASKS_PROGRESS, OnTasksProgress) MESSAGE_HANDLER(WM_UPD_VIEWSTATE, OnUpdateViewState) MESSAGE_HANDLER(WM_LOADDATA, OnLoadData) MESSAGE_HANDLER(WM_CHKUPDATE, OnCheckUpdate) NOTIFY_HANDLER(IDC_SESSIONS, LVN_ITEMCHANGED, OnListItemChanged) NOTIFY_CODE_HANDLER(HDN_ITEMSTATEICONCLICK, OnHeaderItemStateIconClick) NOTIFY_HANDLER(IDC_SESSIONS, NM_CLICK, OnListClick) REFLECT_NOTIFICATIONS() END_MSG_MAP() BEGIN_DLGRESIZE_MAP(CView) DLGRESIZE_CONTROL(IDC_CHOOSE_BKP, DLSZ_MOVE_X) DLGRESIZE_CONTROL(IDC_BACKUP, DLSZ_SIZE_X) DLGRESIZE_CONTROL(IDC_CHOOSE_OUTPUT, DLSZ_MOVE_X) DLGRESIZE_CONTROL(IDC_OUTPUT, DLSZ_SIZE_X) DLGRESIZE_CONTROL(IDC_GRP_USR_CHAT, DLSZ_SIZE_X | DLSZ_SIZE_Y) DLGRESIZE_CONTROL(IDC_VERSIONS, DLSZ_SIZE_X) DLGRESIZE_CONTROL(IDC_PROGRESS, DLSZ_SIZE_X) // DLGRESIZE_CONTROL(IDC_PROGRESS_TEXT, DLSZ_MOVE_Y) DLGRESIZE_CONTROL(IDC_SESSIONS, DLSZ_SIZE_X | DLSZ_SIZE_Y) DLGRESIZE_CONTROL(IDC_SESS_PROGRESS, DLSZ_SIZE_X | DLSZ_SIZE_Y) DLGRESIZE_CONTROL(IDC_GRP_LOGS, DLSZ_SIZE_X | DLSZ_SIZE_Y) DLGRESIZE_CONTROL(IDC_LOGS, DLSZ_SIZE_X | DLSZ_SIZE_Y) DLGRESIZE_CONTROL(IDC_SHOW_LOGS, DLSZ_MOVE_Y) DLGRESIZE_CONTROL(IDC_CANCEL, DLSZ_MOVE_X | DLSZ_MOVE_Y) DLGRESIZE_CONTROL(IDC_EXP_ITNS, DLSZ_MOVE_Y) DLGRESIZE_CONTROL(IDC_CLOSE, DLSZ_MOVE_X | DLSZ_MOVE_Y) DLGRESIZE_CONTROL(IDC_EXPORT, DLSZ_MOVE_X | DLSZ_MOVE_Y) END_DLGRESIZE_MAP() // Handler prototypes (uncomment arguments if needed): // LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) // LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) // LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/) LRESULT OnTimer(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) { UINT_PTR nIDEvent = static_cast(wParam); if (nIDEvent == m_eventIdProgress) { UpdateProgressBar(TRUE); } return 0; } LRESULT OnUpdateViewState(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { m_viewState = static_cast(wParam); if (lParam != 0) { UpdateUI(); } return 0; } LRESULT OnLoadData(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { CLoadingHandler *handler = reinterpret_cast(lParam); if (NULL != handler) { handler->waitForCompletion(); handler->getUsersAndSessions(m_usersAndSessions); LoadUsers(handler->getVersions()); PostMessage(WM_UPD_VIEWSTATE, VS_IDLE, 1); delete handler; } return 0; } LRESULT OnCheckUpdate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { if (0 == wParam) { VersionDetector vd; CString newVersion = vd.GetProductVersion(); CW2A pszNV(CT2W((LPCTSTR)newVersion), CP_UTF8); char userAgent[512] = { 0 }; DWORD size = sizeof(userAgent); ObtainUserAgentString(0, userAgent, &size); CW2A pszUA(CA2W(userAgent), CP_UTF8); CUpdateHandler *handler = new CUpdateHandler(m_hWnd, (LPCSTR)pszNV, (LPCSTR)pszUA); _Module.GetMessageLoop()->AddIdleHandler(handler); handler->startTask(); } else if (1 == wParam) { CUpdateHandler *handler = reinterpret_cast(lParam); if (NULL != handler) { if (_Module.GetMessageLoop()->RemoveIdleHandler(handler)) { AppConfiguration::SetLastCheckUpdateTime(); bool hasNewVersion = handler->hasNewVersion(); CString newVersion = handler->getNewVersion(); CString updateUrl = handler->getUpdateUrl(); delete handler; if (hasNewVersion) { CString text; text.Format(IDS_NEW_VERSION, (LPCTSTR)newVersion); CString caption; caption.LoadStringW(IDR_MAINFRAME); UINT ret = MessageBoxTimeout(NULL, text, caption, MB_OKCANCEL, 0, 6000); if (ret == IDOK) { CT2A url(updateUrl); ::ShellExecute(NULL, TEXT("open"), (LPCTSTR)updateUrl, NULL, NULL, SW_SHOWNORMAL); } } } // If the handler is not in array, throw it away } } return 0; } LRESULT OnBnClickedChooseBkp(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CString text; text.LoadString(IDS_SEL_BACKUP_DIR); CFolderDialog folder(NULL, text, BIF_RETURNONLYFSDIRS | BIF_USENEWUI | BIF_NONEWFOLDERBUTTON); if (IDOK == folder.DoModal()) { CW2A backupDir(CT2W(folder.m_szFolderPath), CP_UTF8); std::vector wechatSources; #ifndef USING_DECODED_ITUNESBACKUP std::unique_ptr parser(new ManifestParser((LPCSTR)backupDir, false)); #else std::unique_ptr parser(new DecodedManifestParser((LPCSTR)backupDir, false)); #endif std::vector backupItems; if (parser->parse(backupItems) && !backupItems.empty()) { for (std::vector::const_iterator it = backupItems.cbegin(); it != backupItems.cend(); ++it) { WechatSource* source = new WechatSource(*it); wechatSources.push_back(source); } UpdateBackups(wechatSources); #ifndef NDEBUG AppConfiguration::SetLastBackupDir(folder.m_szFolderPath); #endif } else { m_logger->debug(parser->getLastError()); MsgBox(m_hWnd, IDS_FAILED_TO_LOAD_BKP); } } return 0; } LRESULT OnBackupSelChange(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CListBox lstboxLogs = GetDlgItem(IDC_LOGS); lstboxLogs.ResetContent(); CComboBox cbmBox = GetDlgItem(IDC_USERS); cbmBox.ResetContent(); CListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS); // listViewCtrl.SetRedraw(FALSE); listViewCtrl.DeleteAllItems(); // listViewCtrl.SetRedraw(TRUE); m_usersAndSessions.clear(); cbmBox = GetDlgItem(IDC_BACKUP); if (cbmBox.GetCurSel() == -1) { return 0; } const WechatSource* wechatSource = m_wechatSources[cbmBox.GetCurSel()]; if (wechatSource->isDevice()) { return 0; } const BackupItem& backupItem = wechatSource->getBackupItem(); if (backupItem.isEncrypted()) { MsgBox(m_hWnd, IDS_ENC_BKP_NOT_SUPPORTED); return 0; } TCHAR buffer[MAX_PATH] = { 0 }; DWORD dwRet = GetCurrentDirectory(MAX_PATH, buffer); if (dwRet == 0) { return 0; } #ifndef NDEBUG m_logger->write("Start loading users and sessions."); #endif SendMessage(WM_NEXTDLGCTL, 0, 0); CW2A resDir(CT2W(buffer), CP_UTF8); PostMessage(WM_UPD_VIEWSTATE, VS_LOADING, 1); std::string backup = backupItem.getPath(); CLoadingHandler *handler = new CLoadingHandler(m_hWnd, (LPCSTR)resDir, backup, m_logger); // _Module.GetMessageLoop()->AddIdleHandler(handler); handler->startTask(); return 0; } LRESULT OnUserSelChange(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS); CComboBox cbmBox = GetDlgItem(IDC_USERS); int curSel = cbmBox.GetCurSel(); if (curSel == -1) { listViewCtrl.DeleteAllItems(); return 0; } #ifndef NDEBUG m_logger->debug("Display Sessions Start"); #endif BOOL allUsers = (curSel == 0); std::string usrName; if (curSel > 0) { std::vector>>::const_iterator it = m_usersAndSessions.cbegin() + curSel - 1; usrName = it->first.getUsrName(); } listViewCtrl.SetRedraw(FALSE); if (listViewCtrl.GetItemCount() > 0) { listViewCtrl.DeleteAllItems(); } LoadSessions(allUsers, usrName); listViewCtrl.SetRedraw(TRUE); #ifndef NDEBUG m_logger->debug("Display Sessions End"); #endif return 0; } LRESULT OnBnClickedChooseOutput(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { CString text; text.LoadString(IDS_SEL_OUTPUT_DIR); CFolderDialog folder(NULL, text, BIF_RETURNONLYFSDIRS | BIF_USENEWUI); TCHAR outputDir[MAX_PATH] = { 0 }; GetDlgItemText(IDC_OUTPUT, outputDir, MAX_PATH); if (_tcscmp(outputDir, TEXT("")) == 0) { HRESULT result = SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, outputDir); } if (_tcscmp(outputDir, TEXT("")) != 0) { folder.SetInitialFolder(outputDir); } if (IDOK == folder.DoModal()) { AppConfiguration::SetLastOutputDir(folder.m_szFolderPath); SetDlgItemText(IDC_OUTPUT, folder.m_szFolderPath); } return 0; } LRESULT OnBnClickedCancel(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { if (MsgBox(m_hWnd, IDS_CANCEL_PROMPT, MB_YESNO) == IDNO) { return 0; } if (NULL == m_exporter) { return 0; } m_exporter->cancel(); return 0; } LRESULT OnBnClickedShowLogs(WORD /*wNotifyCode*/, WORD /*wID*/, HWND hWndCtl, BOOL& /*bHandled*/) { CButton btn = hWndCtl; BOOL showLogs = btn.GetCheck() == BST_CHECKED; CString text; text.LoadString(showLogs ? IDS_HIDE_LOGS : IDS_SHOW_LOGS); btn.SetWindowText(text); UpdateUI(); return 0; } LRESULT OnBnClickedClose(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { ::PostMessage(GetParent(), WM_CLOSE, 0, 0); return 0; } LRESULT OnHeaderItemStateIconClick(int idCtrl, LPNMHDR pnmh, BOOL& /*bHandled*/) { CHeaderCtrl header = m_sessionsListCtrl.GetHeader(); if (pnmh->hwndFrom == header.m_hWnd) { LPNMHEADER pnmHeader = (LPNMHEADER)pnmh; if (pnmHeader->pitem->mask & HDI_FORMAT && pnmHeader->pitem->fmt & HDF_CHECKBOX) { CheckAllItems(m_sessionsListCtrl, !(pnmHeader->pitem->fmt & HDF_CHECKED)); SyncHeaderCheckbox(); return 1; } } return 0; } LRESULT OnListItemChanged(int idCtrl, LPNMHDR pnmh, BOOL& /*bHandled*/) { LPNMLISTVIEW pnmlv = (LPNMLISTVIEW)pnmh; if (pnmlv->uChanged & LVIF_STATE) { if (pnmlv->iItem == m_itemClicked) { SyncHeaderCheckbox(); m_itemClicked = -2; } } return 0; } LRESULT OnListClick(int /*idCtrl*/, LPNMHDR pnmh, BOOL& bHandled) { LPNMITEMACTIVATE pnmia = (LPNMITEMACTIVATE)pnmh; DWORD pos = GetMessagePos(); POINT pt = { LOWORD(pos), HIWORD(pos) }; CListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS); listViewCtrl.ScreenToClient(&pt); UINT flags = 0; int nItem = listViewCtrl.HitTest(pt, &flags); if (flags == LVHT_ONITEMSTATEICON) { m_itemClicked = nItem; // listViewCtrl.SetCheckState(nItem, !listViewCtrl.GetCheckState(nItem)); // SetHeaderCheckbox(); // bHandled = TRUE; } return 0; } LRESULT OnBnClickedExpITunes(WORD /*wNotifyCode*/, WORD /*wID*/, HWND hWndCtl, BOOL& /*bHandled*/) { CComboBox cbmBox = GetDlgItem(IDC_BACKUP); if (cbmBox.GetCurSel() == -1) { MsgBox(m_hWnd, IDS_SEL_BACKUP_DIR); return 0; } const WechatSource* src = m_wechatSources[cbmBox.GetCurSel()]; const BackupItem& backupItem = src->getBackupItem(); if (backupItem.isEncrypted()) { MsgBox(m_hWnd, IDS_ENC_BKP_NOT_SUPPORTED); return 0; } std::string backup = backupItem.getPath(); TCHAR outputDir[MAX_PATH] = { 0 }; GetDlgItemText(IDC_OUTPUT, outputDir, MAX_PATH); if (!::PathFileExists(outputDir)) { MsgBox(m_hWnd, IDS_INVALID_OUTPUT_DIR); return 0; } /* else { DWORD dwAttributes = GetFileAttributes(outputDir); if ((dwAttributes & FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY) { MsgBox(m_hWnd, IDS_READONLY_OUTPUT_DIR); return 0; } } */ CW2A output(CT2W(outputDir), CP_UTF8); CWaitCursor waitCursor; SendMessage(WM_UPD_VIEWSTATE, VS_EXPORTING, 1); ::EnableWindow(hWndCtl, FALSE); ITunesDb iTunesDb(backup, ""); std::vector domains; domains.push_back("AppDomain-com.tencent.xin"); domains.push_back("AppDomainGroup-group.com.tencent.xin"); DWORD idx = 0; std::function func = std::bind(&CView::onCopyFile, this, std::placeholders::_1, std::placeholders::_2, idx); bool ret = iTunesDb.copy((LPCSTR)output, backupItem.getBackupId(), domains, func); ::EnableWindow(hWndCtl, TRUE); SendMessage(WM_UPD_VIEWSTATE, VS_IDLE, 1); if (ret) { PathAppend(outputDir, TEXT("Backup")); OpenFolder(outputDir); } if (!ret) { #ifndef NDEBUG std::string errMsg = iTunesDb.getLastError(); CW2T lpszErrMsg(CA2W(errMsg.c_str(), CP_UTF8)); MsgBox(m_hWnd, (LPCTSTR)lpszErrMsg); #endif } return 0; } bool onCopyFile(const ITunesDb* iTunesDb, const ITunesFile* file, DWORD& idx) { ++idx; if (idx % 32 != 0) { return true; } MSG msg; while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) return false; TranslateMessage(&msg); DispatchMessage(&msg); } return true; } void ExportITunesBackup() { PostMessage(WM_COMMAND, (BN_CLICKED << 16) | IDC_EXP_ITNS, (LPARAM)::GetDlgItem(m_hWnd, IDC_EXP_ITNS)); } LRESULT OnBnClickedExport(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { if (NULL != m_exporter) { return 0; } CComboBox cbmBox = GetDlgItem(IDC_BACKUP); if (cbmBox.GetCurSel() == -1) { MsgBox(m_hWnd, IDS_SEL_BACKUP_DIR); return 0; } const WechatSource* src = m_wechatSources[cbmBox.GetCurSel()]; const BackupItem& backupItem = src->getBackupItem(); if (backupItem.isEncrypted()) { MsgBox(m_hWnd, IDS_ENC_BKP_NOT_SUPPORTED); return 0; } std::string backup = backupItem.getPath(); TCHAR outputDir[MAX_PATH] = { 0 }; GetDlgItemText(IDC_OUTPUT, outputDir, MAX_PATH); if (!::PathFileExists(outputDir)) { MsgBox(m_hWnd, IDS_INVALID_OUTPUT_DIR); return 0; } CW2A output(CT2W(outputDir), CP_UTF8); TCHAR curDir[MAX_PATH] = { 0 }; DWORD dwRet = GetModuleFileName(NULL, curDir, MAX_PATH); if (dwRet == 0) { return 0; } if (!PathRemoveFileSpec(curDir)) { return 0; } if (AppConfiguration::GetIncrementalExporting()) { uint64_t options = 0; std::string exportTime; std::string version; if (Exporter::hasPreviousExporting((LPCSTR)output, options, exportTime, version)) { if (version.empty() && m_usersAndSessions.size() > 1) { // First version of implementation has design issue if (MsgBox(m_hWnd, IDS_INVLD_INCEXP_FOR_MULTI_USERS, MB_OK) == IDOK) { return 0; } } CW2T exportTimeT(CA2W(exportTime.c_str(), CP_UTF8)); CString text; text.Format(IDS_PREV_EXP_FOUND, (LPCTSTR)exportTimeT); MessageBoxTimeout(m_hWnd, text, TEXT(""), MB_OK, 0, 4000); } else { CString text; text.LoadString(IDS_NO_PREV_EXP); MessageBoxTimeout(m_hWnd, text, TEXT(""), MB_OK, 0, 4000); } } #if !defined(NDEBUG) || defined(DBG_PERF) m_logger->setLogPath(outputDir); #endif // CButton btn = GetDlgItem(IDC_DESC_ORDER); ExportOption options = AppConfiguration::BuildOptions(); // bool descOrder = AppConfiguration::GetDescOrder(); // bool saveFilesInSessionFolder = AppConfiguration::GetSavingInSession(); // bool usingRemoteEmoji = AppConfiguration::GetUsingRemoteEmoji(); // bool asyncLoading = AppConfiguration::GetAsyncLoading(); // bool loadingDataOnScroll = AppConfiguration::GetLoadingDataOnScroll(); // bool supportingFilter = AppConfiguration::GetSupportingFilter(); // bool incrementalExp = AppConfiguration::GetIncrementalExporting(); UINT outputFormat = AppConfiguration::GetOutputFormat(); if (options.isPdfMode()) { options.setSyncLoading(); // options.setLoadingDataOnScroll(false); options.supportsFilter(false); } CListBox lstboxLogs = GetDlgItem(IDC_LOGS); lstboxLogs.ResetContent(); CW2A resDir(CT2W(curDir), CP_UTF8); std::map> usersAndSessions; int numberOfSessions = 0; int numberOfRecords = 0; GetCheckedSessionsAndCopyItems(usersAndSessions, numberOfSessions, numberOfRecords); CProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS); progressCtrl.SetWindowText(TEXT("")); progressCtrl.ModifyStyle(PBS_MARQUEE, 0); progressCtrl.SetRange32(1, ((numberOfRecords > 0) ? numberOfRecords : 1)); progressCtrl.SetStep(1); progressCtrl.SetPos(0); UpdateProgressBarText(IDS_EXPORTING_MSGS, 0, true); #if !defined(NDEBUG) || defined(DBG_PERF) m_logger->debug("Record Count:" + std::to_string(numberOfRecords)); #endif if (NULL != m_pdfConverter) { delete m_pdfConverter; m_pdfConverter = NULL; } if (outputFormat == AppConfiguration::OUTPUT_FORMAT_PDF) { m_pdfConverter = new PdfConverterImpl(outputDir); } m_exporter = new Exporter((LPCSTR)resDir, backup, (LPCSTR)output, m_logger, m_pdfConverter); if (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)) { m_exporter->setLanguageCode("zh-Hans"); } m_exporter->setNotifier(m_notifier); // m_exporter->setOrder(!descOrder); // m_exporter->useRemoteEmoji(usingRemoteEmoji); // m_exporter->setSyncLoading(!asyncLoading); // m_exporter->setLoadingDataOnScroll(loadingDataOnScroll); // m_exporter->supportsFilter(supportingFilter); // m_exporter->setIncrementalExporting(incrementalExp); // m_exporter->outputDebugLogs(AppConfiguration::OutputDebugLogs()); // if (AppConfiguration::IncludeSubscriptions()) { // m_exporter->includesSubscription(); } // if (saveFilesInSessionFolder) { // m_exporter->saveFilesInSessionFolder(); } if (options.isTextMode()) { m_exporter->setExtName("txt"); m_exporter->setTemplatesName("templates_txt"); } m_exporter->setOptions(options); m_exporter->filterUsersAndSessions(usersAndSessions); if (m_exporter->run()) { PostMessage(WM_UPD_VIEWSTATE, VS_EXPORTING, 1); } return 0; } LRESULT OnStart(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { if (0 == m_eventIdProgress) { m_eventIdProgress = SetTimer(EVENT_ID_PROGRESS, 100, NULL); } return 0; } LRESULT OnComplete(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { BOOL cancelled = (lParam != 0) ? TRUE : FALSE; if (NULL != m_pdfConverter) { if (!cancelled) { m_pdfConverter->executeCommand(); } delete m_pdfConverter; m_pdfConverter = NULL; } if (m_exporter) { m_exporter->waitForComplition(); delete m_exporter; m_exporter = NULL; } if (0 != m_eventIdProgress) { KillTimer(m_eventIdProgress); m_eventIdProgress = 0; } if (!cancelled && AppConfiguration::GetOpenningFolderAfterExp()) { TCHAR outputDir[MAX_PATH] = { 0 }; GetDlgItemText(IDC_OUTPUT, outputDir, MAX_PATH); OpenFolder( (LPCTSTR)outputDir); } PostMessage(WM_UPD_VIEWSTATE, VS_IDLE, 1); return 0; } LRESULT OnProgress(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { return 0; } LRESULT OnUserSessionStart(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { if (m_eventIdProgress != 0) { KillTimer(m_eventIdProgress); m_eventIdProgress = 0; } return 0; } LRESULT OnUserSessionComplete(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { if (0 == m_eventIdProgress) { m_eventIdProgress = SetTimer(EVENT_ID_PROGRESS, 100, NULL); } return 0; } LRESULT OnSessionStart(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { // wParam: nItem // lParam: NumberOfMessages int nItem = wParam; uint32_t numberOfMessages = static_cast(lParam); m_progressListCtrl.EnsureVisible(nItem, FALSE); m_progressListCtrl.SetProgressBarInfo(nItem, SUBITEM_PROGRESS, numberOfMessages, 0); CString text; text.Format(IDS_SESSION_PROGRESS, 0u, numberOfMessages); m_progressListCtrl.SetItemText(nItem, SUBITEM_PROGRESS, text); return 0; } LRESULT OnSessionComplete(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { m_progressListCtrl.ClearProgressBar(); int nItem = wParam; CString text; text.LoadString((lParam != 0) ? IDS_SESSION_CANCELLED : IDS_SESSION_DONE); m_progressListCtrl.SetItemText(nItem, SUBITEM_PROGRESS, text); return 0; } LRESULT OnSessionProgress(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { int nItem = wParam; uint32_t numberOfMessages = static_cast(lParam); const Session *session = reinterpret_cast(m_progressListCtrl.GetItemData(nItem)); m_progressListCtrl.SetProgressPos(static_cast(lParam)); CString text; text.Format(IDS_SESSION_PROGRESS, numberOfMessages, static_cast(session->getRecordCount())); m_progressListCtrl.SetItemText(nItem, SUBITEM_PROGRESS, text); UpdateProgressBar(); return 0; } LRESULT OnTasksStart(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { CProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS); progressCtrl.SetRange32(0, 100); progressCtrl.SetPos(0); UpdateProgressBarText(IDS_DOWNLOADING_EMOJI, 0, true); return 0; } LRESULT OnTasksComplete(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { CProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS); // progressCtrl.SetRange32(0, 100); progressCtrl.SetPos(100); return 0; } LRESULT OnTasksProgress(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { UINT totalNumberOfTasks = static_cast(wParam - lParam); UINT numberOfTasks = static_cast(wParam - lParam); #if !defined(NDEBUG) || defined(DBG_PERF) std::string timeString = getTimestampString(false, true) + ": "; CA2T szTime(timeString.c_str()); TCHAR szLog[256] = { 0 }; HWND hWndLog = GetDlgItem(IDC_LOGS); _stprintf(szLog, TEXT("%s: Task Queue Size = %u"), (LPCTSTR)szTime, numberOfTasks); ::SendMessage(hWndLog, LB_ADDSTRING, 0, (LPARAM)szLog); LRESULT count = ::SendMessage(hWndLog, LB_GETCOUNT, 0, 0L); ::SendMessage(hWndLog, LB_SETTOPINDEX, count - 1, 0L); #endif CProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS); int percent = (totalNumberOfTasks > 0 && numberOfTasks <= totalNumberOfTasks) ? (numberOfTasks * 100 / totalNumberOfTasks) : 100; progressCtrl.SetPos(percent); UpdateProgressBarText(IDS_DOWNLOADING_EMOJI, lParam, true); //UpdateProgressBarText(IDS_EXPORTING_MSGS, percent); return 0; } BOOL IsViewIdle() const { return m_viewState == VS_IDLE; } protected: void UpdateUI() { CButton btn = GetDlgItem(IDC_SHOW_LOGS); BOOL showLogs = btn.GetCheck() == BST_CHECKED; ::ShowWindow(GetDlgItem(IDC_GRP_LOGS), showLogs ? SW_SHOW : SW_HIDE); ::ShowWindow(GetDlgItem(IDC_LOGS), showLogs ? SW_SHOW : SW_HIDE); ::ShowWindow(GetDlgItem(IDC_GRP_USR_CHAT), showLogs ? SW_HIDE : SW_SHOW); ::ShowWindow(GetDlgItem(IDC_USERS), showLogs ? SW_HIDE : SW_SHOW); ::ShowWindow(GetDlgItem(IDC_VERSIONS), showLogs || m_viewState == VS_EXPORTING ? SW_HIDE : SW_SHOW); ::ShowWindow(GetDlgItem(IDC_SESSIONS), showLogs || m_viewState == VS_EXPORTING ? SW_HIDE : SW_SHOW); ::ShowWindow(GetDlgItem(IDC_SESS_PROGRESS), showLogs || m_viewState != VS_EXPORTING ? SW_HIDE : SW_SHOW); ::ShowWindow(GetDlgItem(IDC_PROGRESS), showLogs || m_viewState != VS_EXPORTING ? SW_HIDE : SW_SHOW); // ::ShowWindow(GetDlgItem(IDC_PROGRESS_TEXT), showLogs || m_viewState != VS_EXPORTING ? SW_HIDE : SW_SHOW); ::ShowWindow(GetDlgItem(IDC_CANCEL), m_viewState == VS_EXPORTING ? SW_SHOW : SW_HIDE); ::ShowWindow(GetDlgItem(IDC_CLOSE), m_viewState != VS_EXPORTING ? SW_SHOW : SW_HIDE); UINT ids[] = { IDC_BACKUP, IDC_CHOOSE_BKP, IDC_CHOOSE_OUTPUT, IDC_USERS/*, IDC_EXP_ITNS*/, IDC_CLOSE, IDC_EXPORT }; for (int idx = 0; idx < sizeof(ids) / sizeof(UINT); ++idx) { ::EnableWindow(GetDlgItem(ids[idx]), m_viewState == VS_IDLE); } m_btnExpITunes.EnableWindow(m_viewState == VS_IDLE); UINT state = (m_viewState == VS_IDLE) ? MF_ENABLED : (MF_DISABLED | MF_GRAYED); ::EnableMenuItem(::GetSystemMenu(::GetParent(m_hWnd), FALSE), SC_CLOSE, MF_BYCOMMAND | state); } void UpdateProgressBarOnDownloadingEmoji(uint32_t restedNumberOfFiles, uint32_t totalNumberOfFiles) { } void UpdateProgressBar(BOOL increaseUpper = FALSE) { UpdateProgressBar(1, increaseUpper ? 1 : 0); } void UpdateProgressBar(int increasedPos, int increasedUpper) { CProgressBarCtrl progressCtrl = GetDlgItem(IDC_PROGRESS); PBRANGE range; int pos = progressCtrl.GetPos(); progressCtrl.GetRange(&range); if ((increasedUpper > 0) || pos == range.iHigh) { range.iHigh += increasedUpper; progressCtrl.SetRange32(range.iLow, range.iHigh); } if (increasedPos > 0) { progressCtrl.OffsetPos(increasedPos); pos = progressCtrl.GetPos(); } int percent = (range.iHigh > range.iLow) ? ((pos - range.iLow) * 100 / (range.iHigh - range.iLow)) : 0; if (percent >= 100) { percent = (pos < range.iHigh) ? 99 : 100; } UpdateProgressBarText(IDS_EXPORTING_MSGS, percent); } void UpdateProgressBarText(UINT stringId, DWORD value, bool forceUpdate = false) { if (forceUpdate || value != m_progressTextCtrl.GetWindowLongPtr(GWLP_USERDATA)) { // Avoid flashing CString text; text.Format(stringId, value); // text.Format(TEXT("%d%%"), percent); m_progressTextCtrl.SetWindowLongPtr(GWLP_USERDATA, value); m_progressTextCtrl.SetWindowText(text); m_progressBarCtrl.SetWindowText(text); } } void UpdateBackups(const std::vector& backupItems, BOOL onLaunch = FALSE) { if (backupItems.empty()) { return; } int selectedIndex = -1; // Add default backup folder for (std::vector::const_iterator it = backupItems.cbegin(); it != backupItems.cend(); ++it) { std::vector::const_iterator it2 = std::find(m_manifests.cbegin(), m_manifests.cend(), *it); if (it2 != m_manifests.cend()) { if (selectedIndex == -1) { selectedIndex = static_cast(std::distance(m_manifests.cbegin(), it2)); } } else { m_manifests.push_back(*it); if (selectedIndex == -1) { selectedIndex = static_cast(m_manifests.size() - 1); } } } // Update int res = -1; CComboBoxEx cmb = GetDlgItem(IDC_BACKUP); cmb.SetRedraw(FALSE); cmb.ResetContent(); for (std::vector::const_iterator it = m_manifests.cbegin(); it != m_manifests.cend(); ++it) { COMBOBOXEXITEM item = { 0 }; item.mask = CBEIF_IMAGE | CBEIF_TEXT | CBEIF_SELECTEDIMAGE; // item.mask = CBEIF_TEXT; CW2T itemText(CA2W(it->toString().c_str(), CP_UTF8)); item.iItem = cmb.GetCount(); item.iImage = item.iItem % 2; item.iSelectedImage = item.iImage; item.pszText = (LPTSTR)itemText; item.iIndent = 16; res = cmb.InsertItem(&item); } cmb.SetRedraw(TRUE); if (selectedIndex != -1 && selectedIndex < cmb.GetCount()) { SetComboBoxCurSel(m_hWnd, cmb, selectedIndex); } } void UpdateBackups(const std::vector& sources, BOOL onLaunch = FALSE) { if (sources.empty()) { return; } int selectedIndex = -1; // Add default backup folder for (std::vector::const_iterator it = sources.cbegin(); it != sources.cend(); ++it) { std::vector::const_iterator it2 = std::find(m_wechatSources.cbegin(), m_wechatSources.cend(), *it); if (it2 != m_wechatSources.cend()) { if (selectedIndex == -1) { selectedIndex = static_cast(std::distance(m_wechatSources.cbegin(), it2)); } } else { m_wechatSources.push_back(*it); if (selectedIndex == -1) { selectedIndex = static_cast(m_wechatSources.size() - 1); } } } // Update int res = -1; CComboBoxEx cmb = GetDlgItem(IDC_BACKUP); cmb.SetRedraw(FALSE); cmb.ResetContent(); for (std::vector::const_iterator it = m_wechatSources.cbegin(); it != m_wechatSources.cend(); ++it) { COMBOBOXEXITEM item = { 0 }; item.mask = CBEIF_IMAGE | CBEIF_TEXT | CBEIF_SELECTEDIMAGE | CBEIF_INDENT; // item.mask = CBEIF_TEXT; CW2T itemText(CA2W((*it)->getDisplayName().c_str(), CP_UTF8)); item.iItem = cmb.GetCount(); item.iImage = (*it)->isDevice() ? 0 : 1; item.iSelectedImage = item.iImage; item.pszText = (LPTSTR)itemText; // item.iIndent = 4; res = cmb.InsertItem(&item); } cmb.SetRedraw(TRUE); if (selectedIndex != -1 && selectedIndex < cmb.GetCount()) { SetComboBoxCurSel(m_hWnd, cmb, selectedIndex); } } void InitializeSessionList() { m_sessionsListCtrl.SubclassWindow(GetDlgItem(IDC_SESSIONS)); CString strColumn1; CString strColumn2; CString strColumn3; CString strColumn4; strColumn1.LoadString(IDS_SESSION_NAME); strColumn2.LoadString(IDS_SESSION_COUNT); strColumn3.LoadString(IDS_SESSION_LAST_MSG); strColumn4.LoadString(IDS_SESSION_USER); DWORD dwStyle = m_sessionsListCtrl.GetExStyle(); dwStyle |= LVS_EX_FULLROWSELECT | LVS_EX_LABELTIP | LVS_EX_GRIDLINES | LVS_EX_CHECKBOXES | LVS_EX_DOUBLEBUFFER; m_sessionsListCtrl.SetExtendedListViewStyle(dwStyle, dwStyle); LVCOLUMN lvc = { 0 }; ListView_InsertColumn(m_sessionsListCtrl, 0, &lvc); lvc.mask = LVCF_TEXT | LVCF_WIDTH; lvc.iSubItem++; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn1; lvc.cx = 256; ListView_InsertColumn(m_sessionsListCtrl, 1, &lvc); lvc.iSubItem++; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn2; lvc.cx = 96; ListView_InsertColumn(m_sessionsListCtrl, 2, &lvc); lvc.iSubItem++; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn3; lvc.cx = 320; ListView_InsertColumn(m_sessionsListCtrl, 3, &lvc); lvc.iSubItem++; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn4; lvc.cx = 128; ListView_InsertColumn(m_sessionsListCtrl, 4, &lvc); // Set column widths ListView_SetColumnWidth(m_sessionsListCtrl, 0, LVSCW_AUTOSIZE_USEHEADER); ListView_SetColumnWidth(m_sessionsListCtrl, 2, LVSCW_AUTOSIZE_USEHEADER); // ListView_SetColumnWidth(listViewCtrl, 1, LVSCW_AUTOSIZE_USEHEADER); // ListView_SetColumnWidth(listViewCtrl, 2, LVSCW_AUTOSIZE_USEHEADER); // ListView_SetColumnWidth(listViewCtrl, 3, LVSCW_AUTOSIZE_USEHEADER); m_sessionsListCtrl.SetColumnSortType(0, LVCOLSORT_NONE); m_sessionsListCtrl.SetColumnSortType(2, LVCOLSORT_LONG); m_sessionsListCtrl.SetColumnSortType(3, LVCOLSORT_NONE); m_sessionsListCtrl.SetColumnSortType(4, LVCOLSORT_NONE); HWND header = ListView_GetHeader(m_sessionsListCtrl); DWORD dwHeaderStyle = ::GetWindowLong(header, GWL_STYLE); dwHeaderStyle |= HDS_CHECKBOXES; ::SetWindowLong(header, GWL_STYLE, dwHeaderStyle); HDITEM hdi = { 0 }; hdi.mask = HDI_FORMAT; Header_GetItem(header, 0, &hdi); hdi.fmt |= HDF_CHECKBOX | HDF_FIXEDWIDTH; Header_SetItem(header, 0, &hdi); } void InitializeSessionProgressList() { m_progressListCtrl.SubclassWindow(GetDlgItem(IDC_SESS_PROGRESS)); CString strColumn0; CString strColumn1; CString strColumn2; CString strColumn3; CString strColumn4; CString strColumn5; strColumn0.LoadString(IDS_SESSION_NUMBER); strColumn1.LoadString(IDS_SESSION_NAME); strColumn2.LoadString(IDS_SESSION_COUNT); strColumn3.LoadString(IDS_SESSION_LAST_MSG); strColumn4.LoadString(IDS_SESSION_USER); strColumn5.LoadString(IDS_SESSION_STATUS); DWORD dwStyle = m_progressListCtrl.GetExStyle(); dwStyle |= LVS_EX_FULLROWSELECT | LVS_EX_LABELTIP | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER; dwStyle &= ~LVS_EX_CHECKBOXES; m_progressListCtrl.SetExtendedListViewStyle(dwStyle, dwStyle); LVCOLUMN lvc = { 0 }; lvc.mask = LVCF_TEXT | LVCF_WIDTH; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn0; lvc.cx = 32; ListView_InsertColumn(m_progressListCtrl, 0, &lvc); lvc.mask = LVCF_TEXT | LVCF_WIDTH; lvc.iSubItem++; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn1; lvc.cx = 256; ListView_InsertColumn(m_progressListCtrl, 1, &lvc); lvc.iSubItem++; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn2; lvc.cx = 96; ListView_InsertColumn(m_progressListCtrl, 2, &lvc); lvc.iSubItem++; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn3; lvc.cx = 320; ListView_InsertColumn(m_progressListCtrl, 3, &lvc); lvc.iSubItem++; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn4; lvc.cx = 128; ListView_InsertColumn(m_progressListCtrl, 4, &lvc); lvc.iSubItem++; lvc.pszText = (LPTSTR)(LPCTSTR)strColumn5; lvc.cx = 192; ListView_InsertColumn(m_progressListCtrl, 5, &lvc); // Set column widths ListView_SetColumnWidth(m_progressListCtrl, 0, LVSCW_AUTOSIZE_USEHEADER); ListView_SetColumnWidth(m_progressListCtrl, 2, LVSCW_AUTOSIZE_USEHEADER); // ListView_SetColumnWidth(m_progressListCtrl, 1, LVSCW_AUTOSIZE_USEHEADER); // ListView_SetColumnWidth(m_progressListCtrl, 2, LVSCW_AUTOSIZE_USEHEADER); // ListView_SetColumnWidth(m_progressListCtrl, 3, LVSCW_AUTOSIZE_USEHEADER); } void GetCheckedSessionsAndCopyItems(std::map>& usersAndSessions, int& numberOfSessions, int& numberOfRecords) { numberOfSessions = 0; numberOfRecords = 0; m_progressListCtrl.SetRedraw(FALSE); if (m_progressListCtrl.GetItemCount() > 0) { m_progressListCtrl.DeleteAllItems(); } TCHAR numberString[32] = { 0 }; CListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS); for (int column = 0; column < listViewCtrl.GetHeader().GetItemCount(); column++) { m_progressListCtrl.SetColumnWidth(column, listViewCtrl.GetColumnWidth(column)); } // std::map> usersAndSessions; for (int nItem = 0; nItem < listViewCtrl.GetItemCount(); nItem++) { if (!listViewCtrl.GetCheckState(nItem)) { continue; } ++numberOfSessions; _itot(m_progressListCtrl.GetItemCount() + 1, numberString, 10); LVITEM lvItem = {}; lvItem.mask = LVIF_TEXT | LVIF_PARAM; lvItem.iItem = m_progressListCtrl.GetItemCount(); lvItem.iSubItem = 0; lvItem.pszText = numberString; lvItem.lParam = listViewCtrl.GetItemData(nItem); int newItem = m_progressListCtrl.InsertItem(&lvItem); CString text; for (int nSubItem = 1; nSubItem < 5; nSubItem++) { listViewCtrl.GetItemText(nItem, nSubItem, text); m_progressListCtrl.AddItem(newItem, nSubItem, text); } Session* session = reinterpret_cast(listViewCtrl.GetItemData(nItem)); if (NULL != session) { numberOfRecords += session->getRecordCount(); session->setData(reinterpret_cast(newItem)); std::string usrName = session->getOwner()->getUsrName(); std::map>::iterator it = usersAndSessions.find(usrName); if (it == usersAndSessions.end()) { it = usersAndSessions.insert(usersAndSessions.end(), std::pair>(usrName, std::map())); } it->second.insert(std::pair(session->getUsrName(), session->getData())); } } ListView_SetColumnWidth(m_progressListCtrl, 0, LVSCW_AUTOSIZE_USEHEADER); m_progressListCtrl.SetRedraw(TRUE); } void LoadUsers(const CString& versions) { CEdit edtVersions = GetDlgItem(IDC_VERSIONS); edtVersions.SetWindowText(versions); CComboBox cbmBox = GetDlgItem(IDC_USERS); if (!m_usersAndSessions.empty()) { CString text; text.LoadString(IDS_ALL_USERS); cbmBox.AddString(text); #ifndef NDEBUG std::string log = std::to_string(m_usersAndSessions.size()) + " users"; m_logger->debug(log); #endif } for (std::vector>>::const_iterator it = m_usersAndSessions.cbegin(); it != m_usersAndSessions.cend(); ++it) { std::string displayName = it->first.getDisplayName(); CW2T pszDisplayName(CA2W(displayName.c_str(), CP_UTF8)); cbmBox.AddString(pszDisplayName); } if (cbmBox.GetCount() > 0) { SetComboBoxCurSel(m_hWnd, cbmBox, 0); } } void LoadSessions(BOOL allUsers, const std::string& usrName) { m_sessionsListCtrl.SetSortColumn(-1); CListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS); CString strDeletedSession; strDeletedSession.LoadStringW(RBN_DELETEDBAND); BOOL includesSubscriptions = AppConfiguration::IncludeSubscriptions(); TCHAR recordCount[16] = { 0 }; for (std::vector>>::const_iterator it = m_usersAndSessions.cbegin(); it != m_usersAndSessions.cend(); ++it) { if (!allUsers) { if (it->first.getUsrName() != usrName) { continue; } } std::string userDisplayName = it->first.getDisplayName(); CW2T pszUserDisplayName(CA2W(userDisplayName.c_str(), CP_UTF8)); for (std::vector::const_iterator it2 = it->second.cbegin(); it2 != it->second.cend(); ++it2) { if (!includesSubscriptions && it2->isSubscription()) { continue; } std::string displayName = it2->getDisplayName(); if (displayName.empty()) { displayName = it2->getUsrName(); } CW2T pszDisplayName(CA2W(displayName.c_str(), CP_UTF8)); LVITEM lvItem = {}; lvItem.mask = LVIF_TEXT | LVIF_PARAM; lvItem.iItem = listViewCtrl.GetItemCount(); lvItem.iSubItem = 0; lvItem.pszText = TEXT(""); // lvItem.state = INDEXTOSTATEIMAGEMASK(2); // lvItem.stateMask = LVIS_STATEIMAGEMASK; int idx = std::distance(it->second.cbegin(), it2); LPARAM lParam = reinterpret_cast(&(*it2)); lvItem.lParam = lParam; int nItem = listViewCtrl.InsertItem(&lvItem); _itot(it2->getRecordCount(), recordCount, 10); if (it2->isDeleted()) { listViewCtrl.AddItem(nItem, 1, pszDisplayName + strDeletedSession); } else { listViewCtrl.AddItem(nItem, 1, pszDisplayName); } listViewCtrl.AddItem(nItem, 2, recordCount); std::string msg; if (it2->isTextMessage()) { if (it2->hasLastMessageUserDisplayName()) { msg = it2->getLastMessageUserDisplayName() + ": " + it2->getLastMessage(); } else { msg = it2->getLastMessage(); } } else { msg = "-"; } CW2T pszDisplayMsg(CA2W(msg.c_str(), CP_UTF8)); listViewCtrl.AddItem(nItem, 3, pszDisplayMsg); listViewCtrl.AddItem(nItem, 4, pszUserDisplayName); // BOOL bRet = listViewCtrl.SetItem(&lvSubItem); listViewCtrl.SetCheckState(nItem, TRUE); } } SetHeaderCheckState(listViewCtrl, TRUE); } void SyncHeaderCheckbox() { // Loop through all of our items. If any of them are // unchecked, we'll want to uncheck the header checkbox. CListViewCtrl listViewCtrl = GetDlgItem(IDC_SESSIONS); BOOL fChecked = TRUE; for (int nItem = 0; nItem < ListView_GetItemCount(listViewCtrl); nItem++) { if (!ListView_GetCheckState(listViewCtrl, nItem)) { fChecked = FALSE; break; } } SetHeaderCheckState(listViewCtrl, fChecked); } }; ================================================ FILE: vcproject/ViewHelper.cpp ================================================ #include "stdafx.h" #include #include "resource.h" #include "ViewHelper.h" #include struct LibraryDeleter { typedef HMODULE pointer; void operator()(HMODULE h) { if (NULL != h) ::FreeLibrary(h); } }; //Functions & other definitions required--> typedef int(__stdcall *MSGBOXAAPI)(IN HWND hWnd, IN LPCSTR lpText, IN LPCSTR lpCaption, IN UINT uType, IN WORD wLanguageId, IN DWORD dwMilliseconds); typedef int(__stdcall *MSGBOXWAPI)(IN HWND hWnd, IN LPCWSTR lpText, IN LPCWSTR lpCaption, IN UINT uType, IN WORD wLanguageId, IN DWORD dwMilliseconds); int MessageBoxTimeoutA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType, WORD wLanguageId, DWORD dwMilliseconds) { std::unique_ptr hUser32(::LoadLibraryA("user32.dll")); if (NULL != hUser32.get()) { MSGBOXAAPI MsgBoxTOA = (MSGBOXAAPI)GetProcAddress(hUser32.get(), "MessageBoxTimeoutA"); if (NULL != MsgBoxTOA) { return MsgBoxTOA(hWnd, lpText, lpCaption, uType, wLanguageId, dwMilliseconds); } } return MessageBoxA(hWnd, lpText, lpCaption, uType); } int MessageBoxTimeoutW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType, WORD wLanguageId, DWORD dwMilliseconds) { std::unique_ptr hUser32(::LoadLibraryW(L"user32.dll")); if (NULL != hUser32.get()) { MSGBOXWAPI MsgBoxTOW = (MSGBOXWAPI)GetProcAddress(hUser32.get(), "MessageBoxTimeoutW"); if (NULL != MsgBoxTOW) { return MsgBoxTOW(hWnd, lpText, lpCaption, uType, wLanguageId, dwMilliseconds); } } return MessageBoxW(hWnd, lpText, lpCaption, uType); } int MsgBox(HWND hWnd, UINT uStrId, UINT uType/* = MB_OK*/) { CString text; text.LoadString(uStrId); return MsgBox(hWnd, text, uType); } int MsgBox(HWND hWnd, const CString& text, UINT uType/* = MB_OK*/) { CString caption; caption.LoadString(IDR_MAINFRAME); return ::MessageBox(hWnd, (LPCTSTR)text, (LPCTSTR)caption, uType); } void SetComboBoxCurSel(HWND hWnd, CComboBox &cbm, int nCurSel) { cbm.SetCurSel(nCurSel); int nID = cbm.GetDlgCtrlID(); ::PostMessage(hWnd, WM_COMMAND, MAKEWPARAM(nID, CBN_SELCHANGE), LPARAM(cbm.m_hWnd)); } void CheckAllItems(CListViewCtrl& listViewCtrl, BOOL fChecked) { for (int nItem = 0; nItem < ListView_GetItemCount(listViewCtrl); nItem++) { ListView_SetCheckState(listViewCtrl, nItem, fChecked); } } void SetHeaderCheckState(CListViewCtrl& listViewCtrl, BOOL fChecked) { // We need to get the current format of the header // and set or remove the HDF_CHECKED flag HWND header = ListView_GetHeader(listViewCtrl); HDITEM hdi = { 0 }; hdi.mask = HDI_FORMAT; Header_GetItem(header, 0, &hdi); if (fChecked) { hdi.fmt |= HDF_CHECKED; } else { hdi.fmt &= ~HDF_CHECKED; } Header_SetItem(header, 0, &hdi); } BOOL SetHeaderCheckState(CListViewCtrl& listViewCtrl) { HWND header = ListView_GetHeader(listViewCtrl); HDITEM hdi = { 0 }; hdi.mask = HDI_FORMAT; Header_GetItem(header, 0, &hdi); return (hdi.fmt & HDF_CHECKED) == HDF_CHECKED ? TRUE : FALSE; } // Return value of GetCurrentExplorerFolders() struct ExplorerFolderInfo { HWND hwnd = nullptr; // window handle of explorer UniquePidlPtr pidl; // PIDL that points to current folder }; // Get information about all currently open explorer windows. // Throws std::system_error exception to report errors. std::vector< ExplorerFolderInfo > GetCurrentExplorerFolders() { CComPtr< IShellWindows > pshWindows; ThrowIfFailed( pshWindows.CoCreateInstance(CLSID_ShellWindows), "Could not create instance of IShellWindows"); long count = 0; ThrowIfFailed( pshWindows->get_Count(&count), "Could not get number of shell windows"); std::vector< ExplorerFolderInfo > result; result.reserve(count); for (long i = 0; i < count; ++i) { ExplorerFolderInfo info; CComVariant vi{ i }; CComPtr< IDispatch > pDisp; ThrowIfFailed( pshWindows->Item(vi, &pDisp), "Could not get item from IShellWindows"); if (!pDisp) // Skip - this shell window was registered with a NULL IDispatch continue; CComQIPtr< IWebBrowserApp > pApp{ pDisp }; if (!pApp) // This window doesn't implement IWebBrowserApp continue; // Get the window handle. pApp->get_HWND(reinterpret_cast(&info.hwnd)); CComQIPtr< IServiceProvider > psp{ pApp }; if (!psp) // This window doesn't implement IServiceProvider continue; CComPtr< IShellBrowser > pBrowser; if (FAILED(psp->QueryService(SID_STopLevelBrowser, &pBrowser))) // This window doesn't provide IShellBrowser continue; CComPtr< IShellView > pShellView; if (FAILED(pBrowser->QueryActiveShellView(&pShellView))) // For some reason there is no active shell view continue; CComQIPtr< IFolderView > pFolderView{ pShellView }; if (!pFolderView) // The shell view doesn't implement IFolderView continue; // Get the interface from which we can finally query the PIDL of // the current folder. CComPtr< IPersistFolder2 > pFolder; if (FAILED(pFolderView->GetFolder(IID_IPersistFolder2, (void**)&pFolder))) continue; LPITEMIDLIST pidl = nullptr; if (SUCCEEDED(pFolder->GetCurFolder(&pidl))) { // Take ownership of the PIDL via std::unique_ptr. info.pidl = UniquePidlPtr{ pidl }; result.push_back(std::move(info)); } } return result; } BOOL OpenFolder(LPCTSTR szFolder) { CT2W wszFolder(szFolder); try { // std::wcout << L"Currently open explorer windows:\n"; for (const auto& info : GetCurrentExplorerFolders()) { CComHeapPtr pPath; if (SUCCEEDED(::SHGetNameFromIDList(info.pidl.get(), SIGDN_FILESYSPATH, &pPath))) { if (wcscmp(wszFolder, pPath) == 0) { SetForegroundWindow(info.hwnd); return TRUE; } // std::wcout << L"hwnd: 0x" << std::hex << info.hwnd // << L", path: " << static_cast(pPath) << L"\n"; } } } catch (std::system_error& e) { // std::cout << "ERROR: " << e.what() << "\nError code: " << e.code() << "\n"; } HINSTANCE inst = ::ShellExecute(NULL, TEXT("open"), szFolder, NULL, NULL, SW_SHOWNORMAL); return (INT_PTR)inst > 32; } ================================================ FILE: vcproject/ViewHelper.h ================================================ #pragma once #include #include // for COM smart pointers #include // for COM smart pointers #include #include #include #include #define MB_TIMEDOUT 32000 int MessageBoxTimeoutA(IN HWND hWnd, IN LPCSTR lpText, IN LPCSTR lpCaption, IN UINT uType, IN WORD wLanguageId, IN DWORD dwMilliseconds); int MessageBoxTimeoutW(IN HWND hWnd, IN LPCWSTR lpText, IN LPCWSTR lpCaption, IN UINT uType, IN WORD wLanguageId, IN DWORD dwMilliseconds); #ifdef UNICODE #define MessageBoxTimeout MessageBoxTimeoutW #else #define MessageBoxTimeout MessageBoxTimeoutA #endif int MsgBox(HWND hWnd, UINT uStrId, UINT uType = MB_OK); int MsgBox(HWND hWnd, const CString& text, UINT uType = MB_OK); void SetComboBoxCurSel(HWND hWnd, CComboBox &cbm, int nCurSel); void CheckAllItems(CListViewCtrl& listViewCtrl, BOOL fChecked); void SetHeaderCheckState(CListViewCtrl& listViewCtrl, BOOL fChecked); BOOL SetHeaderCheckState(CListViewCtrl& listViewCtrl); template< typename T > void ThrowIfFailed(HRESULT hr, T&& msg) { if (FAILED(hr)) throw std::system_error{ hr, std::system_category(), std::forward(msg) }; } // Deleter for a PIDL allocated by the shell. struct CoTaskMemDeleter { void operator()(ITEMIDLIST* pidl) const { ::CoTaskMemFree(pidl); } }; // A smart pointer for PIDLs. using UniquePidlPtr = std::unique_ptr< ITEMIDLIST, CoTaskMemDeleter >; BOOL OpenFolder(LPCTSTR szFolder); ================================================ FILE: vcproject/WechatExporter.cpp ================================================ // WechatExporter.cpp : main source file for WechatExporter.exe // #include "stdafx.h" #include #include #include #include #include "ToolTipButton.h" #include "resource.h" #include "Core.h" #include "BackupDlg.h" #include "View.h" #include "aboutdlg.h" #include "MainFrm.h" CAppModule _Module; int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT) { CMessageLoop theLoop; _Module.AddMessageLoop(&theLoop); CMainFrame wndMain; if(wndMain.CreateEx() == NULL) { ATLTRACE(_T("Main window creation failed!\n")); return 0; } wndMain.ShowWindow(nCmdShow); int nRet = theLoop.Run(); _Module.RemoveMessageLoop(); return nRet; } int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow) { HRESULT hRes = ::CoInitialize(NULL); ATLASSERT(SUCCEEDED(hRes)); if (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)) { ::SetThreadUILanguage(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)); } else { ::SetThreadUILanguage(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)); } AtlInitCommonControls(ICC_PROGRESS_CLASS | ICC_BAR_CLASSES | ICC_LISTVIEW_CLASSES | ICC_USEREX_CLASSES); // add flags to support other controls hRes = _Module.Init(NULL, hInstance); ATLASSERT(SUCCEEDED(hRes)); TCHAR buffer[MAX_PATH] = { 0 }; DWORD dwRet = GetModuleFileName(NULL, buffer, MAX_PATH); if (dwRet > 0) { if (PathRemoveFileSpec(buffer)) { PathAppend(buffer, TEXT("Dlls")); SetDllDirectory(buffer); } } #ifndef NDEBUG else { assert(false); } #endif int nRet = Run(lpstrCmdLine, nCmdShow); _Module.Term(); ::CoUninitialize(); return nRet; } ================================================ FILE: vcproject/WechatExporter.h ================================================ // WechatExporter.h ================================================ FILE: vcproject/WechatExporter.rc ================================================ // Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "atlres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // Chinese (Simplified, PRC) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #pragma code_page(936) ///////////////////////////////////////////////////////////////////////////// // // Menu // IDR_MAINFRAME MENU BEGIN POPUP "ļ(&F)" BEGIN MENUITEM "Զ°汾", ID_FILE_CHK_UPDATE, CHECKED MENUITEM "Ŀ¼", ID_FILE_OPEN_FOLDER, CHECKED MENUITEM "ϸ־", ID_FILE_DBG_LOGS MENUITEM SEPARATOR MENUITEM "iTunesݵ", ID_FILE_EXP_ITUNES MENUITEM SEPARATOR MENUITEM "˳(&X)", ID_APP_EXIT END POPUP "ʽ (&T)" BEGIN MENUITEM "HTML", ID_FORMAT_HTML MENUITEM "ı", ID_FORMAT_TEXT MENUITEM "PDF", ID_FORMAT_PDF END POPUP "ѡ(&O)" BEGIN MENUITEM "Ϣʱ䵹򵼳", ID_OPT_DESC_ORDER MENUITEM "رļ", ID_OPT_DL_EMOJI, CHECKED MENUITEM SEPARATOR MENUITEM "ϻظ¼", ID_OPT_LM_ONSCROLL MENUITEM "ҳ 1000Ϣ/ҳ", ID_OPT_NORMALPAGINATION MENUITEM "ݷҳ", ID_OPT_PAGINATION_YEAR MENUITEM "·ݷҳ", ID_OPT_PAGINATION_MONTH MENUITEM SEPARATOR MENUITEM "ʾϢ", ID_OPT_FILTER MENUITEM SEPARATOR MENUITEM "", ID_OPT_INCREMENTALEXP MENUITEM "ں", ID_OPT_SUBSCRIPTIONS END POPUP "(&H)" BEGIN MENUITEM "ҳ", ID_HELP_HOMEPAGE MENUITEM SEPARATOR MENUITEM "&About WechatExporter", ID_APP_ABOUT END END ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_ABOUTBOX DIALOGEX 0, 0, 187, 102 STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "" FONT 9, "Segoe UI", 0, 0, 0x0 BEGIN DEFPUSHBUTTON "ȷ",IDOK,130,81,50,14 CTEXT "WechatExporter Application v1.0\n\n(c) Copyright 2020",IDC_VERSION,25,57,78,32 ICON IDR_MAINFRAME,IDC_STATIC,55,26,20,20 GROUPBOX "",IDC_STATIC,7,7,115,88 END IDD_WECHATEXPORTER_FORM DIALOGEX 0, 0, 393, 196 STYLE DS_SETFONT | WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS EXSTYLE WS_EX_CLIENTEDGE FONT 9, "Segoe UI", 0, 0, 0x1 BEGIN LTEXT "iTunesĿ¼",IDC_STATIC_BACKUP,7,7,379,8 CONTROL "",IDC_BACKUP,"ComboBoxEx32",CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP,7,18,362,60 PUSHBUTTON "...",IDC_CHOOSE_BKP,371,17,15,14 LTEXT "¼Ŀ¼",IDC_STATIC,7,33,361,8 EDITTEXT IDC_OUTPUT,7,44,362,14,ES_AUTOHSCROLL | ES_READONLY PUSHBUTTON "...",IDC_CHOOSE_OUTPUT,371,44,15,14 GROUPBOX "΢˺ź¼ѡ",IDC_GRP_USR_CHAT,7,62,379,108,0,WS_EX_TRANSPARENT COMBOBOX IDC_USERS,14,74,252,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP EDITTEXT IDC_VERSIONS,270,76,109,14,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER CONTROL "",IDC_SESSIONS,"SysListView32",LVS_REPORT | LVS_ALIGNLEFT | WS_BORDER | WS_TABSTOP,14,90,365,73 LTEXT "100%",IDC_PROGRESS_TEXT,14,76,121,8,NOT WS_VISIBLE,WS_EX_TRANSPARENT CONTROL "",IDC_PROGRESS,"msctls_progress32",NOT WS_VISIBLE | WS_BORDER,270,74,109,12 CONTROL "",IDC_SESS_PROGRESS,"SysListView32",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | NOT WS_VISIBLE | WS_BORDER | WS_TABSTOP,14,90,365,73 GROUPBOX "־(CTRL+Aѡ־/CTRL+Cѡ־)",IDC_GRP_LOGS,7,62,379,108,NOT WS_VISIBLE,WS_EX_TRANSPARENT LISTBOX IDC_LOGS,14,74,365,89,LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | NOT WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP CONTROL "ʾ־",IDC_SHOW_LOGS,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,7,175,50,14 PUSHBUTTON "ȡ(&C)",IDC_CANCEL,336,175,50,14,NOT WS_VISIBLE PUSHBUTTON "iTunes",IDC_EXP_ITNS,72,175,80,14,NOT WS_VISIBLE PUSHBUTTON "(&E)",IDC_EXPORT,279,175,50,14 PUSHBUTTON "ر(&X)",IDC_CLOSE,336,175,50,14 END IDD_BACKUP_DLG DIALOGEX 0, 0, 328, 73 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION CAPTION "΢" FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN PUSHBUTTON "ȡ",IDCANCEL,271,43,50,14 CONTROL "",IDC_PROGRESS1,"msctls_progress32",WS_BORDER,7,43,239,14 LTEXT "ƶ豸΢...",IDC_TITLE,7,7,314,27 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO BEGIN IDD_ABOUTBOX, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 180 TOPMARGIN, 7 BOTTOMMARGIN, 95 END IDD_WECHATEXPORTER_FORM, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 386 TOPMARGIN, 7 BOTTOMMARGIN, 189 END IDD_BACKUP_DLG, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 321 TOPMARGIN, 7 BOTTOMMARGIN, 66 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // AFX_DIALOG_LAYOUT // IDD_WECHATEXPORTER_FORM AFX_DIALOG_LAYOUT BEGIN 0 END IDD_ABOUTBOX AFX_DIALOG_LAYOUT BEGIN 0 END IDD_BACKUP_DLG AFX_DIALOG_LAYOUT BEGIN 0 END ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_IPHONE ICON "res\\iPhone.ico" IDI_ITUNES ICON "res\\iTunes.ico" ///////////////////////////////////////////////////////////////////////////// // // String Table // STRINGTABLE BEGIN IDR_MAINFRAME "Wechat Exporter" IDS_SEL_BACKUP_DIR "ѡiTunesĿ¼" IDS_SEL_OUTPUT_DIR "ѡ¼Ŀ¼" IDS_CANCEL_PROMPT "ȷҪȡ" IDS_STATIC_BACKUP "iTunesĿ¼(:%s)" IDS_SESSION_IDX "" IDS_SESSION_NAME "" IDS_SESSION_COUNT "¼" IDS_ALL_USERS "΢˺" IDS_SESSION_USER "΢˺" IDS_ITUNES_VERSION "iTunes Ѱװ 汾%s" IDS_ITUNES_NOT_INSTALLED "iTunes δװ" IDS_ENC_BKP_NOT_SUPPORTED "ּ֧ܵiTunes BackupʹòʽiPhone/iPad豸" IDS_FAILED_TO_LOAD_BKP "iTunes Backupʧܡ" IDS_INVALID_OUTPUT_DIR "ЧĿ¼ѡ" IDS_TOOLTIP_LOGS "Ctrl+A/Ctrl+C ־" END STRINGTABLE BEGIN ID_APP_EXIT "˳Ӧá\n˳" END STRINGTABLE BEGIN ATL_IDS_IDLEMESSAGE "Ready" END STRINGTABLE BEGIN IDS_NEW_VERSION "°汾%sǷȥأ" IDS_SESSION_NUMBER "" IDS_SESSION_STATUS "" IDS_SESSION_DONE "ѵ" IDS_SESSION_PROGRESS "%u / %u" IDS_SESSION_CANCELLED "ȡ" IDS_SHOW_LOGS "ʾ־" IDS_HIDE_LOGS "־" IDS_VERSIONS "iTunes汾%s, iOS汾%s, ΢Ű汾%s" IDS_NO_PREV_EXP "Ŀ¼²ǰһεϢԡ" IDS_PREV_EXP_FOUND "Ŀ¼·%sĵݣεáģʽǰһεá" IDS_DELETED_SESSION "(ɾ)" IDS_EXPORTING_MSGS "ڵ¼ (%d%%)" IDS_DOWNLOADING_EMOJI "رļʣ: %d" IDS_EXP_ITNS "΢ļiTunes" IDS_SESSION_LAST_MSG "Ϣ" END STRINGTABLE BEGIN IDS_INVLD_INCEXP_FOR_MULTI_USERS "ƴ󣬶ڶ΢˺ŵݸԭܲµĿ¼" END #endif // Chinese (Simplified, PRC) resources ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // English resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL #pragma code_page(1252) ///////////////////////////////////////////////////////////////////////////// // // Menu // IDR_MAINFRAME MENU BEGIN POPUP "&File" BEGIN MENUITEM "Check Update Automatically", ID_FILE_CHK_UPDATE, CHECKED MENUITEM "Open Folder After Exporting", ID_FILE_OPEN_FOLDER, CHECKED MENUITEM "Output Detailed Logs", ID_FILE_DBG_LOGS MENUITEM SEPARATOR MENUITEM "Export iTunes Backup", ID_FILE_EXP_ITUNES MENUITEM SEPARATOR MENUITEM "E&xit", ID_APP_EXIT END POPUP "Forma&t " BEGIN MENUITEM "HTML", ID_FORMAT_HTML MENUITEM "Text", ID_FORMAT_TEXT MENUITEM "PDF", ID_FORMAT_PDF END POPUP "&Options" BEGIN MENUITEM "From Newer To Earlier", ID_OPT_DESC_ORDER MENUITEM "Download Emoji Files to Local Disk", ID_OPT_DL_EMOJI, CHECKED MENUITEM SEPARATOR MENUITEM "Load Messages On Scrolling", ID_OPT_LM_ONSCROLL MENUITEM "Normal Pagination (Every 1000 Messages)", ID_OPT_NORMALPAGINATION MENUITEM "Pagination Based on Year", ID_OPT_PAGINATION_YEAR MENUITEM "Pagination Based on Month", ID_OPT_PAGINATION_MONTH MENUITEM SEPARATOR MENUITEM "Show Message Filter", ID_OPT_FILTER MENUITEM SEPARATOR MENUITEM "Incremental Exporting", ID_OPT_INCREMENTALEXP MENUITEM "Including Subscriptions", ID_OPT_SUBSCRIPTIONS END POPUP "&Help" BEGIN MENUITEM "Home Page", ID_HELP_HOMEPAGE MENUITEM SEPARATOR MENUITEM "&About WechatExporter", ID_APP_ABOUT END END ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_WECHATEXPORTER_FORM DIALOGEX 0, 0, 393, 196 STYLE DS_SETFONT | WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS EXSTYLE WS_EX_CLIENTEDGE FONT 9, "Segoe UI", 0, 0, 0x1 BEGIN LTEXT "iTunes Backup Directory:",IDC_STATIC_BACKUP,7,7,379,8 CONTROL "",IDC_BACKUP,"ComboBoxEx32",CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP,7,18,362,60 PUSHBUTTON "...",IDC_CHOOSE_BKP,371,17,15,14 LTEXT "Output Directory:",IDC_STATIC,7,33,361,8 EDITTEXT IDC_OUTPUT,7,44,362,14,ES_AUTOHSCROLL | ES_READONLY PUSHBUTTON "...",IDC_CHOOSE_OUTPUT,371,44,15,14 GROUPBOX "Wechat Accounts and Chats",IDC_GRP_USR_CHAT,7,62,379,108,0,WS_EX_TRANSPARENT COMBOBOX IDC_USERS,14,74,252,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP EDITTEXT IDC_VERSIONS,270,76,109,14,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER CONTROL "",IDC_SESSIONS,"SysListView32",LVS_REPORT | LVS_ALIGNLEFT | WS_BORDER | WS_TABSTOP,14,90,365,73 LTEXT "100%",IDC_PROGRESS_TEXT,14,76,121,8,NOT WS_VISIBLE,WS_EX_TRANSPARENT CONTROL "",IDC_PROGRESS,"msctls_progress32",NOT WS_VISIBLE | WS_BORDER,270,74,109,12 CONTROL "",IDC_SESS_PROGRESS,"SysListView32",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | NOT WS_VISIBLE | WS_BORDER | WS_TABSTOP,14,90,365,73 GROUPBOX "Logs(CTRL+A:Select All Logs/CTRL+C:Copy Logs)",IDC_GRP_LOGS,7,62,379,108,NOT WS_VISIBLE,WS_EX_TRANSPARENT LISTBOX IDC_LOGS,14,74,365,89,LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | NOT WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP CONTROL "Show Logs",IDC_SHOW_LOGS,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,7,175,50,14 PUSHBUTTON "&Cancel",IDC_CANCEL,336,175,50,14,NOT WS_VISIBLE PUSHBUTTON "Export iTunes Backup",IDC_EXP_ITNS,72,175,80,14,NOT WS_VISIBLE PUSHBUTTON "&Export",IDC_EXPORT,279,175,50,14 PUSHBUTTON "Close",IDC_CLOSE,336,175,50,14 END IDD_ABOUTBOX DIALOGEX 0, 0, 187, 102 STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "About" FONT 9, "Segoe UI", 0, 0, 0x0 BEGIN DEFPUSHBUTTON "OK",IDOK,130,81,50,14 CTEXT "WechatExporter Application v1.0\n\n(c) Copyright 2020",IDC_VERSION,25,57,78,32 ICON IDR_MAINFRAME,IDC_STATIC,55,26,20,20 GROUPBOX "",IDC_STATIC,7,7,115,88 END IDD_BACKUP_DLG DIALOGEX 0, 0, 328, 73 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION CAPTION "Export WeChat Data" FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN PUSHBUTTON "Cancel",IDCANCEL,271,43,50,14 CONTROL "",IDC_PROGRESS1,"msctls_progress32",WS_BORDER,7,43,239,14 LTEXT "Exporting WeChat data from device... ",IDC_TITLE,7,7,314,27 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO BEGIN IDD_WECHATEXPORTER_FORM, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 386 TOPMARGIN, 7 BOTTOMMARGIN, 189 END IDD_ABOUTBOX, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 180 TOPMARGIN, 7 BOTTOMMARGIN, 95 END IDD_BACKUP_DLG, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 321 TOPMARGIN, 7 BOTTOMMARGIN, 66 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // AFX_DIALOG_LAYOUT // IDD_WECHATEXPORTER_FORM AFX_DIALOG_LAYOUT BEGIN 0 END IDD_ABOUTBOX AFX_DIALOG_LAYOUT BEGIN 0 END IDD_BACKUP_DLG AFX_DIALOG_LAYOUT BEGIN 0 END ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDR_MAINFRAME ICON "res\\idr_main.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // VS_VERSION_INFO VERSIONINFO FILEVERSION 1,9,6,0 PRODUCTVERSION 1,9,6,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L #else FILEFLAGS 0x0L #endif FILEOS 0x4L FILETYPE 0x2L FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "000904b0" BEGIN VALUE "FileDescription", "WechatExporter Module" VALUE "FileVersion", "1.9.6.0" VALUE "InternalName", "WechatExporter" VALUE "LegalCopyright", "Copyright 2020" VALUE "OriginalFilename", "WechatExporter.exe" VALUE "ProductName", "WechatExporter Module" VALUE "ProductVersion", "1.9.6.0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x9, 1200 END END #endif // English resources ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""atlres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // String Table // STRINGTABLE BEGIN IDR_MAINFRAME "Wechat Exporter" IDS_SEL_BACKUP_DIR "Please choose an iTunes backup directory." IDS_SEL_OUTPUT_DIR "Please choose an output directory." IDS_CANCEL_PROMPT "Sure to cancel the exporting?" IDS_STATIC_BACKUP "iTunes Backup Directory:(%s)" IDS_SESSION_IDX "No." IDS_SESSION_NAME "Name" IDS_SESSION_COUNT "Number of Msgs" IDS_ALL_USERS "All Wechat Accounts" IDS_SESSION_USER "Wechat Account" IDS_ITUNES_VERSION "iTunes Installed, Version:%s" IDS_ITUNES_NOT_INSTALLED "iTunes NOT Installed" IDS_ENC_BKP_NOT_SUPPORTED "Encrypted iTunes Backup is not supported." IDS_FAILED_TO_LOAD_BKP "Failed to load iTunes backup data." IDS_INVALID_OUTPUT_DIR "The output directory is invalid." IDS_TOOLTIP_LOGS "Ctrl+A/Ctrl+C Copy Logs" END STRINGTABLE BEGIN IDS_NEW_VERSION "Find a new version:%s, Download it now?" IDS_SESSION_NUMBER "NO." IDS_SESSION_STATUS "Status" IDS_SESSION_DONE "Done" IDS_SESSION_PROGRESS "%u / %u" IDS_SESSION_CANCELLED "Cancelled" IDS_SHOW_LOGS "Show Logs" IDS_HIDE_LOGS "Hide Logs" IDS_VERSIONS "iTunes Version: %s, iOS Version: %s, Wechat Version: %s" IDS_NO_PREV_EXP "There is no previous exporting in output directory and ""Incremental Exporting"" will be ignored." IDS_PREV_EXP_FOUND "The previous exporting at %s found, will reuse previous options." IDS_DELETED_SESSION "(Deleted)" IDS_EXPORTING_MSGS "Exporting Messages (%d%%)" IDS_DOWNLOADING_EMOJI "Downloading Emoji Files, Remaing: %d" IDS_EXP_ITNS "Export iTunes Backup Files of WeChat" IDS_SESSION_LAST_MSG "Last Message" END STRINGTABLE BEGIN ATL_IDS_IDLEMESSAGE "Ready" END STRINGTABLE BEGIN ID_APP_EXIT "Quit the application; prompts to save documents\nExit" END STRINGTABLE BEGIN IDS_INVLD_INCEXP_FOR_MULTI_USERS "Incremental Exporting will be invalid for multiple WeChat accounts as for wrong design. \r\nPlease choose another directory for exporting." END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// ================================================ FILE: vcproject/WechatExporter.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28307.1259 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WechatExporter", "WechatExporter.vcxproj", "{F57219E8-8B1F-41E3-B553-03C1884E8E4D}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WechatExporterCmd", "WechatExporterCmd.vcxproj", "{C0637D9A-3C23-495C-B661-5FDCD2169583}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Debug|x64.ActiveCfg = Debug|x64 {F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Debug|x64.Build.0 = Debug|x64 {F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Debug|x86.ActiveCfg = Debug|Win32 {F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Debug|x86.Build.0 = Debug|Win32 {F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Release|x64.ActiveCfg = Release|x64 {F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Release|x64.Build.0 = Release|x64 {F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Release|x86.ActiveCfg = Release|Win32 {F57219E8-8B1F-41E3-B553-03C1884E8E4D}.Release|x86.Build.0 = Release|Win32 {C0637D9A-3C23-495C-B661-5FDCD2169583}.Debug|x64.ActiveCfg = Debug|x64 {C0637D9A-3C23-495C-B661-5FDCD2169583}.Debug|x64.Build.0 = Debug|x64 {C0637D9A-3C23-495C-B661-5FDCD2169583}.Debug|x86.ActiveCfg = Debug|Win32 {C0637D9A-3C23-495C-B661-5FDCD2169583}.Debug|x86.Build.0 = Debug|Win32 {C0637D9A-3C23-495C-B661-5FDCD2169583}.Release|x64.ActiveCfg = Release|x64 {C0637D9A-3C23-495C-B661-5FDCD2169583}.Release|x64.Build.0 = Release|x64 {C0637D9A-3C23-495C-B661-5FDCD2169583}.Release|x86.ActiveCfg = Release|Win32 {C0637D9A-3C23-495C-B661-5FDCD2169583}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4BEDF3C-C880-4E96-939D-8D1BCBEF9ED3} EndGlobalSection EndGlobal ================================================ FILE: vcproject/WechatExporter.vcxproj ================================================ Debug Win32 Release Win32 Debug x64 Release x64 15.0 {F57219E8-8B1F-41E3-B553-03C1884E8E4D} 10.0.17763.0 Application true v141 Unicode Application false v141 Unicode Application true v141 Unicode Application false v141 Unicode true $(ProjectDir)..\releases\windows-static\x86\dbg\lib;$(LibraryPath) $(ProjectDir)..\releases\windows-static\include;$(ProjectDir)..\WechatExporter\core;$(IncludePath) $(SolutionDir)$(Platform)\$(Configuration)\ $(Platform)\$(Configuration)\ true $(ExecutablePath) $(ProjectDir)..\WechatExporter\core;$(ProjectDir)..\releases\windows-libs\include;$(IncludePath) $(ProjectDir)..\releases\windows-libs\x64\dbg\lib;$(LibraryPath) false $(ProjectDir)..\releases\windows-static\x86\lib;$(LibraryPath) $(ProjectDir)..\releases\windows-static\include;$(ProjectDir)..\WechatExporter\core;$(IncludePath) $(SolutionDir)$(Platform)\$(Configuration)\ $(Platform)\$(Configuration)\ false $(ProjectDir)..\releases\windows-libs\x64\rel\lib;$(LibraryPath) $(ProjectDir)..\WechatExporter\core;$(ProjectDir)..\releases\windows-libs\include;$(IncludePath) false true false NotUsing Level3 MultiThreadedDebug EditAndContinue EnableFastChecks Disabled WIN32;_WINDOWS;STRICT;_DEBUG;CURL_STATICLIB;COMPILE_SDK;NOMINMAX;%(PreprocessorDefinitions) Windows true 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) 0x0409 $(IntDir);%(AdditionalIncludeDirectories) _DEBUG;%(PreprocessorDefinitions) false Win32 _DEBUG;%(PreprocessorDefinitions) WechatExporter.h WechatExporter_i.c WechatExporter_p.c true $(IntDir)/WechatExporter.tlb if not exist "$(OutDir)res" mkdir "$(OutDir)res" xcopy "$(ProjectDir)..\WechatExporter\res" "$(OutDir)res" /Y /S /E Level3 MultiThreadedDebug EditAndContinue EnableFastChecks Disabled _WINDOWS;STRICT;_DEBUG;CURL_STATICLIB;COMPILE_SDK;NOMINMAX;%(PreprocessorDefinitions) /source-charset:utf-8 %(AdditionalOptions) NotUsing NoListing Windows true 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) 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 0x0409 $(IntDir);%(AdditionalIncludeDirectories) _DEBUG;%(PreprocessorDefinitions) false _DEBUG;%(PreprocessorDefinitions) WechatExporter.h WechatExporter_i.c WechatExporter_p.c true $(IntDir)/WechatExporter.tlb if not exist "$(OutDir)res" mkdir "$(OutDir)res" xcopy "$(ProjectDir)..\WechatExporter\res" "$(OutDir)res" /Y /S /E /D if not exist "$(OutDir)Dlls" mkdir "$(OutDir)Dlls" xcopy "$(ProjectDir)..\releases\windows-libs\x64\dbg\bin\*.dll" "$(OutDir)Dlls" /Y /S /E /D xcopy "$(ProjectDir)..\releases\windows-libs\x64\dbg\bin\*.pdb" "$(OutDir)Dlls\" /Y /S /E /D NotUsing Level3 MultiThreaded WIN32;_WINDOWS;STRICT;NDEBUG;CURL_STATICLIB;COMPILE_SDK;NOMINMAX;%(PreprocessorDefinitions) Windows 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) 0x0409 $(IntDir);%(AdditionalIncludeDirectories) NDEBUG;%(PreprocessorDefinitions) false Win32 NDEBUG;%(PreprocessorDefinitions) WechatExporter.h WechatExporter_i.c WechatExporter_p.c true $(IntDir)/WechatExporter.tlb if not exist "$(OutDir)res" mkdir "$(OutDir)res" xcopy "$(ProjectDir)..\WechatExporter\res" "$(OutDir)res" /Y /S /E Level3 MultiThreaded _WINDOWS;STRICT;NDEBUG;CURL_STATICLIB;COMPILE_SDK;NOMINMAX;%(PreprocessorDefinitions) NoListing Windows 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) 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 0x0409 $(IntDir);%(AdditionalIncludeDirectories) NDEBUG;%(PreprocessorDefinitions) false NDEBUG;%(PreprocessorDefinitions) WechatExporter.h WechatExporter_i.c WechatExporter_p.c true $(IntDir)/WechatExporter.tlb if not exist "$(OutDir)res" mkdir "$(OutDir)res" xcopy "$(ProjectDir)..\WechatExporter\res" "$(OutDir)res" /Y /S /E /D xcopy "$(ProjectDir)..\WechatExporter\LICENSES" "$(OutDir)LICENSES" /Y /S /E /D /I if not exist "$(OutDir)Dlls" mkdir "$(OutDir)Dlls" xcopy "$(ProjectDir)..\releases\windows-libs\x64\rel\bin\*.dll" "$(OutDir)Dlls\" /Y /D Create Create Create Create ================================================ FILE: vcproject/WechatExporter.vcxproj.filters ================================================  {ecd560c4-f70e-440e-ace2-0ff312561b22} cpp;c;cxx;def;odl;idl;hpj;bat;asm {235b586f-6b11-48f8-9ef6-99f6b8fb9470} h;hpp;hxx;hm;inl;inc {3a67dad0-ed25-40b7-9241-a759a966ea27} rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;jpg;jpeg;jpe;manifest {acf6df2f-565c-4832-a716-eb5f91d982e1} Source Files Source Files core core core core core core core core core core core core Source Files core core core Source Files core core core core core core Header Files Header Files Header Files Header Files Header Files core core core core core core core Header Files Header Files core core Header Files core core Header Files Header Files Header Files Header Files Header Files core core core Header Files Header Files Header Files core core core core Header Files core Header Files core core Header Files core core core Resource Files Resource Files Resource Files Resource Files Resource Files Resource Files ================================================ FILE: vcproject/WechatExporterCmd.cpp ================================================ // WechatExporterCmd.cpp : This file contains the 'main' function. Program execution begins and ends there. // #include #include #include #include #include #include #include #include #include #include "Core.h" #include std::string getCurrentLanguageCode(); static const WCHAR UNICODE_BOM = 0xFEFF; void UPrint(LPCWSTR String) { #ifndef NDEBUG // std::wcout << String; // return; #endif DWORD ConsoleMode; BOOL ConsoleOutput; DWORD FileType; BOOL Result; HANDLE StdOut; DWORD StringCharCount; DWORD Written; // // StdOut describes the standard output device. This can be the console // or (if output has been redirected) a file or some other device type. // StdOut = GetStdHandle(STD_OUTPUT_HANDLE); if (StdOut == INVALID_HANDLE_VALUE) { goto PrintExit; } // // Check whether the handle describes a character device. If it does, then // it may be a console device. A call to GetConsoleMode will fail with // ERROR_INVALID_HANDLE if it is not a console device. // FileType = GetFileType(StdOut); if ((FileType == FILE_TYPE_UNKNOWN) && (GetLastError() != ERROR_SUCCESS)) { goto PrintExit; } FileType &= ~(FILE_TYPE_REMOTE); if (FileType == FILE_TYPE_CHAR) { Result = GetConsoleMode(StdOut, &ConsoleMode); if ((Result == FALSE) && (GetLastError() == ERROR_INVALID_HANDLE)) { ConsoleOutput = FALSE; } else { ConsoleOutput = TRUE; } } else { ConsoleOutput = FALSE; } // // If StdOut is a console device then just use the UNICODE console write // API. This API doesn't work if StdOut has been redirected to a file or // some other device. In this case, write to StdOut using WriteFile. // StringCharCount = (DWORD)wcslen(String); if (ConsoleOutput != FALSE) { WriteConsoleW(StdOut, (PVOID)String, StringCharCount, &Written, NULL); } else { // // Write out a Unicode BOM to ensure proper processing by text readers // WriteFile(StdOut, (PVOID)&UNICODE_BOM, sizeof(UNICODE_BOM), &Written, NULL); // // The number of bytes to write to standard output must exclude the null // terminating character. // WriteFile(StdOut, (PVOID)String, (StringCharCount * sizeof(WCHAR)), &Written, NULL); } PrintExit: return; } class LoggerImpl : public Logger { protected: UINT m_cp; public: LoggerImpl() { m_cp = GetConsoleOutputCP(); if (m_cp != CP_UTF8) { #ifdef NDEBUG // _setmode(_fileno(stdout), _O_U16TEXT); #endif } std::wcout << L"CodePage:" << m_cp << std::endl; } void write(const std::string& log) { if (m_cp == CP_UTF8) { std::cout << log << std::endl; } else { CA2W str(log.c_str(), CP_UTF8); // CW2W targetStr(str, m_cp); UPrint((LPCWSTR)str); UPrint(L"\r\n"); } } void debug(const std::string& log) { if (m_cp == CP_UTF8) { std::cout << "DBG:: " << log << std::endl; } else { CA2W str(log.c_str(), CP_UTF8); // CW2W targetStr(str, m_cp); UPrint(L"DBG::"); UPrint((LPCWSTR)str); UPrint(L"\r\n"); } } }; std::string parseArgumentwithQuatoW(LPCWSTR path) { std::string parsedPath; if (NULL == path) { return parsedPath; } size_t start = 0; size_t length = lstrlenW(path); if (length > 1 && path[length - 1] == '"') { length--; } if (path[0] == '"') { start = 1; length--; } std::wstring value(path, start, length); return std::string((LPCSTR)CW2A(value.c_str(), CP_UTF8)); } int main() { if (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)) { ::SetThreadUILanguage(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)); } else { ::SetThreadUILanguage(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)); } TCHAR curDir[MAX_PATH] = { 0 }; DWORD dwRet = GetModuleFileName(NULL, curDir, MAX_PATH); if (dwRet == 0) { return 1; } if (!PathRemoveFileSpec(curDir)) { return 1; } TCHAR buffer[MAX_PATH] = { 0 }; _tcscpy(buffer, curDir); PathAppend(buffer, TEXT("Dlls")); SetDllDirectory(buffer); int outputFormat = OUTPUT_FORMAT_HTML; int asyncLoading = HTML_OPTION_ONSCROLL; bool outputFilter = false; std::string backupDir; std::string outputDir; std::string account; std::vector sessions; LPWSTR *szArglist = NULL; int nArgs; std::unique_ptr szArglistPtr(szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs), ::LocalFree); if (NULL == szArglist) { return 1; } for (int idx = 1; idx < nArgs; idx++) { if (lstrcmpW(L"--help", szArglist[idx]) == 0) { CW2A fileName(PathFindFileNameW(szArglist[0])); printHelp((LPCSTR)fileName); return 0; } LPWSTR equals_pos = wcschr(szArglist[idx], '='); if (equals_pos == NULL) { continue; } std::wstring name = std::wstring(szArglist[idx], equals_pos - szArglist[idx]); if (name == L"--backup") { backupDir = parseArgumentwithQuatoW(equals_pos + 1); } else if (name == L"--output") { outputDir = parseArgumentwithQuatoW(equals_pos + 1); } else if (name == L"--account") { account = parseArgumentwithQuatoW(equals_pos + 1); } else if (name == L"--session") { sessions.push_back(parseArgumentwithQuatoW(equals_pos + 1)); } else if (name == L"--asyncloading") { if (lstrcmpW(L"sync", equals_pos + 1) == 0) { asyncLoading = HTML_OPTION_SYNC; } else if (lstrcmpW(L"oninit", equals_pos + 1) == 0) { asyncLoading = HTML_OPTION_ONINIT; } } else if (name == L"--filter") { if (lstrcmpW(L"yes", equals_pos + 1) == 0) { outputFilter = true; } } } if (backupDir.empty() || !existsDirectory(backupDir)) { std::cout << "Please input valid iTunes backup directory." << std::endl; return 1; } if (outputDir.empty() || !existsDirectory(outputDir)) { std::cout << "Please input valid output directory." << std::endl; return 1; } if (account.empty()) { std::cout << "Please input account name." << std::endl; return 1; } std::string languageCode = getCurrentLanguageCode(); CW2A workDir(CT2W(curDir), CP_UTF8); LoggerImpl logger; return exportSessions(languageCode, &logger, (LPCSTR)workDir, backupDir, outputDir, account, sessions, outputFormat, asyncLoading, outputFilter); } std::string getCurrentLanguageCode() { std::string languageCode; if (GetUserDefaultUILanguage() == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)) { languageCode = "zh-Hans"; } return languageCode; } ================================================ FILE: vcproject/WechatExporterCmd.vcxproj ================================================ Debug Win32 Release Win32 Debug x64 Release x64 15.0 {C0637D9A-3C23-495C-B661-5FDCD2169583} Win32Proj WechatExporterCmd 10.0.17763.0 Application true v141 Unicode Application false v141 true Unicode Application true v141 Unicode Application false v141 true Unicode false $(ProjectDir)..\WechatExporterCmd;$(ProjectDir)..\WechatExporter\core;$(ProjectDir)..\releases\windows-libs\include;$(IncludePath) $(ProjectDir)..\releases\windows-libs\x64\rel\lib;$(LibraryPath) true true $(ProjectDir)..\WechatExporterCmd;$(ProjectDir)..\WechatExporter\core;$(ProjectDir)..\releases\windows-libs\include;$(IncludePath) $(ProjectDir)..\releases\windows-libs\x64\dbg\lib;$(LibraryPath) false Level3 MaxSpeed true true true NDEBUG;_CONSOLE;NOMINMAX;_CRT_SECURE_NO_WARNINGS;CURL_STATICLIB;COMPILE_SDK;%(PreprocessorDefinitions) true MultiThreaded Console true true true 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) 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 if not exist "$(OutDir)res" mkdir "$(OutDir)res" xcopy "$(ProjectDir)..\WechatExporter\res" "$(OutDir)res" /Y /S /E /D xcopy "$(ProjectDir)..\WechatExporter\LICENSES" "$(OutDir)LICENSES" /Y /S /E /D /I if not exist "$(OutDir)Dlls" mkdir "$(OutDir)Dlls" xcopy "$(ProjectDir)..\releases\windows-libs\x64\rel\bin\*.dll" "$(OutDir)Dlls\" /Y /D Level3 Disabled true WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true Level3 Disabled true _DEBUG;_CONSOLE;NOMINMAX;_CRT_SECURE_NO_WARNINGS;CURL_STATICLIB;COMPILE_SDK;%(PreprocessorDefinitions) true MultiThreadedDebug Console true 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) 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 if not exist "$(OutDir)res" mkdir "$(OutDir)res" xcopy "$(ProjectDir)..\WechatExporter\res" "$(OutDir)res" /Y /S /E /D if not exist "$(OutDir)Dlls" mkdir "$(OutDir)Dlls" xcopy "$(ProjectDir)..\releases\windows-libs\x64\dbg\bin\*.dll" "$(OutDir)Dlls" /Y /S /E /D xcopy "$(ProjectDir)..\releases\windows-libs\x64\dbg\bin\*.pdb" "$(OutDir)Dlls\" /Y /S /E /D Level3 MaxSpeed true true true WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true ================================================ FILE: vcproject/WechatExporterCmd.vcxproj.filters ================================================  {4FC737F1-C7A5-4376-A066-2A32D752A2FF} cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx {93995380-89BD-4b04-88EB-625FBE52EBFB} h;hh;hpp;hxx;hm;inl;inc;ipp;xsd {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms {1c08ec05-7c7c-4054-8d71-e0d198fe35a3} core core core core core core core core core core core core core core core core core core core core Source Files core core core core core core core core core core core core core core core core core core core core core core core core core core Header Files core core ================================================ FILE: vcproject/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by WechatExporter.rc // #define IDD_ABOUTBOX 100 #define IDR_MAINFRAME 128 #define IDD_WECHATEXPORTER_FORM 129 #define IDS_SEL_BACKUP_DIR 129 #define IDS_SEL_OUTPUT_DIR 130 #define IDS_CANCEL_PROMPT 131 #define IDS_STATIC_BACKUP 132 #define IDS_SESSION_IDX 133 #define IDS_SESSION_NAME 134 #define IDS_SESSION_COUNT 135 #define IDS_ALL_USERS 136 #define IDS_SESSION_USER 137 #define IDS_ITUNES_VERSION 138 #define IDS_ITUNES_NOT_INSTALLED 139 #define IDS_ENC_BKP_NOT_SUPPORTED 140 #define IDS_FAILED_TO_LOAD_BKP 141 #define IDS_INVALID_OUTPUT_DIR 142 #define IDS_TOOLTIP_LOGS 143 #define IDS_NEW_VERSION 144 #define IDS_SESSION_NUMBER 145 #define IDS_SESSION_STATUS 146 #define IDS_SESSION_DONE 147 #define IDS_SESSION_PROGRESS 148 #define IDS_SESSION_CANCELLED 149 #define IDS_SHOW_LOGS 150 #define IDS_HIDE_LOGS 151 #define IDS_VERSIONS 152 #define IDS_NO_PREV_EXP 153 #define IDS_PREV_EXP_FOUND 154 #define IDS_DELETED_SESSION 155 #define IDS_EXPORTING_MSGS 156 #define IDS_DOWNLOADING_EMOJI 157 #define IDS_EXP_ITNS 158 #define IDS_SESSION_LAST_MSG 159 #define IDS_INVLD_INCEXP_FOR_MULTI_USERS 160 #define IDD_DIALOG1 204 #define IDD_PROGRESS_DLG 204 #define IDD_BACKUP_DLG 204 #define IDD_BACKUP_DLG1 205 #define IDI_ITUNES 206 #define IDI_IPHONE 207 #define IDI_ICON2 208 #define IDC_BACKUP 1000 #define IDC_CHOOSE_BKP 1001 #define IDC_OUTPUT 1002 #define IDC_CHOOSE_OUTPUT 1003 #define IDC_LOGS 1004 #define IDC_EXPORT 1005 #define IDC_PROGRESS 1006 #define IDC_CANCEL 1007 #define IDC_EXPORT2 1008 #define IDC_EXP_ITNS 1008 #define IDC_STATIC_BACKUP 1010 #define IDC_VERSION 1011 #define IDC_USERS 1012 #define IDC_CLOSE 1013 #define IDC_SESSIONS 1014 #define IDC_GRP_LOGS 1015 #define IDC_GRP_USR_CHAT 1016 #define IDC_SESS_PROGRESS 1017 #define IDC_CHECK1 1018 #define IDC_SHOW_LOGS 1018 #define IDC_PROGRESS_TEXT 1019 #define IDC_VERSIONS 1020 #define IDC_PROGRESS1 1021 #define IDC_TITLE 1022 #define ID_FILE_CHK_UPDATE 32775 #define ID_FORMAT_HTML 32776 #define ID_FORMAT_TEXT 32777 #define ID_FORMAT_PDF 32778 #define ID_OPT_DESC_ORDER 32779 #define ID_OPT_DL_EMOJI 32780 #define ID_OPT_LM_ONSCROLL 32781 #define ID_OPT_NORMALPAGINATION 32782 #define ID_OPT_PAGINATION_YEAR 32783 #define ID_OPT_PAGINATION_MONTH 32784 #define ID_OPT_FILTER 32785 #define ID_OPT_INCREMENTALEXP 32786 #define ID_OPT_SUBSCRIPTIONS 32787 #define ID_HELP_HOMEPAGE 32788 #define ID_FILE_EXP_ITUNES 32789 #define ID_FILE_ITUNESBACKUPEXPORT 32790 #define ID_FILE_DBG_LOGS 32791 #define ID_FILE_OPENNING_FOLDER 32792 #define ID_FILE_OPEN_FOLDER 32793 #define ID_FILE_OPE_FOLDER 32794 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 208 #define _APS_NEXT_COMMAND_VALUE 32795 #define _APS_NEXT_CONTROL_VALUE 1023 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: vcproject/stdafx.cpp ================================================ // stdafx.cpp : source file that includes just the standard includes // WechatExporter.pch will be the pre-compiled header // stdafx.obj will contain the pre-compiled type information #include "stdafx.h" ================================================ FILE: vcproject/stdafx.h ================================================ // stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently // #pragma once // Change these values to use different versions #define WINVER 0x0601 // Windows 7 #define _WIN32_WINNT 0x0601 #define _WIN32_IE 0x0700 #define _RICHEDIT_VER 0x0500 #include #include #include extern CAppModule _Module; #include #include #if defined _M_IX86 #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") #elif defined _M_IA64 #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df' language='*'\"") #elif defined _M_X64 #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") #else #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") #endif