From 1761875dba858a4c883c2f4d847ab00cc7f44dc8 Mon Sep 17 00:00:00 2001 From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:28:10 +0100 Subject: [PATCH 1/3] Add Maps: offline vector maps (first version) Offline vector maps from OpenFreeMap. Download an area once over Wi-Fi and use it offline from the SD card. First version (0.0.1), tested on the LilyGO T-Deck only. --- Apps/Maps/CHANGELOG.md | 40 + Apps/Maps/CMakeLists.txt | 13 + Apps/Maps/LICENSE | 674 ++ Apps/Maps/README.md | 167 + Apps/Maps/THIRD_PARTY.md | 18 + Apps/Maps/docs/MEASUREMENTS.md | 220 + Apps/Maps/docs/PLAN.md | 307 + Apps/Maps/docs/images/about.png | Bin 0 -> 31092 bytes Apps/Maps/docs/images/download-frame.png | Bin 0 -> 44277 bytes Apps/Maps/docs/images/follow.png | Bin 0 -> 27163 bytes Apps/Maps/docs/images/map-z14.png | Bin 0 -> 59878 bytes Apps/Maps/docs/images/map-z16.png | Bin 0 -> 28472 bytes Apps/Maps/docs/images/regions.png | Bin 0 -> 10798 bytes Apps/Maps/host/CMakeLists.txt | 41 + Apps/Maps/host/FakeFont.h | 51 + Apps/Maps/host/FakePosition.h | 37 + Apps/Maps/host/ImageDiff.h | 41 + Apps/Maps/host/PlatformHost.cpp | 90 + Apps/Maps/host/Png.cpp | 28 + Apps/Maps/host/Png.h | 7 + Apps/Maps/host/check_symbols.py | 88 + Apps/Maps/host/device_run.py | 122 + Apps/Maps/host/fetch_tiles.py | 245 + Apps/Maps/host/prepare_tiles.cpp | 178 + Apps/Maps/host/push_region.py | 42 + Apps/Maps/host/render_tile.cpp | 130 + Apps/Maps/host/tests.cpp | 2897 ++++++ Apps/Maps/host/third_party/miniz/LICENSE | 22 + Apps/Maps/host/third_party/miniz/miniz.c | 7833 +++++++++++++++++ Apps/Maps/host/third_party/miniz/miniz.h | 1422 +++ Apps/Maps/host/ui_preview.cpp | 213 + Apps/Maps/main/CMakeLists.txt | 8 + Apps/Maps/main/Source/app/BinFontText.cpp | 154 + Apps/Maps/main/Source/app/BinFontText.h | 48 + Apps/Maps/main/Source/app/Debug.cpp | 144 + Apps/Maps/main/Source/app/Debug.h | 49 + Apps/Maps/main/Source/app/DisplayOutput.cpp | 115 + Apps/Maps/main/Source/app/DisplayOutput.h | 45 + Apps/Maps/main/Source/app/Downloader.cpp | 1015 +++ Apps/Maps/main/Source/app/Downloader.h | 169 + Apps/Maps/main/Source/app/Input.cpp | 131 + Apps/Maps/main/Source/app/Input.h | 54 + Apps/Maps/main/Source/app/IoTask.cpp | 212 + Apps/Maps/main/Source/app/IoTask.h | 92 + Apps/Maps/main/Source/app/LocationSource.cpp | 42 + Apps/Maps/main/Source/app/LocationSource.h | 32 + Apps/Maps/main/Source/app/MapApp.cpp | 91 + Apps/Maps/main/Source/app/MapInput.cpp | 485 + Apps/Maps/main/Source/app/MapScreens.cpp | 455 + Apps/Maps/main/Source/app/MapView.cpp | 739 ++ Apps/Maps/main/Source/app/MapView.h | 232 + .../main/Source/app/PlatformTactility.cpp | 117 + Apps/Maps/main/Source/app/RootCertificates.h | 103 + Apps/Maps/main/Source/app/StallProbe.cpp | 92 + Apps/Maps/main/Source/app/StallProbe.h | 51 + Apps/Maps/main/Source/app/TileWorker.cpp | 791 ++ Apps/Maps/main/Source/app/TileWorker.h | 197 + Apps/Maps/main/Source/mvt/ByteInput.cpp | 48 + Apps/Maps/main/Source/mvt/ByteInput.h | 105 + Apps/Maps/main/Source/mvt/InflateReader.cpp | 242 + Apps/Maps/main/Source/mvt/InflateReader.h | 71 + Apps/Maps/main/Source/mvt/MvtDecoder.cpp | 472 + Apps/Maps/main/Source/mvt/MvtDecoder.h | 95 + Apps/Maps/main/Source/mvt/Schema.cpp | 63 + Apps/Maps/main/Source/mvt/Schema.h | 67 + Apps/Maps/main/Source/platform/Platform.h | 36 + Apps/Maps/main/Source/platform/Position.h | 40 + Apps/Maps/main/Source/render/CacheIndex.cpp | 157 + Apps/Maps/main/Source/render/CacheIndex.h | 77 + Apps/Maps/main/Source/render/CacheWalker.cpp | 239 + Apps/Maps/main/Source/render/CacheWalker.h | 66 + Apps/Maps/main/Source/render/Compose.cpp | 96 + Apps/Maps/main/Source/render/Compose.h | 29 + .../main/Source/render/PreparedScanner.cpp | 183 + .../Maps/main/Source/render/PreparedScanner.h | 46 + Apps/Maps/main/Source/render/PreparedTile.cpp | 493 ++ Apps/Maps/main/Source/render/PreparedTile.h | 163 + Apps/Maps/main/Source/render/Raster.cpp | 340 + Apps/Maps/main/Source/render/Raster.h | 67 + Apps/Maps/main/Source/render/Style.cpp | 456 + Apps/Maps/main/Source/render/Style.h | 93 + Apps/Maps/main/Source/render/TileCache.cpp | 131 + Apps/Maps/main/Source/render/TileCache.h | 57 + Apps/Maps/main/Source/render/TileCodec.cpp | 81 + Apps/Maps/main/Source/render/TileCodec.h | 17 + Apps/Maps/main/Source/render/TileRenderer.cpp | 409 + Apps/Maps/main/Source/render/TileRenderer.h | 106 + Apps/Maps/main/Source/tiles/DownloadPlan.cpp | 67 + Apps/Maps/main/Source/tiles/DownloadPlan.h | 39 + Apps/Maps/main/Source/tiles/IoQueue.h | 77 + Apps/Maps/main/Source/tiles/RegionMeta.cpp | 154 + Apps/Maps/main/Source/tiles/RegionMeta.h | 34 + Apps/Maps/main/Source/tiles/TileJson.cpp | 81 + Apps/Maps/main/Source/tiles/TileJson.h | 22 + Apps/Maps/main/Source/tiles/TileMath.cpp | 89 + Apps/Maps/main/Source/tiles/TileMath.h | 62 + Apps/Maps/main/Source/tiles/TilePaths.cpp | 65 + Apps/Maps/main/Source/tiles/TilePaths.h | 35 + Apps/Maps/main/Source/tiles/TileStore.cpp | 151 + Apps/Maps/main/Source/tiles/TileStore.h | 73 + Apps/Maps/main/Source/ui/Canvas.cpp | 228 + Apps/Maps/main/Source/ui/Canvas.h | 66 + Apps/Maps/main/Source/ui/Controls.cpp | 184 + Apps/Maps/main/Source/ui/Controls.h | 105 + Apps/Maps/main/Source/ui/DownloadFrame.cpp | 82 + Apps/Maps/main/Source/ui/DownloadFrame.h | 37 + Apps/Maps/main/Source/ui/Gestures.cpp | 114 + Apps/Maps/main/Source/ui/Gestures.h | 88 + Apps/Maps/main/Source/ui/MapOverlay.cpp | 208 + Apps/Maps/main/Source/ui/MapOverlay.h | 56 + Apps/Maps/main/Source/ui/PositionTracker.cpp | 52 + Apps/Maps/main/Source/ui/PositionTracker.h | 43 + Apps/Maps/main/Source/ui/Settings.cpp | 148 + Apps/Maps/main/Source/ui/Settings.h | 54 + Apps/Maps/main/Source/ui/Widgets.cpp | 316 + Apps/Maps/main/Source/ui/Widgets.h | 135 + Apps/Maps/manifest.properties | 11 + Apps/Maps/tests/fixtures/14-9058-5729.pbf | Bin 0 -> 232636 bytes Apps/Maps/tests/fixtures/golden-14.png | Bin 0 -> 49798 bytes Apps/Maps/tests/fixtures/golden-16.png | Bin 0 -> 26741 bytes Apps/Maps/tests/fixtures/golden-18.png | Bin 0 -> 7125 bytes Apps/Maps/tests/fixtures/golden.txt | 3 + 122 files changed, 28013 insertions(+) create mode 100644 Apps/Maps/CHANGELOG.md create mode 100644 Apps/Maps/CMakeLists.txt create mode 100644 Apps/Maps/LICENSE create mode 100644 Apps/Maps/README.md create mode 100644 Apps/Maps/THIRD_PARTY.md create mode 100644 Apps/Maps/docs/MEASUREMENTS.md create mode 100644 Apps/Maps/docs/PLAN.md create mode 100644 Apps/Maps/docs/images/about.png create mode 100644 Apps/Maps/docs/images/download-frame.png create mode 100644 Apps/Maps/docs/images/follow.png create mode 100644 Apps/Maps/docs/images/map-z14.png create mode 100644 Apps/Maps/docs/images/map-z16.png create mode 100644 Apps/Maps/docs/images/regions.png create mode 100644 Apps/Maps/host/CMakeLists.txt create mode 100644 Apps/Maps/host/FakeFont.h create mode 100644 Apps/Maps/host/FakePosition.h create mode 100644 Apps/Maps/host/ImageDiff.h create mode 100644 Apps/Maps/host/PlatformHost.cpp create mode 100644 Apps/Maps/host/Png.cpp create mode 100644 Apps/Maps/host/Png.h create mode 100644 Apps/Maps/host/check_symbols.py create mode 100644 Apps/Maps/host/device_run.py create mode 100644 Apps/Maps/host/fetch_tiles.py create mode 100644 Apps/Maps/host/prepare_tiles.cpp create mode 100644 Apps/Maps/host/push_region.py create mode 100644 Apps/Maps/host/render_tile.cpp create mode 100644 Apps/Maps/host/tests.cpp create mode 100644 Apps/Maps/host/third_party/miniz/LICENSE create mode 100644 Apps/Maps/host/third_party/miniz/miniz.c create mode 100644 Apps/Maps/host/third_party/miniz/miniz.h create mode 100644 Apps/Maps/host/ui_preview.cpp create mode 100644 Apps/Maps/main/CMakeLists.txt create mode 100644 Apps/Maps/main/Source/app/BinFontText.cpp create mode 100644 Apps/Maps/main/Source/app/BinFontText.h create mode 100644 Apps/Maps/main/Source/app/Debug.cpp create mode 100644 Apps/Maps/main/Source/app/Debug.h create mode 100644 Apps/Maps/main/Source/app/DisplayOutput.cpp create mode 100644 Apps/Maps/main/Source/app/DisplayOutput.h create mode 100644 Apps/Maps/main/Source/app/Downloader.cpp create mode 100644 Apps/Maps/main/Source/app/Downloader.h create mode 100644 Apps/Maps/main/Source/app/Input.cpp create mode 100644 Apps/Maps/main/Source/app/Input.h create mode 100644 Apps/Maps/main/Source/app/IoTask.cpp create mode 100644 Apps/Maps/main/Source/app/IoTask.h create mode 100644 Apps/Maps/main/Source/app/LocationSource.cpp create mode 100644 Apps/Maps/main/Source/app/LocationSource.h create mode 100644 Apps/Maps/main/Source/app/MapApp.cpp create mode 100644 Apps/Maps/main/Source/app/MapInput.cpp create mode 100644 Apps/Maps/main/Source/app/MapScreens.cpp create mode 100644 Apps/Maps/main/Source/app/MapView.cpp create mode 100644 Apps/Maps/main/Source/app/MapView.h create mode 100644 Apps/Maps/main/Source/app/PlatformTactility.cpp create mode 100644 Apps/Maps/main/Source/app/RootCertificates.h create mode 100644 Apps/Maps/main/Source/app/StallProbe.cpp create mode 100644 Apps/Maps/main/Source/app/StallProbe.h create mode 100644 Apps/Maps/main/Source/app/TileWorker.cpp create mode 100644 Apps/Maps/main/Source/app/TileWorker.h create mode 100644 Apps/Maps/main/Source/mvt/ByteInput.cpp create mode 100644 Apps/Maps/main/Source/mvt/ByteInput.h create mode 100644 Apps/Maps/main/Source/mvt/InflateReader.cpp create mode 100644 Apps/Maps/main/Source/mvt/InflateReader.h create mode 100644 Apps/Maps/main/Source/mvt/MvtDecoder.cpp create mode 100644 Apps/Maps/main/Source/mvt/MvtDecoder.h create mode 100644 Apps/Maps/main/Source/mvt/Schema.cpp create mode 100644 Apps/Maps/main/Source/mvt/Schema.h create mode 100644 Apps/Maps/main/Source/platform/Platform.h create mode 100644 Apps/Maps/main/Source/platform/Position.h create mode 100644 Apps/Maps/main/Source/render/CacheIndex.cpp create mode 100644 Apps/Maps/main/Source/render/CacheIndex.h create mode 100644 Apps/Maps/main/Source/render/CacheWalker.cpp create mode 100644 Apps/Maps/main/Source/render/CacheWalker.h create mode 100644 Apps/Maps/main/Source/render/Compose.cpp create mode 100644 Apps/Maps/main/Source/render/Compose.h create mode 100644 Apps/Maps/main/Source/render/PreparedScanner.cpp create mode 100644 Apps/Maps/main/Source/render/PreparedScanner.h create mode 100644 Apps/Maps/main/Source/render/PreparedTile.cpp create mode 100644 Apps/Maps/main/Source/render/PreparedTile.h create mode 100644 Apps/Maps/main/Source/render/Raster.cpp create mode 100644 Apps/Maps/main/Source/render/Raster.h create mode 100644 Apps/Maps/main/Source/render/Style.cpp create mode 100644 Apps/Maps/main/Source/render/Style.h create mode 100644 Apps/Maps/main/Source/render/TileCache.cpp create mode 100644 Apps/Maps/main/Source/render/TileCache.h create mode 100644 Apps/Maps/main/Source/render/TileCodec.cpp create mode 100644 Apps/Maps/main/Source/render/TileCodec.h create mode 100644 Apps/Maps/main/Source/render/TileRenderer.cpp create mode 100644 Apps/Maps/main/Source/render/TileRenderer.h create mode 100644 Apps/Maps/main/Source/tiles/DownloadPlan.cpp create mode 100644 Apps/Maps/main/Source/tiles/DownloadPlan.h create mode 100644 Apps/Maps/main/Source/tiles/IoQueue.h create mode 100644 Apps/Maps/main/Source/tiles/RegionMeta.cpp create mode 100644 Apps/Maps/main/Source/tiles/RegionMeta.h create mode 100644 Apps/Maps/main/Source/tiles/TileJson.cpp create mode 100644 Apps/Maps/main/Source/tiles/TileJson.h create mode 100644 Apps/Maps/main/Source/tiles/TileMath.cpp create mode 100644 Apps/Maps/main/Source/tiles/TileMath.h create mode 100644 Apps/Maps/main/Source/tiles/TilePaths.cpp create mode 100644 Apps/Maps/main/Source/tiles/TilePaths.h create mode 100644 Apps/Maps/main/Source/tiles/TileStore.cpp create mode 100644 Apps/Maps/main/Source/tiles/TileStore.h create mode 100644 Apps/Maps/main/Source/ui/Canvas.cpp create mode 100644 Apps/Maps/main/Source/ui/Canvas.h create mode 100644 Apps/Maps/main/Source/ui/Controls.cpp create mode 100644 Apps/Maps/main/Source/ui/Controls.h create mode 100644 Apps/Maps/main/Source/ui/DownloadFrame.cpp create mode 100644 Apps/Maps/main/Source/ui/DownloadFrame.h create mode 100644 Apps/Maps/main/Source/ui/Gestures.cpp create mode 100644 Apps/Maps/main/Source/ui/Gestures.h create mode 100644 Apps/Maps/main/Source/ui/MapOverlay.cpp create mode 100644 Apps/Maps/main/Source/ui/MapOverlay.h create mode 100644 Apps/Maps/main/Source/ui/PositionTracker.cpp create mode 100644 Apps/Maps/main/Source/ui/PositionTracker.h create mode 100644 Apps/Maps/main/Source/ui/Settings.cpp create mode 100644 Apps/Maps/main/Source/ui/Settings.h create mode 100644 Apps/Maps/main/Source/ui/Widgets.cpp create mode 100644 Apps/Maps/main/Source/ui/Widgets.h create mode 100644 Apps/Maps/manifest.properties create mode 100644 Apps/Maps/tests/fixtures/14-9058-5729.pbf create mode 100644 Apps/Maps/tests/fixtures/golden-14.png create mode 100644 Apps/Maps/tests/fixtures/golden-16.png create mode 100644 Apps/Maps/tests/fixtures/golden-18.png create mode 100644 Apps/Maps/tests/fixtures/golden.txt diff --git a/Apps/Maps/CHANGELOG.md b/Apps/Maps/CHANGELOG.md new file mode 100644 index 0000000..a23f578 --- /dev/null +++ b/Apps/Maps/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog + +## 0.0.1 (proof of concept, not released yet) + +The first public version: an offline vector map for Tactility, tested on the LilyGO T-Deck. + +**Map** +- Fullscreen map drawn by the app with LVGL stopped. LVGL comes back on every exit. +- OpenFreeMap vector tiles (OpenMapTiles schema) from the SD card: water, land cover and use, parks, buildings, + waterways, roads with casings, railways, boundaries and airports, anti-aliased. +- Zoom 0-19. Levels above 14 are drawn from the z14 vectors, so they stay sharp. +- Pan and zoom by keys (W A S D, I / O), trackball, drag, pinch, double tap and the + / − buttons. +- Your position from Tactility's location service, with an accuracy circle, a direction arrow while moving and + follow mode (G or ⌖). The app never changes location settings. +- Scale bar, and the attribution: shown in full for 5 s, then a © mark that opens it again and leads to + About and licences. +- Hidden controls (☰, + / −, ⌖) that show with a tap, Space or a click, with key hints on keyboards and a + pointer for the trackball in mouse mode. +- Opens at ESA ESTEC the first time, then where you left it. + +**Downloads** +- Download area: a fixed frame in the middle, the map moves and zooms under it. A live tile count, size estimate + and the highest zoom that fits OpenFreeMap's 3,000-tile / 50 MB limit. +- The downloader uses OpenFreeMap's TileJSON, 2 keep-alive HTTPS connections and its own User-Agent. Tiles are + written as `.tmp` and renamed. Downloads resume after a restart, areas share tiles, and the download stops at + 50 MB of real data. +- Download while browsing: with Wi-Fi, tiles on screen that have no detail yet are fetched one at a time and kept + in the region "browsed". +- Regions screen: size, date and status, with delete (tiles other regions use are kept). + +**Speed** +- Prepared tiles (`.prt`) next to each `.pbf`: only what the style draws, quantised to 2048 units per tile, + deflated. About a fifth of the size, half the render time. Converted in idle time. +- One I/O task for all SD card access, tiles on screen first. Two render lanes, a PSRAM cache of 32 rendered + tiles, and prefetch only after a second without input. +- Cold first z14 screen in dense central Budapest: 1.8-2.0 s on the T-Deck (5.6 s in the first device build). + +**Known limits** +- At most 3,000 tiles or 50 MB per download. No labels yet. Touch tested on the T-Deck only. No automatic + region updates yet. diff --git a/Apps/Maps/CMakeLists.txt b/Apps/Maps/CMakeLists.txt new file mode 100644 index 0000000..48e3904 --- /dev/null +++ b/Apps/Maps/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(maps) +project(maps) +tactility_project_post(maps) diff --git a/Apps/Maps/LICENSE b/Apps/Maps/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/Apps/Maps/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU 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. But first, please read +. diff --git a/Apps/Maps/README.md b/Apps/Maps/README.md new file mode 100644 index 0000000..74862a6 --- /dev/null +++ b/Apps/Maps/README.md @@ -0,0 +1,167 @@ +# Tactility Maps + +Offline vector maps for [Tactility](https://github.com/TactilityProject/Tactility) devices. The device +downloads an area from [OpenFreeMap](https://openfreemap.org/) by itself and draws it fullscreen, with +anti-aliased roads, water, parks and buildings, from tiles on the SD card. + +Version 0.0.1, a proof of concept, tested on a LilyGO T-Deck (ESP32-S3, 8 MB PSRAM). Changes are listed in +[CHANGELOG.md](CHANGELOG.md). + +## Install + +Needs Tactility 0.8.0 or newer on an ESP32-S3 board with PSRAM, a FAT-formatted SD card, and Wi-Fi for +downloading maps. Tested on Tactility 0.8.0-dev (main at 0e276da2) on a LilyGO T-Deck. + +1. Install "Maps" from Tactility's App Hub. Or copy the `.app` file to the SD card, open it in the Files app and + confirm "Install". +2. Start "Maps" from the app list. The first time it opens at ESA ESTEC in Noordwijk with no map data yet: move + to your area, open the menu (M, long press or ☰) and choose Download area. + +| Map at z14 | Overzoom z16 | Following the position | +|---|---|---| +| ![Map](docs/images/map-z14.png) | ![Overzoom](docs/images/map-z16.png) | ![Follow](docs/images/follow.png) | + +| Download this area | Regions | About and licences | +|---|---|---| +| ![Download](docs/images/download-frame.png) | ![Regions](docs/images/regions.png) | ![About](docs/images/about.png) | + +Screenshots taken on the T-Deck. Map data © OpenMapTiles © OpenStreetMap contributors. + +## How it works + +- Vector tiles (OpenMapTiles schema) are stored one file per tile in `maps////.pbf` on the SD card. +- The map stops LVGL and draws with Tactility's `pixel_buffer` API, like the built-in Terminal app. +- Tiles are inflated and decoded as a stream, never held whole in memory, and drawn with an exact-area + anti-aliasing rasteriser. Two render tasks (one per core) fill a PSRAM cache. Rendered tiles can also be + cached on the SD card (a setting, off by default). +- Zoom levels above 14 are drawn from the z14 vectors, so they stay sharp. +- Each downloaded tile also gets a prepared copy next to it (`.prt`): only what the style draws, with + quantised coordinates, grouped by draw order. It is about a fifth of the original's size and renders faster. + The original `.pbf` is never changed; it keeps names and house numbers for later features. +- One task does all SD card reads and writes, tiles on screen first. Tiles around the screen are only + prepared after a second without input, and any input cancels them. +- The position comes only from Tactility's location service. The app never changes location settings. +- Menus, lists and dialogs are drawn by the app as well, LVGL stays stopped while the app runs. + +## Using the app + +**Map screen.** The map fills the screen. Always visible: your position (●, with an accuracy +circle), the scale bar, and the © mark. + +| To… | Keyboard | Trackball | Touch | +| --- | --- | --- | --- | +| Move the map | W A S D | Roll | Drag | +| Zoom in / out | I / O | — | Pinch, double tap to zoom in, or the + / − buttons | +| Show / hide controls | Space | Click on empty map | Tap the map | +| Centre on my position and follow | G | — | ⌖ button | +| Stop following | Move the map | Roll | Drag | +| Menu | M | Long click | ☰ button, or long press | +| Download this area | Shift + D | — | Menu → Download area | +| Credits | — | Click © | Tap ©, tap again for About and licences | +| Leave the app | Esc or Backspace | Menu → Exit | Menu → Exit | + +**Controls.** Hidden by default. When shown: ☰ menu top left, + and − in the middle of the right edge, ⌖ +position bottom right above the scale bar. With a keyboard each button shows its key. ⌖ is filled while +following, an outline when not, and crossed out when there is no position. With the trackball in mouse +mode a pointer appears while the controls show; roll to a button and click it, click on empty map to hide +them. On boards with buttons only (no keyboard, no touch), the buttons move the focus through the controls +and press the focused one. + +**Menu:** Download area, Regions, Settings, About and licences, Exit. W/S, trackball or finger to +move; Enter, click or tap to choose; Esc, Backspace or tap outside to close. + +**Downloading:** open Download area. The frame stays in the middle; move and zoom the map under +it. The bar shows tiles, size and max zoom, and updates live. Enter, click or Download starts it; +Esc cancels. It downloads in the background and resumes after a restart; areas share tiles. + +**Regions:** list with size, date and status (complete, downloading, outdated). Select with W/S, +trackball or tap; delete with Del then confirm, or long press then Delete; Esc goes back. +Deleting keeps tiles another region still uses. + +**Browsing:** with Wi-Fi, tiles on screen that have no detail on the card yet are downloaded while you +look at them (a small "Downloading map" label shows) and kept in the region "browsed", so they work +offline later. Without Wi-Fi a short message says that detail is missing here. Only the tiles on screen +are fetched, one at a time. + +**Settings:** SD card cache on or off, prepared tiles deflated or raw, smooth pinch on or off, download +while browsing on or off. Defaults: SD card cache off (switched off, its files are deleted in idle time), +deflated, on, on. The map opens at ESA ESTEC, Noordwijk, the first time, then where you left it. + +**Updates:** after about 30 s without input, outdated regions update quietly and a small +"Updating 42%" label shows. Any input pauses it. Only on Wi-Fi, and only when charging or above +a battery level. *(Planned for M4, not in this version yet.)* + +**Position:** comes from Tactility's location service (GPS or approximate). If it's off, the ⌖ +button is crossed out and pressing it says how to enable it in Tactility's settings. + +## OpenFreeMap's rules + +OpenFreeMap is free and needs no key. Its [terms of service](https://openfreemap.org/tos/) for offline downloads +are enforced in the app: +at most 3,000 tiles and 50 MB per download (the highest zoom level that fits is chosen, the download also +stops at 50 MB of real data), at most 4 parallel requests (the app uses 2), and a User-Agent that names the +app. The tile URL is read from OpenFreeMap's TileJSON. openstreetmap.org tile servers are never used. + +## Known limits + +- One download is at most 3,000 tiles or 50 MB (OpenFreeMap's terms). In dense cities that is only a few hundred + z14 tiles, for example the centre of Budapest. Larger areas need several separate downloads, which you start + yourself. +- No labels yet: no street, place or house names on the map. +- Touch, keyboard and trackball were tested on the LilyGO T-Deck only. Other boards, boards without PSRAM and + monochrome or e-paper displays are untested. +- Regions don't update themselves yet (planned for M4). Delete a region and download it again for newer data. +- Monochrome (1-bit) displays: the map style loses most areas after Tactility's 1-bit conversion (planned for M4). +- The trackball in mouse mode is only lightly tested. + +## Credits + +- Map data: [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, available under the Open + Database License (ODbL). +- Vector tile schema: [OpenMapTiles](https://openmaptiles.org/). +- Tiles served free of charge by [OpenFreeMap](https://openfreemap.org/), see its + [terms](https://openfreemap.org/tos/). +- Built for [Tactility](https://github.com/TactilityProject/Tactility). + +## Attribution + +The full "© OpenMapTiles © OpenStreetMap contributors" shows for 5 seconds when the map opens and then +collapses to a small "©" mark in the bottom left corner. Tapping or clicking it shows the text again for 5 +seconds, tapping the text opens "About and licences", which is also in the menu. This follows the OSMF +attribution guidelines for small screens. + +## Building + +Needs ESP-IDF 6.1 and the Tactility SDK matching the firmware on the device. + +``` +python /tt.py app build +python /tt.py app install --host +python /tt.py app run --host +``` + +The PC build (`host/`, CMake) renders tiles to PNG and runs the unit tests: + +``` +cmake -S host -B host/build +cmake --build host/build --config Release +host/build/Release/render_tile 47.4979 19.0402 14 640 480 out.png +host/build/Release/tests +host/build/Release/prepare_tiles [--write] [--deflate] [--zoom 16] +host/build/Release/ui_preview [--mono] +``` + +`prepare_tiles` converts the tiles of a folder into prepared tiles and compares size, render time and pixels +with the originals (`--write` stores the `.prt` files, which `render_tile --prepared` then draws from). +`ui_preview` draws the app's screens over a fixture tile to PNG, with blocks in place of text. + +`host/fetch_tiles.py` downloads a test area on the PC (same OpenFreeMap rules), `host/push_region.py` +copies it to a device through the development server. + +Measurements on the T-Deck are in [docs/MEASUREMENTS.md](docs/MEASUREMENTS.md), the plan and its decisions in +[docs/PLAN.md](docs/PLAN.md). + +## Licence + +GPL-3.0-or-later, the same as Tactility; see [LICENSE](LICENSE). Third-party code and data are listed in [THIRD_PARTY.md](THIRD_PARTY.md). No code was copied from +the projects listed as prior art in the plan. diff --git a/Apps/Maps/THIRD_PARTY.md b/Apps/Maps/THIRD_PARTY.md new file mode 100644 index 0000000..c32cc8e --- /dev/null +++ b/Apps/Maps/THIRD_PARTY.md @@ -0,0 +1,18 @@ +# Third-party code + +| Component | Where | Licence | Notes | +|---|---|---|---| +| miniz 3.0.2 (Rich Geldreich, RAD Game Tools, Valve Software) | `host/third_party/miniz/` | MIT, see `host/third_party/miniz/LICENSE` | PC build only: inflate and PNG writing for the host tools and tests. The app uses the `tinfl_decompress` exported by Tactility. | + +`main/Source/app/RootCertificates.h` holds the public root certificates of Google Trust Services (GTS Root R1, R4) +and Let's Encrypt (ISRG Root X1, X2), as published at pki.goog and letsencrypt.org, used to verify the HTTPS +connection to OpenFreeMap. + +The rasteriser in `main/Source/render/Raster.cpp` is written for this project. It uses the published signed-area +coverage technique (as described for font-rs and stb_truetype), no code was copied. + +# Map data + +Test fixtures in `tests/fixtures/` are vector tiles from [OpenFreeMap](https://openfreemap.org/), in the +OpenMapTiles schema. © OpenMapTiles © OpenStreetMap contributors. OpenStreetMap data is available under the +Open Database License (ODbL). diff --git a/Apps/Maps/docs/MEASUREMENTS.md b/Apps/Maps/docs/MEASUREMENTS.md new file mode 100644 index 0000000..d512b14 --- /dev/null +++ b/Apps/Maps/docs/MEASUREMENTS.md @@ -0,0 +1,220 @@ +# Measurements + +All on a LilyGO T-Deck (ESP32-S3, 8 MB PSRAM), Tactility 0e276da2, SD card over SPI. +Test area: central Budapest, OpenFreeMap planet 20261025. The z14 tiles there are among the densest +(227-356 KB compressed, 380-610 KB inflated, 1300-1700 drawn features per tile). + +## M1 (2026-10-29), synchronous rendering + +| What | Result | +|---|---| +| LVGL stop / start | 186-238 ms / 806-1067 ms | +| Read one compressed z14 tile from SD | 460-720 ms (about 0.5 MB/s) | +| Render one z14 tile (scan + 3 sweeps) | 750-930 ms (scan 65-100, geometry 360-430, fill 110-130) | +| First z14 screen, 4 tiles, nothing cached | 5.6 s | +| z13 screen, 6 tiles, nothing cached | 3.8 s | +| Overzoomed z15 / z16 screen from z14 data | 4.7 s / 4.6 s | +| Screen from already rendered tiles (pan, zoom back) | 28 ms (compose 4 ms, overlay and display 24 ms) | +| Free memory while the map runs | internal ~117 KB (largest block 31 KB), PSRAM ~4.5 MB with 24 cached tiles | + +Steps that made the difference: the tinfl decompressor in internal RAM instead of PSRAM (scan 580 -> 70 ms), +one read of the compressed file instead of one per pass, filling only the touched columns of each row +(fill 370 -> 120 ms), and no round joins on thin or straight parts of lines. +On the PC the same tiles render in 11-20 ms each. + +## M2 (2026-10-29), interactive map + +Two render lanes (one task per core), PSRAM cache of 24 tiles, SD card cache with run-length coded tiles +(a dense z14 tile codes to 75 % of 128 KB), cache files written only when no visible tile waits. + +| What | Result | +|---|---| +| Pan or zoom over tiles in the PSRAM cache | 35-55 ms per frame | +| z14 screen from the SD card cache (4 tiles) | 1.0 s | +| z14 screen, nothing cached (4 dense tiles) | 5.0 s | +| z15 screen overzoomed from z14, nothing cached | 3.5-4.8 s | +| Read one cached tile from SD | 410-480 ms alone, up to 2.9 s while another lane writes | +| SD card cache write of one tile | 0.5-1.5 s | +| Free memory while the map runs | internal ~64 KB (largest block 31 KB), PSRAM ~2.8-3.2 MB | + +The SD card over SPI (0.25-0.5 MB/s) is the limit for cold and warm starts, rendering a dense tile takes +0.5-0.9 s per lane. Touch gestures (drag, pinch, double tap, long press) are written but not tested yet, +only keys were driven through the debug script. + +## M3 (2026-10-29), download on the device + +| What | Result | +|---|---| +| TileJSON request and parse | about 1.5 s after "Connecting" | +| Small test area, Szeged centre (20 tiles, z0-14) | 1326 KB in 18 s, about 74 KB/s with two connections | +| City frame, Szeged (10.6 x 6.9 km, 74 tiles, z0-14) | complete, 1.1 MB new (20 tiles shared with the first area) | +| Skipping tiles that are already on the card | about 1 s per tile while the map renders (SD card contention) | +| Free internal RAM during a download | about 42-50 KB (mbedTLS buffers are in PSRAM) | + +Size estimate table: mean of 290 real tiles (Budapest, Vienna, London, Split, rural Hungary). Dense city +z14 tiles are 150-260 KB, so the 50 MB limit is reached at roughly 200-370 z14 tiles, long before 3000 tiles. +The downloader stops at 50 MB of real data too, in case the estimate was low. + +Notes: +- Tactility's certificate bundle rejects tiles.openfreemap.org: the server sends GTS Root R4 cross-signed by + GlobalSign Root CA, and the bundle check only looks at the top certificate of the received chain. The app + passes its own roots (GTS R1/R4, ISRG X1/X2) as cert_pem instead. +- The download speed was not measured in detail yet (the serial log was lost in the city run). + +## M3.1 (2026-10-29), host numbers only + +No device in this session: everything below was measured on the PC (Windows, MSVC Release build), on 9 dense +z14 tiles of central Budapest (14/9058-9060/5728-5730, OpenFreeMap planet 20261025, 162-365 KB each). +The PC renders about 50-80 times faster than the T-Deck, so only the ratios carry over. Device numbers are +pending until the T-Deck is back (test list in HANDOFF.md). + +**Prepared tiles (`.prt`) against the originals (`.pbf`), `host/prepare_tiles`:** + +| What | Result | +|---|---| +| Size, raw `.prt` / `.pbf` | 22.4 % (mean 52 KB against 233 KB, per tile 18.8-30.1 %) | +| Size, deflated `.prt` / `.pbf` | 16.1 % (mean 37 KB, per tile 13.5-21.2 %) | +| Features kept | 94-98 % (only what the style draws at z14-z19, smaller than 1 px at z16 dropped) | +| Points kept after 0.5 px (z16) simplification | 89-96 % | +| Render z14 tile, host: `.pbf` / raw `.prt` / deflated `.prt` | 11.0 / 5.1 / 5.2 ms | +| Render z16 overzoom, host: `.pbf` / raw `.prt` / deflated `.prt` | 7.1 / 1.1 / 1.6 ms | +| Conversion of one tile, host | 4.4-4.6 ms (raw and deflated, including tdefl) | +| Pixel difference `.prt` against `.pbf`, z14 | mean 1.4-1.6 of 255, at most 0.02 % of pixels off by more than 64 | +| Pixel difference at z16 / z18 (overzoom, 1024 units per tile) | z16 mean 2.5, 0.63 % strong; z18 mean 2.2, 1.85 % strong | + +On the device the read time should shrink with the size: at the measured 0.5 MB/s a 233 KB `.pbf` took +0.46-0.72 s to read, a 37-52 KB `.prt` should take about 0.07-0.1 s. Whether raw or deflated is faster on the +device (smaller read against an extra inflate of about 200 KB) is the device test's question, the setting +"Prepared tiles" switches between them. + +**Cheaper buildings at native z14** (no anti-aliasing, buildings under 2 px left out), per tile, host: + +| What | Before | After | +|---|---|---| +| Geometry phase, from `.pbf` | 4.60 ms | 4.78 ms | +| Buildings (their geometry and fill), from `.pbf` | 1.86 ms | 1.85 ms | +| Fill phase, from `.pbf` | 1.44 ms | 1.33 ms | +| Geometry phase, from raw `.prt` | 3.85 ms | 4.01 ms | + +On the PC the change is within noise: the anti-aliased coverage maths is cheap on a desktop FPU, and few +central Budapest buildings are under 2 px. The ESP32-S3 pays more for the per-cell float work it skips, so +the device number decides (pending). + +| Device numbers | Status | +|---|---| +| Cold first z14 screen (was 5.0 s) | pending | +| `.prt` against `.pbf` size on the card | pending (host ratio above) | +| `.prt` raw against deflated, read and render time | pending | +| SD raster cache read against rendering from `.prt` (decides whether the SD cache stays) | pending | +| Geometry time with cheaper buildings | pending | +| Worst single tile read while panning (was 2.9 s) | pending | +| Free internal RAM with the new I/O task (6 KB stack) | pending, was ~64 KB while the map runs | + +## M3.1 on the T-Deck (2026-10-29, evening) + +Tactility v0.8.0-dev, same SD card, central Budapest (budapest region, deleted after these runs) and ESA ESTEC. +Logs come from new log lines: "Slow loop", "Slow frame" (lock, compose, overlays, display), "Slow I/O job", +"Stack left". + +**What caused the freezes.** With the first M3.1 build, single frames took 1.4-3.3 s while tiles loaded, and the +render lane on core 0 drew a tile up to 8 times slower than the one on core 1 (3,047 ms against ~400 ms). +Three causes, all fixed: +- the I/O task ran above the app and the lanes, pinned to core 0, and the SD card driver keeps the CPU busy; +- the SD raster cache wrote every rendered tile (0.2-0.9 s each) while the user panned, and on the T-Deck the + display shares the SPI bus with the card, so the frame waited for the write; +- the app's loop had the same priority as two lanes and the I/O task, so it got half a core while tiles loaded. +Later runs also showed slow FAT work: tile lookups in every region folder, a folder scan reading many headers in +one job, deleting 6 cache files per step, and storing downloaded tiles while panning. + +| What | First M3.1 build | Now | +|---|---|---| +| Cold first z14 screen, central Budapest (PSRAM empty) | 5.0 s in M2 | 1.8-2.0 s | +| Cold first z14 screen at ESTEC | | 1.7 s | +| Worst frame while panning | 3.3 s | about 0.3 s | +| Tile read while panning, median (fast pan over 50-110 tiles) | 353 ms | 131-176 ms | +| Worst single tile read while panning | 2.9 s in M2, 1.1 s | 0.25-0.55 s | +| Render of a dense z14 tile from `.prt` | 300-3,047 ms | 300-700 ms (geometry 270-560 ms) | +| Frame while tiles load | | 120-140 ms (36 ms when nothing loads) | + +**`.prt` against `.pbf` on the card** (tiles 14/9058/5727-5731): `.pbf` 992 KB, deflated `.prt` 173 KB, 17.5 %. +The densest tile: 232,636 bytes against 33,646 (14.5 %). + +**Raw against deflated `.prt`**, the same 9 central tiles and view: + +| | raw | deflated | +|---|---|---| +| Read per tile | 157-346 ms | 93-288 ms | +| Inflate | 0 | 11-17 ms | +| Render | 529-678 ms | 544-704 ms | +| First z14 view | 1.84 s | 1.81 s | + +Deflated stays the default: 30 % smaller and a little faster. + +**SD raster cache against rendering from `.prt`.** A cached tile read in 186-580 ms against about 100-150 ms +read plus 300-700 ms render from `.prt`, so the cache saves 0.3-0.5 s per tile on a free core, but it wrote +0.2-0.9 s per tile while the user panned (the freezes) and had grown to 1,122 tiles and 41.9 MB. It is now off by +default; switched off, idle time deletes it (2 files per step). The setting stays. + +**Other numbers** + +| What | Result | +|---|---| +| Free memory, map ready | internal 66 KB, PSRAM 2.0 MB (PSRAM tile cache now 32 tiles, 4 MB) | +| Free memory while tiles render | internal ~51 KB (largest block 31 KB) | +| Stack left, of 8 KB each | app loop 4.8 KB, I/O task 3.6 KB, render lanes 5.6 KB | +| Store one downloaded tile (folders, create, write, rename) | 0.4-0.7 s, now only while the map is left alone | +| Delete the budapest region through the app | 30 s | +| First HTTPS connection after boot | a 1.6-2.0 s pause of all app tasks (also plain drawing code), not on later connections; outside the app, now placed after the first view | + +Not measured on the device yet: cheaper buildings (no A/B run), memory during an area download. + +## M3.2 (2026-10-29), proof-of-concept release + +**Prepared tile precision** (host, `host/prepare_tiles --zoom `, the same 9 dense central Budapest z14 tiles). +With 1024 units per tile a unit is 4 px at z18, and the 0.5 px (at z16) simplification moves points by up to 2 px +at z18, so lines looked stepped at z17-z19. Pixel difference is against rendering the `.pbf` itself, worst tile. + +| Units, simplification | Deflated `.prt` / `.pbf` | Raw | Render z14 / z18, raw | z14 diff (mean, strong) | z18 diff (mean, strong) | +|---|---|---|---|---|---| +| 1024, 0.5 px at z16 (before) | 16.1 % | 22.4 % | 5.3 / 1.0 ms | 1.63, 0.02 % | 2.24, 1.85 % | +| 2048, 0.5 px at z16 | 18.3 % | 23.3 % | 5.2 / 0.9 ms | 1.11, 0.01 % | 2.16, 1.75 % | +| **2048, 0.25 px at z16 (chosen)** | **19.0 %** | **24.3 %** | **5.6 / 0.8 ms** | **1.01, 0.00 %** | **1.75, 0.56 %** | +| 4096, 0.5 px at z16 | 20.9 % | 24.8 % | 5.1 / 0.8 ms | 0.30, 0.00 % | 1.56, 1.45 % | +| 4096, 0.125 px at z16 | 21.8 % | 26.0 % | 5.3 / 1.0 ms | 0.01, 0.00 % | 0.11, 0.00 % | + +Coordinates were already delta-coded zigzag varints, so only the precision changed. 4096 units grow the deflated +files (the default kind) by 30-35 %, over the 25 % budget; 2048 units with a matching simplification (1 px at z18) +grow them by 18 % (mean 37 -> 43 KB), render time stays within noise, and strongly differing pixels at z18 fall +from 1.85 % to 0.56 %. Prepared format version 2: version 1 tiles on the card are redrawn from the `.pbf` and +converted again in idle time. The unit test now renders the fixture tile at z18 (golden-18) and compares the +prepared copy with it: 1024 units fail it (mean 3.18, 2.99 % strong), 2048 pass (1.14, 0.43 %). + +**On the T-Deck (M3.2, developer option `dev_probe=1`: per-second memory, per-core stall probes, task states).** + +| What | Before the fixes | After | +|---|---|---| +| Lowest free internal RAM, download + panning (~4 min) | 183 bytes (largest block 112) | 19.3 KB (largest 9.7 KB) | +| Lowest free PSRAM, same run | 60 KB | 301 KB | +| Failed display transfers ("spi transmit color failed") | 352 | 0 | +| Render lane stack left (tdefl during conversion) | 600 of 8 KB | 2.6 of 10 KB | + +Causes: display strips in PSRAM made the SPI driver allocate a 15 KB internal DMA copy per strip (failed when RAM was +short: the flicker during downloads); network task stacks (28 KB) and three TLS clients in internal RAM; download +bodies kept at their largest size; two conversions at once (~0.4 MB PSRAM each); 32 cached tiles. +Fixes: internal DMA strips of 12 rows, network task stacks in PSRAM on core 1, bodies freed after use, one conversion at +a time, 28 cached tiles, 10 KB lane stacks. + +**Cheaper buildings at native z14, A/B** (central Budapest, 4 views, fresh start each): geometry phase from `.prt` +388 ms per tile with cheaper buildings (18 tiles) against 408 ms without (24 tiles), about 5 % less. First-view times +(0.75-4.0 s) vary more with what the card is doing than with this change. Kept. + +**First HTTPS connection** (see docs/upstream/https-stall.md): DNS and TCP 21-43 ms, TLS handshake 2.0-3.6 s (12.6 s +once while the card was busy). On core 0 it held the map loop and render lane there for its whole length; on core 1 no +stall was seen. Network tasks now run on core 1. + +**Monochrome palette**, forced on the T-Deck (`dev_mono=1`) and thresholded like Tactility's 1-bit conversion +(luma >= 128 is white): menus and text read well, but most of the map disappears in 1-bit: buildings, parks and wood +are light greys above the threshold, and anti-aliased thin casings turn partly white. Greyscale displays are fine. +A 1-bit style (no anti-aliasing, dark fills or outlines) is M4 work. + +Not measured: location switched off and on (needs the user), the 30-minute soak. diff --git a/Apps/Maps/docs/PLAN.md b/Apps/Maps/docs/PLAN.md new file mode 100644 index 0000000..159718f --- /dev/null +++ b/Apps/Maps/docs/PLAN.md @@ -0,0 +1,307 @@ +# Tactility Maps — Project Plan + +Oct 28, 2026 · @Adam + +## Goals and constraints + +Tactility Maps is a standalone, offline-first vector map app, shipped as an external app: the device downloads a region as individual tile files from OpenFreeMap, then renders it fullscreen with LVGL stopped. + +- **Standalone, no hosting cost:** the device fetches its own maps over Wi-Fi from [OpenFreeMap](https://openfreemap.org/), a free public tile server. We host nothing. +- **Standard, stable data:** MVT tiles in the [OpenMapTiles schema](https://openmaptiles.org/schema/), stored on SD as one small file per tile (`z/x/y.pbf`). +- **Fullscreen, no LVGL:** as the maintainer suggested, the app stops the LVGL module and draws through the `pixel_buffer` API, like the built-in Terminal app does. +- **External app:** built with the Tactility SDK as an ELF app, not built into the firmware. +- **Hardware:** ESP32-S3 with PSRAM as the main target, and must also run on boards without PSRAM (Cardputer). E-paper refresh is handled by Tactility; the app only switches to a high-contrast palette when the display is monochrome or greyscale. +- **Licence:** Tactility is GPL-3.0, so borrowed code must be GPL-compatible (Apache-2.0, MIT, BSD are fine). + +## User experience + +The target is a full-quality app on a T-Deck-sized screen (320×240, keyboard, trackball, touch): the map fills the screen, and anything else appears only while it is needed. + +**Screen mock-ups (illustrative values):** + +``` + Map view Download this area ++------------------------------------+ +------------------------------------+ +| (Updating 42%) | |########## dimmed ##################| +| | |####+------------------------+######| +| roads / water / land | |####| |######| +| ▲ | |####| area to download |######| +| ● (you) | |####| |######| +| | |####+------------------------+######| +| © OpenMapTiles © OSM |200 m| | | 1,240 tiles · ~38 MB [Download] | ++------------------------------------+ | max zoom 14 · fits the limit | + Only marker, scale and attribution +------------------------------------+ + stay on screen. The update pill shows The frame stays in the middle, the map + only while updating. moves and zooms under it. Tile count + and size update live; Enter downloads. +``` + +**Principles** + +- **The map is the UI.** No toolbar, no permanent buttons. Always on screen: position marker, scale bar and the attribution, which collapses to a small "©" mark after 5 s (see Decisions). +- **Instant feedback.** Panning moves already-rendered pixels at once; missing tiles show a light placeholder and fill in when ready. Nothing ever waits on the SD card or the network. +- **One short menu** (`M` key, ☰ button or long press): Download area, Regions, Settings, About and licences, Exit. Lists fit on one screen, about 6 rows at 240 px. +- **Quiet background work.** Idle updates show a small pill only while they run. +- **Readable on small screens.** Text at least 12 px (Tactility's IBM Plex Mono from graphics-module), high contrast, monochrome palette when the display needs it. +- **Every action works with all three inputs**, so it also works on Cardputer (keys only) and touch-only boards. + +**Controls** (the manual, as the app works since M3.1) + +**Map screen.** The map fills the screen. Always visible: your position (●, with an accuracy +circle), the scale bar, and the © mark. + +| To… | Keyboard | Trackball | Touch | +| --- | --- | --- | --- | +| Move the map | W A S D | Roll | Drag | +| Zoom in / out | I / O | — | Pinch, double tap to zoom in, or the + / − buttons | +| Show / hide controls | Space | Click on empty map | Tap the map | +| Centre on my position and follow | G | — | ⌖ button | +| Stop following | Move the map | Roll | Drag | +| Menu | M | Long click | ☰ button, or long press | +| Download this area | Shift + D | — | Menu → Download area | +| Credits | — | Click © | Tap ©, tap again for About and licences | +| Leave the app | Esc or Backspace | Menu → Exit | Menu → Exit | + +**Controls.** Hidden by default. When shown: ☰ menu top left, + and − in the middle of the right edge, ⌖ +position bottom right above the scale bar. With a keyboard each button shows its key. ⌖ is filled while +following, an outline when not, and crossed out when there is no position. With the trackball in mouse +mode a pointer appears while the controls show; roll to a button and click it, click on empty map to hide +them. On boards with buttons only (no keyboard, no touch), the buttons move the focus through the controls +and press the focused one. + +**Menu:** Download area, Regions, Settings, About and licences, Exit. W/S, trackball or finger to +move; Enter, click or tap to choose; Esc, Backspace or tap outside to close. + +**Downloading:** open Download area. The frame stays in the middle; move and zoom the map under +it. The bar shows tiles, size and max zoom, and updates live. Enter, click or Download starts it; +Esc cancels. It downloads in the background and resumes after a restart; areas share tiles. + +**Regions:** list with size, date and status (complete, downloading, outdated). Select with W/S, +trackball or tap; delete with Del then confirm, or long press then Delete; Esc goes back. +Deleting keeps tiles another region still uses. + +**Browsing:** with Wi-Fi, tiles on screen that have no detail on the card yet are downloaded while you +look at them (a small "Downloading map" label shows) and kept in the region "browsed", so they work +offline later. Without Wi-Fi a short message says that detail is missing here. Only the tiles on screen +are fetched, one at a time. + +**Settings:** SD card cache on or off, prepared tiles deflated or raw, smooth pinch on or off, download +while browsing on or off. Defaults: SD card cache off (switched off, its files are deleted in idle time), +deflated, on, on. The map opens at ESA ESTEC, Noordwijk. + +**Updates:** after about 30 s without input, outdated regions update quietly and a small +"Updating 42%" label shows. Any input pauses it. Only on Wi-Fi, and only when charging or above +a battery level. *(Planned for M4, not in this version yet.)* + +**Position:** comes from Tactility's location service (GPS or approximate). If it's off, the ⌖ +button is crossed out and pressing it says how to enable it in Tactility's settings. + +The T-Deck keyboard has no arrow keys, which is why panning uses WASD and zoom uses I/O. Lower case d pans, so the download frame opens with Shift+D or from the menu. + +## Prior art: what to take from each + +The best code base to borrow from is **m5-cardputer-offgrid-tiny-map**: it already streams planet PMTiles + MVT on an ESP32-S3, and it is Apache-2.0, so it is GPL-compatible. OrcMaps and mapnavigation are best used for design ideas. + +| Project | Data | Rendering | Licence | What we take | +| --- | --- | --- | --- | --- | +| [m5-cardputer-offgrid-tiny-map](https://github.com/yuiseki/m5-cardputer-offgrid-tiny-map) | Planet PMTiles + MVT on SD, z0–14 | Streaming decode, each tile rendered once and cached on SD as RGB565 | Apache-2.0 | Code: streaming inflate, streaming MVT decoder, tile cache. Works with no PSRAM | +| [OrcMaps](https://github.com/hardcoreerik/OrcMaps) | PMTiles v3 + MVT | Decode into a feature model, then style + render | AGPL-3.0 | Ideas only: format decision write-up, pack manifest, style system, measured tile sizes | +| [mapnavigation](https://github.com/chaitanyaasati/mapnavigation) | Custom VT01 tiles over HTTP | Anti-aliased coverage rasteriser, casing then fill | No licence file | Ideas only: rasteriser technique, draw passes, label collision. Code can't be copied without a licence | +| [IceNav-v3](https://github.com/jgauchia/IceNav-v3) + [Tile-Generator](https://github.com/jgauchia/Tile-Generator) | Custom NAV tiles + ROUTE.bin | LovyanGFX | GPL-3.0 | Later: offline A* routing graph format | + +**Key numbers from these projects:** + +- mapnavigation renders a full screen in 30–120 ms at z10–12 and 350–600 ms at dense z14–16 on an ESP32-S3 with 8 MB PSRAM. +- The Cardputer project's first render of a Tokyo view took about 16 s, then about 0.3 s once tiles were cached. That is with no PSRAM and gzip inflated on the fly. +- OrcMaps measured busy z14 tiles at up to 142 KB decompressed; decoding into a full feature model grew one tile to about 1.2 MiB. So we should draw while decoding, not build a feature tree. +- mapnavigation tried LVGL's ThorVG backend and dropped it (about 1 ms and 8 KB heap per polyline). Another reason to bypass LVGL. + +**Voice commands** in mapnavigation are cloud-based (Groq Whisper + a hosted LLM). Only the final place lookup runs on the device. Out of scope for now. + +## Architecture + +The map view is a streaming pipeline that never holds a whole tile in RAM; the rendered-tile cache is what makes panning fast. + +**Pipeline (tiles are downloaded once, rendered once, then served from cache):** + +``` +DOWNLOAD (Wi-Fi, normal LVGL screen) + [OpenFreeMap] --> [Region downloader] --> [SD card] + free, OpenMapTiles schema one GET per tile, keep-alive maps/region/z/x/y.pbf + ≤3,000 tiles per download max 4 parallel, own User-Agent + rendered tile cache + | +MAP VIEW (fullscreen, LVGL stopped) v + [MVT decoder] <-- [Streaming inflate] <-- [Tile file reader] + streams one ring at a time tinfl, 32 KB window opens z/x/y.pbf by path + layer, class, geometry no whole tile in RAM overzooms past z14 + | + v + [Rasteriser] --> [TILE CACHE] --> [Compositor] + RGB565, coverage AA PSRAM LRU, then SD viewport, GPS marker + fill, casing, line passes render once, reuse touch, keys, location_get + | + v + [Display] + pixel_buffer_blit to panel + display_draw_bitmap +``` + +On a cache hit the compositor reads straight from the tile cache; only the first view of an area runs the full decode. A rendered 256×256 RGB565 tile is 128 KB, so PSRAM holds a few dozen before older ones spill to SD. + +**Code layout (one ESP-IDF component, testable on a PC):** + +- `tiles/`: tile maths (lat/lon to z/x/y), tile file paths, downloader (OpenFreeMap rules built in) +- `mvt/`: streaming gzip + protobuf reader and geometry command decoder +- `render/`: rasteriser, style table (colour and monochrome palettes), tile cache +- `app/`: Tactility glue (LVGL stop/start, display, touch, keys, location, region picker UI) +- `host/`: PC build that renders a tile to PNG, for tests and style tuning without hardware + +## Tactility integration + +Everything the app needs is already exported to external apps; the built-in Terminal app (`Tactility/Source/app/terminal/main.cpp`) is the template for a fullscreen, no-LVGL app. + +| Need | Tactility API | Notes | +| --- | --- | --- | +| Take over the screen | `module_stop(&lvgl_module)` on start, `module_start(&lvgl_module)` on exit | Exactly what Terminal's `runFullscreen()` does | +| Find the display | `device_get_first_active_by_type(&DISPLAY_TYPE, …)` | Then `display_get_resolution_x/y`, `display_get_color_format` | +| Pick the palette | `display_get_color_format()` | `MONOCHROME` or `GRAYSCALE8` → high-contrast palette; e-paper refresh itself is Tactility's job | +| Draw | `pixel_buffer_create` (PSRAM, 64-byte aligned), `pixel_buffer_get_row`, `pixel_buffer_blit` | Render in RGB565; `blit` converts to the panel's format | +| Push to panel | `display_draw_bitmap(…)` per dirty region, or `display_get_frame_buffer` + `pixel_buffer_wrap` when the driver has 2 hardware buffers | Send a full frame when `DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME` is set | +| Touch | `device_get_first_active_by_type(&TOUCH_TYPE, …)`, `touch_read_data`, `touch_get_touched_points` | Terminal's `TouchInput.cpp` shows the pattern; we add gesture detection | +| Keys | `keyboard_subscribe` / `keyboard_poll` | WASD pan, I/O zoom (see Controls) | +| Position | `location_get`, `location_subscribe` (`struct Location`: lat, lon, accuracy, course, speed) | GPS is set up in Tactility's own GPS settings, so the app needn't touch UART/NMEA | +| Download | `esp_http_client_*` (exported by http-module) | Needed over `http_download_start`, which can't set a User-Agent or reuse one TLS connection across tiles | +| Inflate | `tinfl_decompress` (exported by platform-esp32) | Vector tiles are usually stored gzip-compressed; check the first two bytes and inflate if needed (32 KB window) | +| Storage | SD card, FAT only (FatFs with long names; exFAT is not enabled) | See "Tile storage on FAT" below | + +**Before exiting fullscreen** the app must restore LVGL even on error paths, otherwise the device is left with a dead UI. Terminal guards this with a `lvgl_active` flag. + +**Map, area frame and small overlays** (pill, scale bar, toasts) are drawn by the app itself, so they match the map and need no LVGL. **Menus, the Regions screen, settings and About** are drawn by the app too, with a small toolkit (list, button, toast, confirm dialog): restarting LVGL took 0.8-1.1 s in M1, too slow for opening a menu, so LVGL stays stopped while the app runs (see Decisions). + +## Map data source + +Use OpenFreeMap's public tile server: it is free, needs no API key, and its terms explicitly allow offline downloads in apps, so we host nothing. + +**OpenFreeMap's rules for offline downloads** ([terms](https://openfreemap.org/tos/), [quick start](https://openfreemap.org/quick_start/)): + +- At most 3,000 tiles or 50 MB per download. Larger downloads need permission (info@openfreemap.org). +- At most 4 parallel requests. +- A unique User-Agent identifying the app, e.g. `TactilityMaps/0.1 (+project URL)`. + +**What 3,000 tiles covers.** At z14 a tile is about 2.4 km wide at the equator and less further north, so a city of 20 × 20 km is a few hundred tiles across all zooms. A whole country at z14 is far over the limit. The region picker should show the tile count and lower the max zoom (z12 is 16× fewer tiles than z14) until the region fits, rather than splitting one area into several downloads to dodge the limit. + +**Why OpenMapTiles is the stable choice.** It is a versioned, documented schema that has been in wide use for years, and OpenFreeMap builds its tiles with it. We map its layers (`water`, `landuse`, `transportation`, `building`, `place`, …) through one style table, so a later schema change touches one file. + +**Don't use** openstreetmap.org tile servers for downloads: their [policy](https://operations.osmfoundation.org/policies/vector/) forbids downloading tiles in advance. + +**Attribution:** show "© OpenMapTiles © OpenStreetMap contributors" on the map at all times. + +### Tile storage on FAT + +Tactility mounts SD cards as FAT only, so we store one file per tile in a `z/x/y` folder tree, which keeps every folder small. + +| FAT limit | Value | Effect on us | +| --- | --- | --- | +| Max file size | 4 GB − 1 byte | Irrelevant: a tile is tens of KB | +| Max entries per folder | 65,534 (fewer with long names, which use extra entries) | A `z/x/` folder holds one column of tiles; a city has dozens per folder | +| Folder lookup | Linear scan in FatFs | Small folders keep `fopen` fast | +| Smallest file on disk | One cluster (often 32 KB on large cards) | Small tiles waste space; ~3,000 tiles may waste tens of MB, acceptable | +| Filenames | 8.3 names avoid long-name entries | `maps//14/9076/5636.pbf` fits 8.3 at every level if region names are ≤ 8 chars | + +Rendered-tile cache files (128 KB RGB565 each) use the same `z/x/y` layout under `cache/`. + +## Downloading maps + +The app offers two ways to pick an area, and both end in the same downloader: a list of tiles fetched within OpenFreeMap's limits. + +| Mode | How it works | Strength | Catch | +| --- | --- | --- | --- | +| **Download this area** (Google Maps style, build first) | Frame an area on the map; tile count and size estimate update live as you move or resize it | Exact, fits the 3,000-tile limit naturally | User has to frame the area | +| **Region catalog** (CoMaps style, build second) | Browse continent → country → region → city from a catalog file shipped with the app ([Geofabrik's region index](https://download.geofabrik.de/) as the source) | Familiar, one tap, uses the real outline so sea and neighbours are skipped | Big countries exceed 3,000 tiles at z14, so max zoom drops automatically (e.g. a 50,000 km² country fits to z12, not z13) | + +- **Tile count is exact; size is an estimate.** Single tile files don't report their size in advance, so size = tile count × a typical size per zoom, from a table we measure once from real OpenFreeMap tiles. +- **Max zoom is chosen for the user:** the highest zoom that fits the limit, which they can lower. +- **Downloads run in a background task** with progress, pause and resume; the map stays usable. +- **Regions screen:** list of downloaded regions with size, date and update status; delete or update from there. +- **Overlapping regions share tiles.** A tile already on the card is skipped, and deleting a region keeps tiles another region still uses. + +### Updates while the map is idle + +When the map is open but untouched, the app quietly refreshes outdated regions. + +- Starts after about 30 s with no input; any touch, key or trackball movement pauses it at once, so panning never competes for the SD card, CPU or Wi-Fi. +- One request at a time, tiles near the current view first. +- Each region keeps a `meta.json` with the source version and download date. A newer source version marks the region outdated. OpenFreeMap rebuilds weekly; updating monthly is plenty. +- If OpenFreeMap sends ETag or Last-Modified headers, unchanged tiles cost almost nothing to check (to be tested). +- New tiles are written to `.tmp` and renamed into place; the rendered copy is dropped from the cache and redrawn if on screen. +- The TLS connection (about 40 KB of internal RAM) stays open while updating; it is closed before rendering only if free-RAM measurements show we need to. +- Progress is saved, so it resumes the next time the map sits idle, even after a restart. +- Only on Wi-Fi, and only when charging or above a battery threshold. + +## Milestones + +Build the renderer on a PC first, then put a static map on the device, and only then add downloading; M3 is the point where the app becomes standalone. + +| Milestone | Work | Done when | +| --- | --- | --- | +| **M0 · Host prototype** | Streaming inflate, MVT decoder and tile maths in plain C/C++; rasteriser + style table; PC build renders a tile to PNG; unit tests with real OpenFreeMap tiles | PC render of a sample region looks right | +| **M1 · Static map on device** | External app, fullscreen: stop LVGL, draw with pixel_buffer; reads z/x/y tile files copied to SD by hand; measure render time and LVGL stop/start time | One screen at z14 under 1 s (target) | +| **M2 · Interactive map** | Pan and zoom with touch and keys; tile cache in PSRAM + SD; GPS marker from location_get, follow mode; OSM attribution always visible | Panning a cached area feels instant | +| **M3 · Download on device** | Tile downloads from OpenFreeMap, within its limits; "Download this area" frame with live tile count and size; downloader with resume, progress, max 4 parallel | A city downloads with no PC involved | +| **— PoC release —** | Publish, get the Tactility maintainer's feedback, then email OpenFreeMap about larger downloads | | +| **M4 · Polish and more devices** | Region catalog, idle-time updates, monochrome palette; waypoints, track recording, GPX import and export; smooth zoom animation for +/− and double tap (affine compositor, snap to whole zoom levels); test on T-Deck, Cardputer and a monochrome display | Runs on boards with and without PSRAM | +| **M5 · Navigation** | Online routing (OSRM, Valhalla or GraphHopper); route following, next turn, off-route detection; offline routing graph later (see IceNav ROUTE.bin); map rotation on the same compositor: two-finger rotate, compass button that resets to north, heading-up while following using GPS course, larger tile margin for the ~400 px diagonal | Turn-by-turn on a downloaded region | + +M0 can start right away and needs no hardware. In the end nothing was ported from the Cardputer project: inflate uses miniz's tinfl directly, and the decoder and rasteriser were written for this project, so no Apache NOTICE is needed. Labels are left for after M5. The proof-of-concept release comes after M3; we publish it, get the Tactility maintainer's feedback, then contact OpenFreeMap about larger downloads before continuing. + +## Decisions + +**Decided (29 Oct 2026):** + +- No paid hosting: tiles come from OpenFreeMap, within its offline-download rules. +- External ELF app, not built into the firmware. +- One file per tile in a `z/x/y` tree on FAT, no single big archive. +- E-paper refresh is Tactility's job; the app only swaps to a monochrome palette. +- Stable schema: OpenMapTiles from OpenFreeMap, the only supported tile source. +- Labels come later. +- **Larger regions:** for the proof of concept, lower max zoom automatically for big areas and let users combine downloads (e.g. a country to z12 plus cities to z14). After the PoC release, once the Tactility maintainer likes it, email OpenFreeMap (info@openfreemap.org) to ask for permission above 3,000 tiles. +- **TLS memory:** an HTTPS connection needs about 40 KB of internal RAM. We measure free RAM while the app runs and close the connection before rendering only if memory turns out short. +- **Attribution on a small screen (29 Oct 2026):** the full "© OpenMapTiles © OpenStreetMap contributors" shows for 5 s when the map opens, then collapses to a small "©" mark in the bottom left corner. Tapping it shows the text again, and the menu has "About and licences". The OSMF attribution guidelines allow collapsing after five seconds as long as the licence stays easy to find, OpenMapTiles asks for a visible credit on the map. Attribution only in settings would not be allowed. +- **Licensing:** copy only from Apache/MIT/BSD/GPL code. mapnavigation has no licence, so we write our own rasteriser using the same published technique (signed-area coverage, as in font-rs and stb_truetype). + +**Decided (29 Oct 2026, M3.1):** + +- Prepared tiles: a render-ready `.prt` copy next to the untouched original `.pbf`. +- Position comes only from Tactility's location API; the app never changes location settings. +- UI is drawn by the app; LVGL stays stopped while the app runs. +- SD raster cache off by default (device test, 29 Oct 2026): reading a cached tile saved 0.3-0.5 s against drawing it from its `.prt`, but writing the cache (0.2-0.9 s per tile, on the SPI bus the T-Deck's display shares) froze the screen while panning, and it grew to 42 MB. The setting stays; switched off, idle time deletes it. See MEASUREMENTS.md. +- Download while browsing (user request, 29 Oct 2026): with Wi-Fi, the tiles on screen that lack detail are fetched one at a time into the region "browsed"; offline the map says detail is missing. This is normal map viewing in OpenFreeMap's terms (no limit there), it still uses the app's User-Agent and stays within 4 parallel requests (2 for an area download, 1 for browsing). A setting switches it off. +- Zoom and rotation: in the PoC only pinch is smooth (live scaling of cached pixels, snap to a whole zoom level on release). Animated +/− zoom comes in M4, map rotation in M5, both on one affine compositor that samples cached tiles; vectors are not re-rendered per frame. Off on e-paper. + +## References + +- [Tactility](https://github.com/TactilityProject/Tactility): Terminal app (`Tactility/Source/app/terminal/`), `Modules/graphics-module/include/graphics/pixel_buffer.h`, `TactilityKernel/include/tactility/drivers/display.h`, `Modules/location-module`, `Modules/http-module`, SD mounting in `Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp` +- [Tactility PR #658: pixel_buffer and graphics-module](https://github.com/TactilityProject/Tactility/pull/658) +- [Tactility PR #660: optional DisplayApi frame buffers](https://github.com/TactilityProject/Tactility/pull/660) +- [OpenFreeMap terms of service](https://openfreemap.org/tos/) and [quick start](https://openfreemap.org/quick_start/) +- [OpenMapTiles schema](https://openmaptiles.org/schema/) +- [m5-cardputer-offgrid-tiny-map](https://github.com/yuiseki/m5-cardputer-offgrid-tiny-map) (Apache-2.0), see its HACKSTER.md for the streaming design +- [OrcMaps](https://github.com/hardcoreerik/OrcMaps): `docs/FORMAT_DECISION.md`, `docs/PERFORMANCE.md` +- [mapnavigation](https://github.com/chaitanyaasati/mapnavigation): `maps/raster.cpp`, `tools/tile_format.md` +- [IceNav-v3](https://github.com/jgauchia/IceNav-v3) and [Tile-Generator](https://github.com/jgauchia/Tile-Generator) +- [OSMF vector tile usage policy](https://operations.osmfoundation.org/policies/vector/) + +## Status (29 Oct 2026, PoC release candidate 0.0.1) + +M0-M3 are done and tested on the T-Deck: keys, touch (drag, pinch, double tap, long press, overlay buttons, menus, +download screen) and the trackball in keyboard mode. Decisions as they are now, beyond the list above: +- Prepared tiles use 2048 units per tile, format version 2 (1024 looked stepped at z17-z19, 4096 was 35 % larger). +- Network tasks run on core 1 with their stacks in PSRAM; display strips are internal DMA memory; one tile conversion + at a time; 28 cached tiles. These came from the memory measurements during downloads. +- An approximate (IP) position shows a ring with a "~25 km" label instead of an accuracy circle; GPS is unchanged. +- The map opens at ESA ESTEC the first time, then where it was left (view.txt). +- Developer options only in settings.txt: dev_mono (monochrome palette on any display), dev_probe (stall probes, + memory log, HTTPS timing). + +Moved to M4: 1-bit monochrome style, idle region updates, automatic region names (requested after the touch test), +trackball mouse mode polish, the 30-minute soak test. diff --git a/Apps/Maps/docs/images/about.png b/Apps/Maps/docs/images/about.png new file mode 100644 index 0000000000000000000000000000000000000000..16fdb6df0fb219036404ffbccaf9bf40bcf3cd2c GIT binary patch literal 31092 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 LNkvXXu0mjfoRa$0 literal 0 HcmV?d00001 diff --git a/Apps/Maps/docs/images/download-frame.png b/Apps/Maps/docs/images/download-frame.png new file mode 100644 index 0000000000000000000000000000000000000000..54f698ebcabc1f8c9a79220bf82ff65458d999f0 GIT binary patch literal 44277 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%0AR7^bVvst624X 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 literal 0 HcmV?d00001 diff --git a/Apps/Maps/docs/images/map-z16.png b/Apps/Maps/docs/images/map-z16.png new file mode 100644 index 0000000000000000000000000000000000000000..07bdf0928a85a021eee9e385026cade644bbfa8f GIT binary patch literal 28472 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%0Db~M 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 literal 0 HcmV?d00001 diff --git a/Apps/Maps/docs/images/regions.png b/Apps/Maps/docs/images/regions.png new file mode 100644 index 0000000000000000000000000000000000000000..ec4aeef5e7e06f738a5ac652bb7593f0b35edb0b GIT binary patch literal 10798 zcmX|nWl)?=u=TUJTX1)RyIXLACb(O02rTYl2`(?e-QC@Fan}TQcMGn$-1}91f2L+? zq-UnO&*`eu;a^nb&{2p{002P$EHAAF0H9Cr{Yxa6_x4SdNdy2GZ$C>*Xm~E48M0QB zeomUmuC1|AluA;TQiNscv-oZkrkh?_0Scih?-{i&aM%YWAv9(o==`Vm7ZS|eiICKh z(*ui6&f#ey0g>w{UT55hh=_^`i@F$(P%F_$B+BML5BJ#_9LoID=^QkbEiP71+icEG``<3#J7t}E(-#yD9|G;w}Y4x z*aNlRc1g!DF*R$0Cbe&W-A(gso$lOonzez@61_wasX+f9ImZrK^~ccV4B)PO0~YEA zI?cMRh%0}ejMLP=Vbe`i82?3Tp&a}4sR2dwl4y2_sT(Haqc#bQXC;n)C31kRq``r= zoT)gZ+P|~w4ea;ucKdYq`#Uwn_}|&>RQHR>r7i~asvOorD{j<62h?dnkfgzfXX4!d z{Df)jU+70J`s3zm|C<5H`T2QFU;RZEy6-+bIFGTVp|%+=g?5chh4 zGy4Gg`@tVOkM0umA{BgY&pxzo@@0eENE!*zM1W&zBo+cBbwfD{KK`+JgD*4o`-dWV z8%p61fh1aM|Lf@OBk=bE^;5&Bak_~3Uf=WMD93esJzgUntN*kgdd9Cvp&w!t zdG=`t2aSscq+Vn(@|t%9->5$nZBKIm2FeNyFakSTm0%}B#7?hb#qz%yq>O@yMcAm$ zaZEh1ArxG>o!%awXgI>PrSqfJm?5#NVf5CtHM%G$1IE5P{M-wnfRw(A4~zSOemn=3dy~D3Dg)lJO@J2|-}UCf{-Z=K1#8akvTr=P+d3}_ z7^Ka>HvcBZW>OOKS=szb(HEbOfZGZsiDl%9_h$^k&ho#{$Vr1;ER*C;6ALfwWdZOx z?Eu1@;2iupJ$CEUy^n26G)HiXZfG8jJ^4H#456Quvscx|kC!Vxc7~~X4Ifa8Guj*g zd$HTAo}h2b*`*W52&R$*@~{OqR#~t*s?h>%LT52ho&qPnX#2``1q6so zYHXfrapUsN%t}wlur)KM2pxtEfAl&q#OX!1( z1`{HL#^D&^S>P}(SavuF3(cL@eAHzkgWmqSB~aGUyW1C(E1U&Ls}L%Lodp1UsAgqg zRhhG~Mni)mhDqWx2EK-!bL$TRfm{#e+7D z8n!X`5BaAU*hBPl`QQ50fRJLAO@kRpH`Bw;4H9o=yNgziRjkfZbSJJ^jY`X8_)W9J zi4sihMaw^ZWI$o@Id4-DYyEEkd{K-Iql&8mtLCRfkFupQt&h~_1$4k1z(NHHYqt=f z^ol*<^QS)?Q00XTZ2B&VApg$9FJuW#gC_gl5tYWC2x<#>24VA|o48?^lYjoNxR~(=$&}VhXA@T7Mn-6?)Nc zZC%|s;+ZFI#e?9OOOny=?SFCgmZD*c}d z-lWIB;`M)yqR9VF&(M16dD@+g#x$J`F|$PbmL zUEp3Q?Rn`;4XlhcI1Wq-;aSZ*o@*EOf+q(DPA^A5e_UxH$edKqwzi=HC(n}ZuqFu> zkRcMlvk6xu$S9k8yUe;5OnCZV0|MlV#0d*}_|V?8Ol^XUX=xor93+hdNreS433L8K z3>9#EMfWJ!UIhKA4RC*O@d$ApUbE1dmz22^-Ak72fFzsvZ^+JF=Cesjf_5H_8I)!}}P1Jt?e%lU&psEuCgD&PD|3J&J^qE5gA8}O> zYuGBZK!kXdS9Cw*`;yg0a>kchZcq`~5x(h>AeqQvT#pWurfbFts{B{*64g>585&7| z({HPbJ8$K&$?q%`gWO8c*ZVYh+PC)pv@&_`Z0Acl%V}L4QtzPr)k4ywv`=*$V#<8f z5$;`41%aj`2vSwlkP%UYrjcn}cuC!F0yX&3V$-3fM7*)w{v`Q6aFEh|g~=BZGG#!i z?r@%w9YSVFo@&5?Y)h3UPqMSaw-tAbzSG+L?lQ8lZnvz`!59oJ&B)RfJlW=}hYE4< z(CE%->Q`&cc|oN?H)vdU@TY``uy74(XSWO&mOq$Z!}dn>u;5t`^hSBA2DtS_q;WE} z&0i^fLul*>KL!^@ipYJpVA>07YOo*FB=r-!_>$Wh!4@9Q6n?2|=BJ?p1E0K5Fr;FB zLMTlJ=2?x6FdP@NC~lF9@nVKSCZC1lOpBPu?>t_ofh4my+jEmRqUqIHPYx(``j}JF z4%kG`&kSh1RF+TA|CHX^Vg~U!C*f)DR$qYQu+8VxIMlHI`F9AFiWE*Wrmr2!<|-u zEas|!skP7h)i-p)h=}?WY-gL1D={gY%Wslj%jT&b=ARuhQ4Cq_y**ZtXbglPtzUTB ztlohVc@z5`KK~ap5g_jf83g+OV_75s3{Mg9>{OGdF~#=t*mK_D{E%f;{d1gZygNo- z2fVTu*&-T{e6kPP!X~IreTp5y5OqWq>vlF#;A~>M$VB_g@W6>z2VOe4q@Y#4(tf(Ge@EPv6aIMl`3@ z#=~lDT#AaEAC!V zs1Qf-wYQhCsRur_t&!X61qw$o%UnkjhZ5wx3Fbh^fh7i}GW5f%U_R&S+FBvUNyoA` zMs(TaGm@ZAxP4)F!EKQw96L9u3e3iup?^!);|1OgO(xUvp0!rbPRswNk zQL2hseM!Y<0@#QvNjj( z3-8uAS2MBLSKf5=3T7e_L>$cuA)Jq#QCZE@4usXS(9A#a8!5{6v zcIe{}SS7atQIMH?3des`3N%aFK$Ovp1*#D!uL7e&MeNp?Mmp?@RWS!w%W-v17DhP6g36_?zt&&79S;P>B7QvDSnQ74ycqIKnqREnF=wp@qno9l167S_WD&p4!Guq{KNmH znf8IP)x~`EZ${s>@YX8DL5HNvFu==b{7Y|yN#!x5o5`3@d?wDZg8r+Tg-ll;^lqeziV!q`() zBV;(g98JKR5cU2ywaUf>SmN-(aX(@K!s8;+^y<3sjT$RoHRb%{Z>ORTmitY^-x3z* zHD*7hx=(WM(0App(oHPs5ozFbQi{rO5eIJSkvM)uOa8LM;8Aq(?>392*D0EmhN(e< zlU49NPKGX*;$Cm^^~&4I=%bX?9qx|C0^r!qC- zXngKZ{`y_sa3*L^Dm}ijOtu&)mgt>yrZ-|EGCg$yJv|1D+}U9!!Y|J5W9Vao`)g2a z*GtT{@#Cv>Gm~^O88@1q8PcDdXU2oAAfHC1g=-7hUiYU z_Fqp1xK&;Z|KB6Zo?`zc3jvnEPw=}DimY>sKc z^5}7YyF)oEDdE3Q3Im+m=O6=#sw6STKU9!<4V$y8(hj`n!Amu!1*1bOu&m-{W(LX| z%;uA`n<`LckR4LYr3hMV)5EJnBV!G?huwT0XL!Cx-xOG*^|4?w_?+x#P*aqKGgQ!G zb6FLAYXyJVWEr=0T`?Q_fV$*~8p}@;Qmn~rxh3>k^KDL?V7&n81d`=B*htJvOnx%u z7C09H`&wQ@?A`gBjwtVWCXai4CmXw5d3r_bKh^9&CEE#J_;YxM*V%V!n=J0Zwo93J zU_Ef8x+wBb^ktVTQPZ^u3@sQ%Gs(AOhli3heB0yVYD`-{Fx>oXOj7MaHQ%9LbfYJ` zQi}jtAj;5bkVJo`v?yvO6kaFEi!7kc7a~Ndm{LJi_wZ-pA3leYu zK^<|_UybfoMzIVf5YM&R+@sf99$kdgk`ywu*w^<<=5;ee$e6z~<{jEJ(lc{r<~M)o zBP$n-Zc-{wSf@S-Y6pQ$S-n--aLGp--PL~or>H&l6>pU+blSa)yGj5dZe?YFT{FVzlp3^|92ufg}|<eXa;Nl)^r}J4KZFT+Kq*~eux)2>)lLF5&t}x;=k%WnxiZteGKu^LaYRXNI z`iWxr!!S}!Gkh|6e^+0!*5$cb>u?1Bn8FaLW1q{ufMWHP z!%#ij503%i)dPMv1*Ry4;B$@EAg}7iumXL?|N6JJ1B@i$1u3y@+Lh>a@)720zxaM~AU zeCoI*^<6(a678bv2X5D!_#fa(r{%g)*j2NL7TdS7hz>_o?$5AbOinCbIzfv!bC-)3 z<)%j{hmCkSTuN4`5PYuYyfC8UcmmMnOX815`u3C;=nD5z?s?aGP*Iv+x<;im{AjjJ}QnPGkKMWt~=3_l=CWm4DGJU0WDZg^U*FqJq)N@TY4c^O>v>V>D6)TM(k1;$R z1;7DP-RZ&{i+7$m%gc#55$nk1YZXU-b*l#P>aQ3yA{=}Uv7j=TIJlMO?q(`<{f=mm z`$8GH_=c|H-U-8ruMVBf4YbTuyTzlZX4luFKEg}gvXPU82*wzYNbh#`;?(3!*zyXo zwGJH5SmS&6&B~G@5*YB^%I+vpZO1_cU)C=Hi_ ztX?{+ufWA9mY@H=OT(Mw@Ec%o&haHU@L47@1!kx<%BP>LKN_z@{^Bp(l$K=hR--Gn zdW&rAHb8x|`-eOjOtyJ88$I_@m-W_cu54EczeS@EB`F4o0ITTU{J9^LvA-C!9ScgW zxFeV}mdu!MkaTbZ1h+Q}D^xwW%PLgqW0D;aMHh#Qb3ZEJsXAQ4PdTLTS$vpDaiSdl z;)@&o9)A`P>n2ec>H^LAa{F+>kiSR@dLnM#J>RU z-AEJ;?r3|`=waP!v#D9G$LfLuJj}Aoy%6PJlh}m#%LTJoC;bs1{HU8%7?=w)Q{`mE zzj(|N^^elp$v0Rbi4ppaE87Zc;&RDM>kPZ^p10vp zrKpEY1&+_P-($|)hiR<<%j)3SSPA9u;hjbaY@Y2m6KeIGhChAYZ@kShaVvU=CAB=Q}k=3a;UeT2A}dLnIE z#~blV2=TvJ@o?Sy8g%XJc2~maRCfDYZ2%aqmx+EiEKUx!f)G?eo>SjIGn~S8H_x(5CHB z^=k8-JqC2kj6)h}Xl<=l5a1`p*8AQ!u5(ZaVDQA)lR>>q=Pw0};+NESwX*fps+wWP zlRBqmKI5O0d+P%#87@`pM$6^Fd_v_hmhN0X+<2q-fXghcK$j)Ae7sX~IGeUvlI^RO zWiw}Oql)?2CRAqE1Id5km@}0?sZ*~t6;f}PI0JVw0S7w?ZGg?wryeEot3Ifv zw*L$$^N3rzZslN2EATuC2b{Iwn{psoye>Am*({0Uz0LBlkBWk zJlZn=p7J=~EnE4cs;;f8%jd|YiNDa}mF_+QoFBS3_tGeSgeYdKI-l+=oe70H^L9)V zb_*g}>`gM*8}6s;GuqkCP(>f#cZ-!$A7H>j0}?Mj^m*CM0nqu^6QkOWzQoUs#N4_f zlIM$9ovk|$f}o7LPGKjj()So&B`lC62s%0O)GzSTUe{N=urok6L4-P=)gj@I3FG(D z@d+9azsDNGU*>XHb@|f4ITs=shp@9_Nnmx(sQdfZF8#*5-d9g`&)B7leDE_$)9K+C zSSV2v^PTySZR1m0Q@`_GmGHE&KWFLTNWnUgLDu(-0Cr^fn~qh#m#IUg&nubo19*S| zbx?^1%0#9w6W^FsX|3@>uFX8FnLmH`D09;s{nK>aVdd2O_~ZAOJTIup4X|1*p4p;v z^8yUCkeFSNT^sS6?SO+hXP=f`(S^4RoXGOmXVWpvO^wwu+O1Q|SF)1nvX`ss(cdOR zO?TvXHaA}L;&{*{`J!!t05(aa`!>3dW3=4;uu@fi#ALlKg(zgToMX8R2hH6VWKfyR z4t#j<%TkoN-){9LglV5I`?Xu48kMnoI7C7-u}D(KsU zOgWp#XN@=)qC37$1%t^F&dR#w`^HEMr9f1Dzfr?`b;Lfy9)V;D(*e?!t*#8p{T6_= zZQ7ydrrb7?*}^vT*fqSKPMx`sx?`{}Au-e9U{^V|yl_!!uj99WKH;6#4_g6FB=)Pg z#zRfMMa>esA3jFPg!f*!RGq66KnvnQemZd3G>KU)x#rG=gpIF184jRFLYP{iZ(a7zN z^xfHulawdK%53*!rXc39E|t#s2J*)GFJh5;Vs7iD$#g-#8F16#!Q+ERMXy;WwnINd ziJnMPJeY%E8xFVpR@N`dbie<8#VJzQ#K83R0d=g5oooC$Je$5*#_wY!nW>{-OKHk8 z_8MKQ#y8w@*lOy`I6#sfa1y}?N<0rf|In9A9i%UyZ?iJ6#8n-<6-GlX(7^|1zO4J54FxojHgd848bFq9N59cQsgz$8@v*qI>oQ zsr;OyV?=mEqaJ1G^O^{tWwDoD1w*eB2Gvs);T$}anGynY%9=JkH3Il-p3zb9wC1 zKo4O&*~Z3m&2KrDe;I@okjdc7z{I37@CvYUS%FhvxG)X)t~R1y`@BTrim*u)#E%rd ztf|cX<-TUXVlXhEzB+K{C3HYMZW#{YtOyqZ8 znLiJ0eA5|mjlwmvkdIB}@SpefBZmgL-z$oj!DhcSfUNbw7XjOnG z70vCk)T`Ey^^hnQWM zm>;pT<(Y59SV9Ttf@pM@+HXsW1S2T!pTyQ2tLk+?NM`1lQb#?wZ7p=)R7m^lbE!CgSax?1qf<0?o&-9Tfo|m82B{W8 zpP}KW_*8C93TOGib7bhu0o?P@WiUI^f&qNRekA1#Kz=dQkc(x6qyTi-j%TT|w$ z&7X%{Kd>viOIpXB|MS`K`$>WnS zDui7M>7IMJD8GDF#(;VDm;$zW)b~VQ2Ni1P*?G;(GJ%4|+Sb-u(vMN5U=@X|;dY*^ z!Ae8-qUi)2tn@6b;`;?idRleiI)4w1R*#c8UN$XF!Y+rQ&C}B<)jQm_aeHldv0>Vri;J#&(Ywc$3OoDOGKn3Jj=ZIEo_`kPuceci=%^lw z;^}sQy&PTdsxH!QpLE3P=idW3vEAi!b9(Sfc3kH&b10U2Pc*jYaavQ zNvAP=9|;T^mo^l#4l9@CC;3$?C-HxOuS4v`Nd~|OcJ4P!RVX3GLD`u}_ zls|AajR`GzZq&Gy9Z+QXPu(#F@6{!LxvxTHdVf+H)gj*f#euAK6kKQJW1K5YW{-WK z?|614VQ@YJjOs9%vmu&2b$)n|TNbf5cS@nO?_>`>LP(+K8Ytodbw3NS5HoHjD7t$# zMAb4qaf=8p(*qW?vq zyS+;rD6Nl}VBR^iV2AvtH}OnHS?hkqnX87tPPISR&O8z^2P;0F4!*WXfqfUd>E14< zBhb&_jncN*$PXE2By?jC0+!Q0p;n+NkU{ft1vCfue_{UVam2#5ar^Z1@DvA2gMh#{ zQhOK?o=kcau`*I08AqNP2px9sT=0CQvg;iitz0?037M zor4?xvBHa9Gy!KFegxyjPaekI6i#$bR|1nFhHz$DRk{KLupCypge|bb5tVYrc_N{s zhVeO%nM$tvG9}gP0~sU@bi4JEwp)8G3H#J|Kd1L zDB8IAjw!NbKHVhBBh+CwB0fTQBg^~ot)po}tZ_xIL8wMOy5nYnhCTI_O*8X?|4$eE zUC1ufWmhUTn)=NtYwmy@xl~T)fx|Z0`2#@Wp|=E7OwN(>rU$I19d95*F%2UfYxcKD zzMTnDRyMhDp?@02nNr{cd>vs@ruXEd9B;O34zrHJ#5G=(mtV0dVgTUD9QH1xJMEV>pnjj%8hnL<3Y%2NGoC)ZXbKj9- zfHoFlI_+3093y=)Tu6kEfR%2|CK;wB+hwc};mI+N?auao7Kg!C;E@)x6`dEu0 z`y3bf)Tc`)@-pZ1PRFgb%JlG1OR8@4@s`j^aJvZzjxH5K57M=ZX`o3Q}{ z<@eUYG-UL1vaH2iaXdA~s;U!YkPV&}$G}UI25s#M!qm9(G5AuZ-XyVo7U1u5(W7Kh z*zd$aJ9H-?3en|>q0cDhPf5)ns>P2=RfA(v4d-NcMTW{z`~DlHDW@17ptd%ejTDATy$yDTmcV*6PmBdZ12Zp4$HAqKkjJFu_XPB*=5QxYYHu<8rS-cTAH|I_x2iS z>m@ZS1wXD-(fAj?vVnoAA;3=aaQPXW*nKnNqa(Qc>3WrCPZ!4Ng9@v7gPuS~yV4Z5 zpo>7+JWmIn#p=>}Xr)xy(*A?Rc6z#_HulC$!{wkiMO*Xd78^p9t;;@+U@=bM{KyuG z;L+?qbacM|%E-@BKP%Fg;c(u5fZwaC{45&1j0F`WYu2_@Pdu9}*!EnfgoxgMD6?dE zCeF&V{?Tn~3N({rz#a*!(8zf9xMNjfi7Fc46Bue%@5E}Es->Qvo`FUf0dhn%b+)eI q( + +namespace maps { + +class FakeFont final : public Font { +public: + explicit FakeFont(int advance = 8, int line = 16) : advance(advance), line(line) {} + + int lineHeight() const override { return line; } + + int textWidth(const char* text) override { return characters(text) * advance; } + + void drawText(Canvas& canvas, int x, int y, const char* text, uint16_t color) override { + for (const char* cursor = text; *cursor != '\0'; cursor++) { + if ((static_cast(*cursor) & 0xC0) == 0x80) { + continue; + } + if (*cursor != ' ') { + // A block of the x-height, taller for capitals and digits + const bool tall = (*cursor >= 'A' && *cursor <= 'Z') || (*cursor >= '0' && *cursor <= '9'); + const int top = tall ? 3 : 6; + canvas.fillRect(x + 1, y + top, advance - 2, line - top - 4, color); + } + x += advance; + } + } + +private: + static int characters(const char* text) { + int count = 0; + for (; *text != '\0'; text++) { + if ((static_cast(*text) & 0xC0) != 0x80) { + count++; + } + } + return count; + } + + int advance; + int line; +}; + +} // namespace maps diff --git a/Apps/Maps/host/FakePosition.h b/Apps/Maps/host/FakePosition.h new file mode 100644 index 0000000..0460cfd --- /dev/null +++ b/Apps/Maps/host/FakePosition.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +// The PC stand-in for Tactility's location module: tests set what it reports. + +#include "../main/Source/platform/Position.h" + +#include + +namespace maps { + +class FakePosition final : public PositionSource { +public: + bool enabled = true; + bool hasFix = false; + PositionFix fix { NAN, NAN, NAN, NAN, NAN, 0 }; + int reads = 0; + + void set(double latitude, double longitude, float accuracy, float course = NAN, float speed = NAN) { + hasFix = true; + fix = { latitude, longitude, accuracy, course, speed, 1761700000000 }; + } + + PositionRead read(PositionFix& out) override { + reads++; + if (!enabled) { + return PositionRead::Off; + } + if (!hasFix) { + return PositionRead::NoFix; + } + out = fix; + return PositionRead::Fix; + } +}; + +} // namespace maps diff --git a/Apps/Maps/host/ImageDiff.h b/Apps/Maps/host/ImageDiff.h new file mode 100644 index 0000000..45d068f --- /dev/null +++ b/Apps/Maps/host/ImageDiff.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +// Compares two RGB565 images, for checking that prepared tiles render like their originals. + +#include +#include +#include + +namespace maps { + +struct ImageDiff { + /** Mean absolute difference per colour channel, scaled to 0-255. */ + double meanDifference; + /** Share of pixels where a channel differs by more than 64 of 255. */ + double strongShare; +}; + +inline ImageDiff compareImages(const uint16_t* a, const uint16_t* b, size_t pixels) { + double sum = 0.0; + size_t strong = 0; + for (size_t i = 0; i < pixels; i++) { + const int channels[3][2] = { + { (a[i] >> 11) * 255 / 31, (b[i] >> 11) * 255 / 31 }, + { ((a[i] >> 5) & 63) * 255 / 63, ((b[i] >> 5) & 63) * 255 / 63 }, + { (a[i] & 31) * 255 / 31, (b[i] & 31) * 255 / 31 }, + }; + bool isStrong = false; + for (const auto& channel : channels) { + const int difference = std::abs(channel[0] - channel[1]); + sum += difference; + isStrong = isStrong || difference > 64; + } + if (isStrong) { + strong++; + } + } + return { sum / (3.0 * static_cast(pixels)), static_cast(strong) / static_cast(pixels) }; +} + +} // namespace maps diff --git a/Apps/Maps/host/PlatformHost.cpp b/Apps/Maps/host/PlatformHost.cpp new file mode 100644 index 0000000..92bd394 --- /dev/null +++ b/Apps/Maps/host/PlatformHost.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "../main/Source/platform/Platform.h" + +#include +#include +#include +#include +#include + +namespace maps { + +void* largeAlloc(size_t size) { + return malloc(size); +} + +void* fastAlloc(size_t size) { + return malloc(size); +} + +void largeFree(void* pointer) { + free(pointer); +} + +uint64_t nowUs() { + using namespace std::chrono; + return static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()); +} + +uint32_t nowMs() { + using namespace std::chrono; + return static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()); +} + +void logInfo(const char* format, ...) { + va_list args; + va_start(args, format); + vprintf(format, args); + va_end(args); + printf("\n"); +} + +void logError(const char* format, ...) { + va_list args; + va_start(args, format); + fprintf(stderr, "ERROR: "); + vfprintf(stderr, format, args); + va_end(args); + fprintf(stderr, "\n"); +} + +void logMemory(const char* label) { + (void)label; +} + +bool fileExists(const char* path) { + std::error_code error; + return std::filesystem::is_regular_file(path, error); +} + +bool fileSize(const char* path, uint32_t& size) { + std::error_code error; + if (!std::filesystem::is_regular_file(path, error)) { + return false; + } + size = static_cast(std::filesystem::file_size(path, error)); + return !error; +} + +bool removeFile(const char* path) { + std::error_code error; + return std::filesystem::is_regular_file(path, error) && std::filesystem::remove(path, error); +} + +bool removeFolder(const char* path) { + std::error_code error; + return std::filesystem::is_directory(path, error) && std::filesystem::is_empty(path, error) && std::filesystem::remove(path, error); +} + +bool listDirectory(const char* path, DirectoryCallback callback, void* context) { + std::error_code error; + for (const auto& entry : std::filesystem::directory_iterator(path, error)) { + const std::string name = entry.path().filename().string(); + if (!callback(name.c_str(), entry.is_directory(), context)) { + break; + } + } + return !error; +} + +} // namespace maps diff --git a/Apps/Maps/host/Png.cpp b/Apps/Maps/host/Png.cpp new file mode 100644 index 0000000..cb26405 --- /dev/null +++ b/Apps/Maps/host/Png.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Png.h" + +#include +#include +#include + +bool writePngRgb565(const char* path, const uint16_t* pixels, int width, int height) { + std::vector rgb(static_cast(width) * height * 3); + for (int i = 0; i < width * height; i++) { + const uint16_t c = pixels[i]; + rgb[i * 3 + 0] = static_cast(((c >> 11) & 0x1F) * 255 / 31); + rgb[i * 3 + 1] = static_cast(((c >> 5) & 0x3F) * 255 / 63); + rgb[i * 3 + 2] = static_cast((c & 0x1F) * 255 / 31); + } + size_t size = 0; + void* png = tdefl_write_image_to_png_file_in_memory(rgb.data(), width, height, 3, &size); + if (png == nullptr) { + return false; + } + FILE* file = fopen(path, "wb"); + const bool written = file != nullptr && fwrite(png, 1, size, file) == size; + if (file != nullptr) { + fclose(file); + } + mz_free(png); + return written; +} diff --git a/Apps/Maps/host/Png.h b/Apps/Maps/host/Png.h new file mode 100644 index 0000000..c9ca93c --- /dev/null +++ b/Apps/Maps/host/Png.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +/** Writes RGB565 pixels as a 24-bit PNG file. */ +bool writePngRgb565(const char* path, const uint16_t* pixels, int width, int height); diff --git a/Apps/Maps/host/check_symbols.py b/Apps/Maps/host/check_symbols.py new file mode 100644 index 0000000..33d6296 --- /dev/null +++ b/Apps/Maps/host/check_symbols.py @@ -0,0 +1,88 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +"""Lists symbols that an app ELF needs but Tactility does not export to apps. + +Usage: python check_symbols.py +Tactility exports app symbols with DEFINE_MODULE_SYMBOL(...) in Modules/*, TactilityKernel and Tactility. +""" +import os +import re +import subprocess +import sys + + +OTHER_TARGET = "CONFIG_IDF_TARGET_ESP32P4" + + +def without_other_targets(text): + """Drops the lines that are only compiled for the ESP32-P4, e.g. the 64-bit atomics in platform-esp32.""" + kept = [] + stack = [] # per open #if: True when its current branch is P4-only, False when not, None when unrelated + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("#if"): + if OTHER_TARGET in stripped and not stripped.startswith("#ifndef") and "!" not in stripped: + stack.append(True) + elif stripped.startswith("#ifndef") and OTHER_TARGET in stripped: + stack.append(False) + else: + stack.append(None) + elif stripped.startswith("#else") and stack: + if stack[-1] is not None: + stack[-1] = not stack[-1] + elif stripped.startswith("#endif") and stack: + stack.pop() + elif True not in stack: + kept.append(line) + return "\n".join(kept) + + +def exported_symbols(repo): + names = set() + pattern = re.compile(r"(?:DEFINE_MODULE_SYMBOL\w*|ESP_ELFSYM_EXPORT|DEFINE_SYMBOL)\(\s*(\w+)\s*[,)]") + alias = re.compile(r'\{\s*"(\w+)"\s*,') + for root, dirs, files in os.walk(repo): + parts = os.path.relpath(root, repo).split(os.sep) + if any(part.startswith(("build", "managed_components", ".git", "release")) for part in parts): + continue + for file in files: + if not file.endswith((".c", ".cpp", ".h")): + continue + path = os.path.join(root, file) + try: + text = open(path, encoding="utf-8", errors="ignore").read() + except OSError: + continue + if "DEFINE_MODULE_SYMBOL" not in text and "ESP_ELFSYM" not in text and "ModuleSymbol" not in text: + continue + text = without_other_targets(text) + names.update(pattern.findall(text)) + if "ModuleSymbol" in text: + names.update(alias.findall(text)) + return names + + +def needed_symbols(elf, readelf): + output = subprocess.run([readelf, "--dyn-syms", "-W", elf], capture_output=True, text=True, check=True).stdout + names = set() + for line in output.splitlines(): + parts = line.split() + if len(parts) >= 8 and parts[6] == "UND" and parts[7]: + names.add(parts[7].split("@")[0]) + return names + + +def main(): + if len(sys.argv) != 4: + print(__doc__) + return 2 + exported = exported_symbols(sys.argv[2]) + needed = needed_symbols(sys.argv[1], sys.argv[3]) + missing = sorted(n for n in needed if n not in exported) + print(f"needed: {len(needed)}, exported by Tactility: {len(exported)}, missing: {len(missing)}") + for name in missing: + print(" MISSING", name) + return 1 if missing else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Apps/Maps/host/device_run.py b/Apps/Maps/host/device_run.py new file mode 100644 index 0000000..a41d8c8 --- /dev/null +++ b/Apps/Maps/host/device_run.py @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +"""Runs the app on a device with a debug script, records the serial log and fetches the screenshots. + +Usage: python host/device_run.py [seconds] +The script is copied to the app's folder as debug.txt (see main/Source/app/Debug.h for its commands). +Every "shot " in it is fetched as /.png. The debug.txt is removed afterwards. +""" +import os +import re +import struct +import subprocess +import sys +import threading +import time +import zlib + +import serial + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.dirname(HERE) +TOOLS = os.path.join(REPO, "reference", "Tactility", "Buildscripts", "TactilitySDK", "Tools") +DEVICE_ROOT = "/sdcard/tactility/user/app/tactility.maps" +APP_ID = "tactility.maps" + + +def tool(name, *arguments): + return subprocess.run([sys.executable, os.path.join(TOOLS, name), *arguments], capture_output=True, text=True) + + +def bmp_to_png(bmp_path, png_path): + data = open(bmp_path, "rb").read() + offset, = struct.unpack_from(" 0 else y + row = data[offset + source_row * row_bytes: offset + source_row * row_bytes + width * 3] + raw.append(0) + for x in range(width): + b, g, r = row[x * 3: x * 3 + 3] + raw += bytes((r, g, b)) + + def chunk(kind, body): + return struct.pack(">I", len(body)) + kind + body + struct.pack(">I", zlib.crc32(kind + body) & 0xFFFFFFFF) + + png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, abs(height), 8, 2, 0, 0, 0)) + png += chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + chunk(b"IEND", b"") + open(png_path, "wb").write(png) + + +def record(port, seconds, path, stop): + connection = serial.Serial() + connection.port = port + connection.baudrate = 115200 + connection.timeout = 0.2 + connection.dtr = False + connection.rts = False + connection.open() + end = time.time() + seconds + with open(path, "wb") as log: + while time.time() < end and not stop.is_set(): + data = connection.read(4096) + if data: + log.write(data) + log.flush() + connection.close() + + +def main(): + if len(sys.argv) < 5: + print(__doc__) + return 2 + host, port, script, out = sys.argv[1:5] + seconds = float(sys.argv[5]) if len(sys.argv) > 5 else 60 + os.makedirs(out, exist_ok=True) + shots = re.findall(r"^\s*shot\s+(\S+)", open(script).read(), re.MULTILINE) + result = tool("push.py", "-h", host, script, f"{DEVICE_ROOT}/debug.txt") + if result.returncode != 0: + print("push failed:", result.stderr or result.stdout) + return 1 + log_path = os.path.join(out, "serial.log") + stop = threading.Event() + recorder = threading.Thread(target=record, args=(port, seconds, log_path, stop)) + recorder.start() + time.sleep(0.5) + env = dict(os.environ) + env["PYTHONIOENCODING"] = "utf-8" + run = subprocess.run([sys.executable, os.path.join(REPO, "reference", "Tactility", "tt.py"), "app", "run", "--host", host], + cwd=REPO, capture_output=True, text=True, env=env) + print("run:", (run.stdout + run.stderr).strip().splitlines()[-1:] or "") + # The app writes "LVGL started" once it has left the map + deadline = time.time() + seconds + while time.time() < deadline: + time.sleep(1) + try: + if b"LVGL started" in open(log_path, "rb").read(): + break + except OSError: + pass + time.sleep(1) + stop.set() + recorder.join() + for name in shots: + local_bmp = os.path.join(out, name + ".bmp") + result = tool("pull.py", "-h", host, f"{DEVICE_ROOT}/{name}.bmp", local_bmp) + if result.returncode == 0 and os.path.exists(local_bmp): + bmp_to_png(local_bmp, os.path.join(out, name + ".png")) + os.remove(local_bmp) + print("shot", name + ".png") + else: + print("shot", name, "not fetched:", (result.stderr or result.stdout).strip()) + tool("shell.py", "-h", host, f"rm {DEVICE_ROOT}/debug.txt") + text = open(log_path, "rb").read().decode("utf-8", "replace") + for line in text.splitlines(): + if "Maps:" in line or "Maps" in line.split(":")[0] or "Guru" in line or "abort" in line or "assert" in line.lower(): + print(line.strip()) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Apps/Maps/host/fetch_tiles.py b/Apps/Maps/host/fetch_tiles.py new file mode 100644 index 0000000..0d7352e --- /dev/null +++ b/Apps/Maps/host/fetch_tiles.py @@ -0,0 +1,245 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +#!/usr/bin/env python3 +"""Small, polite OpenFreeMap vector tile fetcher for TactilityMaps. + +Usage: + python host/fetch_tiles.py --out --region \ + --bbox west,south,east,north --minzoom 0 --maxzoom 14 + +Tiles are stored byte-for-byte as received (gzip-compressed MVT) in +/maps////.pbf, plus /maps//meta.json. + +Rules enforced (OpenFreeMap terms): + * Tile URL template is read from the TileJSON at TILEJSON_URL, never hard-coded. + * Never talks to openstreetmap.org servers. + * Identifying User-Agent on every request. + * Refuses jobs above MAX_TILES tiles (all zooms combined). + * Strictly sequential requests. +""" + +import argparse +import datetime +import json +import math +import os +import re +import sys +import time +import urllib.error +import urllib.request + +TILEJSON_URL = "https://tiles.openfreemap.org/planet" +USER_AGENT = "TactilityMaps/0.1 (+https://github.com/AdaSzi/TactilityMaps)" +MAX_TILES = 3000 +MAX_BYTES = 50 * 1000 * 1000 # OpenFreeMap: 50 MB per download +MAX_ZOOM = 14 # OpenFreeMap vector tiles end at z14 +MAX_LAT = 85.05112878 # Web Mercator limit +REGION_RE = re.compile(r"^[a-z0-9_-]{1,8}$") +REQUEST_DELAY_S = 0.05 # small pause between requests, on top of being sequential +TIMEOUT_S = 30 +RETRIES = 2 + + +def lonlat_to_tile(lon, lat, z): + """Standard slippy-map tile index for a point at zoom z (clamped to the grid).""" + lat = max(-MAX_LAT, min(MAX_LAT, lat)) + n = 1 << z + x = int(math.floor((lon + 180.0) / 360.0 * n)) + lat_rad = math.radians(lat) + y = int(math.floor((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)) + return max(0, min(n - 1, x)), max(0, min(n - 1, y)) + + +def tile_range(bbox, z): + """Return (x0, x1, y0, y1) inclusive for the bbox at zoom z.""" + west, south, east, north = bbox + x0, y0 = lonlat_to_tile(west, north, z) # north-west corner: smallest x and y + x1, y1 = lonlat_to_tile(east, south, z) # south-east corner: largest x and y + return x0, x1, y0, y1 + + +def plan(bbox, minzoom, maxzoom): + """Return list of (z, x0, x1, y0, y1, count) and the total count.""" + rows = [] + total = 0 + for z in range(minzoom, maxzoom + 1): + x0, x1, y0, y1 = tile_range(bbox, z) + count = (x1 - x0 + 1) * (y1 - y0 + 1) + rows.append((z, x0, x1, y0, y1, count)) + total += count + return rows, total + + +class NoDecodeRedirectHandler(urllib.request.HTTPRedirectHandler): + """Default redirect handling; urllib never decodes gzip bodies on its own.""" + + +def make_request(url, gzip_tiles): + headers = {"User-Agent": USER_AGENT} + if gzip_tiles: + headers["Accept-Encoding"] = "gzip" + return urllib.request.Request(url, headers=headers) + + +def http_get(url, gzip_tiles): + """GET url. Returns (status, body_bytes). 404 is returned as (404, b'').""" + last_err = None + for attempt in range(RETRIES + 1): + try: + with urllib.request.urlopen(make_request(url, gzip_tiles), timeout=TIMEOUT_S) as resp: + return resp.status, resp.read() + except urllib.error.HTTPError as e: + if e.code in (204, 404): + return e.code, b"" + last_err = e + if e.code < 500 and e.code != 429: + break + except (urllib.error.URLError, TimeoutError, ConnectionError) as e: + last_err = e + time.sleep(1.0 + attempt * 2.0) + raise RuntimeError("request failed for %s: %s" % (url, last_err)) + + +def get_template(): + status, body = http_get(TILEJSON_URL, False) + tj = json.loads(body.decode("utf-8")) + template = tj["tiles"][0] + if "openstreetmap.org" in template.lower(): + raise RuntimeError("refusing tile server on openstreetmap.org: " + template) + for key in ("{z}", "{x}", "{y}"): + if key not in template: + raise RuntimeError("tile template lacks %s: %s" % (key, template)) + return template + + +def parse_args(argv): + p = argparse.ArgumentParser(description="Polite OpenFreeMap vector tile downloader.") + p.add_argument("--out", required=True, help="output root (tiles go to /maps//...)") + p.add_argument("--region", required=True, help="region name, 1-8 chars of [a-z0-9_-]") + p.add_argument("--bbox", required=True, help="west,south,east,north in degrees") + p.add_argument("--minzoom", type=int, default=0) + p.add_argument("--maxzoom", type=int, default=14) + return p.parse_args(argv) + + +def main(argv=None): + args = parse_args(argv) + + if not REGION_RE.match(args.region): + print("error: region must be 1-8 chars of [a-z0-9_-]", file=sys.stderr) + return 2 + try: + bbox = [float(v) for v in args.bbox.split(",")] + if len(bbox) != 4: + raise ValueError + except ValueError: + print("error: --bbox must be west,south,east,north", file=sys.stderr) + return 2 + west, south, east, north = bbox + if not (-180 <= west < east <= 180 and -90 <= south < north <= 90): + print("error: invalid bbox (need -180<=west MAX_TILES: + print("Refusing: %d tiles exceeds the limit of %d." % (total, MAX_TILES)) + return 1 + + template = get_template() + print("Tile template:", template) + + region_dir = os.path.join(args.out, "maps", args.region) + os.makedirs(region_dir, exist_ok=True) + + downloaded = 0 + skipped = 0 + empty = [] + total_bytes = 0 + per_zoom = {z: [0, 0] for z in range(args.minzoom, args.maxzoom + 1)} # z -> [count, bytes] + failed = 0 + + def write_meta(): + meta = { + "name": args.region, + "bbox": bbox, + "minzoom": args.minzoom, + "maxzoom": args.maxzoom, + "source": template, + "date": datetime.date.today().isoformat(), + "total": total, + "done": downloaded + skipped + len(empty), + "bytes": total_bytes, + } + tmp = os.path.join(region_dir, "meta.json.tmp") + with open(tmp, "w", encoding="utf-8") as f: + json.dump(meta, f) + os.replace(tmp, os.path.join(region_dir, "meta.json")) + + try: + for z, x0, x1, y0, y1, _count in rows: + for x in range(x0, x1 + 1): + xdir = os.path.join(region_dir, str(z), str(x)) + for y in range(y0, y1 + 1): + path = os.path.join(xdir, "%d.pbf" % y) + if os.path.exists(path): + skipped += 1 + size = os.path.getsize(path) + per_zoom[z][0] += 1 + per_zoom[z][1] += size + continue + url = template.replace("{z}", str(z)).replace("{x}", str(x)).replace("{y}", str(y)) + try: + status, body = http_get(url, True) + except RuntimeError as e: + print(" ERROR z%d/%d/%d: %s" % (z, x, y, e), file=sys.stderr) + failed += 1 + time.sleep(REQUEST_DELAY_S) + continue + if status in (204, 404) or (status == 200 and len(body) == 0): + empty.append((z, x, y, status)) + print(" empty tile z%d/%d/%d (HTTP %d)" % (z, x, y, status)) + time.sleep(REQUEST_DELAY_S) + continue + os.makedirs(xdir, exist_ok=True) + tmp = path + ".tmp" + with open(tmp, "wb") as f: + f.write(body) + os.replace(tmp, path) + downloaded += 1 + total_bytes += len(body) + if total_bytes > MAX_BYTES: + print("Stopping: downloaded bytes exceed the %d byte limit." % MAX_BYTES) + failed += 1 + raise SystemExit(1) + per_zoom[z][0] += 1 + per_zoom[z][1] += len(body) + time.sleep(REQUEST_DELAY_S) + print("zoom %d done (downloaded so far: %d, skipped: %d)" % (z, downloaded, skipped)) + finally: + write_meta() + + print() + print("Tiles downloaded: %d" % downloaded) + print("Tiles skipped (already present): %d" % skipped) + print("Empty tiles (204/404): %d" % len(empty)) + print("Failed requests: %d" % failed) + print("Total bytes (downloaded this run): %d" % total_bytes) + all_bytes = sum(v[1] for v in per_zoom.values()) + print("Total bytes on disk for this region: %d" % all_bytes) + print() + print("%-5s %8s %14s" % ("zoom", "tiles", "avg bytes")) + for z in sorted(per_zoom): + cnt, size = per_zoom[z] + avg = size // cnt if cnt else 0 + print("%-5d %8d %14d" % (z, cnt, avg)) + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Apps/Maps/host/prepare_tiles.cpp b/Apps/Maps/host/prepare_tiles.cpp new file mode 100644 index 0000000..8e31374 --- /dev/null +++ b/Apps/Maps/host/prepare_tiles.cpp @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Converts the vector tiles of a folder into prepared tiles and compares them with the originals: +// size, render time and how different the rendered pixels are. +// Usage: prepare_tiles [--write] [--deflate] [--zoom ] +// is a folder with maps////.pbf inside. --write stores .prt next to each original, +// deflated with --deflate. --zoom renders at another zoom level (overzoom) instead of the tile's own. + +#include "../main/Source/render/PreparedTile.h" +#include "../main/Source/render/TileRenderer.h" +#include "ImageDiff.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace maps; +namespace fs = std::filesystem; + +namespace { + +std::vector readFile(const fs::path& path) { + std::ifstream file(path, std::ios::binary); + return std::vector(std::istreambuf_iterator(file), std::istreambuf_iterator()); +} + +struct Totals { + int tiles = 0; + uint64_t pbfBytes = 0; + uint64_t rawBytes = 0; + uint64_t deflatedBytes = 0; + double pbfMs = 0; + double pbfGeometryMs = 0; + double pbfFillMs = 0; + double pbfBuildingMs = 0; + double rawFillMs = 0; + double rawBuildingMs = 0; + double rawMs = 0; + double rawGeometryMs = 0; + double deflatedMs = 0; + double convertMs = 0; + double worstMean = 0; + double worstStrong = 0; +}; + +/** Renders a few times and keeps the fastest, so the numbers are steady. */ +template +RenderStats fastest(Render render) { + RenderStats best {}; + best.totalMs = UINT32_MAX; + for (int i = 0; i < 3; i++) { + RenderStats stats {}; + if (!render(stats)) { + return {}; + } + if (stats.totalMs < best.totalMs) { + best = stats; + } + } + return best; +} + +} // namespace + +int main(int argc, char* argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: prepare_tiles [--write] [--deflate] [--zoom ]\n"); + return 2; + } + bool write = false; + bool deflateWritten = false; + int zoom = -1; + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "--write") == 0) { + write = true; + } else if (strcmp(argv[i], "--deflate") == 0) { + deflateWritten = true; + } else if (strcmp(argv[i], "--zoom") == 0 && i + 1 < argc) { + zoom = atoi(argv[++i]); + } + } + auto* renderer = new TileRenderer(); + auto* converter = new TileConverter(); + if (!renderer->init()) { + return 1; + } + std::vector original(TILE_SIZE * TILE_SIZE); + std::vector prepared(TILE_SIZE * TILE_SIZE); + Totals totals; + printf("%-14s %8s %8s %8s %6s %6s | %7s %7s %7s | %6s %6s\n", "tile", "pbf", "raw", "deflate", "raw%", "defl%", + "pbf ms", "raw ms", "defl ms", "mean", "strong"); + for (const auto& entry : fs::recursive_directory_iterator(argv[1])) { + if (!entry.is_regular_file() || entry.path().extension() != ".pbf") { + continue; + } + const fs::path& path = entry.path(); + const TileKey source { atoi(path.parent_path().parent_path().filename().string().c_str()), + atoi(path.parent_path().filename().string().c_str()), atoi(path.stem().string().c_str()) }; + TileKey target = source; + if (zoom > source.z) { + // The top left descendant at the requested zoom level + target = { zoom, source.x << (zoom - source.z), source.y << (zoom - source.z) }; + } + const std::vector pbf = readFile(path); + ByteWriter raw; + ByteWriter deflated; + ConvertStats convertStats {}; + MemoryInput input(pbf.data(), pbf.size()); + if (!converter->convert(renderer->tileDecoder(), &input, source, static_cast(pbf.size()), "host", false, raw, &convertStats) || + !converter->convert(renderer->tileDecoder(), &input, source, static_cast(pbf.size()), "host", true, deflated, nullptr)) { + fprintf(stderr, "%s: conversion failed\n", path.string().c_str()); + continue; + } + const RenderStats pbfStats = fastest([&](RenderStats& stats) { + MemoryInput again(pbf.data(), pbf.size()); + return renderer->render(&again, source, target, colorPalette(), original.data(), &stats); + }); + const RenderStats deflatedStats = fastest([&](RenderStats& stats) { + return renderer->renderPrepared(deflated.bytes(), deflated.size(), target, colorPalette(), prepared.data(), &stats); + }); + const RenderStats rawStats = fastest([&](RenderStats& stats) { + return renderer->renderPrepared(raw.bytes(), raw.size(), target, colorPalette(), prepared.data(), &stats); + }); + const ImageDiff diff = compareImages(original.data(), prepared.data(), original.size()); + char name[32]; + snprintf(name, sizeof(name), "%d/%d/%d", source.z, source.x, source.y); + printf("%-14s %8zu %8zu %8zu %5.1f%% %5.1f%% | %7u %7u %7u | %6.2f %5.2f%%\n", name, pbf.size(), raw.size(), deflated.size(), + 100.0 * raw.size() / pbf.size(), 100.0 * deflated.size() / pbf.size(), pbfStats.totalMs, rawStats.totalMs, deflatedStats.totalMs, + diff.meanDifference, 100.0 * diff.strongShare); + printf(" features %u -> %u, points %u -> %u, converted in %u ms; geometry %u -> %u ms, fill %u -> %u ms\n", + convertStats.featuresIn, convertStats.featuresKept, convertStats.pointsIn, convertStats.pointsKept, convertStats.totalMs, + pbfStats.geometryMs, rawStats.geometryMs, pbfStats.fillMs, rawStats.fillMs); + totals.tiles++; + totals.pbfBytes += pbf.size(); + totals.rawBytes += raw.size(); + totals.deflatedBytes += deflated.size(); + totals.pbfMs += pbfStats.totalMs; + totals.pbfGeometryMs += pbfStats.geometryUs / 1000.0; + totals.pbfFillMs += pbfStats.fillUs / 1000.0; + totals.pbfBuildingMs += pbfStats.buildingUs / 1000.0; + totals.rawFillMs += rawStats.fillUs / 1000.0; + totals.rawBuildingMs += rawStats.buildingUs / 1000.0; + totals.rawMs += rawStats.totalMs; + totals.rawGeometryMs += rawStats.geometryUs / 1000.0; + totals.deflatedMs += deflatedStats.totalMs; + totals.convertMs += convertStats.totalMs; + totals.worstMean = diff.meanDifference > totals.worstMean ? diff.meanDifference : totals.worstMean; + totals.worstStrong = diff.strongShare > totals.worstStrong ? diff.strongShare : totals.worstStrong; + if (write) { + const ByteWriter& chosen = deflateWritten ? deflated : raw; + fs::path out = path; + out.replace_extension(".prt"); + std::ofstream file(out, std::ios::binary); + file.write(reinterpret_cast(chosen.bytes()), static_cast(chosen.size())); + } + } + if (totals.tiles == 0) { + fprintf(stderr, "No tiles found\n"); + return 1; + } + const double n = totals.tiles; + printf("\n%d tiles: .prt/.pbf size raw %.1f%%, deflated %.1f%%\n", totals.tiles, 100.0 * totals.rawBytes / totals.pbfBytes, + 100.0 * totals.deflatedBytes / totals.pbfBytes); + printf("mean per tile: pbf %.0f KB, raw %.0f KB, deflated %.0f KB\n", totals.pbfBytes / n / 1024, totals.rawBytes / n / 1024, + totals.deflatedBytes / n / 1024); + printf("mean render: pbf %.1f ms (geometry %.2f, fill %.2f, buildings %.2f), raw %.1f ms (geometry %.2f, fill %.2f, buildings %.2f), " + "deflated %.1f ms; conversion %.1f ms\n", + totals.pbfMs / n, totals.pbfGeometryMs / n, totals.pbfFillMs / n, totals.pbfBuildingMs / n, totals.rawMs / n, totals.rawGeometryMs / n, + totals.rawFillMs / n, totals.rawBuildingMs / n, totals.deflatedMs / n, totals.convertMs / n); + printf("worst pixel difference: mean %.2f of 255, strong %.2f%%\n", totals.worstMean, 100.0 * totals.worstStrong); + delete converter; + delete renderer; + return 0; +} diff --git a/Apps/Maps/host/push_region.py b/Apps/Maps/host/push_region.py new file mode 100644 index 0000000..cfaca5b --- /dev/null +++ b/Apps/Maps/host/push_region.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +"""Copies a downloaded region to a device through its development server. + +Usage: python host/push_region.py + holds maps//... as written by fetch_tiles.py. Files go to the app's user data folder. +push creates missing folders on the device. The shell's mkdir is avoided, it overflowed its stack on Tactility 0e276da2. +""" +import os +import subprocess +import sys + +TOOLS = os.path.join(os.path.dirname(__file__), "..", "reference", "Tactility", "Buildscripts", "TactilitySDK", "Tools") +DEVICE_ROOT = "/sdcard/tactility/user/app/tactility.maps" + + +def push(host, local, remote): + return subprocess.run([sys.executable, os.path.join(TOOLS, "push.py"), "-h", host, local, remote], capture_output=True, text=True) + + +def main(): + if len(sys.argv) != 4: + print(__doc__) + return 2 + host, root, region = sys.argv[1:] + local_region = os.path.join(root, "maps", region) + files = [] + for folder, _, names in os.walk(local_region): + for name in names: + if not name.endswith(".tmp"): + files.append(os.path.join(folder, name)) + files.sort() + for index, path in enumerate(files): + relative = os.path.relpath(path, root).replace(os.sep, "/") + remote = f"{DEVICE_ROOT}/{relative}" + result = push(host, path, remote) + status = "ok" if result.returncode == 0 else ("FAILED " + (result.stderr or result.stdout).strip()) + print(f"[{index + 1}/{len(files)}] {relative} {status}", flush=True) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Apps/Maps/host/render_tile.cpp b/Apps/Maps/host/render_tile.cpp new file mode 100644 index 0000000..21d6b75 --- /dev/null +++ b/Apps/Maps/host/render_tile.cpp @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Renders a view of the map around a coordinate to a PNG file, with the same code the app uses. +// Usage: render_tile [--mono] [--prepared] +// is a folder with maps////.pbf inside, or one region folder. --prepared draws from the +// .prt copies next to the originals where they are current (see prepare_tiles --write). + +#include "../main/Source/render/Compose.h" +#include "../main/Source/render/PreparedTile.h" +#include "../main/Source/render/TileRenderer.h" +#include "../main/Source/tiles/TileStore.h" +#include "Png.h" + +#include +#include +#include +#include +#include +#include + +using namespace maps; + +namespace { + +struct Context { + TileStore store; + TileRenderer renderer; + const Palette* palette; + bool prepared = false; + std::map> tiles; + uint32_t totalMs = 0; +}; + +uint64_t keyOf(const TileKey& tile) { + return (static_cast(tile.z) << 58) | (static_cast(tile.x) << 29) | static_cast(tile.y); +} + +const uint16_t* lookup(const TileKey& tile, void* data) { + auto* context = static_cast(data); + const auto found = context->tiles.find(keyOf(tile)); + if (found != context->tiles.end()) { + return found->second.empty() ? nullptr : found->second.data(); + } + std::vector& pixels = context->tiles[keyOf(tile)]; + TileFile file {}; + if (!context->store.find(tile, file)) { + printf("tile %d/%d/%d: no data\n", tile.z, tile.x, tile.y); + return nullptr; + } + const TileKey source = file.tile; + pixels.resize(TILE_SIZE * TILE_SIZE); + RenderStats stats {}; + char preparedFile[PATH_MAX_LENGTH]; + FileBuffer buffer; + PreparedHeader header {}; + if (context->prepared && preparedPath(preparedFile, sizeof(preparedFile), context->store.root(), context->store.regionName(file.region), source) && + buffer.load(preparedFile) && readPreparedHeader(buffer.bytes(), buffer.size(), header) && isPreparedCurrent(header, source, file.size)) { + if (!context->renderer.renderPrepared(buffer.bytes(), buffer.size(), tile, *context->palette, pixels.data(), &stats)) { + pixels.clear(); + return nullptr; + } + printf("tile %d/%d/%d from .prt: ", tile.z, tile.x, tile.y); + } else { + FileInput input; + if (!input.open(file.path)) { + return nullptr; + } + if (!context->renderer.render(&input, source, tile, *context->palette, pixels.data(), &stats)) { + pixels.clear(); + return nullptr; + } + } + context->totalMs += stats.totalMs; + printf("tile %d/%d/%d from %d/%d/%d: %u ms (scan %u ms), %u sweeps, %u features, %u bytes inflated\n", + tile.z, tile.x, tile.y, source.z, source.x, source.y, stats.totalMs, stats.scanMs, stats.sweeps, stats.features, stats.decompressedBytes); + return pixels.data(); +} + +} // namespace + +int main(int argc, char* argv[]) { + if (argc < 8) { + fprintf(stderr, "Usage: render_tile [--mono]\n"); + return 2; + } + const double lat = atof(argv[2]); + const double lon = atof(argv[3]); + const int zoom = atoi(argv[4]); + const int width = atoi(argv[5]); + const int height = atoi(argv[6]); + bool mono = false; + bool prepared = false; + for (int i = 8; i < argc; i++) { + mono = mono || strcmp(argv[i], "--mono") == 0; + prepared = prepared || strcmp(argv[i], "--prepared") == 0; + } + if (zoom < 0 || zoom > MAX_VIEW_ZOOM || width <= 0 || height <= 0 || width > 8192 || height > 8192) { + fprintf(stderr, "Invalid zoom or size\n"); + return 2; + } + + auto* context = new Context(); + context->palette = mono ? &monochromePalette() : &colorPalette(); + context->prepared = prepared; + const std::filesystem::path dir(argv[1]); + if (std::filesystem::is_directory(dir / "maps")) { + context->store.setRoot(dir.string().c_str()); + context->store.scanRegions(); + } else { + context->store.setRoot(dir.parent_path().parent_path().string().c_str()); + context->store.addRegion(dir.filename().string().c_str()); + } + if (context->store.regionCount() == 0) { + fprintf(stderr, "No regions found in %s\n", argv[1]); + return 1; + } + if (!context->renderer.init()) { + fprintf(stderr, "Out of memory\n"); + return 1; + } + + std::vector view(static_cast(width) * height); + const int missing = composeView(view.data(), width, height, lonToWorldX(lon, zoom), latToWorldY(lat, zoom), zoom, lookup, context, context->palette->placeholder); + printf("rendered %zu tiles in %u ms, %d missing\n", context->tiles.size(), context->totalMs, missing); + if (!writePngRgb565(argv[7], view.data(), width, height)) { + fprintf(stderr, "Could not write %s\n", argv[7]); + return 1; + } + delete context; + return 0; +} diff --git a/Apps/Maps/host/tests.cpp b/Apps/Maps/host/tests.cpp new file mode 100644 index 0000000..77f7c88 --- /dev/null +++ b/Apps/Maps/host/tests.cpp @@ -0,0 +1,2897 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Unit tests for the PC build of tiles/, mvt/ and render/. No framework: CHECK counts failures. +// Usage: tests [--update] --update rewrites the golden hashes and images in the fixture folder. + +#include "../main/Source/mvt/ByteInput.h" +#include "../main/Source/mvt/InflateReader.h" +#include "../main/Source/mvt/MvtDecoder.h" +#include "../main/Source/render/CacheIndex.h" +#include "../main/Source/render/CacheWalker.h" +#include "../main/Source/render/Compose.h" +#include "../main/Source/render/PreparedScanner.h" +#include "../main/Source/render/PreparedTile.h" +#include "../main/Source/render/Raster.h" +#include "../main/Source/render/Style.h" +#include "../main/Source/render/TileCache.h" +#include "../main/Source/render/TileCodec.h" +#include "../main/Source/render/TileRenderer.h" +#include "../main/Source/tiles/DownloadPlan.h" +#include "../main/Source/tiles/IoQueue.h" +#include "../main/Source/tiles/RegionMeta.h" +#include "../main/Source/tiles/TileJson.h" +#include "../main/Source/tiles/TileMath.h" +#include "../main/Source/tiles/TilePaths.h" +#include "../main/Source/tiles/TileStore.h" +#include "../main/Source/ui/Canvas.h" +#include "../main/Source/ui/Controls.h" +#include "../main/Source/ui/DownloadFrame.h" +#include "../main/Source/ui/Gestures.h" +#include "../main/Source/ui/Widgets.h" +#include "FakeFont.h" +#include "../main/Source/ui/PositionTracker.h" +#include "../main/Source/ui/Settings.h" +#include "FakePosition.h" +#include "ImageDiff.h" +#include "Png.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace maps; + +#ifndef FIXTURE_DIR +#define FIXTURE_DIR "../tests/fixtures" +#endif + +namespace { + +int checkCount = 0; +int failureCount = 0; + +} // namespace + +#define CHECK(condition) \ + do { \ + checkCount++; \ + if (!(condition)) { \ + failureCount++; \ + printf("%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #condition); \ + } \ + } while (0) + +namespace { + +using Bytes = std::vector; + +bool nearlyEqual(double a, double b, double tolerance) { + return std::fabs(a - b) <= tolerance; +} + +// --------------------------------------------------------------------------------------------------------------- +// Protobuf encoding helpers +// --------------------------------------------------------------------------------------------------------------- + +void putVarint(Bytes& out, uint64_t value) { + while (value >= 0x80) { + out.push_back(static_cast(value | 0x80)); + value >>= 7; + } + out.push_back(static_cast(value)); +} + +uint32_t zigzagEncode(int32_t value) { + return (static_cast(value) << 1) ^ static_cast(value >> 31); +} + +void putTag(Bytes& out, uint32_t field, uint32_t wire) { + putVarint(out, (static_cast(field) << 3) | wire); +} + +void putVarintField(Bytes& out, uint32_t field, uint64_t value) { + putTag(out, field, 0); + putVarint(out, value); +} + +void putBytesField(Bytes& out, uint32_t field, const Bytes& content) { + putTag(out, field, 2); + putVarint(out, content.size()); + out.insert(out.end(), content.begin(), content.end()); +} + +void putStringField(Bytes& out, uint32_t field, const char* text) { + Bytes content(text, text + strlen(text)); + putBytesField(out, field, content); +} + +/** Geometry commands as a protobuf packed array of command integers and zigzag deltas. */ +class GeometryBuilder { +public: + int32_t cursorX = 0; + int32_t cursorY = 0; + Bytes data; + + void moveTo(int32_t x, int32_t y) { + putVarint(data, (1u << 3) | 1u); + delta(x, y); + } + + /** Starts a LineTo command with a point count, then call delta/point for each point. */ + void lineTo(const std::vector& points) { + putVarint(data, (static_cast(points.size()) << 3) | 2u); + for (const TilePoint& point : points) { + delta(point.x, point.y); + } + } + + void closePath() { putVarint(data, (1u << 3) | 7u); } + +private: + void delta(int32_t x, int32_t y) { + putVarint(data, zigzagEncode(x - cursorX)); + putVarint(data, zigzagEncode(y - cursorY)); + cursorX = x; + cursorY = y; + } +}; + +Bytes featureMessage(const std::vector& tags, uint32_t type, const GeometryBuilder& geometry) { + Bytes feature; + if (!tags.empty()) { + Bytes packed; + for (const uint32_t tag : tags) { + putVarint(packed, tag); + } + putBytesField(feature, 2, packed); + } + putVarintField(feature, 3, type); + putBytesField(feature, 4, geometry.data); + return feature; +} + +Bytes layerMessage(const char* name, const std::vector& features, const std::vector& keys, const std::vector& values) { + Bytes layer; + putStringField(layer, 1, name); + for (const Bytes& feature : features) { + putBytesField(layer, 2, feature); + } + for (const char* key : keys) { + putStringField(layer, 3, key); + } + for (const char* value : values) { + Bytes valueMessage; + putStringField(valueMessage, 1, value); + putBytesField(layer, 4, valueMessage); + } + putVarintField(layer, 5, 4096); + putVarintField(layer, 15, 2); + return layer; +} + +constexpr int LONG_LINE_POINTS = 600; + +TilePoint longLinePoint(int i) { + return { i * 3 - 100, (i * 7) % 50 - 25 }; +} + +/** water (polygon), poi (unknown, must be skipped), transportation (two-part line), roads2 (long line). */ +Bytes buildTile() { + Bytes tile; + + GeometryBuilder polygon; + polygon.moveTo(10, 20); + polygon.lineTo({ { 30, 20 }, { 30, 40 }, { 10, 40 } }); + polygon.closePath(); + putBytesField(tile, 3, layerMessage("water", { featureMessage({ 0, 0 }, 3, polygon) }, { "class" }, { "lake" })); + + GeometryBuilder point; + point.moveTo(1, 1); + putBytesField(tile, 3, layerMessage("poi", { featureMessage({ 0, 0 }, 1, point) }, { "class" }, { "bar" })); + + GeometryBuilder line; + line.moveTo(5, 5); + line.lineTo({ { 20, 5 }, { 20, -25 } }); + line.moveTo(100, 100); + line.lineTo({ { 120, 90 } }); + GeometryBuilder longLine; + longLine.moveTo(longLinePoint(0).x, longLinePoint(0).y); + std::vector rest; + for (int i = 1; i < LONG_LINE_POINTS; i++) { + rest.push_back(longLinePoint(i)); + } + longLine.lineTo(rest); + putBytesField( + tile, + 3, + layerMessage("transportation", { featureMessage({ 0, 0 }, 2, line), featureMessage({ 0, 1 }, 2, longLine) }, { "class" }, { "primary", "minor" }) + ); + return tile; +} + +// --------------------------------------------------------------------------------------------------------------- +// Recording sink +// --------------------------------------------------------------------------------------------------------------- + +struct RecordedRing { + std::vector> chunks; + bool closed = false; + + /** Points with the point that the decoder repeats at the start of each later chunk removed. */ + std::vector flatten(bool* repeatsOk = nullptr) const { + std::vector result; + for (size_t c = 0; c < chunks.size(); c++) { + size_t start = 0; + if (c > 0) { + start = 1; + if (repeatsOk != nullptr && !(chunks[c][0].x == chunks[c - 1].back().x && chunks[c][0].y == chunks[c - 1].back().y)) { + *repeatsOk = false; + } + } + for (size_t i = start; i < chunks[c].size(); i++) { + result.push_back(chunks[c][i]); + } + } + return result; + } +}; + +struct RecordedFeature { + FeatureInfo info; + std::vector rings; + bool ended = false; +}; + +class RecordingSink final : public GeometrySink { +public: + std::vector features; + bool accept = true; + int balanceErrors = 0; + + bool beginFeature(const FeatureInfo& info) override { + if (!accept) { + return false; + } + features.push_back({}); + features.back().info = info; + return true; + } + + void beginRing() override { + if (features.empty() || ringOpen) { + balanceErrors++; + return; + } + ringOpen = true; + features.back().rings.emplace_back(); + } + + void points(const TilePoint* points, int count) override { + if (!ringOpen || count <= 0) { + balanceErrors++; + return; + } + features.back().rings.back().chunks.emplace_back(points, points + count); + } + + void endRing(bool closed) override { + if (!ringOpen) { + balanceErrors++; + return; + } + ringOpen = false; + features.back().rings.back().closed = closed; + } + + void endFeature() override { + if (ringOpen || features.empty()) { + balanceErrors++; + return; + } + features.back().ended = true; + } + +private: + bool ringOpen = false; +}; + +std::string describe(const RecordingSink& sink) { + std::string text; + char buffer[128]; + for (const RecordedFeature& feature : sink.features) { + snprintf(buffer, sizeof(buffer), "F l%d g%d c%d s%d;", static_cast(feature.info.layer), feature.info.geometryType, static_cast(feature.info.featureClass), static_cast(feature.info.subclass)); + text += buffer; + for (const RecordedRing& ring : feature.rings) { + text += ring.closed ? "R1:" : "R0:"; + for (const TilePoint& point : ring.flatten()) { + snprintf(buffer, sizeof(buffer), "%d,%d ", point.x, point.y); + text += buffer; + } + text += ";"; + } + } + return text; +} + +bool samePoint(const TilePoint& point, int x, int y) { + return point.x == x && point.y == y; +} + +/** Decodes a tile with the given bytes and returns the description of water followed by transportation. */ +bool decodeTile(const Bytes& bytes, RecordingSink& water, RecordingSink& roads, InflateReader::Format* format = nullptr) { + MvtDecoder decoder; + if (!decoder.init()) { + return false; + } + MemoryInput input(bytes.data(), bytes.size()); + if (!decoder.scan(&input)) { + return false; + } + if (format != nullptr) { + *format = decoder.format(); + } + const Layer waterLayer = Layer::Water; + const Layer roadLayer = Layer::Transportation; + return decoder.sweep(&waterLayer, 1, water) && decoder.sweep(&roadLayer, 1, roads); +} + +// --------------------------------------------------------------------------------------------------------------- +// 1. Tile maths +// --------------------------------------------------------------------------------------------------------------- + +void testTileMath() { + printf("tile maths\n"); + const TileKey budapest = tileAt(47.4979, 19.0402, 14); + CHECK(budapest == (TileKey { 14, 9058, 5729 })); + CHECK(tileAt(0.0, 0.0, 1) == (TileKey { 1, 1, 1 })); + CHECK(tileAt(0.0, 0.0, 0) == (TileKey { 0, 0, 0 })); + // Edges of the world stay inside the tile grid + CHECK(tileAt(89.0, 180.0, 3) == (TileKey { 3, 7, 0 })); + CHECK(tileAt(-89.0, -180.0, 3) == (TileKey { 3, 0, 7 })); + + const int zooms[] = { 0, 10, 19 }; + const double lons[] = { -179.9, -73.5, 0.0, 19.0402, 151.2 }; + const double lats[] = { -80.0, -33.8, 0.0, 47.4979, 84.0 }; + for (const int z : zooms) { + for (const double lon : lons) { + CHECK(nearlyEqual(worldXToLon(lonToWorldX(lon, z), z), lon, 1e-9)); + } + for (const double lat : lats) { + CHECK(nearlyEqual(worldYToLat(latToWorldY(lat, z), z), lat, 1e-9)); + } + } + + // Latitudes beyond the Mercator limit are clamped + for (const int z : zooms) { + const double top = latToWorldY(MAX_LATITUDE, z); + const double bottom = latToWorldY(-MAX_LATITUDE, z); + CHECK(std::isfinite(latToWorldY(89.0, z))); + CHECK(std::isfinite(latToWorldY(-89.0, z))); + CHECK(nearlyEqual(latToWorldY(89.0, z), top, 1e-9)); + CHECK(nearlyEqual(latToWorldY(-89.0, z), bottom, 1e-9)); + CHECK(top >= -0.01 && top <= 0.01); + CHECK(nearlyEqual(bottom, static_cast(TILE_SIZE) * static_cast(1 << z), 0.01)); + } + + const BoundingBox box { 19.00, 47.47, 19.10, 47.53 }; + int64_t sum = 0; + for (int z = 0; z <= 14; z++) { + const TileRange range = tileRange(box, z); + CHECK(range.minX <= range.maxX); + CHECK(range.minY <= range.maxY); + sum += range.count(); + } + CHECK(sum == 58); + CHECK(tileCount(box, 0, 14) == 58); + CHECK(tileCount(box, 14, 14) == tileRange(box, 14).count()); + + CHECK(ancestorOf({ 14, 9058, 5729 }, 12) == (TileKey { 12, 2264, 1432 })); + CHECK(ancestorOf({ 14, 9058, 5729 }, 14) == (TileKey { 14, 9058, 5729 })); + CHECK(ancestorOf({ 14, 9058, 5729 }, 0) == (TileKey { 0, 0, 0 })); + CHECK(wrapTileX(-1, 3) == 7); + CHECK(wrapTileX(8, 3) == 0); + CHECK(wrapTileX(3, 3) == 3); + CHECK(wrapTileX(-9, 3) == 7); + CHECK(nearlyEqual(metresPerPixel(0.0, 0), 156543.03, 0.1)); + CHECK(nearlyEqual(metresPerPixel(0.0, 1), 156543.03 / 2.0, 0.1)); + CHECK(metresPerPixel(60.0, 5) < metresPerPixel(0.0, 5)); + CHECK(isValidTile({ 14, 9058, 5729 })); + CHECK(!isValidTile({ 2, 4, 0 })); + CHECK(!isValidTile({ 2, 0, -1 })); +} + +// --------------------------------------------------------------------------------------------------------------- +// 2. Paths +// --------------------------------------------------------------------------------------------------------------- + +void testPaths() { + printf("paths\n"); + char path[PATH_MAX_LENGTH]; + CHECK(tilePath(path, sizeof(path), "/sd", "budapest", { 14, 9058, 5729 })); + CHECK(strcmp(path, "/sd/maps/budapest/14/9058/5729.pbf") == 0); + CHECK(tileFolderPath(path, sizeof(path), "/sd", "budapest", { 14, 9058, 5729 })); + CHECK(strcmp(path, "/sd/maps/budapest/14/9058") == 0); + CHECK(cachePath(path, sizeof(path), "/sd", "c3", { 14, 9058, 5729 })); + CHECK(strcmp(path, "/sd/cache/c3/14/9058/5729") == 0); + CHECK(cacheFolderPath(path, sizeof(path), "/sd", "m3", { 14, 9058, 5729 })); + CHECK(strcmp(path, "/sd/cache/m3/14/9058") == 0); + CHECK(isInsideCache("/sd/cache/c3/14/9058/5729", "/sd")); + CHECK(isInsideCache("/sd/cache/14", "/sd")); + CHECK(!isInsideCache("/sd/cache/", "/sd")); + CHECK(!isInsideCache("/sd/cache", "/sd")); + CHECK(!isInsideCache("/sd/maps/budapest/14/9058/5729.pbf", "/sd")); + CHECK(!isInsideCache("/sd/cache/../maps/budapest", "/sd")); + CHECK(!isInsideCache("/other/cache/c3", "/sd")); + CHECK(preparedPath(path, sizeof(path), "/sd", "budapest", { 14, 9058, 5729 })); + CHECK(strcmp(path, "/sd/maps/budapest/14/9058/5729.prt") == 0); + CHECK(preparedTempPath(path, sizeof(path), "/sd", "budapest", { 14, 9058, 5729 })); + CHECK(strcmp(path, "/sd/maps/budapest/14/9058/5729.ptm") == 0); + char small[10]; + CHECK(!tilePath(small, sizeof(small), "/sd", "budapest", { 14, 9058, 5729 })); + + CHECK(isValidRegionName("budapest")); + CHECK(isValidRegionName("a_b-1")); + CHECK(!isValidRegionName("")); + CHECK(!isValidRegionName("toolongname")); + CHECK(!isValidRegionName("Upper")); + CHECK(!isValidRegionName("a/b")); + CHECK(!isValidRegionName("a b")); + CHECK(!isValidRegionName("a.b")); +} + +// --------------------------------------------------------------------------------------------------------------- +// 3. MVT decoding +// --------------------------------------------------------------------------------------------------------------- + +void testZigzagHelper() { + // Sanity of the test encoder itself: known protobuf zigzag values + CHECK(zigzagEncode(0) == 0); + CHECK(zigzagEncode(-1) == 1); + CHECK(zigzagEncode(1) == 2); + CHECK(zigzagEncode(-20) == 39); + Bytes encoded; + putVarint(encoded, 300); + CHECK(encoded.size() == 2 && encoded[0] == 0xAC && encoded[1] == 0x02); +} + +void checkDecodedTile(const RecordingSink& water, const RecordingSink& roads) { + CHECK(water.balanceErrors == 0); + CHECK(roads.balanceErrors == 0); + + CHECK(water.features.size() == 1); + if (water.features.size() == 1) { + const RecordedFeature& feature = water.features[0]; + CHECK(feature.ended); + CHECK(feature.info.layer == Layer::Water); + CHECK(feature.info.geometryType == GEOMETRY_POLYGON); + CHECK(feature.info.featureClass == Value::lake); + CHECK(feature.rings.size() == 1); + if (feature.rings.size() == 1) { + const RecordedRing& ring = feature.rings[0]; + CHECK(ring.closed); + const std::vector points = ring.flatten(); + CHECK(points.size() == 4); + if (points.size() == 4) { + CHECK(samePoint(points[0], 10, 20)); + CHECK(samePoint(points[1], 30, 20)); + CHECK(samePoint(points[2], 30, 40)); + CHECK(samePoint(points[3], 10, 40)); + } + } + } + + CHECK(roads.features.size() == 2); + if (roads.features.size() == 2) { + const RecordedFeature& feature = roads.features[0]; + CHECK(feature.ended); + CHECK(feature.info.layer == Layer::Transportation); + CHECK(feature.info.geometryType == GEOMETRY_LINE); + CHECK(feature.info.featureClass == Value::primary); + CHECK(feature.rings.size() == 2); + if (feature.rings.size() == 2) { + CHECK(!feature.rings[0].closed); + CHECK(!feature.rings[1].closed); + const std::vector first = feature.rings[0].flatten(); + const std::vector second = feature.rings[1].flatten(); + CHECK(first.size() == 3); + if (first.size() == 3) { + CHECK(samePoint(first[0], 5, 5)); + CHECK(samePoint(first[1], 20, 5)); + CHECK(samePoint(first[2], 20, -25)); + } + CHECK(second.size() == 2); + if (second.size() == 2) { + CHECK(samePoint(second[0], 100, 100)); + CHECK(samePoint(second[1], 120, 90)); + } + } + + // The long line is delivered in chunks of at most CHUNK_POINTS, each later chunk starting with the last + // point of the one before. + const RecordedFeature& longFeature = roads.features[1]; + CHECK(longFeature.info.featureClass == Value::minor); + CHECK(longFeature.rings.size() == 1); + if (longFeature.rings.size() == 1) { + const RecordedRing& ring = longFeature.rings[0]; + CHECK(!ring.closed); + CHECK(ring.chunks.size() > 1); + bool repeatsOk = true; + const std::vector points = ring.flatten(&repeatsOk); + CHECK(repeatsOk); + for (const auto& chunk : ring.chunks) { + CHECK(chunk.size() <= static_cast(MvtDecoder::CHUNK_POINTS)); + } + CHECK(points.size() == static_cast(LONG_LINE_POINTS)); + if (points.size() == static_cast(LONG_LINE_POINTS)) { + bool inOrder = true; + for (int i = 0; i < LONG_LINE_POINTS; i++) { + const TilePoint expected = longLinePoint(i); + if (!samePoint(points[static_cast(i)], expected.x, expected.y)) { + inOrder = false; + } + } + CHECK(inOrder); + } + } + } +} + +void testMvtDecoding() { + printf("mvt decoding\n"); + testZigzagHelper(); + const Bytes tile = buildTile(); + + MvtDecoder decoder; + CHECK(decoder.init()); + MemoryInput input(tile.data(), tile.size()); + CHECK(decoder.scan(&input)); + CHECK(decoder.format() == InflateReader::Format::Raw); + CHECK(decoder.decompressedSize() == tile.size()); + CHECK(decoder.fileIndex(Layer::Water) == 0); + CHECK(decoder.fileIndex(Layer::Transportation) == 2); + CHECK(decoder.fileIndex(Layer::Building) == -1); + CHECK(decoder.fileIndex(Layer::Aeroway) == -1); + CHECK(decoder.extent(Layer::Water) == 4096); + + RecordingSink water; + RecordingSink roads; + const Layer waterLayer = Layer::Water; + const Layer roadLayer = Layer::Transportation; + CHECK(decoder.sweep(&waterLayer, 1, water)); + CHECK(decoder.sweep(&roadLayer, 1, roads)); + checkDecodedTile(water, roads); + + // Both layers in one sweep arrive in file order, a missing layer is ignored, and the poi layer never shows up. + { + RecordingSink both; + const Layer layers[] = { Layer::Transportation, Layer::Building, Layer::Water }; + CHECK(decoder.sweep(layers, 3, both)); + CHECK(both.features.size() == 3); + if (both.features.size() == 3) { + CHECK(both.features[0].info.layer == Layer::Water); + CHECK(both.features[1].info.layer == Layer::Transportation); + CHECK(both.features[2].info.layer == Layer::Transportation); + } + CHECK(both.balanceErrors == 0); + } + + // A sink that refuses the features gets no geometry and the decoder skips it properly + { + RecordingSink refusing; + refusing.accept = false; + const Layer layers[] = { Layer::Water, Layer::Transportation }; + CHECK(decoder.sweep(layers, 2, refusing)); + CHECK(refusing.features.empty()); + CHECK(refusing.balanceErrors == 0); + } + + // The same decoder can scan another tile again + { + MemoryInput again(tile.data(), tile.size()); + CHECK(decoder.scan(&again)); + RecordingSink water2; + CHECK(decoder.sweep(&waterLayer, 1, water2)); + CHECK(water2.features.size() == 1); + } + + // Exactly CHUNK_POINTS points arrive in one chunk, one more point makes a second chunk with the repeat + for (const int total : { MvtDecoder::CHUNK_POINTS, MvtDecoder::CHUNK_POINTS + 1, MvtDecoder::CHUNK_POINTS * 2 }) { + GeometryBuilder geometry; + geometry.moveTo(0, 0); + std::vector rest; + for (int i = 1; i < total; i++) { + rest.push_back({ i, -i }); + } + geometry.lineTo(rest); + Bytes edgeTile; + putBytesField(edgeTile, 3, layerMessage("water", { featureMessage({ 0, 0 }, 2, geometry) }, { "class" }, { "lake" })); + MvtDecoder edgeDecoder; + CHECK(edgeDecoder.init()); + MemoryInput edgeInput(edgeTile.data(), edgeTile.size()); + CHECK(edgeDecoder.scan(&edgeInput)); + RecordingSink sink; + CHECK(edgeDecoder.sweep(&waterLayer, 1, sink)); + CHECK(sink.features.size() == 1); + if (sink.features.size() == 1 && sink.features[0].rings.size() == 1) { + bool repeatsOk = true; + const std::vector points = sink.features[0].rings[0].flatten(&repeatsOk); + CHECK(repeatsOk); + CHECK(points.size() == static_cast(total)); + bool inOrder = points.size() == static_cast(total); + for (size_t i = 0; inOrder && i < points.size(); i++) { + inOrder = samePoint(points[i], static_cast(i), -static_cast(i)); + } + CHECK(inOrder); + } else { + CHECK(false); + } + } + + // A garbage command id makes the sweep fail instead of looping or crashing + { + Bytes geometry; + putVarint(geometry, (1u << 3) | 5u); + Bytes feature; + putVarintField(feature, 3, 2); + putBytesField(feature, 4, geometry); + Bytes layer; + putStringField(layer, 1, "water"); + putBytesField(layer, 2, feature); + Bytes badTile; + putBytesField(badTile, 3, layer); + MvtDecoder badDecoder; + CHECK(badDecoder.init()); + MemoryInput badInput(badTile.data(), badTile.size()); + CHECK(badDecoder.scan(&badInput)); + RecordingSink sink; + CHECK(!badDecoder.sweep(&waterLayer, 1, sink)); + } +} + +// --------------------------------------------------------------------------------------------------------------- +// 4. Inflate +// --------------------------------------------------------------------------------------------------------------- + +Bytes zlibCompress(const Bytes& data) { + mz_ulong size = mz_compressBound(static_cast(data.size())); + Bytes out(size); + if (mz_compress(out.data(), &size, data.data(), static_cast(data.size())) != MZ_OK) { + return {}; + } + out.resize(size); + return out; +} + +Bytes gzipCompress(const Bytes& data) { + size_t bodySize = 0; + void* body = tdefl_compress_mem_to_heap(data.data(), data.size(), &bodySize, TDEFL_DEFAULT_MAX_PROBES); + if (body == nullptr) { + return {}; + } + Bytes out = { 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }; + out.insert(out.end(), static_cast(body), static_cast(body) + bodySize); + mz_free(body); + const uint32_t crc = static_cast(mz_crc32(MZ_CRC32_INIT, data.data(), data.size())); + const uint32_t isize = static_cast(data.size()); + for (int i = 0; i < 4; i++) { + out.push_back(static_cast(crc >> (8 * i))); + } + for (int i = 0; i < 4; i++) { + out.push_back(static_cast(isize >> (8 * i))); + } + return out; +} + +/** Returns at most a few bytes per read, to test refilling the input buffer. */ +class DribbleInput final : public ByteInput { +public: + DribbleInput(const Bytes& data, size_t step) : data(data), step(step) {} + + int read(uint8_t* buffer, size_t size) override { + size_t count = data.size() - position; + if (count > step) { + count = step; + } + if (count > size) { + count = size; + } + memcpy(buffer, data.data() + position, count); + position += count; + return static_cast(count); + } + + bool rewind() override { + position = 0; + return true; + } + +private: + const Bytes& data; + size_t step; + size_t position = 0; +}; + +Bytes readAllByGet(InflateReader& reader) { + Bytes out; + while (true) { + const int byte = reader.get(); + if (byte < 0) { + break; + } + out.push_back(static_cast(byte)); + } + return out; +} + +/** Compressible data a few times bigger than the 32 KB window. */ +Bytes buildLargePayload() { + Bytes data; + uint32_t state = 12345; + for (int i = 0; i < 150000; i++) { + state = state * 1664525u + 1013904223u; + const uint32_t r = state >> 16; + // Mostly repeating text with some noise, so matches reach far back + data.push_back(static_cast((i % 211 < 150) ? 'a' + (i % 13) : r & 0xFF)); + } + return data; +} + +void testInflate() { + printf("inflate\n"); + const Bytes tile = buildTile(); + const Bytes zlibTile = zlibCompress(tile); + const Bytes gzipTile = gzipCompress(tile); + CHECK(!zlibTile.empty()); + CHECK(!gzipTile.empty()); + + struct Variant { + const Bytes* bytes; + InflateReader::Format expected; + const char* name; + }; + const Variant variants[] = { + { &tile, InflateReader::Format::Raw, "raw" }, + { &zlibTile, InflateReader::Format::Zlib, "zlib" }, + { &gzipTile, InflateReader::Format::Gzip, "gzip" }, + }; + + for (const Variant& variant : variants) { + InflateReader reader; + CHECK(reader.init()); + MemoryInput input(variant.bytes->data(), variant.bytes->size()); + CHECK(reader.begin(&input)); + CHECK(reader.format() == variant.expected); + const Bytes output = readAllByGet(reader); + CHECK(output == tile); + CHECK(!reader.failed()); + CHECK(reader.tell() == tile.size()); + CHECK(reader.get() == -1); + + // begin() again starts over + MemoryInput again(variant.bytes->data(), variant.bytes->size()); + CHECK(reader.begin(&again)); + Bytes viaRead(tile.size()); + CHECK(reader.read(viaRead.data(), viaRead.size()) == tile.size()); + CHECK(viaRead == tile); + + // skip() + MemoryInput third(variant.bytes->data(), variant.bytes->size()); + CHECK(reader.begin(&third)); + CHECK(reader.skip(10)); + CHECK(reader.tell() == 10); + CHECK(reader.get() == tile[10]); + CHECK(reader.tell() == 11); + CHECK(reader.skip(tile.size() - 11)); + CHECK(reader.tell() == tile.size()); + CHECK(!reader.skip(1)); + } + + // Decoding the compressed tiles gives the same result as the raw tile + { + RecordingSink rawWater; + RecordingSink rawRoads; + CHECK(decodeTile(tile, rawWater, rawRoads)); + for (const Variant& variant : variants) { + RecordingSink water; + RecordingSink roads; + InflateReader::Format format = InflateReader::Format::Raw; + CHECK(decodeTile(*variant.bytes, water, roads, &format)); + CHECK(format == variant.expected); + CHECK(describe(water) == describe(rawWater)); + CHECK(describe(roads) == describe(rawRoads)); + CHECK(water.features.size() == 1); + checkDecodedTile(water, roads); + } + } + + // A payload larger than the window, read in odd sized pieces and from an input that returns few bytes per read + { + const Bytes large = buildLargePayload(); + const Bytes largeZlib = zlibCompress(large); + const Bytes largeGzip = gzipCompress(large); + CHECK(largeZlib.size() > 0 && largeZlib.size() < large.size()); + for (const Variant& variant : { Variant { &large, InflateReader::Format::Raw, "raw" }, Variant { &largeZlib, InflateReader::Format::Zlib, "zlib" }, Variant { &largeGzip, InflateReader::Format::Gzip, "gzip" } }) { + InflateReader reader; + CHECK(reader.init()); + MemoryInput input(variant.bytes->data(), variant.bytes->size()); + CHECK(reader.begin(&input)); + CHECK(reader.format() == variant.expected); + Bytes output; + uint8_t piece[777]; + while (true) { + const size_t count = reader.read(piece, sizeof(piece)); + output.insert(output.end(), piece, piece + count); + if (count < sizeof(piece)) { + break; + } + } + CHECK(output == large); + CHECK(!reader.failed()); + + DribbleInput dribble(*variant.bytes, 7); + CHECK(reader.begin(&dribble)); + CHECK(readAllByGet(reader) == large); + CHECK(!reader.failed()); + + MemoryInput third(variant.bytes->data(), variant.bytes->size()); + CHECK(reader.begin(&third)); + CHECK(reader.skip(100000)); + CHECK(reader.get() == large[100000]); + CHECK(reader.skip(large.size() - 100001)); + CHECK(reader.get() == -1); + } + } + + // Truncated and damaged input must not crash and must be reported + { + for (const size_t keep : { gzipTile.size() / 2, gzipTile.size() - 9, static_cast(12) }) { + Bytes cut(gzipTile.begin(), gzipTile.begin() + static_cast(keep)); + InflateReader reader; + CHECK(reader.init()); + MemoryInput input(cut.data(), cut.size()); + const bool started = reader.begin(&input); + Bytes output = started ? readAllByGet(reader) : Bytes(); + CHECK(!started || reader.failed() || output.size() < tile.size()); + CHECK(output.size() <= tile.size()); + } + // Header only partly there + { + Bytes cut(gzipTile.begin(), gzipTile.begin() + 5); + InflateReader reader; + CHECK(reader.init()); + MemoryInput input(cut.data(), cut.size()); + CHECK(!reader.begin(&input) || reader.failed() || readAllByGet(reader).empty()); + } + // Truncated zlib + { + Bytes cut(zlibTile.begin(), zlibTile.begin() + static_cast(zlibTile.size() / 2)); + InflateReader reader; + CHECK(reader.init()); + MemoryInput input(cut.data(), cut.size()); + CHECK(reader.begin(&input)); + const Bytes output = readAllByGet(reader); + CHECK(reader.failed() || output.size() < tile.size()); + } + // The decoder reports a truncated tile + { + Bytes cut(gzipTile.begin(), gzipTile.begin() + static_cast(gzipTile.size() / 2)); + MvtDecoder decoder; + CHECK(decoder.init()); + MemoryInput input(cut.data(), cut.size()); + const bool scanned = decoder.scan(&input); + RecordingSink sink; + const Layer layer = Layer::Transportation; + const bool swept = scanned && decoder.sweep(&layer, 1, sink); + CHECK(!scanned || !swept || sink.features.size() < 2); + } + // Empty input + { + InflateReader reader; + CHECK(reader.init()); + MemoryInput input(nullptr, 0); + const bool started = reader.begin(&input); + CHECK(!started || reader.get() == -1); + } + } +} + +// --------------------------------------------------------------------------------------------------------------- +// 5. Raster +// --------------------------------------------------------------------------------------------------------------- + +constexpr int RASTER_SIZE = 64; + +std::vector whiteBuffer() { + return std::vector(static_cast(RASTER_SIZE) * RASTER_SIZE, 0xFFFF); +} + +uint16_t pixelAt(const std::vector& pixels, int x, int y) { + return pixels[static_cast(y) * RASTER_SIZE + static_cast(x)]; +} + +double coverageOf(uint16_t pixel) { + const int green = (pixel >> 5) & 63; + return 1.0 - static_cast(green) / 63.0; +} + +double totalCoverage(const std::vector& pixels) { + double sum = 0.0; + for (const uint16_t pixel : pixels) { + sum += coverageOf(pixel); + } + return sum; +} + +/** True when no pixel of darker is lighter than the same pixel of lighter (black on white, so green tells). */ +bool notLighter(const std::vector& darker, const std::vector& lighter) { + for (size_t i = 0; i < darker.size(); i++) { + if (coverageOf(darker[i]) + 1e-9 < coverageOf(lighter[i])) { + return false; + } + } + return true; +} + +void addRectangle(Raster& raster, float x0, float y0, float x1, float y1, bool reversed) { + if (!reversed) { + raster.addLine(x0, y0, x1, y0); + raster.addLine(x1, y0, x1, y1); + raster.addLine(x1, y1, x0, y1); + raster.addLine(x0, y1, x0, y0); + } else { + raster.addLine(x0, y0, x0, y1); + raster.addLine(x0, y1, x1, y1); + raster.addLine(x1, y1, x1, y0); + raster.addLine(x1, y0, x0, y0); + } +} + +void testRaster() { + printf("raster\n"); + Raster raster; + CHECK(raster.init(RASTER_SIZE, RASTER_SIZE)); + CHECK(raster.width() == RASTER_SIZE); + CHECK(raster.height() == RASTER_SIZE); + CHECK(raster.isEmpty()); + + // Fractional rectangle + std::vector forward = whiteBuffer(); + addRectangle(raster, 2.25f, 2.25f, 12.75f, 7.5f, false); + CHECK(!raster.isEmpty()); + raster.fill(forward.data(), RASTER_SIZE, 0x0000, 255); + CHECK(raster.isEmpty()); + bool interiorOk = true; + for (int y = 3; y <= 6; y++) { + for (int x = 3; x <= 11; x++) { + if (pixelAt(forward, x, y) != 0x0000) { + interiorOk = false; + } + } + } + CHECK(interiorOk); + bool outsideOk = true; + for (int y = 0; y < RASTER_SIZE; y++) { + for (int x = 0; x < RASTER_SIZE; x++) { + const bool outside = x < 2 || x > 12 || y < 2 || y > 7; + if (outside && pixelAt(forward, x, y) != 0xFFFF) { + outsideOk = false; + } + } + } + CHECK(outsideOk); + const double area = 10.5 * 5.25; + const double covered = totalCoverage(forward); + CHECK(std::fabs(covered - area) <= area * 0.02); + if (std::fabs(covered - area) > area * 0.02) { + printf(" coverage %.3f, expected %.3f\n", covered, area); + } + // The edge pixels are partly covered, not all or nothing + CHECK(pixelAt(forward, 2, 4) != 0xFFFF && pixelAt(forward, 2, 4) != 0x0000); + CHECK(pixelAt(forward, 12, 4) != 0xFFFF && pixelAt(forward, 12, 4) != 0x0000); + + // Opposite winding gives the same pixels + std::vector reversed = whiteBuffer(); + addRectangle(raster, 2.25f, 2.25f, 12.75f, 7.5f, true); + raster.fill(reversed.data(), RASTER_SIZE, 0x0000, 255); + CHECK(reversed == forward); + + // Opacity blends instead of replacing + { + std::vector half = whiteBuffer(); + addRectangle(raster, 10.0f, 10.0f, 20.0f, 20.0f, false); + raster.fill(half.data(), RASTER_SIZE, 0x0000, 128); + const double c = coverageOf(pixelAt(half, 15, 15)); + CHECK(c > 0.4 && c < 0.6); + CHECK(pixelAt(half, 25, 25) == 0xFFFF); + } + + // clear() throws the shape away + { + std::vector cleared = whiteBuffer(); + addRectangle(raster, 10.0f, 10.0f, 20.0f, 20.0f, false); + raster.clear(); + CHECK(raster.isEmpty()); + raster.fill(cleared.data(), RASTER_SIZE, 0x0000, 255); + CHECK(cleared == whiteBuffer()); + } + + // Partly outside the raster: left, top, right and bottom + { + std::vector pixels = whiteBuffer(); + addRectangle(raster, -10.0f, -10.0f, 20.0f, 20.0f, false); + raster.fill(pixels.data(), RASTER_SIZE, 0x0000, 255); + CHECK(pixelAt(pixels, 0, 0) == 0x0000); + CHECK(pixelAt(pixels, 5, 5) == 0x0000); + CHECK(pixelAt(pixels, 19, 19) == 0x0000); + CHECK(pixelAt(pixels, 20, 20) == 0xFFFF); + CHECK(pixelAt(pixels, 20, 5) == 0xFFFF); + CHECK(pixelAt(pixels, 5, 20) == 0xFFFF); + CHECK(nearlyEqual(totalCoverage(pixels), 400.0, 400.0 * 0.02)); + + std::vector far = whiteBuffer(); + addRectangle(raster, 50.0f, 50.0f, 100.0f, 100.0f, false); + raster.fill(far.data(), RASTER_SIZE, 0x0000, 255); + CHECK(pixelAt(far, 49, 49) == 0xFFFF); + CHECK(pixelAt(far, 50, 50) == 0x0000); + CHECK(pixelAt(far, 63, 63) == 0x0000); + CHECK(pixelAt(far, 63, 50) == 0x0000); + CHECK(pixelAt(far, 50, 63) == 0x0000); + CHECK(pixelAt(far, 49, 60) == 0xFFFF); + CHECK(pixelAt(far, 60, 49) == 0xFFFF); + CHECK(nearlyEqual(totalCoverage(far), 14.0 * 14.0, 14.0 * 14.0 * 0.02)); + + // A shape completely outside paints nothing + std::vector none = whiteBuffer(); + addRectangle(raster, -50.0f, -50.0f, -10.0f, -10.0f, false); + addRectangle(raster, 80.0f, 10.0f, 120.0f, 30.0f, false); + addRectangle(raster, 10.0f, 80.0f, 30.0f, 120.0f, false); + raster.fill(none.data(), RASTER_SIZE, 0x0000, 255); + CHECK(none == whiteBuffer()); + + // A huge shape, and an edge with huge coordinates + std::vector huge = whiteBuffer(); + addRectangle(raster, -1.0e6f, -1.0e6f, 1.0e6f, 1.0e6f, false); + raster.fill(huge.data(), RASTER_SIZE, 0x0000, 255); + CHECK(pixelAt(huge, 0, 0) == 0x0000); + CHECK(pixelAt(huge, 63, 63) == 0x0000); + CHECK(pixelAt(huge, 32, 32) == 0x0000); + } + + // A triangle that sticks out on the left side keeps its visible part + { + std::vector pixels = whiteBuffer(); + raster.addLine(-20.0f, 10.0f, 30.0f, 10.0f); + raster.addLine(30.0f, 10.0f, 30.0f, 40.0f); + raster.addLine(30.0f, 40.0f, -20.0f, 10.0f); + raster.fill(pixels.data(), RASTER_SIZE, 0x0000, 255); + CHECK(pixelAt(pixels, 28, 35) == 0x0000); + CHECK(pixelAt(pixels, 29, 39) != 0xFFFF); + CHECK(pixelAt(pixels, 35, 35) == 0xFFFF); + CHECK(pixelAt(pixels, 2, 11) == 0x0000); + CHECK(pixelAt(pixels, 2, 25) == 0xFFFF); + // Area of the visible part: the triangle (-20,10) (30,10) (30,40) clipped at x >= 0 + // Line from (-20,10) to (30,40) has y = 10 + 0.6 * (x + 20); at x=0 y=22, so the visible area is + // the trapezoid with parallel sides 12 (x=0) and 30 (x=30) over a width of 30 = 630 + CHECK(nearlyEqual(totalCoverage(pixels), 630.0, 630.0 * 0.02)); + } + + // A stroked polyline that doubles back does not cancel + { + std::vector pixels = whiteBuffer(); + raster.addSegment(10.0f, 32.0f, 40.0f, 32.0f, 2.0f); + raster.addDot(10.0f, 32.0f, 2.0f); + raster.addDot(40.0f, 32.0f, 2.0f); + raster.addSegment(40.0f, 32.0f, 10.0f, 32.0f, 2.0f); + raster.addDot(10.0f, 32.0f, 2.0f); + raster.fill(pixels.data(), RASTER_SIZE, 0x0000, 255); + CHECK(pixelAt(pixels, 25, 30) == 0x0000); + CHECK(pixelAt(pixels, 25, 31) == 0x0000); + CHECK(pixelAt(pixels, 25, 32) == 0x0000); + CHECK(pixelAt(pixels, 25, 33) == 0x0000); + CHECK(pixelAt(pixels, 25, 29) == 0xFFFF); + CHECK(pixelAt(pixels, 25, 34) == 0xFFFF); + CHECK(pixelAt(pixels, 40, 32) == 0x0000); + CHECK(pixelAt(pixels, 10, 32) == 0x0000); + CHECK(pixelAt(pixels, 41, 32) != 0xFFFF); + CHECK(pixelAt(pixels, 40, 31) == 0x0000); + + // Drawn once, no pixel is covered more than when doubled back (partly covered edge pixels add up, so they + // can be darker when doubled, but never lighter) + std::vector single = whiteBuffer(); + raster.addSegment(10.0f, 32.0f, 40.0f, 32.0f, 2.0f); + raster.addDot(10.0f, 32.0f, 2.0f); + raster.addDot(40.0f, 32.0f, 2.0f); + raster.fill(single.data(), RASTER_SIZE, 0x0000, 255); + CHECK(notLighter(pixels, single)); + + // Diagonal segments in both directions overlapping each other + std::vector diagonal = whiteBuffer(); + raster.addSegment(8.0f, 8.0f, 50.0f, 40.0f, 1.5f); + raster.addSegment(50.0f, 40.0f, 8.0f, 8.0f, 1.5f); + raster.addSegment(8.0f, 8.0f, 50.0f, 40.0f, 1.5f); + raster.fill(diagonal.data(), RASTER_SIZE, 0x0000, 255); + // The middle of the line at x=29, y=24 + CHECK(pixelAt(diagonal, 29, 24) == 0x0000); + std::vector diagonalOnce = whiteBuffer(); + raster.addSegment(8.0f, 8.0f, 50.0f, 40.0f, 1.5f); + raster.fill(diagonalOnce.data(), RASTER_SIZE, 0x0000, 255); + CHECK(notLighter(diagonal, diagonalOnce)); + } + + // Holes: opposite winding stays uncovered, equal winding is filled (nonzero rule) + { + std::vector holed = whiteBuffer(); + addRectangle(raster, 10.0f, 10.0f, 50.0f, 50.0f, false); + addRectangle(raster, 20.0f, 20.0f, 40.0f, 40.0f, true); + raster.fill(holed.data(), RASTER_SIZE, 0x0000, 255); + CHECK(pixelAt(holed, 15, 15) == 0x0000); + CHECK(pixelAt(holed, 45, 45) == 0x0000); + CHECK(pixelAt(holed, 15, 45) == 0x0000); + CHECK(pixelAt(holed, 30, 30) == 0xFFFF); + CHECK(pixelAt(holed, 20, 20) == 0xFFFF); + CHECK(pixelAt(holed, 39, 39) == 0xFFFF); + CHECK(pixelAt(holed, 30, 15) == 0x0000); + CHECK(pixelAt(holed, 30, 45) == 0x0000); + CHECK(nearlyEqual(totalCoverage(holed), 40.0 * 40.0 - 20.0 * 20.0, 1200.0 * 0.02)); + + std::vector filled = whiteBuffer(); + addRectangle(raster, 10.0f, 10.0f, 50.0f, 50.0f, false); + addRectangle(raster, 20.0f, 20.0f, 40.0f, 40.0f, false); + raster.fill(filled.data(), RASTER_SIZE, 0x0000, 255); + CHECK(pixelAt(filled, 30, 30) == 0x0000); + CHECK(pixelAt(filled, 15, 15) == 0x0000); + + // Holed shape with the outer ring wound the other way gives the same pixels + std::vector holedReversed = whiteBuffer(); + addRectangle(raster, 10.0f, 10.0f, 50.0f, 50.0f, true); + addRectangle(raster, 20.0f, 20.0f, 40.0f, 40.0f, false); + raster.fill(holedReversed.data(), RASTER_SIZE, 0x0000, 255); + CHECK(holedReversed == holed); + } + + // Degenerate input: NaN, infinity, zero length segments + { + std::vector pixels = whiteBuffer(); + raster.addLine(std::nanf(""), 1.0f, 5.0f, 9.0f); + raster.addLine(1.0f, 1.0f, INFINITY, 9.0f); + raster.addSegment(5.0f, 5.0f, 5.0f, 5.0f, 2.0f); + raster.fill(pixels.data(), RASTER_SIZE, 0x0000, 255); + CHECK(pixels == whiteBuffer()); + } + + // blendRgb565 end points + CHECK(blendRgb565(0x0000, 0xFFFF, 0) == 0xFFFF); + CHECK(blendRgb565(0x0000, 0xFFFF, 32) == 0x0000); + CHECK(blendRgb565(0xF800, 0x0000, 32) == 0xF800); +} + +// --------------------------------------------------------------------------------------------------------------- +// 6. Golden images +// --------------------------------------------------------------------------------------------------------------- + +uint64_t fnv1a(const uint16_t* pixels, size_t count) { + uint64_t hash = 0xCBF29CE484222325ull; + for (size_t i = 0; i < count; i++) { + hash ^= static_cast(pixels[i] & 0xFF); + hash *= 0x100000001B3ull; + hash ^= static_cast(pixels[i] >> 8); + hash *= 0x100000001B3ull; + } + return hash; +} + +struct GoldenEntry { + std::string name; + uint64_t hash; +}; + +std::vector readGolden(const char* path, bool* found) { + std::vector entries; + FILE* file = fopen(path, "r"); + *found = file != nullptr; + if (file == nullptr) { + return entries; + } + char name[64]; + unsigned long long hash; + while (fscanf(file, "%63s %llx", name, &hash) == 2) { + entries.push_back({ name, static_cast(hash) }); + } + fclose(file); + return entries; +} + +void testGolden(bool update) { + printf("golden images\n"); + const char* goldenPath = FIXTURE_DIR "/golden.txt"; + const char* fixturePath = FIXTURE_DIR "/14-9058-5729.pbf"; + struct Case { + const char* name; + TileKey target; + }; + const Case cases[] = { + { "tile-14", { 14, 9058, 5729 } }, + { "overzoom-16", { 16, 36232, 22916 } }, + { "overzoom-18", { 18, 144936, 91672 } }, + }; + const TileKey source { 14, 9058, 5729 }; + + TileRenderer renderer; + CHECK(renderer.init()); + std::vector results; + std::vector> images; + for (const Case& testCase : cases) { + FileInput input; + CHECK(input.open(fixturePath)); + std::vector pixels(static_cast(TILE_SIZE) * TILE_SIZE, 0x1234); + RenderStats stats {}; + const bool ok = renderer.render(&input, source, testCase.target, colorPalette(), pixels.data(), &stats); + CHECK(ok); + CHECK(stats.sweeps <= 4); + CHECK(stats.features > 100); + printf(" %s: %u features, %u sweeps, %u bytes inflated\n", testCase.name, stats.features, stats.sweeps, stats.decompressedBytes); + size_t differing = 0; + for (const uint16_t pixel : pixels) { + if (pixel != colorPalette().background) { + differing++; + } + } + CHECK(differing > pixels.size() / 100); + results.push_back({ testCase.name, fnv1a(pixels.data(), pixels.size()) }); + images.push_back(std::move(pixels)); + printf(" %s hash %016llx\n", testCase.name, static_cast(results.back().hash)); + } + + // Rendering again with the same renderer gives the same pixels + { + FileInput input; + CHECK(input.open(fixturePath)); + std::vector again(static_cast(TILE_SIZE) * TILE_SIZE); + CHECK(renderer.render(&input, source, cases[0].target, colorPalette(), again.data(), nullptr)); + CHECK(again == images[0]); + } + + bool found = false; + const std::vector golden = readGolden(goldenPath, &found); + if (update || !found || golden.empty()) { + FILE* file = fopen(goldenPath, "w"); + CHECK(file != nullptr); + if (file != nullptr) { + for (const GoldenEntry& entry : results) { + fprintf(file, "%s %016llx\n", entry.name.c_str(), static_cast(entry.hash)); + } + fclose(file); + printf(" wrote %s\n", goldenPath); + } + CHECK(writePngRgb565(FIXTURE_DIR "/golden-14.png", images[0].data(), TILE_SIZE, TILE_SIZE)); + CHECK(writePngRgb565(FIXTURE_DIR "/golden-16.png", images[1].data(), TILE_SIZE, TILE_SIZE)); + CHECK(writePngRgb565(FIXTURE_DIR "/golden-18.png", images[2].data(), TILE_SIZE, TILE_SIZE)); + return; + } + + for (size_t i = 0; i < results.size(); i++) { + const GoldenEntry* expected = nullptr; + for (const GoldenEntry& entry : golden) { + if (entry.name == results[i].name) { + expected = &entry; + } + } + const bool match = expected != nullptr && expected->hash == results[i].hash; + CHECK(match); + if (!match) { + char path[256]; + snprintf(path, sizeof(path), FIXTURE_DIR "/actual-%s.png", results[i].name.c_str()); + writePngRgb565(path, images[i].data(), TILE_SIZE, TILE_SIZE); + printf(" %s differs: expected %016llx, got %016llx, wrote %s\n", results[i].name.c_str(), expected != nullptr ? static_cast(expected->hash) : 0ull, static_cast(results[i].hash), path); + } + } +} + +// --------------------------------------------------------------------------------------------------------------- +// 7. Compose +// --------------------------------------------------------------------------------------------------------------- + +struct FakeTiles { + std::vector pixels[4]; + TileKey skip { -1, -1, -1 }; + bool skipAll = false; + int calls = 0; + int lastZoom = -1; + bool outOfRange = false; + + FakeTiles() { + const uint16_t colors[4] = { 0xF800, 0x07E0, 0x001F, 0xFFE0 }; + for (int i = 0; i < 4; i++) { + pixels[i].assign(static_cast(TILE_SIZE) * TILE_SIZE, colors[i]); + } + } +}; + +const uint16_t* fakeLookup(const TileKey& tile, void* context) { + auto* tiles = static_cast(context); + tiles->calls++; + tiles->lastZoom = tile.z; + if (tile.x < 0 || tile.x > 1 || tile.y < 0 || tile.y > 1) { + tiles->outOfRange = true; + return nullptr; + } + if (tiles->skipAll || tile == tiles->skip) { + return nullptr; + } + return tiles->pixels[tile.y * 2 + tile.x].data(); +} + +void testCompose() { + printf("compose\n"); + constexpr uint16_t RED = 0xF800; + constexpr uint16_t GREEN = 0x07E0; + constexpr uint16_t BLUE = 0x001F; + constexpr uint16_t YELLOW = 0xFFE0; + constexpr uint16_t PLACEHOLDER = 0x8410; + constexpr int W = 300; + constexpr int H = 200; + + auto expectedColor = [](int x, int y) -> uint16_t { + // The view starts at world pixel (106, 156) so the corner of the tiles (256, 256) is at (150, 100) + if (y < 100) { + return x < 150 ? RED : GREEN; + } + return x < 150 ? BLUE : YELLOW; + }; + + { + FakeTiles tiles; + std::vector view(static_cast(W) * H, 0); + const int missing = composeView(view.data(), W, H, 256.0, 256.0, 1, fakeLookup, &tiles, PLACEHOLDER); + CHECK(missing == 0); + CHECK(tiles.calls == 4); + CHECK(tiles.lastZoom == 1); + bool allOk = true; + for (int y = 0; y < H; y++) { + for (int x = 0; x < W; x++) { + if (view[static_cast(y) * W + static_cast(x)] != expectedColor(x, y)) { + allOk = false; + } + } + } + CHECK(allOk); + CHECK(view[0] == RED); + CHECK(view[149] == RED); + CHECK(view[150] == GREEN); + CHECK(view[static_cast(99) * W + 149] == RED); + CHECK(view[static_cast(100) * W + 149] == BLUE); + CHECK(view[static_cast(100) * W + 150] == YELLOW); + CHECK(view[static_cast(H - 1) * W + (W - 1)] == YELLOW); + } + + // One tile missing + { + FakeTiles tiles; + tiles.skip = { 1, 1, 0 }; + std::vector view(static_cast(W) * H, 0); + const int missing = composeView(view.data(), W, H, 256.0, 256.0, 1, fakeLookup, &tiles, PLACEHOLDER); + CHECK(missing == 1); + bool allOk = true; + for (int y = 0; y < H; y++) { + for (int x = 0; x < W; x++) { + const uint16_t expected = (x >= 150 && y < 100) ? PLACEHOLDER : expectedColor(x, y); + if (view[static_cast(y) * W + static_cast(x)] != expected) { + allOk = false; + } + } + } + CHECK(allOk); + } + + // Every tile missing + { + FakeTiles tiles; + tiles.skipAll = true; + std::vector view(static_cast(W) * H, 0); + const int missing = composeView(view.data(), W, H, 256.0, 256.0, 1, fakeLookup, &tiles, PLACEHOLDER); + CHECK(missing == 4); + bool allOk = true; + for (const uint16_t pixel : view) { + if (pixel != PLACEHOLDER) { + allOk = false; + } + } + CHECK(allOk); + } + + // A view across the antimeridian wraps to the tile on the other side (x -1 is tile 1 at zoom 1) + { + FakeTiles tiles; + constexpr int S = 100; + std::vector view(static_cast(S) * S, 0); + const int missing = composeView(view.data(), S, S, 0.0, 256.0, 1, fakeLookup, &tiles, PLACEHOLDER); + CHECK(missing == 0); + CHECK(!tiles.outOfRange); + // The view covers world x -50..49, y 206..305 + CHECK(view[static_cast(0) * S + 0] == GREEN); + CHECK(view[static_cast(0) * S + 49] == GREEN); + CHECK(view[static_cast(0) * S + 50] == RED); + CHECK(view[static_cast(S - 1) * S + 49] == YELLOW); + CHECK(view[static_cast(S - 1) * S + 50] == BLUE); + } + + // Above the top of the world there are no tiles, which count as missing without asking for them + { + FakeTiles tiles; + constexpr int S = 100; + std::vector view(static_cast(S) * S, 0); + const int missing = composeView(view.data(), S, S, 256.0, 0.0, 1, fakeLookup, &tiles, PLACEHOLDER); + CHECK(missing == 2); + CHECK(!tiles.outOfRange); + CHECK(view[0] == PLACEHOLDER); + CHECK(view[static_cast(S - 1) * S] == RED); + CHECK(view[static_cast(S - 1) * S + (S - 1)] == GREEN); + } +} + +void testTileCodec() { + printf("tile codec\n"); + // A rendered tile: flat areas, anti-aliased lines and a run longer than one token can hold + TileRenderer renderer; + CHECK(renderer.init()); + FileInput input; + CHECK(input.open(FIXTURE_DIR "/14-9058-5729.pbf")); + const size_t count = static_cast(TILE_SIZE) * TILE_SIZE; + std::vector pixels(count); + CHECK(renderer.render(&input, { 14, 9058, 5729 }, { 14, 9058, 5729 }, colorPalette(), pixels.data(), nullptr)); + std::vector encoded(count + 256); + const size_t used = encodeTile(pixels.data(), count, encoded.data(), encoded.size()); + CHECK(used > 0 && used < count); + printf(" rendered tile: %zu of %zu words (%.0f%%)\n", used, count, 100.0 * used / count); + std::vector decoded(count, 0); + CHECK(maps::decodeTile(encoded.data(), used, decoded.data(), count)); + CHECK(decoded == pixels); + + std::vector flat(count, 0xABCD); + const size_t flatUsed = encodeTile(flat.data(), count, encoded.data(), encoded.size()); + CHECK(flatUsed > 0 && flatUsed <= 8); + CHECK(maps::decodeTile(encoded.data(), flatUsed, decoded.data(), count)); + CHECK(decoded == flat); + + std::vector noise(count); + for (size_t i = 0; i < count; i++) { + noise[i] = static_cast(i * 2654435761u >> 7); + } + const size_t noiseUsed = encodeTile(noise.data(), count, encoded.data(), encoded.size()); + CHECK(noiseUsed > 0 && noiseUsed <= count + 8); + CHECK(maps::decodeTile(encoded.data(), noiseUsed, decoded.data(), count)); + CHECK(decoded == noise); + CHECK(encodeTile(noise.data(), count, encoded.data(), count / 2) == 0); + CHECK(!maps::decodeTile(encoded.data(), 3, decoded.data(), count)); +} + +// --------------------------------------------------------------------------------------------------------------- +// 8. Download plan +// --------------------------------------------------------------------------------------------------------------- + +/** Walks a whole plan with tileAtIndex and checks order, range, uniqueness and the end of the sequence. */ +void checkPlanEnumeration(const DownloadPlan& plan) { + std::set> seen; + bool allValid = true; + bool zoomsOrdered = true; + bool insideRange = true; + int previousZoom = plan.minZoom; + for (int64_t index = 0; index < plan.tiles; index++) { + TileKey tile { -1, -1, -1 }; + if (!tileAtIndex(plan, index, tile) || !isValidTile(tile)) { + allValid = false; + continue; + } + if (tile.z < previousZoom) { + zoomsOrdered = false; + } + previousZoom = tile.z; + if (tile.z < plan.minZoom || tile.z > plan.maxZoom) { + insideRange = false; + } else { + const TileRange range = tileRange(plan.box, tile.z); + if (tile.x < range.minX || tile.x > range.maxX || tile.y < range.minY || tile.y > range.maxY) { + insideRange = false; + } + } + seen.insert({ tile.z, tile.x, tile.y }); + } + CHECK(allValid); + CHECK(zoomsOrdered); + CHECK(insideRange); + CHECK(static_cast(seen.size()) == plan.tiles); + TileKey tile { -1, -1, -1 }; + CHECK(!tileAtIndex(plan, plan.tiles, tile)); + CHECK(!tileAtIndex(plan, plan.tiles + 1000, tile)); + CHECK(!tileAtIndex(plan, -1, tile)); +} + +bool withinLimits(int64_t tiles, uint64_t bytes) { + return tiles <= MAX_DOWNLOAD_TILES && bytes <= MAX_DOWNLOAD_BYTES; +} + +void testDownloadPlan() { + printf("download plan\n"); + CHECK(typicalTileBytes(14) > 0); + CHECK(typicalTileBytes(20) == typicalTileBytes(14)); + CHECK(typicalTileBytes(0) > 0); + CHECK(typicalTileBytes(-1) == 0); + + const BoundingBox budapest { 19.03, 47.49, 19.05, 47.505 }; + { + uint64_t manual = 0; + for (int z = 0; z <= 14; z++) { + manual += static_cast(tileRange(budapest, z).count()) * typicalTileBytes(z); + } + CHECK(estimateBytes(budapest, 0, 14) == manual); + uint64_t partial = 0; + for (int z = 6; z <= 9; z++) { + partial += static_cast(tileRange(budapest, z).count()) * typicalTileBytes(z); + } + CHECK(estimateBytes(budapest, 6, 9) == partial); + CHECK(estimateBytes(budapest, 5, 4) == 0); + } + + const DownloadPlan small = planDownload(budapest, 14); + CHECK(small.fits); + CHECK(small.minZoom == 0); + CHECK(small.maxZoom == 14); + CHECK(small.tiles == tileCount(budapest, 0, 14)); + CHECK(small.estimatedBytes == estimateBytes(budapest, 0, 14)); + CHECK(small.estimatedBytes <= MAX_DOWNLOAD_BYTES); + CHECK(small.tiles > 0 && small.tiles <= MAX_DOWNLOAD_TILES); + checkPlanEnumeration(small); + + // A lower requested zoom is respected, a higher one is clamped to the source zoom + { + const DownloadPlan low = planDownload(budapest, 8); + CHECK(low.fits); + CHECK(low.maxZoom == 8); + CHECK(low.tiles == tileCount(budapest, 0, 8)); + const DownloadPlan high = planDownload(budapest, 19); + CHECK(high.fits); + CHECK(high.maxZoom == MAX_SOURCE_ZOOM); + } + + // A whole country has to lower the max zoom, and the next zoom up would break a limit + { + const BoundingBox hungary { 16.1, 45.7, 22.9, 48.6 }; + const DownloadPlan country = planDownload(hungary, 14); + CHECK(country.fits); + CHECK(country.maxZoom < 14); + CHECK(country.maxZoom >= 0); + CHECK(country.tiles <= MAX_DOWNLOAD_TILES); + CHECK(country.estimatedBytes <= 50000000ull); + CHECK(country.tiles == tileCount(hungary, 0, country.maxZoom)); + CHECK(country.estimatedBytes == estimateBytes(hungary, 0, country.maxZoom)); + const int next = country.maxZoom + 1; + CHECK(!withinLimits(tileCount(hungary, 0, next), estimateBytes(hungary, 0, next))); + printf(" hungary: max zoom %d, %lld tiles, %llu bytes\n", country.maxZoom, static_cast(country.tiles), static_cast(country.estimatedBytes)); + checkPlanEnumeration(country); + } + + // The whole world still gives a plan that respects the limits + { + const BoundingBox world { -180.0, -85.0, 180.0, 85.0 }; + const DownloadPlan all = planDownload(world, 14); + CHECK(all.maxZoom <= 14); + CHECK(all.tiles <= MAX_DOWNLOAD_TILES); + CHECK(all.estimatedBytes <= MAX_DOWNLOAD_BYTES); + if (all.fits) { + CHECK(all.tiles == tileCount(world, 0, all.maxZoom)); + CHECK(all.estimatedBytes == estimateBytes(world, 0, all.maxZoom)); + if (all.maxZoom < 14) { + const int next = all.maxZoom + 1; + CHECK(!withinLimits(tileCount(world, 0, next), estimateBytes(world, 0, next))); + } + checkPlanEnumeration(all); + } + printf(" world: fits %d, max zoom %d, %lld tiles, %llu bytes\n", all.fits ? 1 : 0, all.maxZoom, static_cast(all.tiles), static_cast(all.estimatedBytes)); + } + + // A hand-made plan with a minimum zoom above 0 enumerates from that zoom + { + DownloadPlan custom = small; + custom.minZoom = 10; + custom.maxZoom = 12; + custom.tiles = tileCount(custom.box, 10, 12); + checkPlanEnumeration(custom); + TileKey first { 0, 0, 0 }; + CHECK(tileAtIndex(custom, 0, first)); + CHECK(first.z == 10); + const TileRange range = tileRange(custom.box, 10); + CHECK(first.x == range.minX && first.y == range.minY); + } +} + +// --------------------------------------------------------------------------------------------------------------- +// 9. Region meta +// --------------------------------------------------------------------------------------------------------------- + +constexpr const char* TEST_TILE_TEMPLATE = "https://tiles.openfreemap.org/planet/20261025_080012_pt/{z}/{x}/{y}.pbf"; + +bool sameMeta(const RegionMeta& a, const RegionMeta& b) { + return strcmp(a.name, b.name) == 0 && nearlyEqual(a.box.west, b.box.west, 1e-6) && nearlyEqual(a.box.south, b.box.south, 1e-6) && + nearlyEqual(a.box.east, b.box.east, 1e-6) && nearlyEqual(a.box.north, b.box.north, 1e-6) && a.minZoom == b.minZoom && + a.maxZoom == b.maxZoom && strcmp(a.source, b.source) == 0 && strcmp(a.version, b.version) == 0 && strcmp(a.date, b.date) == 0 && + a.total == b.total && a.done == b.done && a.bytes == b.bytes; +} + +void testRegionMeta() { + printf("region meta\n"); + RegionMeta meta = {}; + strcpy(meta.name, "area01"); + meta.box = { 19.031234, 47.491234, 19.051234, 47.501234 }; + meta.minZoom = 0; + meta.maxZoom = 13; + strcpy(meta.source, TEST_TILE_TEMPLATE); + strcpy(meta.version, "20261025_080012_pt"); + strcpy(meta.date, "2026-10-29"); + meta.total = 1234; + meta.done = 56; + meta.bytes = 7890123; + + char text[1024]; + CHECK(formatRegionMeta(meta, text, sizeof(text))); + RegionMeta parsed = {}; + CHECK(parseRegionMeta(text, parsed)); + CHECK(sameMeta(meta, parsed)); + CHECK(strcmp(parsed.name, "area01") == 0); + CHECK(parsed.total == 1234 && parsed.done == 56 && parsed.bytes == 7890123); + CHECK(!parsed.isComplete()); + + // Too small an output buffer is refused + char tiny[40]; + CHECK(!formatRegionMeta(meta, tiny, sizeof(tiny))); + + // The meta.json written by host/fetch_tiles.py (Python json.dump): no name, version, total or bytes + { + const char* fetched = + "{\"bbox\": [16.1, 45.7, 22.9, 48.6], \"minzoom\": 0, \"maxzoom\": 11, " + "\"source\": \"https://tiles.openfreemap.org/planet/20261025_080012_pt/{z}/{x}/{y}.pbf\", " + "\"date\": \"2026-10-29\", \"done\": 77}"; + RegionMeta fromScript = {}; + CHECK(parseRegionMeta(fetched, fromScript)); + const BoundingBox hungary { 16.1, 45.7, 22.9, 48.6 }; + CHECK(nearlyEqual(fromScript.box.west, 16.1, 1e-9)); + CHECK(nearlyEqual(fromScript.box.south, 45.7, 1e-9)); + CHECK(nearlyEqual(fromScript.box.east, 22.9, 1e-9)); + CHECK(nearlyEqual(fromScript.box.north, 48.6, 1e-9)); + CHECK(fromScript.minZoom == 0 && fromScript.maxZoom == 11); + CHECK(strcmp(fromScript.source, TEST_TILE_TEMPLATE) == 0); + CHECK(strcmp(fromScript.date, "2026-10-29") == 0); + CHECK(fromScript.total == tileCount(hungary, 0, 11)); + CHECK(fromScript.total > 0); + CHECK(fromScript.done == 77); + CHECK(fromScript.name[0] == '\0'); + CHECK(fromScript.bytes == 0); + CHECK(!fromScript.isComplete()); + } + + // Invalid inputs + { + RegionMeta bad = {}; + CHECK(!parseRegionMeta("{\"minzoom\": 0, \"maxzoom\": 5, \"done\": 1}", bad)); + CHECK(!parseRegionMeta("{\"bbox\": [1, 2, 3], \"minzoom\": 0, \"maxzoom\": 5}", bad)); + CHECK(!parseRegionMeta("{\"bbox\": [16.1, 45.7, 22.9, 48.6], \"minzoom\": 0, \"maxzoom\": 15}", bad)); + CHECK(!parseRegionMeta("{\"bbox\": [16.1, 45.7, 22.9, 48.6], \"minzoom\": 6, \"maxzoom\": 5}", bad)); + CHECK(!parseRegionMeta("{\"bbox\": [16.1, 45.7, 22.9, 48.6], \"minzoom\": -1, \"maxzoom\": 5}", bad)); + CHECK(!parseRegionMeta("{\"bbox\": [16.1, 45.7, 22.9, 48.6], \"maxzoom\": 5}", bad)); + CHECK(!parseRegionMeta("this is not json at all", bad)); + CHECK(!parseRegionMeta("", bad)); + // minzoom equal to maxzoom 14 is the valid edge + CHECK(parseRegionMeta("{\"bbox\": [16.1, 45.7, 22.9, 48.6], \"minzoom\": 14, \"maxzoom\": 14}", bad)); + } + + // Completeness: done >= total > 0 + { + RegionMeta state = {}; + state.total = 0; + state.done = 0; + CHECK(!state.isComplete()); + state.total = 10; + state.done = 9; + CHECK(!state.isComplete()); + state.done = 10; + CHECK(state.isComplete()); + state.done = 11; + CHECK(state.isComplete()); + state.total = 0; + state.done = 5; + CHECK(!state.isComplete()); + const DownloadPlan plan = meta.plan(); + CHECK(plan.minZoom == 0 && plan.maxZoom == 13 && plan.tiles == 1234 && plan.fits); + } + + // Files: write, read back, overwrite, no ".tmp" left behind + { + const std::filesystem::path folder = std::filesystem::temp_directory_path(); + const std::string path = (folder / "tmaps_meta_test.json").string(); + const std::string temporary = path + ".tmp"; + std::filesystem::remove(path); + std::filesystem::remove(temporary); + CHECK(path.size() < PATH_MAX_LENGTH); + CHECK(!readRegionMeta(path.c_str(), parsed)); + CHECK(writeRegionMeta(path.c_str(), meta)); + CHECK(std::filesystem::exists(path)); + CHECK(!std::filesystem::exists(temporary)); + RegionMeta fromFile = {}; + CHECK(readRegionMeta(path.c_str(), fromFile)); + CHECK(sameMeta(meta, fromFile)); + + meta.done = 1234; + meta.bytes = 9999999; + CHECK(writeRegionMeta(path.c_str(), meta)); + CHECK(!std::filesystem::exists(temporary)); + RegionMeta again = {}; + CHECK(readRegionMeta(path.c_str(), again)); + CHECK(sameMeta(meta, again)); + CHECK(again.isComplete()); + std::filesystem::remove(path); + std::filesystem::remove(temporary); + + // A folder that does not exist cannot be written + const std::string missing = (folder / "tmaps_no_such_folder" / "meta.json").string(); + CHECK(!writeRegionMeta(missing.c_str(), meta)); + } +} + +// --------------------------------------------------------------------------------------------------------------- +// 10. TileJSON +// --------------------------------------------------------------------------------------------------------------- + +void testTileJson() { + printf("tilejson\n"); + const char* document = + "{\"tilejson\":\"3.0.0\",\"name\":\"OpenFreeMap Planet\"," + "\"tiles\":[\"https://tiles.openfreemap.org/planet/20261025_080012_pt/{z}/{x}/{y}.pbf\"]," + "\"minzoom\":0,\"maxzoom\":14," + "\"vector_layers\":[{\"id\":\"water\",\"fields\":{\"class\":\"String\"}},{\"id\":\"building\",\"fields\":{}}]}"; + char urlTemplate[160]; + char version[48]; + CHECK(parseTileJson(document, urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + CHECK(strcmp(urlTemplate, TEST_TILE_TEMPLATE) == 0); + CHECK(strcmp(version, "20261025_080012_pt") == 0); + + // Pretty printed JSON works as well + { + const char* pretty = "{\n \"tilejson\": \"3.0.0\",\n \"tiles\": [\n \"https://tiles.openfreemap.org/planet/20261025_080012_pt/{z}/{x}/{y}.pbf\"\n ]\n}\n"; + CHECK(parseTileJson(pretty, urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + CHECK(strcmp(urlTemplate, TEST_TILE_TEMPLATE) == 0); + CHECK(strcmp(version, "20261025_080012_pt") == 0); + } + + // Rejected documents + CHECK(!parseTileJson("{\"tilejson\":\"3.0.0\",\"minzoom\":0}", urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + CHECK(!parseTileJson("{\"tiles\":[\"http://tiles.openfreemap.org/planet/20261025_080012_pt/{z}/{x}/{y}.pbf\"]}", urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + CHECK(!parseTileJson("{\"tiles\":[\"https://tiles.openfreemap.org/planet/20261025_080012_pt/{z}/{x}/y.pbf\"]}", urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + CHECK(!parseTileJson("{\"tiles\":[\"https://tiles.openfreemap.org/planet/20261025_080012_pt/{x}/{y}.pbf\"]}", urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + CHECK(!parseTileJson("{\"tiles\":[\"https://tile.openstreetmap.org/{z}/{x}/{y}.png\"]}", urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + CHECK(!parseTileJson("{\"tiles\":[]}", urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + CHECK(!parseTileJson("{\"tiles\":[\"https://tiles.openfreemap.org/planet/20261025_080012_pt/{z}/{x}/{y}.pbf", urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + CHECK(!parseTileJson("", urlTemplate, sizeof(urlTemplate), version, sizeof(version))); + + // The template buffer must hold the whole template and its terminator + { + const size_t length = strlen(TEST_TILE_TEMPLATE); + char buffer[160]; + CHECK(!parseTileJson(document, buffer, 20, version, sizeof(version))); + CHECK(!parseTileJson(document, buffer, length, version, sizeof(version))); + CHECK(parseTileJson(document, buffer, length + 1, version, sizeof(version))); + CHECK(strcmp(buffer, TEST_TILE_TEMPLATE) == 0); + } + + // A version buffer that is too small leaves an empty version instead of overflowing + { + char smallVersion[8]; + memset(smallVersion, 'x', sizeof(smallVersion)); + CHECK(parseTileJson(document, urlTemplate, sizeof(urlTemplate), smallVersion, sizeof(smallVersion))); + CHECK(smallVersion[0] == '\0'); + } + + // Filling in the placeholders + char url[160]; + CHECK(formatTileUrl(TEST_TILE_TEMPLATE, { 14, 9058, 5729 }, url, sizeof(url))); + CHECK(strcmp(url, "https://tiles.openfreemap.org/planet/20261025_080012_pt/14/9058/5729.pbf") == 0); + CHECK(formatTileUrl(TEST_TILE_TEMPLATE, { 0, 0, 0 }, url, sizeof(url))); + CHECK(strcmp(url, "https://tiles.openfreemap.org/planet/20261025_080012_pt/0/0/0.pbf") == 0); + { + const char* expected = "https://tiles.openfreemap.org/planet/20261025_080012_pt/14/9058/5729.pbf"; + const size_t length = strlen(expected); + char exact[160]; + CHECK(formatTileUrl(TEST_TILE_TEMPLATE, { 14, 9058, 5729 }, exact, length + 1)); + CHECK(strcmp(exact, expected) == 0); + CHECK(!formatTileUrl(TEST_TILE_TEMPLATE, { 14, 9058, 5729 }, exact, length)); + CHECK(!formatTileUrl(TEST_TILE_TEMPLATE, { 14, 9058, 5729 }, exact, 10)); + CHECK(!formatTileUrl(TEST_TILE_TEMPLATE, { 14, 9058, 5729 }, exact, 1)); + } + // A buffer that ends inside a placeholder value + { + char shortBuffer[sizeof("https://tiles.openfreemap.org/planet/20261025_080012_pt/14/90")]; + CHECK(!formatTileUrl(TEST_TILE_TEMPLATE, { 14, 9058, 5729 }, shortBuffer, sizeof(shortBuffer))); + } +} + +} // namespace + +// --------------------------------------------------------------------------------------------------------------- +// Prepared tiles +// --------------------------------------------------------------------------------------------------------------- + +std::vector readFixture(const char* path) { + std::vector bytes; + FILE* file = fopen(path, "rb"); + if (file == nullptr) { + return bytes; + } + uint8_t buffer[4096]; + size_t count; + while ((count = fread(buffer, 1, sizeof(buffer), file)) > 0) { + bytes.insert(bytes.end(), buffer, buffer + count); + } + fclose(file); + return bytes; +} + +void testPreparedTiles() { + printf("prepared tiles\n"); + const std::vector pbf = readFixture(FIXTURE_DIR "/14-9058-5729.pbf"); + CHECK(!pbf.empty()); + const TileKey source { 14, 9058, 5729 }; + TileRenderer renderer; + CHECK(renderer.init()); + TileConverter converter; + ByteWriter raw; + ByteWriter deflated; + ConvertStats stats {}; + MemoryInput input(pbf.data(), pbf.size()); + CHECK(converter.convert(renderer.tileDecoder(), &input, source, static_cast(pbf.size()), "20261025_080012_pt", false, raw, &stats)); + CHECK(converter.convert(renderer.tileDecoder(), &input, source, static_cast(pbf.size()), "20261025_080012_pt", true, deflated, nullptr)); + printf(" pbf %zu bytes, raw %zu, deflated %zu, features %u -> %u\n", pbf.size(), raw.size(), deflated.size(), stats.featuresIn, stats.featuresKept); + CHECK(raw.size() < pbf.size() / 2); + CHECK(deflated.size() < raw.size()); + CHECK(stats.featuresKept > 100 && stats.featuresKept <= stats.featuresIn); + + // Header + PreparedHeader header {}; + CHECK(readPreparedHeader(raw.bytes(), raw.size(), header)); + CHECK(header.version == PREPARED_FORMAT_VERSION); + CHECK(header.tile == source); + CHECK(header.extent == PREPARED_EXTENT); + CHECK(header.sourceSize == pbf.size()); + CHECK(header.flags == 0); + CHECK(header.bodySize == header.storedSize); + CHECK(PREPARED_HEADER_SIZE + header.storedSize == raw.size()); + CHECK(strcmp(header.sourceVersion, "20261025_080012_pt") == 0); + CHECK(isPreparedCurrent(header, source, static_cast(pbf.size()))); + CHECK(!isPreparedCurrent(header, source, static_cast(pbf.size()) + 1)); + CHECK(!isPreparedCurrent(header, { 14, 9058, 5730 }, static_cast(pbf.size()))); + PreparedHeader older = header; + older.version = PREPARED_FORMAT_VERSION - 1; + CHECK(!isPreparedCurrent(older, source, static_cast(pbf.size()))); + PreparedHeader deflatedHeader {}; + CHECK(readPreparedHeader(deflated.bytes(), deflated.size(), deflatedHeader)); + CHECK((deflatedHeader.flags & PREPARED_FLAG_DEFLATED) != 0); + CHECK(deflatedHeader.bodySize == header.bodySize); + CHECK(!readPreparedHeader(pbf.data(), pbf.size(), header)); + + // The prepared copy renders like the original, within a small tolerance, at the native and overzoomed levels. + // z18 is the golden-18 tile: with 1024 units per tile its lines were visibly stepped (3.0 % of pixels strongly off, mean 3.2). + const TileKey targets[] = { source, { 16, 36232, 22916 }, { 18, 144936, 91672 } }; + const double maxMean[] = { 3.0, 4.0, 3.0 }; + const double maxStrong[] = { 0.005, 0.02, 0.006 }; + const char* failedNames[] = { FIXTURE_DIR "/actual-prt-14.png", FIXTURE_DIR "/actual-prt-16.png", FIXTURE_DIR "/actual-prt-18.png" }; + for (int i = 0; i < 3; i++) { + std::vector original(static_cast(TILE_SIZE) * TILE_SIZE); + std::vector fromRaw(original.size()); + std::vector fromDeflated(original.size()); + MemoryInput again(pbf.data(), pbf.size()); + CHECK(renderer.render(&again, source, targets[i], colorPalette(), original.data(), nullptr)); + RenderStats preparedStats {}; + CHECK(renderer.renderPrepared(raw.bytes(), raw.size(), targets[i], colorPalette(), fromRaw.data(), &preparedStats)); + CHECK(renderer.renderPrepared(deflated.bytes(), deflated.size(), targets[i], colorPalette(), fromDeflated.data(), nullptr)); + CHECK(fromRaw == fromDeflated); + const ImageDiff diff = compareImages(original.data(), fromRaw.data(), original.size()); + printf(" z%d: mean difference %.2f, strong %.2f%%, %u features drawn\n", targets[i].z, diff.meanDifference, 100.0 * diff.strongShare, preparedStats.features); + CHECK(diff.meanDifference < maxMean[i]); + CHECK(diff.strongShare < maxStrong[i]); + if (diff.meanDifference >= maxMean[i] || diff.strongShare >= maxStrong[i]) { + writePngRgb565(failedNames[i], fromRaw.data(), TILE_SIZE, TILE_SIZE); + } + } + + // A prepared tile only draws its own tile and the tiles inside it + std::vector pixels(static_cast(TILE_SIZE) * TILE_SIZE); + CHECK(!renderer.renderPrepared(raw.bytes(), raw.size(), { 14, 9059, 5729 }, colorPalette(), pixels.data(), nullptr)); + CHECK(!renderer.renderPrepared(raw.bytes(), raw.size(), { 13, 4529, 2864 }, colorPalette(), pixels.data(), nullptr)); + // Damaged files are refused, not drawn + CHECK(!renderer.renderPrepared(raw.bytes(), raw.size() / 2, source, colorPalette(), pixels.data(), nullptr)); + std::vector broken(deflated.bytes(), deflated.bytes() + deflated.size()); + broken[PREPARED_HEADER_SIZE + 20] ^= 0x5A; + CHECK(!renderer.renderPrepared(broken.data(), broken.size(), source, colorPalette(), pixels.data(), nullptr)); + std::vector badSection(raw.bytes(), raw.bytes() + raw.size()); + badSection[PREPARED_HEADER_SIZE] = 0x7F; + CHECK(!renderer.renderPrepared(badSection.data(), badSection.size(), source, colorPalette(), pixels.data(), nullptr)); + + // A prepared tile of a vector tile without drawn layers is valid and draws the background + { + Bytes layer; + putVarintField(layer, 15, 2); + putStringField(layer, 1, "poi"); + Bytes tile; + putBytesField(tile, 3, layer); + MemoryInput emptyInput(tile.data(), tile.size()); + ByteWriter out; + CHECK(converter.convert(renderer.tileDecoder(), &emptyInput, { 3, 1, 2 }, 0, nullptr, false, out, nullptr)); + CHECK(renderer.renderPrepared(out.bytes(), out.size(), { 3, 1, 2 }, colorPalette(), pixels.data(), nullptr)); + bool plain = true; + for (const uint16_t pixel : pixels) { + plain = plain && pixel == colorPalette().background; + } + CHECK(plain); + } + + // ByteWriter basics + ByteWriter writer; + writer.putVarint(300); + writer.putZigzag(-3); + writer.putU32(0x01020304); + CHECK(writer.size() == 7); + CHECK(writer.bytes()[0] == 0xAC && writer.bytes()[1] == 0x02); + CHECK(writer.bytes()[2] == 5); + CHECK(writer.bytes()[3] == 4 && writer.bytes()[6] == 1); + writer.setU32(3, 0xAABBCCDD); + CHECK(writer.bytes()[3] == 0xDD); + for (int i = 0; i < 100000; i++) { + writer.put(static_cast(i)); + } + CHECK(writer.size() == 100007 && !writer.failed()); + CHECK(writer.bytes()[100006] == static_cast(99999)); +} + +// --------------------------------------------------------------------------------------------------------------- +// Prepared tiles on the card, settings +// --------------------------------------------------------------------------------------------------------------- + +bool writeBytes(const std::filesystem::path& path, const uint8_t* bytes, size_t size) { + std::filesystem::create_directories(path.parent_path()); + FILE* file = fopen(path.string().c_str(), "wb"); + if (file == nullptr) { + return false; + } + const bool ok = fwrite(bytes, 1, size, file) == size; + fclose(file); + return ok; +} + +struct Collected { + std::vector> tiles; + size_t limit = 1000; +}; + +bool collectStale(const TileKey& tile, int region, void* context) { + auto* collected = static_cast(context); + if (collected->tiles.size() >= collected->limit) { + return false; + } + collected->tiles.push_back({ tile, region }); + return true; +} + +void testPreparedScanner() { + printf("prepared scanner\n"); + namespace fs = std::filesystem; + const fs::path root = fs::temp_directory_path() / "tactilitymaps-tests"; + std::error_code error; + fs::remove_all(root, error); + const std::vector pbf = readFixture(FIXTURE_DIR "/14-9058-5729.pbf"); + const TileKey tiles[] = { { 14, 9058, 5729 }, { 14, 9058, 5730 }, { 14, 9059, 5729 }, { 13, 4529, 2864 } }; + for (const TileKey& tile : tiles) { + char path[PATH_MAX_LENGTH]; + CHECK(tilePath(path, sizeof(path), root.string().c_str(), "city", tile)); + CHECK(writeBytes(path, pbf.data(), pbf.size())); + } + // A stray file and a folder that is not a number are skipped + CHECK(writeBytes(root / "maps" / "city" / "14" / "9058" / "notes.txt", pbf.data(), 4)); + fs::create_directories(root / "maps" / "city" / "14" / "tmp", error); + RegionMeta meta {}; + strcpy(meta.version, "20261025_080012_pt"); + meta.total = 1; + meta.done = 1; + CHECK(writeRegionMeta((root / "maps" / "city" / "meta.json").string().c_str(), meta)); + + TileStore store; + store.setRoot(root.string().c_str()); + CHECK(store.scanRegions() == 1); + CHECK(strcmp(store.regionVersion(0), "20261025_080012_pt") == 0); + TileFile file {}; + CHECK(store.find({ 16, 36232, 22916 }, file)); + CHECK(file.tile == tiles[0]); + CHECK(file.size == pbf.size()); + CHECK(file.region == 0); + + // Lookups skip regions that cannot have a tile, each look costs a FAT folder search on the device + CHECK(store.mayHave(0, tiles[0])); + store.setRegionBounds("city", { 19.03, 47.49, 19.05, 47.50 }, 0, 14); + CHECK(store.mayHave(0, tiles[0])); + CHECK(store.mayHave(0, { 14, 9059, 5729 })); + CHECK(!store.mayHave(0, { 14, 9200, 5729 })); + CHECK(!store.mayHave(0, { 14, 9058, 5000 })); + store.setRegionBounds("city", { 19.03, 47.49, 19.05, 47.50 }, 0, 12); + CHECK(!store.mayHave(0, tiles[0])); + CHECK(!store.find({ 16, 36232, 22916 }, file) || file.tile.z <= 12); + // A zoom range with min above max covers all zoom levels + store.setRegionBounds("city", { 19.03, 47.49, 19.05, 47.50 }, 15, 14); + CHECK(store.mayHave(0, tiles[0])); + CHECK(store.mayHave(0, { 3, 4, 2 })); + CHECK(store.find({ 16, 36232, 22916 }, file) && file.tile == tiles[0]); + + PreparedScanner scanner; + Collected collected; + int steps = 0; + while (scanner.step(store, collectStale, &collected) && steps < 100) { + steps++; + } + CHECK(scanner.isComplete()); + CHECK(collected.tiles.size() == 4); + + // Converting one makes it current, the scan then skips it + TileRenderer renderer; + CHECK(renderer.init()); + TileConverter converter; + ByteWriter out; + MemoryInput input(pbf.data(), pbf.size()); + CHECK(converter.convert(renderer.tileDecoder(), &input, tiles[0], static_cast(pbf.size()), store.regionVersion(0), true, out, nullptr)); + char prepared[PATH_MAX_LENGTH]; + CHECK(preparedPath(prepared, sizeof(prepared), root.string().c_str(), "city", tiles[0])); + CHECK(std::string(prepared).substr(std::string(prepared).size() - 13) == "9058/5729.prt"); + CHECK(writeBytes(prepared, out.bytes(), out.size())); + CHECK(PreparedScanner::isCurrent(prepared, tiles[0], static_cast(pbf.size()))); + CHECK(!PreparedScanner::isCurrent(prepared, tiles[0], static_cast(pbf.size()) - 1)); + CHECK(!PreparedScanner::isCurrent(prepared, tiles[1], static_cast(pbf.size()))); + // A prepared tile with the wrong source tile or a broken header counts as stale + char second[PATH_MAX_LENGTH]; + CHECK(preparedPath(second, sizeof(second), root.string().c_str(), "city", tiles[1])); + CHECK(writeBytes(second, out.bytes(), out.size())); + CHECK(!PreparedScanner::isCurrent(second, tiles[1], static_cast(pbf.size()))); + scanner.restart(); + collected.tiles.clear(); + while (scanner.step(store, collectStale, &collected)) { + } + CHECK(collected.tiles.size() == 3); + bool hasFirst = false; + for (const auto& entry : collected.tiles) { + hasFirst = hasFirst || entry.first == tiles[0]; + } + CHECK(!hasFirst); + + // A full queue stops a step, the same folder is looked at again + scanner.restart(); + collected.tiles.clear(); + collected.limit = 1; + int guard = 0; + while (scanner.step(store, collectStale, &collected) && guard++ < 200) { + if (collected.tiles.size() == 1 && collected.limit == 1) { + collected.limit = 1000; + } + } + CHECK(collected.tiles.size() == 3); + + // The original is never touched + char original[PATH_MAX_LENGTH]; + CHECK(tilePath(original, sizeof(original), root.string().c_str(), "city", tiles[0])); + CHECK(readFixture(original) == pbf); + fs::remove_all(root, error); +} + +void testSettings() { + printf("settings\n"); + Settings settings; + CHECK(!settings.sdCache && settings.deflatePrepared && settings.smoothPinch && settings.browseDownload); + parseSettings("sd_cache=0\nunknown=1\ndeflate_prepared=x\nsmooth_pinch=0", settings); + CHECK(!settings.sdCache); + CHECK(settings.deflatePrepared); + CHECK(!settings.smoothPinch); + CHECK(!settings.devMonochrome && !settings.devProbe); + parseSettings("dev_mono=1", settings); + CHECK(settings.devMonochrome); + settings.devMonochrome = false; + char text[128]; + CHECK(formatSettings(settings, text, sizeof(text))); + CHECK(strcmp(text, "sd_cache=0\ndeflate_prepared=1\nsmooth_pinch=0\nbrowse_download=1\ndev_mono=0\ndev_probe=0\n") == 0); + Settings parsed; + parseSettings(text, parsed); + CHECK(!parsed.sdCache && parsed.deflatePrepared && !parsed.smoothPinch); + CHECK(!formatSettings(settings, text, 10)); + const std::string path = (std::filesystem::temp_directory_path() / "tactilitymaps-settings.txt").string(); + CHECK(writeSettings(path.c_str(), settings)); + Settings read; + CHECK(readSettings(path.c_str(), read)); + CHECK(!read.sdCache && !read.smoothPinch); + std::remove(path.c_str()); + Settings missing; + CHECK(!readSettings(path.c_str(), missing)); + CHECK(!missing.sdCache); + + // The last view + SavedView view; + view.lat = 52.2186; + view.lon = -4.4206; + view.zoom = 16; + CHECK(formatSavedView(view, text, sizeof(text))); + CHECK(strcmp(text, "52218600 -4420600 16\n") == 0); + SavedView back; + CHECK(parseSavedView(text, back)); + CHECK(nearlyEqual(back.lat, 52.2186, 1e-9) && nearlyEqual(back.lon, -4.4206, 1e-9) && back.zoom == 16); + CHECK(!parseSavedView("", back)); + CHECK(!parseSavedView("52218600 4420600", back)); + CHECK(!parseSavedView("90000000 0 14", back)); + CHECK(!parseSavedView("0 0 20", back)); + CHECK(!parseSavedView("x 0 14", back)); + const std::string viewPath = (std::filesystem::temp_directory_path() / "tactilitymaps-view.txt").string(); + CHECK(writeSavedView(viewPath.c_str(), view)); + SavedView readView; + CHECK(readSavedView(viewPath.c_str(), readView)); + CHECK(readView.zoom == 16 && nearlyEqual(readView.lat, 52.2186, 1e-9)); + std::remove(viewPath.c_str()); + CHECK(!readSavedView(viewPath.c_str(), readView)); +} + +// --------------------------------------------------------------------------------------------------------------- +// Tile cache: magnified tiles go stale but stay visible until redrawn +// --------------------------------------------------------------------------------------------------------------- + +void testTileCache() { + printf("tile cache\n"); + TileCache cache; + CHECK(cache.init(3) == 3); + // Drawn from its z12 parent because its own z14 tile was not on the card yet + const TileKey magnified { 14, 100, 200 }; + const TileKey native { 14, 25, 50 }; + uint16_t* pixels = cache.reserve(magnified); + CHECK(pixels != nullptr); + pixels[0] = 0x1111; + cache.commit(magnified, 12); + pixels = cache.reserve(native); + CHECK(pixels != nullptr); + cache.commit(native, 14); + CHECK(cache.contains(magnified) && cache.contains(native)); + + // After a download stored tiles, the magnified tile must be drawn again, but is still shown meanwhile + cache.dropMagnified(); + CHECK(!cache.contains(magnified)); + CHECK(cache.contains(native)); + CHECK(cache.find(magnified) != nullptr && cache.find(magnified)[0] == 0x1111); + + // Its replacement takes over on commit, and only one copy is left + pixels = cache.reserve(magnified); + CHECK(pixels != nullptr); + CHECK(cache.find(magnified) != nullptr && cache.find(magnified)[0] == 0x1111); + pixels[0] = 0x2222; + cache.commit(magnified, 14); + CHECK(cache.contains(magnified)); + CHECK(cache.find(magnified)[0] == 0x2222); + cache.dropMagnified(); + CHECK(cache.contains(magnified)); + // Both remaining slots are in use by current tiles: the old copy was freed for reuse + const TileKey other { 14, 26, 50 }; + CHECK(cache.reserve(other) != nullptr); + cache.release(other); + CHECK(cache.contains(native)); +} + +// --------------------------------------------------------------------------------------------------------------- +// I/O queue order +// --------------------------------------------------------------------------------------------------------------- + +void testIoQueue() { + printf("io queue\n"); + IoQueue queue; + int item = -1; + CHECK(!queue.pop(item)); + CHECK(queue.push(1, IoPriority::Background)); + CHECK(queue.push(2, IoPriority::Prefetch)); + CHECK(queue.push(3, IoPriority::Background)); + CHECK(queue.push(4, IoPriority::Visible)); + CHECK(queue.push(5, IoPriority::Prefetch)); + CHECK(queue.push(6, IoPriority::Visible)); + CHECK(queue.hasMoreUrgent(IoPriority::Prefetch)); + CHECK(!queue.hasMoreUrgent(IoPriority::Visible)); + // Visible first, then prefetch, then background, the oldest first within each + const int expected[] = { 4, 6, 2, 5, 1, 3 }; + const IoPriority priorities[] = { IoPriority::Visible, IoPriority::Visible, IoPriority::Prefetch, IoPriority::Prefetch, + IoPriority::Background, IoPriority::Background }; + for (int i = 0; i < 6; i++) { + IoPriority priority = IoPriority::Count; + CHECK(queue.pop(item, &priority)); + CHECK(item == expected[i]); + CHECK(priority == priorities[i]); + if (i == 1) { + // A visible job arriving later still goes before the waiting prefetch + CHECK(queue.push(7, IoPriority::Visible)); + CHECK(queue.pop(item, &priority)); + CHECK(item == 7); + } + } + CHECK(queue.size() == 0); + CHECK(!queue.hasMoreUrgent(IoPriority::Background)); + for (int i = 0; i < 8; i++) { + CHECK(queue.push(i, IoPriority::Background)); + } + CHECK(!queue.push(9, IoPriority::Visible)); +} + +// --------------------------------------------------------------------------------------------------------------- +// SD card cache: index, cap and tidying +// --------------------------------------------------------------------------------------------------------------- + +void testCacheIndex() { + printf("cache index\n"); + CacheIndex index; + CHECK(index.init(10, 1000)); + TileKey tile {}; + CHECK(!index.nextEviction(tile)); + index.put({ 14, 1, 1 }, 300); + index.put({ 14, 1, 2 }, 300); + index.put({ 14, 1, 3 }, 300); + CHECK(index.totalBytes() == 900); + CHECK(index.contains({ 14, 1, 2 })); + CHECK(!index.nextEviction(tile)); + // Reading tile 1 makes tile 2 the least recently used + index.touch({ 14, 1, 1 }); + index.put({ 14, 1, 4 }, 300); + CHECK(index.isOverCap()); + CHECK(index.nextEviction(tile)); + CHECK(tile == TileKey({ 14, 1, 2 })); + index.erase(tile); + CHECK(index.totalBytes() == 900); + // Eviction stops at 90 % of the cap + CHECK(!index.nextEviction(tile)); + // Replacing a tile updates the total + index.put({ 14, 1, 1 }, 100); + CHECK(index.totalBytes() == 700); + CHECK(index.size() == 3); + + // Saved and loaded with sizes and order of use + ByteWriter bytes; + index.save(bytes); + CacheIndex loaded; + CHECK(loaded.init(10, 1000)); + CHECK(loaded.load(bytes.bytes(), bytes.size())); + CHECK(loaded.isComplete()); + CHECK(loaded.size() == 3); + CHECK(loaded.totalBytes() == 700); + loaded.put({ 14, 1, 5 }, 700); + CHECK(loaded.nextEviction(tile)); + CHECK(tile == TileKey({ 14, 1, 3 })); + loaded.erase(tile); + // Over 90 % still: the next one goes too + CHECK(loaded.nextEviction(tile)); + CHECK(tile == TileKey({ 14, 1, 4 })); + CHECK(!loaded.load(bytes.bytes(), bytes.size() - 1)); + CHECK(loaded.size() == 0 && !loaded.isComplete()); + + // A full index evicts too, even below the byte cap + CacheIndex small; + CHECK(small.init(4, 1000000)); + for (int i = 0; i < 4; i++) { + small.put({ 10, i, 0 }, 10); + } + CHECK(small.isFull()); + small.put({ 10, 9, 9 }, 10); + CHECK(!small.contains({ 10, 9, 9 })); + CHECK(small.nextEviction(tile)); + CHECK(tile == TileKey({ 10, 0, 0 })); +} + +void testCacheWalker() { + printf("cache walker\n"); + namespace fs = std::filesystem; + const fs::path root = fs::temp_directory_path() / "tactilitymaps-cache"; + std::error_code error; + fs::remove_all(root, error); + const uint8_t bytes[100] = {}; + // The current style, an old style, the old layout without a style folder, and map data that must stay + CHECK(writeBytes(root / "cache" / "c2" / "14" / "9058" / "5729", bytes, 100)); + CHECK(writeBytes(root / "cache" / "c2" / "14" / "9058" / "5730", bytes, 50)); + CHECK(writeBytes(root / "cache" / "c2" / "13" / "4529" / "2864", bytes, 10)); + CHECK(writeBytes(root / "cache" / "c1" / "14" / "9058" / "5729", bytes, 100)); + CHECK(writeBytes(root / "cache" / "c1" / "index.bin", bytes, 16)); + CHECK(writeBytes(root / "cache" / "14" / "9058" / "5729", bytes, 100)); + CHECK(writeBytes(root / "cache" / "14" / "9058" / "5729.tmp", bytes, 100)); + CHECK(writeBytes(root / "maps" / "city" / "14" / "9058" / "5729.pbf", bytes, 100)); + CHECK(writeBytes(root / "maps" / "city" / "14" / "9058" / "5729.prt", bytes, 100)); + + CacheIndex index; + CHECK(index.init(100)); + CacheWalker walker; + // Forward slashes, as on the device: the cache guard refuses backslashes + const std::string rootText = root.generic_string(); + walker.begin(rootText.c_str(), "c2"); + int steps = 0; + while (walker.step(index) && steps < 1000) { + steps++; + } + CHECK(walker.isComplete()); + CHECK(index.isComplete()); + CHECK(index.size() == 3); + CHECK(index.totalBytes() == 160); + CHECK(index.contains({ 14, 9058, 5730 })); + CHECK(fs::exists(root / "cache" / "c2" / "14" / "9058" / "5729")); + CHECK(!fs::exists(root / "cache" / "c1")); + CHECK(!fs::exists(root / "cache" / "14")); + CHECK(fs::exists(root / "maps" / "city" / "14" / "9058" / "5729.pbf")); + CHECK(fs::exists(root / "maps" / "city" / "14" / "9058" / "5729.prt")); + + // A complete index is kept as it is, its folder is not walked again + walker.begin(rootText.c_str(), "c2"); + index.erase({ 14, 9058, 5730 }); + while (walker.step(index)) { + } + CHECK(index.size() == 2); + + // With the SD card cache switched off the current style's files go as well, a few per step + CHECK(writeBytes(root / "cache" / "c2" / "14" / "9058" / "5731", bytes, 10)); + CHECK(writeBytes(root / "cache" / "c2" / "14" / "9058" / "5732", bytes, 10)); + CHECK(writeBytes(root / "cache" / "c2" / "14" / "9058" / "5733", bytes, 10)); + CHECK(writeBytes(root / "cache" / "c2" / "14" / "9058" / "5734", bytes, 10)); + CHECK(writeBytes(root / "cache" / "c2" / "14" / "9058" / "5735", bytes, 10)); + CHECK(writeBytes(root / "cache" / "c2" / "index.bin", bytes, 16)); + CacheIndex unused; + CHECK(unused.init(10)); + CacheWalker cleaner; + cleaner.begin(rootText.c_str(), "c2", false); + int cleanSteps = 0; + while (cleaner.step(unused) && cleanSteps < 1000) { + cleanSteps++; + } + CHECK(cleanSteps >= 3); + CHECK(!fs::exists(root / "cache" / "c2")); + CHECK(fs::exists(root / "maps" / "city" / "14" / "9058" / "5729.pbf")); + + // Without a cache folder the walk ends at once with an empty, complete index + CacheIndex empty; + CHECK(empty.init(10)); + CacheWalker none; + none.begin((root / "nothing").generic_string().c_str(), "c2"); + while (none.step(empty)) { + } + CHECK(empty.isComplete() && empty.size() == 0); + fs::remove_all(root, error); +} + +// --------------------------------------------------------------------------------------------------------------- +// Touch gestures, live pinch scaling +// --------------------------------------------------------------------------------------------------------------- + +struct GestureLog { + Gestures gestures; + std::vector seen; + + void feed(uint8_t count, int x0, int y0, int x1, int y1, uint32_t now) { + const TouchSample sample { count, { x0, x1 }, { y0, y1 } }; + Gesture out[Gestures::MAX_GESTURES]; + const int found = gestures.update(sample, now, out); + for (int i = 0; i < found; i++) { + seen.push_back(out[i]); + } + } + + void touch(int x, int y, uint32_t now) { feed(1, x, y, 0, 0, now); } + void release(uint32_t now) { feed(0, 0, 0, 0, 0, now); } + + int countOf(GestureType type) const { + int count = 0; + for (const Gesture& gesture : seen) { + count += gesture.type == type ? 1 : 0; + } + return count; + } +}; + +void testGestures() { + printf("gestures\n"); + { + // A single tap: Tap at once, SingleTap after the double tap time + GestureLog log; + log.touch(100, 100, 0); + log.touch(102, 101, 50); + log.release(100); + CHECK(log.countOf(GestureType::Tap) == 1); + CHECK(log.countOf(GestureType::SingleTap) == 0); + log.release(200); + CHECK(log.countOf(GestureType::SingleTap) == 0); + log.release(100 + Gestures::DOUBLE_TAP_MS); + CHECK(log.countOf(GestureType::SingleTap) == 1); + CHECK(log.seen.back().x == 102 && log.seen.back().y == 101); + CHECK(log.countOf(GestureType::Drag) == 0); + } + { + // A double tap is never a single tap + GestureLog log; + log.touch(100, 100, 0); + log.release(60); + log.touch(105, 98, 150); + log.release(200); + log.release(1000); + CHECK(log.countOf(GestureType::Tap) == 2); + CHECK(log.countOf(GestureType::DoubleTap) == 1); + CHECK(log.countOf(GestureType::SingleTap) == 0); + // Two taps far apart are two single taps + GestureLog apart; + apart.touch(10, 10, 0); + apart.release(50); + apart.touch(200, 200, 100); + apart.release(150); + apart.release(1000); + CHECK(apart.countOf(GestureType::DoubleTap) == 0); + CHECK(apart.countOf(GestureType::SingleTap) == 1); + } + { + // A tap used by a button does not count as a single tap later + GestureLog log; + log.touch(100, 100, 0); + log.release(50); + log.gestures.consumeTap(); + log.release(1000); + CHECK(log.countOf(GestureType::SingleTap) == 0); + } + { + // A drag moves with the finger, including the part inside the slop, and is no tap + GestureLog log; + log.touch(100, 100, 0); + log.touch(105, 100, 20); + log.touch(120, 110, 40); + log.touch(130, 110, 60); + log.release(80); + log.release(1000); + int dx = 0; + int dy = 0; + for (const Gesture& gesture : log.seen) { + if (gesture.type == GestureType::Drag) { + dx += gesture.dx; + dy += gesture.dy; + } + } + CHECK(dx == 30 && dy == 10); + CHECK(log.countOf(GestureType::Tap) == 0); + CHECK(log.countOf(GestureType::SingleTap) == 0); + } + { + // A long press is no tap + GestureLog log; + log.touch(100, 100, 0); + log.touch(101, 100, Gestures::LONG_PRESS_MS + 10); + log.touch(101, 100, Gestures::LONG_PRESS_MS + 50); + log.release(Gestures::LONG_PRESS_MS + 100); + log.release(3000); + CHECK(log.countOf(GestureType::LongPress) == 1); + CHECK(log.countOf(GestureType::Tap) == 0); + CHECK(log.countOf(GestureType::SingleTap) == 0); + } + { + // A pinch: start, moves with the scale, end; the finger left behind neither drags nor taps + GestureLog log; + log.touch(100, 100, 0); + log.feed(2, 100, 100, 200, 100, 20); + log.feed(2, 50, 100, 250, 100, 40); + log.touch(50, 100, 60); + log.touch(10, 100, 80); + log.release(100); + log.release(1000); + CHECK(log.countOf(GestureType::PinchStart) == 1); + CHECK(log.countOf(GestureType::PinchMove) == 1); + CHECK(log.countOf(GestureType::PinchEnd) == 1); + CHECK(log.countOf(GestureType::Drag) == 0); + CHECK(log.countOf(GestureType::Tap) == 0); + CHECK(log.countOf(GestureType::SingleTap) == 0); + for (const Gesture& gesture : log.seen) { + if (gesture.type == GestureType::PinchEnd) { + CHECK(nearlyEqual(gesture.scale, 2.0, 1e-3)); + CHECK(gesture.startX == 150 && gesture.x == 150); + } + } + } + + // Zoom snapping after a pinch + CHECK(snapZoom(14, 1.0f, 19) == 14); + CHECK(snapZoom(14, 1.3f, 19) == 14); + CHECK(snapZoom(14, 1.5f, 19) == 15); + CHECK(snapZoom(14, 3.0f, 19) == 16); + CHECK(snapZoom(14, 0.6f, 19) == 13); + CHECK(snapZoom(19, 4.0f, 19) == 19); + CHECK(snapZoom(0, 0.1f, 19) == 0); + + // Scaling the screen picture around the pinch centre + constexpr int W = 8; + constexpr int H = 6; + uint16_t source[W * H]; + for (int i = 0; i < W * H; i++) { + source[i] = static_cast(i); + } + uint16_t target[W * H]; + scaleView(source, target, W, H, 1.0f, 4.0f, 3.0f, 4.0f, 3.0f, 0xFFFF); + CHECK(memcmp(source, target, sizeof(source)) == 0); + // Twice as large around the centre: the centre pixel stays, its neighbours spread out + scaleView(source, target, W, H, 2.0f, 4.0f, 3.0f, 4.0f, 3.0f, 0xFFFF); + CHECK(target[3 * W + 4] == source[3 * W + 4]); + CHECK(target[3 * W + 5] == source[3 * W + 4]); + CHECK(target[3 * W + 6] == source[3 * W + 5]); + CHECK(target[0] == source[1 * W + 2]); + // Half the size around the centre of pixel 4, 3: it stays, its neighbours come from two pixels away + scaleView(source, target, W, H, 0.5f, 4.5f, 3.5f, 4.5f, 3.5f, 0xFFFF); + CHECK(target[0] == 0xFFFF); + CHECK(target[3 * W + 4] == source[3 * W + 4]); + CHECK(target[3 * W + 5] == source[3 * W + 6]); + CHECK(target[3 * W + 3] == source[3 * W + 2]); + CHECK(target[2 * W + 4] == source[1 * W + 4]); + // Moved along with the fingers + scaleView(source, target, W, H, 1.0f, 4.0f, 3.0f, 5.0f, 3.0f, 0xFFFF); + CHECK(target[3 * W + 5] == source[3 * W + 4]); + CHECK(target[3 * W + 0] == 0xFFFF); +} + +// --------------------------------------------------------------------------------------------------------------- +// UI toolkit +// --------------------------------------------------------------------------------------------------------------- + +void testWidgets() { + printf("widgets\n"); + FakeFont font; + std::vector pixels(320 * 240, 0); + Canvas canvas { pixels.data(), 320, 240 }; + + // Text wraps at spaces and never runs past the width + char lines[3][80]; + const int count = wrapText(font, "Location is off. Enable it in Tactility's Settings.", 200, &lines[0][0], sizeof(lines[0]), 3); + CHECK(count == 3); + for (int i = 0; i < count; i++) { + CHECK(font.textWidth(lines[i]) <= 200); + } + CHECK(strcmp(lines[0], "Location is off. Enable") == 0); + CHECK(wrapText(font, "short", 200, &lines[0][0], sizeof(lines[0]), 3) == 1); + CHECK(strcmp(lines[0], "short") == 0); + + // A full screen list: about six rows on 240 px, keys move and choose, the selection stays visible + ListView list; + list.setTitle("< Regions"); + list.setHint("hint"); + for (int i = 0; i < 12; i++) { + char label[16]; + snprintf(label, sizeof(label), "row %d", i); + list.add(label, "1 MB"); + } + list.layoutFullScreen(320, 240, font); + CHECK(list.rowHeight() >= font.lineHeight() + 8); + const int visible = (240 - list.rowHeight()) / list.rowHeight(); + CHECK(visible >= 5 && visible <= 8); + CHECK(list.handleKey('s') == UiAction::Moved); + CHECK(list.handleKey(keys::DOWN) == UiAction::Moved); + CHECK(list.selected() == 2); + CHECK(list.handleKey('w') == UiAction::Moved); + CHECK(list.selected() == 1); + CHECK(list.handleKey(keys::UP) == UiAction::Moved); + CHECK(list.handleKey(keys::UP) == UiAction::None); + CHECK(list.handleKey(keys::ENTER) == UiAction::Chosen); + CHECK(list.handleKey(keys::ESCAPE) == UiAction::Back); + CHECK(list.handleKey(keys::BACKSPACE) == UiAction::Back); + CHECK(list.handleKey(keys::DELETE) == UiAction::Delete); + CHECK(list.move(100) == UiAction::Moved); + CHECK(list.selected() == 11); + // The selected row is on screen, so a tap on it finds it + bool found = false; + for (int y = 0; y < 240 && !found; y++) { + found = list.rowAt(100, y) == 11; + } + CHECK(found); + CHECK(list.rowAt(100, 5) == -1); + // A tap on the title goes back, a tap on a row chooses it + CHECK(list.handleTap(100, 5) == UiAction::Back); + int rowY = -1; + for (int y = 0; y < 240 && rowY < 0; y++) { + if (list.rowAt(100, y) >= 0 && list.rowAt(100, y) != 11) { + rowY = y; + } + } + const int tapped = list.rowAt(100, rowY); + CHECK(list.handleTap(100, rowY) == UiAction::Chosen); + CHECK(list.selected() == tapped); + list.draw(canvas, font, uiTheme(colorPalette(), false)); + + // A popup closes on a tap outside + ListView menu; + menu.add("Download area"); + menu.add("Regions"); + menu.add("Settings"); + menu.add("About and licences"); + menu.layoutPopup(320, 240, font, 220); + CHECK(menu.bounds().width == 220); + CHECK(menu.bounds().x == 50); + CHECK(menu.handleTap(5, 5) == UiAction::Back); + const Rect bounds = menu.bounds(); + CHECK(menu.rowAt(bounds.x + 20, bounds.y + bounds.height - 6) == 3); + CHECK(menu.handleTap(bounds.x + 20, bounds.y + bounds.height - 6) == UiAction::Chosen); + CHECK(menu.selected() == 3); + CHECK(menu.handleKey(keys::DELETE) == UiAction::Delete); + + // A dialog starts on Cancel, so Enter alone never confirms + Dialog dialog; + dialog.open("Delete area01?", "Delete", true); + dialog.layout(320, 240, font); + CHECK(dialog.handleKey(keys::ENTER) == UiAction::Back); + CHECK(!dialog.isOpen()); + dialog.open("Delete area01?", "Delete", true); + CHECK(dialog.handleKey(keys::RIGHT) == UiAction::Moved); + CHECK(dialog.handleKey(keys::ENTER) == UiAction::Chosen); + dialog.open("Delete area01?", "Delete", true); + CHECK(dialog.handleKey('y') == UiAction::Chosen); + dialog.open("Delete area01?", "Delete", true); + CHECK(dialog.handleKey(keys::ESCAPE) == UiAction::Back); + dialog.open("Delete area01?", "Delete", true); + dialog.layout(320, 240, font); + CHECK(dialog.handleTap(2, 2) == UiAction::Back); + dialog.draw(canvas, font, uiTheme(colorPalette(), false)); + + // Monochrome: solid black and white, no grey text + const UiTheme mono = uiTheme(monochromePalette(), true); + CHECK(mono.text == 0x0000 && mono.panel == 0xFFFF); + CHECK(mono.muted == mono.text); + CHECK(mono.panelOpacity == 255); +} + +// --------------------------------------------------------------------------------------------------------------- +// Control overlay and attribution +// --------------------------------------------------------------------------------------------------------------- + +void testControls() { + printf("controls\n"); + Controls controls; + controls.layout(320, 240, 212); + // Hidden at first: nothing to hit + CHECK(!controls.isVisible()); + CHECK(controls.hit(20, 20) == Control::None); + controls.toggle(); + CHECK(controls.isVisible()); + // Layout: about 32 px, at least 6 px from the edges, never over the screen centre + for (int i = 0; i < static_cast(Control::Count); i++) { + const Rect& r = controls.rect(static_cast(i)); + CHECK(r.width == 32 && r.height == 32); + CHECK(r.x >= 6 && r.y >= 6 && r.x + r.width <= 314 && r.y + r.height <= 234); + CHECK(!r.contains(160, 120)); + for (int j = i + 1; j < static_cast(Control::Count); j++) { + const Rect& other = controls.rect(static_cast(j)); + const bool overlap = r.x < other.x + other.width && other.x < r.x + r.width && r.y < other.y + other.height && other.y < r.y + r.height; + CHECK(!overlap); + } + } + const Rect& menu = controls.rect(Control::Menu); + CHECK(menu.x == 6 && menu.y == 6); + const Rect& zoomIn = controls.rect(Control::ZoomIn); + const Rect& zoomOut = controls.rect(Control::ZoomOut); + CHECK(zoomIn.x == 320 - 6 - 32 && zoomIn.y + zoomIn.height <= 120 && zoomOut.y >= 120); + const Rect& positionButton = controls.rect(Control::Position); + CHECK(positionButton.x == zoomIn.x && positionButton.y + positionButton.height <= 212 - 6 + 1); + CHECK(controls.hit(menu.x + 10, menu.y + 10) == Control::Menu); + CHECK(controls.hit(zoomOut.x + 16, zoomOut.y + 16) == Control::ZoomOut); + CHECK(controls.hit(160, 120) == Control::None); + controls.toggle(); + CHECK(controls.hit(menu.x + 10, menu.y + 10) == Control::None); + + // Keys of the buttons + CHECK(Controls::controlForKey('g') == Control::Position); + CHECK(Controls::controlForKey('M') == Control::Menu); + CHECK(Controls::controlForKey('x') == Control::None); + CHECK(Controls::keyOf(Control::ZoomIn) == 'I'); + + // Focus for boards with only buttons + controls.setInputs({ true, false, false }); + CHECK(controls.usesFocus()); + controls.show(); + CHECK(controls.focused() == Control::Menu); + controls.moveFocus(1); + CHECK(controls.focused() == Control::ZoomIn); + controls.moveFocus(-2); + CHECK(controls.focused() == Control::Position); + controls.setInputs({ true, true, false }); + CHECK(!controls.usesFocus()); + + // Drawn in colour and monochrome without errors, with and without key hints + FakeFont font; + std::vector pixels(320 * 240, 0x1234); + Canvas canvas { pixels.data(), 320, 240 }; + controls.show(); + controls.draw(canvas, font, uiTheme(colorPalette(), false), PositionButton::Unavailable); + controls.setInputs({ false, true, false }); + controls.draw(canvas, font, uiTheme(monochromePalette(), true), PositionButton::Following); + CHECK(pixels[(menu.y + 16) * 320 + menu.x + 16] != 0x1234); + CHECK(pixels[120 * 320 + 160] == 0x1234); + + // The attribution in two steps + Attribution attribution; + const Rect bar { 0, 220, 300, 18 }; + const Rect mark { 0, 222, 18, 18 }; + attribution.expand(1000); + CHECK(attribution.isExpanded()); + CHECK(!attribution.update(5999)); + CHECK(attribution.update(6000)); + CHECK(!attribution.isExpanded()); + CHECK(attribution.tap(200, 100, bar, mark, 7000) == Attribution::Tap::Missed); + CHECK(attribution.tap(5, 230, bar, mark, 7000) == Attribution::Tap::Expanded); + CHECK(attribution.isExpanded()); + CHECK(attribution.tap(150, 225, bar, mark, 8000) == Attribution::Tap::OpenAbout); + CHECK(attribution.update(12000)); +} + +// --------------------------------------------------------------------------------------------------------------- +// Download screen +// --------------------------------------------------------------------------------------------------------------- + +void testDownloadScreen() { + printf("download screen\n"); + const DownloadLayout layout = downloadLayout(320, 240, 16); + // The frame fills the map area above the info bar, minus an even margin + CHECK(layout.bar.y + layout.bar.height == 240); + CHECK(layout.bar.height == 42); + CHECK(layout.frame.x == 10 && layout.frame.y == 10); + CHECK(320 - (layout.frame.x + layout.frame.width) == 10); + CHECK(layout.bar.y - (layout.frame.y + layout.frame.height) == 10); + CHECK(layout.button.contains(layout.button.x + 1, layout.button.y + 1)); + CHECK(layout.bar.contains(layout.button.x, layout.button.y)); + CHECK(layout.button.x + layout.button.width <= 320); + + // The ground under the frame follows the map: the same frame covers four times the area one zoom level out + const double centerX = lonToWorldX(19.04, 14); + const double centerY = latToWorldY(47.50, 14); + const BoundingBox near = screenBounds(layout.frame, centerX, centerY, 14, 320, 240); + const BoundingBox far = screenBounds(layout.frame, centerX / 2, centerY / 2, 13, 320, 240); + CHECK(near.west < near.east && near.south < near.north); + CHECK(nearlyEqual((far.east - far.west) / (near.east - near.west), 2.0, 1e-6)); + // The frame sits a little above the screen centre, the info bar takes the bottom + CHECK(near.north > 47.50 && near.south < 47.50); + // Panning moves the frame's ground with the map + const BoundingBox moved = screenBounds(layout.frame, centerX + 100, centerY, 14, 320, 240); + CHECK(moved.west > near.west); + + // Live numbers: a city frame fits, and the max zoom drops for a large one + char first[48]; + char second[48]; + const DownloadPlan city = planDownload(near, MAX_SOURCE_ZOOM); + describePlan(city, first, sizeof(first), second, sizeof(second)); + CHECK(city.fits && city.maxZoom == 14); + CHECK(strstr(first, "tiles") != nullptr); + CHECK(strcmp(second, "Max zoom 14") == 0); + const BoundingBox wide = screenBounds(layout.frame, lonToWorldX(19.04, 9), latToWorldY(47.5, 9), 9, 320, 240); + const DownloadPlan country = planDownload(wide, MAX_SOURCE_ZOOM); + describePlan(country, first, sizeof(first), second, sizeof(second)); + CHECK(!country.fits || country.maxZoom < 14); + if (country.fits) { + CHECK(strstr(second, "to fit") != nullptr); + CHECK(country.tiles <= MAX_DOWNLOAD_TILES && country.estimatedBytes <= MAX_DOWNLOAD_BYTES); + } + DownloadPlan tooLarge {}; + tooLarge.fits = false; + describePlan(tooLarge, first, sizeof(first), second, sizeof(second)); + CHECK(strstr(first, "Too large") != nullptr); + + FakeFont font; + std::vector pixels(320 * 240, 0x1234); + Canvas canvas { pixels.data(), 320, 240 }; + drawDownloadScreen(canvas, font, uiTheme(colorPalette(), false), layout, city); + // Inside the frame the map is untouched, outside it is dimmed + CHECK(pixels[120 * 320 + 160] == 0x1234); + CHECK(pixels[3 * 320 + 3] != 0x1234); +} + +// --------------------------------------------------------------------------------------------------------------- +// Position from the location service +// --------------------------------------------------------------------------------------------------------------- + +void testPosition() { + printf("position\n"); + FakePosition source; + PositionTracker tracker; + + // Location switched off in the system settings + source.enabled = false; + CHECK(tracker.update(source)); + CHECK(tracker.state() == PositionState::Off); + CHECK(!tracker.hasFix()); + CHECK(strstr(tracker.unavailableMessage(), "off") != nullptr); + CHECK(!tracker.update(source)); + + // On, no fix yet + source.enabled = true; + CHECK(tracker.update(source)); + CHECK(tracker.state() == PositionState::Waiting); + CHECK(strstr(tracker.unavailableMessage(), "No position") != nullptr); + CHECK(tracker.accuracyRadius(10.0) == 0.0f); + + // An approximate fix: a circle, never an arrow, even with a course + source.set(47.4979, 19.0402, 3000.0f, 90.0f, 5.0f); + CHECK(tracker.update(source)); + CHECK(tracker.state() == PositionState::Approximate); + CHECK(tracker.hasFix()); + CHECK(!tracker.showsArrow()); + CHECK(nearlyEqual(tracker.accuracyRadius(10.0), 300.0, 1e-3)); + CHECK(!tracker.update(source)); + + // A precise fix while standing still: no arrow + source.set(47.5, 19.05, 8.0f, NAN, 0.2f); + CHECK(tracker.update(source)); + CHECK(tracker.state() == PositionState::Precise); + CHECK(!tracker.showsArrow()); + CHECK(nearlyEqual(tracker.fix().latitude, 47.5, 1e-9)); + + // Moving with a known course: the arrow shows + source.set(47.5, 19.05, 8.0f, 45.0f, 3.0f); + CHECK(tracker.update(source)); + CHECK(tracker.showsArrow()); + // A course without a speed is not enough + source.set(47.5, 19.05, 8.0f, 45.0f, NAN); + CHECK(tracker.update(source)); + CHECK(!tracker.showsArrow()); + + // Unknown accuracy: precise, no circle + source.set(47.5, 19.05, NAN); + tracker.update(source); + CHECK(tracker.state() == PositionState::Precise); + CHECK(tracker.accuracyRadius(1.0) == 0.0f); + + // A fix with unknown coordinates is no fix + source.set(NAN, NAN, 5.0f); + tracker.update(source); + CHECK(!tracker.hasFix()); + + // Switching location off drops the fix + source.set(47.5, 19.05, 5.0f); + tracker.update(source); + CHECK(tracker.hasFix()); + source.enabled = false; + CHECK(tracker.update(source)); + CHECK(!tracker.hasFix()); + CHECK(tracker.state() == PositionState::Off); +} + +int main(int argc, char** argv) { + bool update = false; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--update") == 0) { + update = true; + } + } + + testTileMath(); + testPaths(); + testMvtDecoding(); + testInflate(); + testRaster(); + testGolden(update); + testCompose(); + testTileCodec(); + testDownloadPlan(); + testRegionMeta(); + testTileJson(); + testPosition(); + testPreparedTiles(); + testPreparedScanner(); + testSettings(); + testTileCache(); + testIoQueue(); + testCacheIndex(); + testCacheWalker(); + testGestures(); + testWidgets(); + testControls(); + testDownloadScreen(); + + printf("%d checks, %d failed\n", checkCount, failureCount); + return failureCount == 0 ? 0 : 1; +} diff --git a/Apps/Maps/host/third_party/miniz/LICENSE b/Apps/Maps/host/third_party/miniz/LICENSE new file mode 100644 index 0000000..b6ff45a --- /dev/null +++ b/Apps/Maps/host/third_party/miniz/LICENSE @@ -0,0 +1,22 @@ +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + +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 +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. diff --git a/Apps/Maps/host/third_party/miniz/miniz.c b/Apps/Maps/host/third_party/miniz/miniz.c new file mode 100644 index 0000000..8d0032f --- /dev/null +++ b/Apps/Maps/host/third_party/miniz/miniz.c @@ -0,0 +1,7833 @@ +#include "miniz.h" +/************************************************************************** + * + * Copyright 2013-2014 RAD Game Tools and Valve Software + * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + * 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 + * 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. + * + **************************************************************************/ + + + +typedef unsigned char mz_validate_uint16[sizeof(mz_uint16) == 2 ? 1 : -1]; +typedef unsigned char mz_validate_uint32[sizeof(mz_uint32) == 4 ? 1 : -1]; +typedef unsigned char mz_validate_uint64[sizeof(mz_uint64) == 8 ? 1 : -1]; + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------- zlib-style API's */ + +mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len) +{ + mz_uint32 i, s1 = (mz_uint32)(adler & 0xffff), s2 = (mz_uint32)(adler >> 16); + size_t block_len = buf_len % 5552; + if (!ptr) + return MZ_ADLER32_INIT; + while (buf_len) + { + for (i = 0; i + 7 < block_len; i += 8, ptr += 8) + { + s1 += ptr[0], s2 += s1; + s1 += ptr[1], s2 += s1; + s1 += ptr[2], s2 += s1; + s1 += ptr[3], s2 += s1; + s1 += ptr[4], s2 += s1; + s1 += ptr[5], s2 += s1; + s1 += ptr[6], s2 += s1; + s1 += ptr[7], s2 += s1; + } + for (; i < block_len; ++i) + s1 += *ptr++, s2 += s1; + s1 %= 65521U, s2 %= 65521U; + buf_len -= block_len; + block_len = 5552; + } + return (s2 << 16) + s1; +} + +/* Karl Malbrain's compact CRC-32. See "A compact CCITT crc16 and crc32 C implementation that balances processor cache usage against speed": http://www.geocities.com/malbrain/ */ +#if 0 + mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len) + { + static const mz_uint32 s_crc32[16] = { 0, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c }; + mz_uint32 crcu32 = (mz_uint32)crc; + if (!ptr) + return MZ_CRC32_INIT; + crcu32 = ~crcu32; + while (buf_len--) + { + mz_uint8 b = *ptr++; + crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b & 0xF)]; + crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b >> 4)]; + } + return ~crcu32; + } +#elif defined(USE_EXTERNAL_MZCRC) +/* If USE_EXTERNAL_CRC is defined, an external module will export the + * mz_crc32() symbol for us to use, e.g. an SSE-accelerated version. + * Depending on the impl, it may be necessary to ~ the input/output crc values. + */ +mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len); +#else +/* Faster, but larger CPU cache footprint. + */ +mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len) +{ + static const mz_uint32 s_crc_table[256] = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, + 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, + 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, + 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, + 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, + 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, + 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, + 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, + 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, + 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, + 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, + 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, + 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, + 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, + 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, + 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, + 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, + 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, + 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, + 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, + 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, + 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, + 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, + 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, + 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, + 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, + 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + mz_uint32 crc32 = (mz_uint32)crc ^ 0xFFFFFFFF; + const mz_uint8 *pByte_buf = (const mz_uint8 *)ptr; + + while (buf_len >= 4) + { + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF]; + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[1]) & 0xFF]; + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[2]) & 0xFF]; + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[3]) & 0xFF]; + pByte_buf += 4; + buf_len -= 4; + } + + while (buf_len) + { + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF]; + ++pByte_buf; + --buf_len; + } + + return ~crc32; +} +#endif + +void mz_free(void *p) +{ + MZ_FREE(p); +} + +MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size) +{ + (void)opaque, (void)items, (void)size; + return MZ_MALLOC(items * size); +} +MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address) +{ + (void)opaque, (void)address; + MZ_FREE(address); +} +MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size) +{ + (void)opaque, (void)address, (void)items, (void)size; + return MZ_REALLOC(address, items * size); +} + +const char *mz_version(void) +{ + return MZ_VERSION; +} + +#ifndef MINIZ_NO_ZLIB_APIS + +#ifndef MINIZ_NO_DEFLATE_APIS + +int mz_deflateInit(mz_streamp pStream, int level) +{ + return mz_deflateInit2(pStream, level, MZ_DEFLATED, MZ_DEFAULT_WINDOW_BITS, 9, MZ_DEFAULT_STRATEGY); +} + +int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy) +{ + tdefl_compressor *pComp; + mz_uint comp_flags = TDEFL_COMPUTE_ADLER32 | tdefl_create_comp_flags_from_zip_params(level, window_bits, strategy); + + if (!pStream) + return MZ_STREAM_ERROR; + if ((method != MZ_DEFLATED) || ((mem_level < 1) || (mem_level > 9)) || ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS))) + return MZ_PARAM_ERROR; + + pStream->data_type = 0; + pStream->adler = MZ_ADLER32_INIT; + pStream->msg = NULL; + pStream->reserved = 0; + pStream->total_in = 0; + pStream->total_out = 0; + if (!pStream->zalloc) + pStream->zalloc = miniz_def_alloc_func; + if (!pStream->zfree) + pStream->zfree = miniz_def_free_func; + + pComp = (tdefl_compressor *)pStream->zalloc(pStream->opaque, 1, sizeof(tdefl_compressor)); + if (!pComp) + return MZ_MEM_ERROR; + + pStream->state = (struct mz_internal_state *)pComp; + + if (tdefl_init(pComp, NULL, NULL, comp_flags) != TDEFL_STATUS_OKAY) + { + mz_deflateEnd(pStream); + return MZ_PARAM_ERROR; + } + + return MZ_OK; +} + +int mz_deflateReset(mz_streamp pStream) +{ + if ((!pStream) || (!pStream->state) || (!pStream->zalloc) || (!pStream->zfree)) + return MZ_STREAM_ERROR; + pStream->total_in = pStream->total_out = 0; + tdefl_init((tdefl_compressor *)pStream->state, NULL, NULL, ((tdefl_compressor *)pStream->state)->m_flags); + return MZ_OK; +} + +int mz_deflate(mz_streamp pStream, int flush) +{ + size_t in_bytes, out_bytes; + mz_ulong orig_total_in, orig_total_out; + int mz_status = MZ_OK; + + if ((!pStream) || (!pStream->state) || (flush < 0) || (flush > MZ_FINISH) || (!pStream->next_out)) + return MZ_STREAM_ERROR; + if (!pStream->avail_out) + return MZ_BUF_ERROR; + + if (flush == MZ_PARTIAL_FLUSH) + flush = MZ_SYNC_FLUSH; + + if (((tdefl_compressor *)pStream->state)->m_prev_return_status == TDEFL_STATUS_DONE) + return (flush == MZ_FINISH) ? MZ_STREAM_END : MZ_BUF_ERROR; + + orig_total_in = pStream->total_in; + orig_total_out = pStream->total_out; + for (;;) + { + tdefl_status defl_status; + in_bytes = pStream->avail_in; + out_bytes = pStream->avail_out; + + defl_status = tdefl_compress((tdefl_compressor *)pStream->state, pStream->next_in, &in_bytes, pStream->next_out, &out_bytes, (tdefl_flush)flush); + pStream->next_in += (mz_uint)in_bytes; + pStream->avail_in -= (mz_uint)in_bytes; + pStream->total_in += (mz_uint)in_bytes; + pStream->adler = tdefl_get_adler32((tdefl_compressor *)pStream->state); + + pStream->next_out += (mz_uint)out_bytes; + pStream->avail_out -= (mz_uint)out_bytes; + pStream->total_out += (mz_uint)out_bytes; + + if (defl_status < 0) + { + mz_status = MZ_STREAM_ERROR; + break; + } + else if (defl_status == TDEFL_STATUS_DONE) + { + mz_status = MZ_STREAM_END; + break; + } + else if (!pStream->avail_out) + break; + else if ((!pStream->avail_in) && (flush != MZ_FINISH)) + { + if ((flush) || (pStream->total_in != orig_total_in) || (pStream->total_out != orig_total_out)) + break; + return MZ_BUF_ERROR; /* Can't make forward progress without some input. + */ + } + } + return mz_status; +} + +int mz_deflateEnd(mz_streamp pStream) +{ + if (!pStream) + return MZ_STREAM_ERROR; + if (pStream->state) + { + pStream->zfree(pStream->opaque, pStream->state); + pStream->state = NULL; + } + return MZ_OK; +} + +mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len) +{ + (void)pStream; + /* This is really over conservative. (And lame, but it's actually pretty tricky to compute a true upper bound given the way tdefl's blocking works.) */ + return MZ_MAX(128 + (source_len * 110) / 100, 128 + source_len + ((source_len / (31 * 1024)) + 1) * 5); +} + +int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level) +{ + int status; + mz_stream stream; + memset(&stream, 0, sizeof(stream)); + + /* In case mz_ulong is 64-bits (argh I hate longs). */ + if ((mz_uint64)(source_len | *pDest_len) > 0xFFFFFFFFU) + return MZ_PARAM_ERROR; + + stream.next_in = pSource; + stream.avail_in = (mz_uint32)source_len; + stream.next_out = pDest; + stream.avail_out = (mz_uint32)*pDest_len; + + status = mz_deflateInit(&stream, level); + if (status != MZ_OK) + return status; + + status = mz_deflate(&stream, MZ_FINISH); + if (status != MZ_STREAM_END) + { + mz_deflateEnd(&stream); + return (status == MZ_OK) ? MZ_BUF_ERROR : status; + } + + *pDest_len = stream.total_out; + return mz_deflateEnd(&stream); +} + +int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len) +{ + return mz_compress2(pDest, pDest_len, pSource, source_len, MZ_DEFAULT_COMPRESSION); +} + +mz_ulong mz_compressBound(mz_ulong source_len) +{ + return mz_deflateBound(NULL, source_len); +} + +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + +#ifndef MINIZ_NO_INFLATE_APIS + +typedef struct +{ + tinfl_decompressor m_decomp; + mz_uint m_dict_ofs, m_dict_avail, m_first_call, m_has_flushed; + int m_window_bits; + mz_uint8 m_dict[TINFL_LZ_DICT_SIZE]; + tinfl_status m_last_status; +} inflate_state; + +int mz_inflateInit2(mz_streamp pStream, int window_bits) +{ + inflate_state *pDecomp; + if (!pStream) + return MZ_STREAM_ERROR; + if ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS)) + return MZ_PARAM_ERROR; + + pStream->data_type = 0; + pStream->adler = 0; + pStream->msg = NULL; + pStream->total_in = 0; + pStream->total_out = 0; + pStream->reserved = 0; + if (!pStream->zalloc) + pStream->zalloc = miniz_def_alloc_func; + if (!pStream->zfree) + pStream->zfree = miniz_def_free_func; + + pDecomp = (inflate_state *)pStream->zalloc(pStream->opaque, 1, sizeof(inflate_state)); + if (!pDecomp) + return MZ_MEM_ERROR; + + pStream->state = (struct mz_internal_state *)pDecomp; + + tinfl_init(&pDecomp->m_decomp); + pDecomp->m_dict_ofs = 0; + pDecomp->m_dict_avail = 0; + pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT; + pDecomp->m_first_call = 1; + pDecomp->m_has_flushed = 0; + pDecomp->m_window_bits = window_bits; + + return MZ_OK; +} + +int mz_inflateInit(mz_streamp pStream) +{ + return mz_inflateInit2(pStream, MZ_DEFAULT_WINDOW_BITS); +} + +int mz_inflateReset(mz_streamp pStream) +{ + inflate_state *pDecomp; + if (!pStream) + return MZ_STREAM_ERROR; + + pStream->data_type = 0; + pStream->adler = 0; + pStream->msg = NULL; + pStream->total_in = 0; + pStream->total_out = 0; + pStream->reserved = 0; + + pDecomp = (inflate_state *)pStream->state; + + tinfl_init(&pDecomp->m_decomp); + pDecomp->m_dict_ofs = 0; + pDecomp->m_dict_avail = 0; + pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT; + pDecomp->m_first_call = 1; + pDecomp->m_has_flushed = 0; + /* pDecomp->m_window_bits = window_bits */; + + return MZ_OK; +} + +int mz_inflate(mz_streamp pStream, int flush) +{ + inflate_state *pState; + mz_uint n, first_call, decomp_flags = TINFL_FLAG_COMPUTE_ADLER32; + size_t in_bytes, out_bytes, orig_avail_in; + tinfl_status status; + + if ((!pStream) || (!pStream->state)) + return MZ_STREAM_ERROR; + if (flush == MZ_PARTIAL_FLUSH) + flush = MZ_SYNC_FLUSH; + if ((flush) && (flush != MZ_SYNC_FLUSH) && (flush != MZ_FINISH)) + return MZ_STREAM_ERROR; + + pState = (inflate_state *)pStream->state; + if (pState->m_window_bits > 0) + decomp_flags |= TINFL_FLAG_PARSE_ZLIB_HEADER; + orig_avail_in = pStream->avail_in; + + first_call = pState->m_first_call; + pState->m_first_call = 0; + if (pState->m_last_status < 0) + return MZ_DATA_ERROR; + + if (pState->m_has_flushed && (flush != MZ_FINISH)) + return MZ_STREAM_ERROR; + pState->m_has_flushed |= (flush == MZ_FINISH); + + if ((flush == MZ_FINISH) && (first_call)) + { + /* MZ_FINISH on the first call implies that the input and output buffers are large enough to hold the entire compressed/decompressed file. */ + decomp_flags |= TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF; + in_bytes = pStream->avail_in; + out_bytes = pStream->avail_out; + status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pStream->next_out, pStream->next_out, &out_bytes, decomp_flags); + pState->m_last_status = status; + pStream->next_in += (mz_uint)in_bytes; + pStream->avail_in -= (mz_uint)in_bytes; + pStream->total_in += (mz_uint)in_bytes; + pStream->adler = tinfl_get_adler32(&pState->m_decomp); + pStream->next_out += (mz_uint)out_bytes; + pStream->avail_out -= (mz_uint)out_bytes; + pStream->total_out += (mz_uint)out_bytes; + + if (status < 0) + return MZ_DATA_ERROR; + else if (status != TINFL_STATUS_DONE) + { + pState->m_last_status = TINFL_STATUS_FAILED; + return MZ_BUF_ERROR; + } + return MZ_STREAM_END; + } + /* flush != MZ_FINISH then we must assume there's more input. */ + if (flush != MZ_FINISH) + decomp_flags |= TINFL_FLAG_HAS_MORE_INPUT; + + if (pState->m_dict_avail) + { + n = MZ_MIN(pState->m_dict_avail, pStream->avail_out); + memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n); + pStream->next_out += n; + pStream->avail_out -= n; + pStream->total_out += n; + pState->m_dict_avail -= n; + pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1); + return ((pState->m_last_status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK; + } + + for (;;) + { + in_bytes = pStream->avail_in; + out_bytes = TINFL_LZ_DICT_SIZE - pState->m_dict_ofs; + + status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pState->m_dict, pState->m_dict + pState->m_dict_ofs, &out_bytes, decomp_flags); + pState->m_last_status = status; + + pStream->next_in += (mz_uint)in_bytes; + pStream->avail_in -= (mz_uint)in_bytes; + pStream->total_in += (mz_uint)in_bytes; + pStream->adler = tinfl_get_adler32(&pState->m_decomp); + + pState->m_dict_avail = (mz_uint)out_bytes; + + n = MZ_MIN(pState->m_dict_avail, pStream->avail_out); + memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n); + pStream->next_out += n; + pStream->avail_out -= n; + pStream->total_out += n; + pState->m_dict_avail -= n; + pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1); + + if (status < 0) + return MZ_DATA_ERROR; /* Stream is corrupted (there could be some uncompressed data left in the output dictionary - oh well). */ + else if ((status == TINFL_STATUS_NEEDS_MORE_INPUT) && (!orig_avail_in)) + return MZ_BUF_ERROR; /* Signal caller that we can't make forward progress without supplying more input or by setting flush to MZ_FINISH. */ + else if (flush == MZ_FINISH) + { + /* The output buffer MUST be large to hold the remaining uncompressed data when flush==MZ_FINISH. */ + if (status == TINFL_STATUS_DONE) + return pState->m_dict_avail ? MZ_BUF_ERROR : MZ_STREAM_END; + /* status here must be TINFL_STATUS_HAS_MORE_OUTPUT, which means there's at least 1 more byte on the way. If there's no more room left in the output buffer then something is wrong. */ + else if (!pStream->avail_out) + return MZ_BUF_ERROR; + } + else if ((status == TINFL_STATUS_DONE) || (!pStream->avail_in) || (!pStream->avail_out) || (pState->m_dict_avail)) + break; + } + + return ((status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK; +} + +int mz_inflateEnd(mz_streamp pStream) +{ + if (!pStream) + return MZ_STREAM_ERROR; + if (pStream->state) + { + pStream->zfree(pStream->opaque, pStream->state); + pStream->state = NULL; + } + return MZ_OK; +} +int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len) +{ + mz_stream stream; + int status; + memset(&stream, 0, sizeof(stream)); + + /* In case mz_ulong is 64-bits (argh I hate longs). */ + if ((mz_uint64)(*pSource_len | *pDest_len) > 0xFFFFFFFFU) + return MZ_PARAM_ERROR; + + stream.next_in = pSource; + stream.avail_in = (mz_uint32)*pSource_len; + stream.next_out = pDest; + stream.avail_out = (mz_uint32)*pDest_len; + + status = mz_inflateInit(&stream); + if (status != MZ_OK) + return status; + + status = mz_inflate(&stream, MZ_FINISH); + *pSource_len = *pSource_len - stream.avail_in; + if (status != MZ_STREAM_END) + { + mz_inflateEnd(&stream); + return ((status == MZ_BUF_ERROR) && (!stream.avail_in)) ? MZ_DATA_ERROR : status; + } + *pDest_len = stream.total_out; + + return mz_inflateEnd(&stream); +} + +int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len) +{ + return mz_uncompress2(pDest, pDest_len, pSource, &source_len); +} + +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + +const char *mz_error(int err) +{ + static struct + { + int m_err; + const char *m_pDesc; + } s_error_descs[] = + { + { MZ_OK, "" }, { MZ_STREAM_END, "stream end" }, { MZ_NEED_DICT, "need dictionary" }, { MZ_ERRNO, "file error" }, { MZ_STREAM_ERROR, "stream error" }, { MZ_DATA_ERROR, "data error" }, { MZ_MEM_ERROR, "out of memory" }, { MZ_BUF_ERROR, "buf error" }, { MZ_VERSION_ERROR, "version error" }, { MZ_PARAM_ERROR, "parameter error" } + }; + mz_uint i; + for (i = 0; i < sizeof(s_error_descs) / sizeof(s_error_descs[0]); ++i) + if (s_error_descs[i].m_err == err) + return s_error_descs[i].m_pDesc; + return NULL; +} + +#endif /*MINIZ_NO_ZLIB_APIS */ + +#ifdef __cplusplus +} +#endif + +/* + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + 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 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. + + For more information, please refer to +*/ +/************************************************************************** + * + * Copyright 2013-2014 RAD Game Tools and Valve Software + * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + * 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 + * 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. + * + **************************************************************************/ + + + +#ifndef MINIZ_NO_DEFLATE_APIS + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------- Low-level Compression (independent from all decompression API's) */ + +/* Purposely making these tables static for faster init and thread safety. */ +static const mz_uint16 s_tdefl_len_sym[256] = + { + 257, 258, 259, 260, 261, 262, 263, 264, 265, 265, 266, 266, 267, 267, 268, 268, 269, 269, 269, 269, 270, 270, 270, 270, 271, 271, 271, 271, 272, 272, 272, 272, + 273, 273, 273, 273, 273, 273, 273, 273, 274, 274, 274, 274, 274, 274, 274, 274, 275, 275, 275, 275, 275, 275, 275, 275, 276, 276, 276, 276, 276, 276, 276, 276, + 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, + 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, + 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, + 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, + 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, + 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 285 + }; + +static const mz_uint8 s_tdefl_len_extra[256] = + { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0 + }; + +static const mz_uint8 s_tdefl_small_dist_sym[512] = + { + 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17 + }; + +static const mz_uint8 s_tdefl_small_dist_extra[512] = + { + 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7 + }; + +static const mz_uint8 s_tdefl_large_dist_sym[128] = + { + 0, 0, 18, 19, 20, 20, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29 + }; + +static const mz_uint8 s_tdefl_large_dist_extra[128] = + { + 0, 0, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13 + }; + +/* Radix sorts tdefl_sym_freq[] array by 16-bit key m_key. Returns ptr to sorted values. */ +typedef struct +{ + mz_uint16 m_key, m_sym_index; +} tdefl_sym_freq; +static tdefl_sym_freq *tdefl_radix_sort_syms(mz_uint num_syms, tdefl_sym_freq *pSyms0, tdefl_sym_freq *pSyms1) +{ + mz_uint32 total_passes = 2, pass_shift, pass, i, hist[256 * 2]; + tdefl_sym_freq *pCur_syms = pSyms0, *pNew_syms = pSyms1; + MZ_CLEAR_ARR(hist); + for (i = 0; i < num_syms; i++) + { + mz_uint freq = pSyms0[i].m_key; + hist[freq & 0xFF]++; + hist[256 + ((freq >> 8) & 0xFF)]++; + } + while ((total_passes > 1) && (num_syms == hist[(total_passes - 1) * 256])) + total_passes--; + for (pass_shift = 0, pass = 0; pass < total_passes; pass++, pass_shift += 8) + { + const mz_uint32 *pHist = &hist[pass << 8]; + mz_uint offsets[256], cur_ofs = 0; + for (i = 0; i < 256; i++) + { + offsets[i] = cur_ofs; + cur_ofs += pHist[i]; + } + for (i = 0; i < num_syms; i++) + pNew_syms[offsets[(pCur_syms[i].m_key >> pass_shift) & 0xFF]++] = pCur_syms[i]; + { + tdefl_sym_freq *t = pCur_syms; + pCur_syms = pNew_syms; + pNew_syms = t; + } + } + return pCur_syms; +} + +/* tdefl_calculate_minimum_redundancy() originally written by: Alistair Moffat, alistair@cs.mu.oz.au, Jyrki Katajainen, jyrki@diku.dk, November 1996. */ +static void tdefl_calculate_minimum_redundancy(tdefl_sym_freq *A, int n) +{ + int root, leaf, next, avbl, used, dpth; + if (n == 0) + return; + else if (n == 1) + { + A[0].m_key = 1; + return; + } + A[0].m_key += A[1].m_key; + root = 0; + leaf = 2; + for (next = 1; next < n - 1; next++) + { + if (leaf >= n || A[root].m_key < A[leaf].m_key) + { + A[next].m_key = A[root].m_key; + A[root++].m_key = (mz_uint16)next; + } + else + A[next].m_key = A[leaf++].m_key; + if (leaf >= n || (root < next && A[root].m_key < A[leaf].m_key)) + { + A[next].m_key = (mz_uint16)(A[next].m_key + A[root].m_key); + A[root++].m_key = (mz_uint16)next; + } + else + A[next].m_key = (mz_uint16)(A[next].m_key + A[leaf++].m_key); + } + A[n - 2].m_key = 0; + for (next = n - 3; next >= 0; next--) + A[next].m_key = A[A[next].m_key].m_key + 1; + avbl = 1; + used = dpth = 0; + root = n - 2; + next = n - 1; + while (avbl > 0) + { + while (root >= 0 && (int)A[root].m_key == dpth) + { + used++; + root--; + } + while (avbl > used) + { + A[next--].m_key = (mz_uint16)(dpth); + avbl--; + } + avbl = 2 * used; + dpth++; + used = 0; + } +} + +/* Limits canonical Huffman code table's max code size. */ +enum +{ + TDEFL_MAX_SUPPORTED_HUFF_CODESIZE = 32 +}; +static void tdefl_huffman_enforce_max_code_size(int *pNum_codes, int code_list_len, int max_code_size) +{ + int i; + mz_uint32 total = 0; + if (code_list_len <= 1) + return; + for (i = max_code_size + 1; i <= TDEFL_MAX_SUPPORTED_HUFF_CODESIZE; i++) + pNum_codes[max_code_size] += pNum_codes[i]; + for (i = max_code_size; i > 0; i--) + total += (((mz_uint32)pNum_codes[i]) << (max_code_size - i)); + while (total != (1UL << max_code_size)) + { + pNum_codes[max_code_size]--; + for (i = max_code_size - 1; i > 0; i--) + if (pNum_codes[i]) + { + pNum_codes[i]--; + pNum_codes[i + 1] += 2; + break; + } + total--; + } +} + +static void tdefl_optimize_huffman_table(tdefl_compressor *d, int table_num, int table_len, int code_size_limit, int static_table) +{ + int i, j, l, num_codes[1 + TDEFL_MAX_SUPPORTED_HUFF_CODESIZE]; + mz_uint next_code[TDEFL_MAX_SUPPORTED_HUFF_CODESIZE + 1]; + MZ_CLEAR_ARR(num_codes); + if (static_table) + { + for (i = 0; i < table_len; i++) + num_codes[d->m_huff_code_sizes[table_num][i]]++; + } + else + { + tdefl_sym_freq syms0[TDEFL_MAX_HUFF_SYMBOLS], syms1[TDEFL_MAX_HUFF_SYMBOLS], *pSyms; + int num_used_syms = 0; + const mz_uint16 *pSym_count = &d->m_huff_count[table_num][0]; + for (i = 0; i < table_len; i++) + if (pSym_count[i]) + { + syms0[num_used_syms].m_key = (mz_uint16)pSym_count[i]; + syms0[num_used_syms++].m_sym_index = (mz_uint16)i; + } + + pSyms = tdefl_radix_sort_syms(num_used_syms, syms0, syms1); + tdefl_calculate_minimum_redundancy(pSyms, num_used_syms); + + for (i = 0; i < num_used_syms; i++) + num_codes[pSyms[i].m_key]++; + + tdefl_huffman_enforce_max_code_size(num_codes, num_used_syms, code_size_limit); + + MZ_CLEAR_ARR(d->m_huff_code_sizes[table_num]); + MZ_CLEAR_ARR(d->m_huff_codes[table_num]); + for (i = 1, j = num_used_syms; i <= code_size_limit; i++) + for (l = num_codes[i]; l > 0; l--) + d->m_huff_code_sizes[table_num][pSyms[--j].m_sym_index] = (mz_uint8)(i); + } + + next_code[1] = 0; + for (j = 0, i = 2; i <= code_size_limit; i++) + next_code[i] = j = ((j + num_codes[i - 1]) << 1); + + for (i = 0; i < table_len; i++) + { + mz_uint rev_code = 0, code, code_size; + if ((code_size = d->m_huff_code_sizes[table_num][i]) == 0) + continue; + code = next_code[code_size]++; + for (l = code_size; l > 0; l--, code >>= 1) + rev_code = (rev_code << 1) | (code & 1); + d->m_huff_codes[table_num][i] = (mz_uint16)rev_code; + } +} + +#define TDEFL_PUT_BITS(b, l) \ + do \ + { \ + mz_uint bits = b; \ + mz_uint len = l; \ + MZ_ASSERT(bits <= ((1U << len) - 1U)); \ + d->m_bit_buffer |= (bits << d->m_bits_in); \ + d->m_bits_in += len; \ + while (d->m_bits_in >= 8) \ + { \ + if (d->m_pOutput_buf < d->m_pOutput_buf_end) \ + *d->m_pOutput_buf++ = (mz_uint8)(d->m_bit_buffer); \ + d->m_bit_buffer >>= 8; \ + d->m_bits_in -= 8; \ + } \ + } \ + MZ_MACRO_END + +#define TDEFL_RLE_PREV_CODE_SIZE() \ + { \ + if (rle_repeat_count) \ + { \ + if (rle_repeat_count < 3) \ + { \ + d->m_huff_count[2][prev_code_size] = (mz_uint16)(d->m_huff_count[2][prev_code_size] + rle_repeat_count); \ + while (rle_repeat_count--) \ + packed_code_sizes[num_packed_code_sizes++] = prev_code_size; \ + } \ + else \ + { \ + d->m_huff_count[2][16] = (mz_uint16)(d->m_huff_count[2][16] + 1); \ + packed_code_sizes[num_packed_code_sizes++] = 16; \ + packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_repeat_count - 3); \ + } \ + rle_repeat_count = 0; \ + } \ + } + +#define TDEFL_RLE_ZERO_CODE_SIZE() \ + { \ + if (rle_z_count) \ + { \ + if (rle_z_count < 3) \ + { \ + d->m_huff_count[2][0] = (mz_uint16)(d->m_huff_count[2][0] + rle_z_count); \ + while (rle_z_count--) \ + packed_code_sizes[num_packed_code_sizes++] = 0; \ + } \ + else if (rle_z_count <= 10) \ + { \ + d->m_huff_count[2][17] = (mz_uint16)(d->m_huff_count[2][17] + 1); \ + packed_code_sizes[num_packed_code_sizes++] = 17; \ + packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_z_count - 3); \ + } \ + else \ + { \ + d->m_huff_count[2][18] = (mz_uint16)(d->m_huff_count[2][18] + 1); \ + packed_code_sizes[num_packed_code_sizes++] = 18; \ + packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_z_count - 11); \ + } \ + rle_z_count = 0; \ + } \ + } + +static const mz_uint8 s_tdefl_packed_code_size_syms_swizzle[] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + +static void tdefl_start_dynamic_block(tdefl_compressor *d) +{ + int num_lit_codes, num_dist_codes, num_bit_lengths; + mz_uint i, total_code_sizes_to_pack, num_packed_code_sizes, rle_z_count, rle_repeat_count, packed_code_sizes_index; + mz_uint8 code_sizes_to_pack[TDEFL_MAX_HUFF_SYMBOLS_0 + TDEFL_MAX_HUFF_SYMBOLS_1], packed_code_sizes[TDEFL_MAX_HUFF_SYMBOLS_0 + TDEFL_MAX_HUFF_SYMBOLS_1], prev_code_size = 0xFF; + + d->m_huff_count[0][256] = 1; + + tdefl_optimize_huffman_table(d, 0, TDEFL_MAX_HUFF_SYMBOLS_0, 15, MZ_FALSE); + tdefl_optimize_huffman_table(d, 1, TDEFL_MAX_HUFF_SYMBOLS_1, 15, MZ_FALSE); + + for (num_lit_codes = 286; num_lit_codes > 257; num_lit_codes--) + if (d->m_huff_code_sizes[0][num_lit_codes - 1]) + break; + for (num_dist_codes = 30; num_dist_codes > 1; num_dist_codes--) + if (d->m_huff_code_sizes[1][num_dist_codes - 1]) + break; + + memcpy(code_sizes_to_pack, &d->m_huff_code_sizes[0][0], num_lit_codes); + memcpy(code_sizes_to_pack + num_lit_codes, &d->m_huff_code_sizes[1][0], num_dist_codes); + total_code_sizes_to_pack = num_lit_codes + num_dist_codes; + num_packed_code_sizes = 0; + rle_z_count = 0; + rle_repeat_count = 0; + + memset(&d->m_huff_count[2][0], 0, sizeof(d->m_huff_count[2][0]) * TDEFL_MAX_HUFF_SYMBOLS_2); + for (i = 0; i < total_code_sizes_to_pack; i++) + { + mz_uint8 code_size = code_sizes_to_pack[i]; + if (!code_size) + { + TDEFL_RLE_PREV_CODE_SIZE(); + if (++rle_z_count == 138) + { + TDEFL_RLE_ZERO_CODE_SIZE(); + } + } + else + { + TDEFL_RLE_ZERO_CODE_SIZE(); + if (code_size != prev_code_size) + { + TDEFL_RLE_PREV_CODE_SIZE(); + d->m_huff_count[2][code_size] = (mz_uint16)(d->m_huff_count[2][code_size] + 1); + packed_code_sizes[num_packed_code_sizes++] = code_size; + } + else if (++rle_repeat_count == 6) + { + TDEFL_RLE_PREV_CODE_SIZE(); + } + } + prev_code_size = code_size; + } + if (rle_repeat_count) + { + TDEFL_RLE_PREV_CODE_SIZE(); + } + else + { + TDEFL_RLE_ZERO_CODE_SIZE(); + } + + tdefl_optimize_huffman_table(d, 2, TDEFL_MAX_HUFF_SYMBOLS_2, 7, MZ_FALSE); + + TDEFL_PUT_BITS(2, 2); + + TDEFL_PUT_BITS(num_lit_codes - 257, 5); + TDEFL_PUT_BITS(num_dist_codes - 1, 5); + + for (num_bit_lengths = 18; num_bit_lengths >= 0; num_bit_lengths--) + if (d->m_huff_code_sizes[2][s_tdefl_packed_code_size_syms_swizzle[num_bit_lengths]]) + break; + num_bit_lengths = MZ_MAX(4, (num_bit_lengths + 1)); + TDEFL_PUT_BITS(num_bit_lengths - 4, 4); + for (i = 0; (int)i < num_bit_lengths; i++) + TDEFL_PUT_BITS(d->m_huff_code_sizes[2][s_tdefl_packed_code_size_syms_swizzle[i]], 3); + + for (packed_code_sizes_index = 0; packed_code_sizes_index < num_packed_code_sizes;) + { + mz_uint code = packed_code_sizes[packed_code_sizes_index++]; + MZ_ASSERT(code < TDEFL_MAX_HUFF_SYMBOLS_2); + TDEFL_PUT_BITS(d->m_huff_codes[2][code], d->m_huff_code_sizes[2][code]); + if (code >= 16) + TDEFL_PUT_BITS(packed_code_sizes[packed_code_sizes_index++], "\02\03\07"[code - 16]); + } +} + +static void tdefl_start_static_block(tdefl_compressor *d) +{ + mz_uint i; + mz_uint8 *p = &d->m_huff_code_sizes[0][0]; + + for (i = 0; i <= 143; ++i) + *p++ = 8; + for (; i <= 255; ++i) + *p++ = 9; + for (; i <= 279; ++i) + *p++ = 7; + for (; i <= 287; ++i) + *p++ = 8; + + memset(d->m_huff_code_sizes[1], 5, 32); + + tdefl_optimize_huffman_table(d, 0, 288, 15, MZ_TRUE); + tdefl_optimize_huffman_table(d, 1, 32, 15, MZ_TRUE); + + TDEFL_PUT_BITS(1, 2); +} + +static const mz_uint mz_bitmasks[17] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF }; + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN && MINIZ_HAS_64BIT_REGISTERS +static mz_bool tdefl_compress_lz_codes(tdefl_compressor *d) +{ + mz_uint flags; + mz_uint8 *pLZ_codes; + mz_uint8 *pOutput_buf = d->m_pOutput_buf; + mz_uint8 *pLZ_code_buf_end = d->m_pLZ_code_buf; + mz_uint64 bit_buffer = d->m_bit_buffer; + mz_uint bits_in = d->m_bits_in; + +#define TDEFL_PUT_BITS_FAST(b, l) \ + { \ + bit_buffer |= (((mz_uint64)(b)) << bits_in); \ + bits_in += (l); \ + } + + flags = 1; + for (pLZ_codes = d->m_lz_code_buf; pLZ_codes < pLZ_code_buf_end; flags >>= 1) + { + if (flags == 1) + flags = *pLZ_codes++ | 0x100; + + if (flags & 1) + { + mz_uint s0, s1, n0, n1, sym, num_extra_bits; + mz_uint match_len = pLZ_codes[0]; + mz_uint match_dist = (pLZ_codes[1] | (pLZ_codes[2] << 8)); + pLZ_codes += 3; + + MZ_ASSERT(d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][s_tdefl_len_sym[match_len]], d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); + TDEFL_PUT_BITS_FAST(match_len & mz_bitmasks[s_tdefl_len_extra[match_len]], s_tdefl_len_extra[match_len]); + + /* This sequence coaxes MSVC into using cmov's vs. jmp's. */ + s0 = s_tdefl_small_dist_sym[match_dist & 511]; + n0 = s_tdefl_small_dist_extra[match_dist & 511]; + s1 = s_tdefl_large_dist_sym[match_dist >> 8]; + n1 = s_tdefl_large_dist_extra[match_dist >> 8]; + sym = (match_dist < 512) ? s0 : s1; + num_extra_bits = (match_dist < 512) ? n0 : n1; + + MZ_ASSERT(d->m_huff_code_sizes[1][sym]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[1][sym], d->m_huff_code_sizes[1][sym]); + TDEFL_PUT_BITS_FAST(match_dist & mz_bitmasks[num_extra_bits], num_extra_bits); + } + else + { + mz_uint lit = *pLZ_codes++; + MZ_ASSERT(d->m_huff_code_sizes[0][lit]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); + + if (((flags & 2) == 0) && (pLZ_codes < pLZ_code_buf_end)) + { + flags >>= 1; + lit = *pLZ_codes++; + MZ_ASSERT(d->m_huff_code_sizes[0][lit]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); + + if (((flags & 2) == 0) && (pLZ_codes < pLZ_code_buf_end)) + { + flags >>= 1; + lit = *pLZ_codes++; + MZ_ASSERT(d->m_huff_code_sizes[0][lit]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); + } + } + } + + if (pOutput_buf >= d->m_pOutput_buf_end) + return MZ_FALSE; + + memcpy(pOutput_buf, &bit_buffer, sizeof(mz_uint64)); + pOutput_buf += (bits_in >> 3); + bit_buffer >>= (bits_in & ~7); + bits_in &= 7; + } + +#undef TDEFL_PUT_BITS_FAST + + d->m_pOutput_buf = pOutput_buf; + d->m_bits_in = 0; + d->m_bit_buffer = 0; + + while (bits_in) + { + mz_uint32 n = MZ_MIN(bits_in, 16); + TDEFL_PUT_BITS((mz_uint)bit_buffer & mz_bitmasks[n], n); + bit_buffer >>= n; + bits_in -= n; + } + + TDEFL_PUT_BITS(d->m_huff_codes[0][256], d->m_huff_code_sizes[0][256]); + + return (d->m_pOutput_buf < d->m_pOutput_buf_end); +} +#else +static mz_bool tdefl_compress_lz_codes(tdefl_compressor *d) +{ + mz_uint flags; + mz_uint8 *pLZ_codes; + + flags = 1; + for (pLZ_codes = d->m_lz_code_buf; pLZ_codes < d->m_pLZ_code_buf; flags >>= 1) + { + if (flags == 1) + flags = *pLZ_codes++ | 0x100; + if (flags & 1) + { + mz_uint sym, num_extra_bits; + mz_uint match_len = pLZ_codes[0], match_dist = (pLZ_codes[1] | (pLZ_codes[2] << 8)); + pLZ_codes += 3; + + MZ_ASSERT(d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); + TDEFL_PUT_BITS(d->m_huff_codes[0][s_tdefl_len_sym[match_len]], d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); + TDEFL_PUT_BITS(match_len & mz_bitmasks[s_tdefl_len_extra[match_len]], s_tdefl_len_extra[match_len]); + + if (match_dist < 512) + { + sym = s_tdefl_small_dist_sym[match_dist]; + num_extra_bits = s_tdefl_small_dist_extra[match_dist]; + } + else + { + sym = s_tdefl_large_dist_sym[match_dist >> 8]; + num_extra_bits = s_tdefl_large_dist_extra[match_dist >> 8]; + } + MZ_ASSERT(d->m_huff_code_sizes[1][sym]); + TDEFL_PUT_BITS(d->m_huff_codes[1][sym], d->m_huff_code_sizes[1][sym]); + TDEFL_PUT_BITS(match_dist & mz_bitmasks[num_extra_bits], num_extra_bits); + } + else + { + mz_uint lit = *pLZ_codes++; + MZ_ASSERT(d->m_huff_code_sizes[0][lit]); + TDEFL_PUT_BITS(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); + } + } + + TDEFL_PUT_BITS(d->m_huff_codes[0][256], d->m_huff_code_sizes[0][256]); + + return (d->m_pOutput_buf < d->m_pOutput_buf_end); +} +#endif /* MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN && MINIZ_HAS_64BIT_REGISTERS */ + +static mz_bool tdefl_compress_block(tdefl_compressor *d, mz_bool static_block) +{ + if (static_block) + tdefl_start_static_block(d); + else + tdefl_start_dynamic_block(d); + return tdefl_compress_lz_codes(d); +} + +static const mz_uint s_tdefl_num_probes[11]; + +static int tdefl_flush_block(tdefl_compressor *d, int flush) +{ + mz_uint saved_bit_buf, saved_bits_in; + mz_uint8 *pSaved_output_buf; + mz_bool comp_block_succeeded = MZ_FALSE; + int n, use_raw_block = ((d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS) != 0) && (d->m_lookahead_pos - d->m_lz_code_buf_dict_pos) <= d->m_dict_size; + mz_uint8 *pOutput_buf_start = ((d->m_pPut_buf_func == NULL) && ((*d->m_pOut_buf_size - d->m_out_buf_ofs) >= TDEFL_OUT_BUF_SIZE)) ? ((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs) : d->m_output_buf; + + d->m_pOutput_buf = pOutput_buf_start; + d->m_pOutput_buf_end = d->m_pOutput_buf + TDEFL_OUT_BUF_SIZE - 16; + + MZ_ASSERT(!d->m_output_flush_remaining); + d->m_output_flush_ofs = 0; + d->m_output_flush_remaining = 0; + + *d->m_pLZ_flags = (mz_uint8)(*d->m_pLZ_flags >> d->m_num_flags_left); + d->m_pLZ_code_buf -= (d->m_num_flags_left == 8); + + if ((d->m_flags & TDEFL_WRITE_ZLIB_HEADER) && (!d->m_block_index)) + { + const mz_uint8 cmf = 0x78; + mz_uint8 flg, flevel = 3; + mz_uint header, i, mz_un = sizeof(s_tdefl_num_probes) / sizeof(mz_uint); + + /* Determine compression level by reversing the process in tdefl_create_comp_flags_from_zip_params() */ + for (i = 0; i < mz_un; i++) + if (s_tdefl_num_probes[i] == (d->m_flags & 0xFFF)) break; + + if (i < 2) + flevel = 0; + else if (i < 6) + flevel = 1; + else if (i == 6) + flevel = 2; + + header = cmf << 8 | (flevel << 6); + header += 31 - (header % 31); + flg = header & 0xFF; + + TDEFL_PUT_BITS(cmf, 8); + TDEFL_PUT_BITS(flg, 8); + } + + TDEFL_PUT_BITS(flush == TDEFL_FINISH, 1); + + pSaved_output_buf = d->m_pOutput_buf; + saved_bit_buf = d->m_bit_buffer; + saved_bits_in = d->m_bits_in; + + if (!use_raw_block) + comp_block_succeeded = tdefl_compress_block(d, (d->m_flags & TDEFL_FORCE_ALL_STATIC_BLOCKS) || (d->m_total_lz_bytes < 48)); + + /* If the block gets expanded, forget the current contents of the output buffer and send a raw block instead. */ + if (((use_raw_block) || ((d->m_total_lz_bytes) && ((d->m_pOutput_buf - pSaved_output_buf + 1U) >= d->m_total_lz_bytes))) && + ((d->m_lookahead_pos - d->m_lz_code_buf_dict_pos) <= d->m_dict_size)) + { + mz_uint i; + d->m_pOutput_buf = pSaved_output_buf; + d->m_bit_buffer = saved_bit_buf, d->m_bits_in = saved_bits_in; + TDEFL_PUT_BITS(0, 2); + if (d->m_bits_in) + { + TDEFL_PUT_BITS(0, 8 - d->m_bits_in); + } + for (i = 2; i; --i, d->m_total_lz_bytes ^= 0xFFFF) + { + TDEFL_PUT_BITS(d->m_total_lz_bytes & 0xFFFF, 16); + } + for (i = 0; i < d->m_total_lz_bytes; ++i) + { + TDEFL_PUT_BITS(d->m_dict[(d->m_lz_code_buf_dict_pos + i) & TDEFL_LZ_DICT_SIZE_MASK], 8); + } + } + /* Check for the extremely unlikely (if not impossible) case of the compressed block not fitting into the output buffer when using dynamic codes. */ + else if (!comp_block_succeeded) + { + d->m_pOutput_buf = pSaved_output_buf; + d->m_bit_buffer = saved_bit_buf, d->m_bits_in = saved_bits_in; + tdefl_compress_block(d, MZ_TRUE); + } + + if (flush) + { + if (flush == TDEFL_FINISH) + { + if (d->m_bits_in) + { + TDEFL_PUT_BITS(0, 8 - d->m_bits_in); + } + if (d->m_flags & TDEFL_WRITE_ZLIB_HEADER) + { + mz_uint i, a = d->m_adler32; + for (i = 0; i < 4; i++) + { + TDEFL_PUT_BITS((a >> 24) & 0xFF, 8); + a <<= 8; + } + } + } + else + { + mz_uint i, z = 0; + TDEFL_PUT_BITS(0, 3); + if (d->m_bits_in) + { + TDEFL_PUT_BITS(0, 8 - d->m_bits_in); + } + for (i = 2; i; --i, z ^= 0xFFFF) + { + TDEFL_PUT_BITS(z & 0xFFFF, 16); + } + } + } + + MZ_ASSERT(d->m_pOutput_buf < d->m_pOutput_buf_end); + + memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0); + memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1); + + d->m_pLZ_code_buf = d->m_lz_code_buf + 1; + d->m_pLZ_flags = d->m_lz_code_buf; + d->m_num_flags_left = 8; + d->m_lz_code_buf_dict_pos += d->m_total_lz_bytes; + d->m_total_lz_bytes = 0; + d->m_block_index++; + + if ((n = (int)(d->m_pOutput_buf - pOutput_buf_start)) != 0) + { + if (d->m_pPut_buf_func) + { + *d->m_pIn_buf_size = d->m_pSrc - (const mz_uint8 *)d->m_pIn_buf; + if (!(*d->m_pPut_buf_func)(d->m_output_buf, n, d->m_pPut_buf_user)) + return (d->m_prev_return_status = TDEFL_STATUS_PUT_BUF_FAILED); + } + else if (pOutput_buf_start == d->m_output_buf) + { + int bytes_to_copy = (int)MZ_MIN((size_t)n, (size_t)(*d->m_pOut_buf_size - d->m_out_buf_ofs)); + memcpy((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs, d->m_output_buf, bytes_to_copy); + d->m_out_buf_ofs += bytes_to_copy; + if ((n -= bytes_to_copy) != 0) + { + d->m_output_flush_ofs = bytes_to_copy; + d->m_output_flush_remaining = n; + } + } + else + { + d->m_out_buf_ofs += n; + } + } + + return d->m_output_flush_remaining; +} + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES +#ifdef MINIZ_UNALIGNED_USE_MEMCPY +static mz_uint16 TDEFL_READ_UNALIGNED_WORD(const mz_uint8* p) +{ + mz_uint16 ret; + memcpy(&ret, p, sizeof(mz_uint16)); + return ret; +} +static mz_uint16 TDEFL_READ_UNALIGNED_WORD2(const mz_uint16* p) +{ + mz_uint16 ret; + memcpy(&ret, p, sizeof(mz_uint16)); + return ret; +} +#else +#define TDEFL_READ_UNALIGNED_WORD(p) *(const mz_uint16 *)(p) +#define TDEFL_READ_UNALIGNED_WORD2(p) *(const mz_uint16 *)(p) +#endif +static MZ_FORCEINLINE void tdefl_find_match(tdefl_compressor *d, mz_uint lookahead_pos, mz_uint max_dist, mz_uint max_match_len, mz_uint *pMatch_dist, mz_uint *pMatch_len) +{ + mz_uint dist, pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK, match_len = *pMatch_len, probe_pos = pos, next_probe_pos, probe_len; + mz_uint num_probes_left = d->m_max_probes[match_len >= 32]; + const mz_uint16 *s = (const mz_uint16 *)(d->m_dict + pos), *p, *q; + mz_uint16 c01 = TDEFL_READ_UNALIGNED_WORD(&d->m_dict[pos + match_len - 1]), s01 = TDEFL_READ_UNALIGNED_WORD2(s); + MZ_ASSERT(max_match_len <= TDEFL_MAX_MATCH_LEN); + if (max_match_len <= match_len) + return; + for (;;) + { + for (;;) + { + if (--num_probes_left == 0) + return; +#define TDEFL_PROBE \ + next_probe_pos = d->m_next[probe_pos]; \ + if ((!next_probe_pos) || ((dist = (mz_uint16)(lookahead_pos - next_probe_pos)) > max_dist)) \ + return; \ + probe_pos = next_probe_pos & TDEFL_LZ_DICT_SIZE_MASK; \ + if (TDEFL_READ_UNALIGNED_WORD(&d->m_dict[probe_pos + match_len - 1]) == c01) \ + break; + TDEFL_PROBE; + TDEFL_PROBE; + TDEFL_PROBE; + } + if (!dist) + break; + q = (const mz_uint16 *)(d->m_dict + probe_pos); + if (TDEFL_READ_UNALIGNED_WORD2(q) != s01) + continue; + p = s; + probe_len = 32; + do + { + } while ((TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && + (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (--probe_len > 0)); + if (!probe_len) + { + *pMatch_dist = dist; + *pMatch_len = MZ_MIN(max_match_len, (mz_uint)TDEFL_MAX_MATCH_LEN); + break; + } + else if ((probe_len = ((mz_uint)(p - s) * 2) + (mz_uint)(*(const mz_uint8 *)p == *(const mz_uint8 *)q)) > match_len) + { + *pMatch_dist = dist; + if ((*pMatch_len = match_len = MZ_MIN(max_match_len, probe_len)) == max_match_len) + break; + c01 = TDEFL_READ_UNALIGNED_WORD(&d->m_dict[pos + match_len - 1]); + } + } +} +#else +static MZ_FORCEINLINE void tdefl_find_match(tdefl_compressor *d, mz_uint lookahead_pos, mz_uint max_dist, mz_uint max_match_len, mz_uint *pMatch_dist, mz_uint *pMatch_len) +{ + mz_uint dist, pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK, match_len = *pMatch_len, probe_pos = pos, next_probe_pos, probe_len; + mz_uint num_probes_left = d->m_max_probes[match_len >= 32]; + const mz_uint8 *s = d->m_dict + pos, *p, *q; + mz_uint8 c0 = d->m_dict[pos + match_len], c1 = d->m_dict[pos + match_len - 1]; + MZ_ASSERT(max_match_len <= TDEFL_MAX_MATCH_LEN); + if (max_match_len <= match_len) + return; + for (;;) + { + for (;;) + { + if (--num_probes_left == 0) + return; +#define TDEFL_PROBE \ + next_probe_pos = d->m_next[probe_pos]; \ + if ((!next_probe_pos) || ((dist = (mz_uint16)(lookahead_pos - next_probe_pos)) > max_dist)) \ + return; \ + probe_pos = next_probe_pos & TDEFL_LZ_DICT_SIZE_MASK; \ + if ((d->m_dict[probe_pos + match_len] == c0) && (d->m_dict[probe_pos + match_len - 1] == c1)) \ + break; + TDEFL_PROBE; + TDEFL_PROBE; + TDEFL_PROBE; + } + if (!dist) + break; + p = s; + q = d->m_dict + probe_pos; + for (probe_len = 0; probe_len < max_match_len; probe_len++) + if (*p++ != *q++) + break; + if (probe_len > match_len) + { + *pMatch_dist = dist; + if ((*pMatch_len = match_len = probe_len) == max_match_len) + return; + c0 = d->m_dict[pos + match_len]; + c1 = d->m_dict[pos + match_len - 1]; + } + } +} +#endif /* #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES */ + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN +#ifdef MINIZ_UNALIGNED_USE_MEMCPY +static mz_uint32 TDEFL_READ_UNALIGNED_WORD32(const mz_uint8* p) +{ + mz_uint32 ret; + memcpy(&ret, p, sizeof(mz_uint32)); + return ret; +} +#else +#define TDEFL_READ_UNALIGNED_WORD32(p) *(const mz_uint32 *)(p) +#endif +static mz_bool tdefl_compress_fast(tdefl_compressor *d) +{ + /* Faster, minimally featured LZRW1-style match+parse loop with better register utilization. Intended for applications where raw throughput is valued more highly than ratio. */ + mz_uint lookahead_pos = d->m_lookahead_pos, lookahead_size = d->m_lookahead_size, dict_size = d->m_dict_size, total_lz_bytes = d->m_total_lz_bytes, num_flags_left = d->m_num_flags_left; + mz_uint8 *pLZ_code_buf = d->m_pLZ_code_buf, *pLZ_flags = d->m_pLZ_flags; + mz_uint cur_pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK; + + while ((d->m_src_buf_left) || ((d->m_flush) && (lookahead_size))) + { + const mz_uint TDEFL_COMP_FAST_LOOKAHEAD_SIZE = 4096; + mz_uint dst_pos = (lookahead_pos + lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK; + mz_uint num_bytes_to_process = (mz_uint)MZ_MIN(d->m_src_buf_left, TDEFL_COMP_FAST_LOOKAHEAD_SIZE - lookahead_size); + d->m_src_buf_left -= num_bytes_to_process; + lookahead_size += num_bytes_to_process; + + while (num_bytes_to_process) + { + mz_uint32 n = MZ_MIN(TDEFL_LZ_DICT_SIZE - dst_pos, num_bytes_to_process); + memcpy(d->m_dict + dst_pos, d->m_pSrc, n); + if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1)) + memcpy(d->m_dict + TDEFL_LZ_DICT_SIZE + dst_pos, d->m_pSrc, MZ_MIN(n, (TDEFL_MAX_MATCH_LEN - 1) - dst_pos)); + d->m_pSrc += n; + dst_pos = (dst_pos + n) & TDEFL_LZ_DICT_SIZE_MASK; + num_bytes_to_process -= n; + } + + dict_size = MZ_MIN(TDEFL_LZ_DICT_SIZE - lookahead_size, dict_size); + if ((!d->m_flush) && (lookahead_size < TDEFL_COMP_FAST_LOOKAHEAD_SIZE)) + break; + + while (lookahead_size >= 4) + { + mz_uint cur_match_dist, cur_match_len = 1; + mz_uint8 *pCur_dict = d->m_dict + cur_pos; + mz_uint first_trigram = TDEFL_READ_UNALIGNED_WORD32(pCur_dict) & 0xFFFFFF; + mz_uint hash = (first_trigram ^ (first_trigram >> (24 - (TDEFL_LZ_HASH_BITS - 8)))) & TDEFL_LEVEL1_HASH_SIZE_MASK; + mz_uint probe_pos = d->m_hash[hash]; + d->m_hash[hash] = (mz_uint16)lookahead_pos; + + if (((cur_match_dist = (mz_uint16)(lookahead_pos - probe_pos)) <= dict_size) && ((TDEFL_READ_UNALIGNED_WORD32(d->m_dict + (probe_pos &= TDEFL_LZ_DICT_SIZE_MASK)) & 0xFFFFFF) == first_trigram)) + { + const mz_uint16 *p = (const mz_uint16 *)pCur_dict; + const mz_uint16 *q = (const mz_uint16 *)(d->m_dict + probe_pos); + mz_uint32 probe_len = 32; + do + { + } while ((TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && + (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (--probe_len > 0)); + cur_match_len = ((mz_uint)(p - (const mz_uint16 *)pCur_dict) * 2) + (mz_uint)(*(const mz_uint8 *)p == *(const mz_uint8 *)q); + if (!probe_len) + cur_match_len = cur_match_dist ? TDEFL_MAX_MATCH_LEN : 0; + + if ((cur_match_len < TDEFL_MIN_MATCH_LEN) || ((cur_match_len == TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 8U * 1024U))) + { + cur_match_len = 1; + *pLZ_code_buf++ = (mz_uint8)first_trigram; + *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1); + d->m_huff_count[0][(mz_uint8)first_trigram]++; + } + else + { + mz_uint32 s0, s1; + cur_match_len = MZ_MIN(cur_match_len, lookahead_size); + + MZ_ASSERT((cur_match_len >= TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 1) && (cur_match_dist <= TDEFL_LZ_DICT_SIZE)); + + cur_match_dist--; + + pLZ_code_buf[0] = (mz_uint8)(cur_match_len - TDEFL_MIN_MATCH_LEN); +#ifdef MINIZ_UNALIGNED_USE_MEMCPY + memcpy(&pLZ_code_buf[1], &cur_match_dist, sizeof(cur_match_dist)); +#else + *(mz_uint16 *)(&pLZ_code_buf[1]) = (mz_uint16)cur_match_dist; +#endif + pLZ_code_buf += 3; + *pLZ_flags = (mz_uint8)((*pLZ_flags >> 1) | 0x80); + + s0 = s_tdefl_small_dist_sym[cur_match_dist & 511]; + s1 = s_tdefl_large_dist_sym[cur_match_dist >> 8]; + d->m_huff_count[1][(cur_match_dist < 512) ? s0 : s1]++; + + d->m_huff_count[0][s_tdefl_len_sym[cur_match_len - TDEFL_MIN_MATCH_LEN]]++; + } + } + else + { + *pLZ_code_buf++ = (mz_uint8)first_trigram; + *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1); + d->m_huff_count[0][(mz_uint8)first_trigram]++; + } + + if (--num_flags_left == 0) + { + num_flags_left = 8; + pLZ_flags = pLZ_code_buf++; + } + + total_lz_bytes += cur_match_len; + lookahead_pos += cur_match_len; + dict_size = MZ_MIN(dict_size + cur_match_len, (mz_uint)TDEFL_LZ_DICT_SIZE); + cur_pos = (cur_pos + cur_match_len) & TDEFL_LZ_DICT_SIZE_MASK; + MZ_ASSERT(lookahead_size >= cur_match_len); + lookahead_size -= cur_match_len; + + if (pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) + { + int n; + d->m_lookahead_pos = lookahead_pos; + d->m_lookahead_size = lookahead_size; + d->m_dict_size = dict_size; + d->m_total_lz_bytes = total_lz_bytes; + d->m_pLZ_code_buf = pLZ_code_buf; + d->m_pLZ_flags = pLZ_flags; + d->m_num_flags_left = num_flags_left; + if ((n = tdefl_flush_block(d, 0)) != 0) + return (n < 0) ? MZ_FALSE : MZ_TRUE; + total_lz_bytes = d->m_total_lz_bytes; + pLZ_code_buf = d->m_pLZ_code_buf; + pLZ_flags = d->m_pLZ_flags; + num_flags_left = d->m_num_flags_left; + } + } + + while (lookahead_size) + { + mz_uint8 lit = d->m_dict[cur_pos]; + + total_lz_bytes++; + *pLZ_code_buf++ = lit; + *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1); + if (--num_flags_left == 0) + { + num_flags_left = 8; + pLZ_flags = pLZ_code_buf++; + } + + d->m_huff_count[0][lit]++; + + lookahead_pos++; + dict_size = MZ_MIN(dict_size + 1, (mz_uint)TDEFL_LZ_DICT_SIZE); + cur_pos = (cur_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK; + lookahead_size--; + + if (pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) + { + int n; + d->m_lookahead_pos = lookahead_pos; + d->m_lookahead_size = lookahead_size; + d->m_dict_size = dict_size; + d->m_total_lz_bytes = total_lz_bytes; + d->m_pLZ_code_buf = pLZ_code_buf; + d->m_pLZ_flags = pLZ_flags; + d->m_num_flags_left = num_flags_left; + if ((n = tdefl_flush_block(d, 0)) != 0) + return (n < 0) ? MZ_FALSE : MZ_TRUE; + total_lz_bytes = d->m_total_lz_bytes; + pLZ_code_buf = d->m_pLZ_code_buf; + pLZ_flags = d->m_pLZ_flags; + num_flags_left = d->m_num_flags_left; + } + } + } + + d->m_lookahead_pos = lookahead_pos; + d->m_lookahead_size = lookahead_size; + d->m_dict_size = dict_size; + d->m_total_lz_bytes = total_lz_bytes; + d->m_pLZ_code_buf = pLZ_code_buf; + d->m_pLZ_flags = pLZ_flags; + d->m_num_flags_left = num_flags_left; + return MZ_TRUE; +} +#endif /* MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN */ + +static MZ_FORCEINLINE void tdefl_record_literal(tdefl_compressor *d, mz_uint8 lit) +{ + d->m_total_lz_bytes++; + *d->m_pLZ_code_buf++ = lit; + *d->m_pLZ_flags = (mz_uint8)(*d->m_pLZ_flags >> 1); + if (--d->m_num_flags_left == 0) + { + d->m_num_flags_left = 8; + d->m_pLZ_flags = d->m_pLZ_code_buf++; + } + d->m_huff_count[0][lit]++; +} + +static MZ_FORCEINLINE void tdefl_record_match(tdefl_compressor *d, mz_uint match_len, mz_uint match_dist) +{ + mz_uint32 s0, s1; + + MZ_ASSERT((match_len >= TDEFL_MIN_MATCH_LEN) && (match_dist >= 1) && (match_dist <= TDEFL_LZ_DICT_SIZE)); + + d->m_total_lz_bytes += match_len; + + d->m_pLZ_code_buf[0] = (mz_uint8)(match_len - TDEFL_MIN_MATCH_LEN); + + match_dist -= 1; + d->m_pLZ_code_buf[1] = (mz_uint8)(match_dist & 0xFF); + d->m_pLZ_code_buf[2] = (mz_uint8)(match_dist >> 8); + d->m_pLZ_code_buf += 3; + + *d->m_pLZ_flags = (mz_uint8)((*d->m_pLZ_flags >> 1) | 0x80); + if (--d->m_num_flags_left == 0) + { + d->m_num_flags_left = 8; + d->m_pLZ_flags = d->m_pLZ_code_buf++; + } + + s0 = s_tdefl_small_dist_sym[match_dist & 511]; + s1 = s_tdefl_large_dist_sym[(match_dist >> 8) & 127]; + d->m_huff_count[1][(match_dist < 512) ? s0 : s1]++; + d->m_huff_count[0][s_tdefl_len_sym[match_len - TDEFL_MIN_MATCH_LEN]]++; +} + +static mz_bool tdefl_compress_normal(tdefl_compressor *d) +{ + const mz_uint8 *pSrc = d->m_pSrc; + size_t src_buf_left = d->m_src_buf_left; + tdefl_flush flush = d->m_flush; + + while ((src_buf_left) || ((flush) && (d->m_lookahead_size))) + { + mz_uint len_to_move, cur_match_dist, cur_match_len, cur_pos; + /* Update dictionary and hash chains. Keeps the lookahead size equal to TDEFL_MAX_MATCH_LEN. */ + if ((d->m_lookahead_size + d->m_dict_size) >= (TDEFL_MIN_MATCH_LEN - 1)) + { + mz_uint dst_pos = (d->m_lookahead_pos + d->m_lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK, ins_pos = d->m_lookahead_pos + d->m_lookahead_size - 2; + mz_uint hash = (d->m_dict[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] << TDEFL_LZ_HASH_SHIFT) ^ d->m_dict[(ins_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK]; + mz_uint num_bytes_to_process = (mz_uint)MZ_MIN(src_buf_left, TDEFL_MAX_MATCH_LEN - d->m_lookahead_size); + const mz_uint8 *pSrc_end = pSrc ? pSrc + num_bytes_to_process : NULL; + src_buf_left -= num_bytes_to_process; + d->m_lookahead_size += num_bytes_to_process; + while (pSrc != pSrc_end) + { + mz_uint8 c = *pSrc++; + d->m_dict[dst_pos] = c; + if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1)) + d->m_dict[TDEFL_LZ_DICT_SIZE + dst_pos] = c; + hash = ((hash << TDEFL_LZ_HASH_SHIFT) ^ c) & (TDEFL_LZ_HASH_SIZE - 1); + d->m_next[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] = d->m_hash[hash]; + d->m_hash[hash] = (mz_uint16)(ins_pos); + dst_pos = (dst_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK; + ins_pos++; + } + } + else + { + while ((src_buf_left) && (d->m_lookahead_size < TDEFL_MAX_MATCH_LEN)) + { + mz_uint8 c = *pSrc++; + mz_uint dst_pos = (d->m_lookahead_pos + d->m_lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK; + src_buf_left--; + d->m_dict[dst_pos] = c; + if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1)) + d->m_dict[TDEFL_LZ_DICT_SIZE + dst_pos] = c; + if ((++d->m_lookahead_size + d->m_dict_size) >= TDEFL_MIN_MATCH_LEN) + { + mz_uint ins_pos = d->m_lookahead_pos + (d->m_lookahead_size - 1) - 2; + mz_uint hash = ((d->m_dict[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] << (TDEFL_LZ_HASH_SHIFT * 2)) ^ (d->m_dict[(ins_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK] << TDEFL_LZ_HASH_SHIFT) ^ c) & (TDEFL_LZ_HASH_SIZE - 1); + d->m_next[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] = d->m_hash[hash]; + d->m_hash[hash] = (mz_uint16)(ins_pos); + } + } + } + d->m_dict_size = MZ_MIN(TDEFL_LZ_DICT_SIZE - d->m_lookahead_size, d->m_dict_size); + if ((!flush) && (d->m_lookahead_size < TDEFL_MAX_MATCH_LEN)) + break; + + /* Simple lazy/greedy parsing state machine. */ + len_to_move = 1; + cur_match_dist = 0; + cur_match_len = d->m_saved_match_len ? d->m_saved_match_len : (TDEFL_MIN_MATCH_LEN - 1); + cur_pos = d->m_lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK; + if (d->m_flags & (TDEFL_RLE_MATCHES | TDEFL_FORCE_ALL_RAW_BLOCKS)) + { + if ((d->m_dict_size) && (!(d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS))) + { + mz_uint8 c = d->m_dict[(cur_pos - 1) & TDEFL_LZ_DICT_SIZE_MASK]; + cur_match_len = 0; + while (cur_match_len < d->m_lookahead_size) + { + if (d->m_dict[cur_pos + cur_match_len] != c) + break; + cur_match_len++; + } + if (cur_match_len < TDEFL_MIN_MATCH_LEN) + cur_match_len = 0; + else + cur_match_dist = 1; + } + } + else + { + tdefl_find_match(d, d->m_lookahead_pos, d->m_dict_size, d->m_lookahead_size, &cur_match_dist, &cur_match_len); + } + if (((cur_match_len == TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 8U * 1024U)) || (cur_pos == cur_match_dist) || ((d->m_flags & TDEFL_FILTER_MATCHES) && (cur_match_len <= 5))) + { + cur_match_dist = cur_match_len = 0; + } + if (d->m_saved_match_len) + { + if (cur_match_len > d->m_saved_match_len) + { + tdefl_record_literal(d, (mz_uint8)d->m_saved_lit); + if (cur_match_len >= 128) + { + tdefl_record_match(d, cur_match_len, cur_match_dist); + d->m_saved_match_len = 0; + len_to_move = cur_match_len; + } + else + { + d->m_saved_lit = d->m_dict[cur_pos]; + d->m_saved_match_dist = cur_match_dist; + d->m_saved_match_len = cur_match_len; + } + } + else + { + tdefl_record_match(d, d->m_saved_match_len, d->m_saved_match_dist); + len_to_move = d->m_saved_match_len - 1; + d->m_saved_match_len = 0; + } + } + else if (!cur_match_dist) + tdefl_record_literal(d, d->m_dict[MZ_MIN(cur_pos, sizeof(d->m_dict) - 1)]); + else if ((d->m_greedy_parsing) || (d->m_flags & TDEFL_RLE_MATCHES) || (cur_match_len >= 128)) + { + tdefl_record_match(d, cur_match_len, cur_match_dist); + len_to_move = cur_match_len; + } + else + { + d->m_saved_lit = d->m_dict[MZ_MIN(cur_pos, sizeof(d->m_dict) - 1)]; + d->m_saved_match_dist = cur_match_dist; + d->m_saved_match_len = cur_match_len; + } + /* Move the lookahead forward by len_to_move bytes. */ + d->m_lookahead_pos += len_to_move; + MZ_ASSERT(d->m_lookahead_size >= len_to_move); + d->m_lookahead_size -= len_to_move; + d->m_dict_size = MZ_MIN(d->m_dict_size + len_to_move, (mz_uint)TDEFL_LZ_DICT_SIZE); + /* Check if it's time to flush the current LZ codes to the internal output buffer. */ + if ((d->m_pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) || + ((d->m_total_lz_bytes > 31 * 1024) && (((((mz_uint)(d->m_pLZ_code_buf - d->m_lz_code_buf) * 115) >> 7) >= d->m_total_lz_bytes) || (d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS)))) + { + int n; + d->m_pSrc = pSrc; + d->m_src_buf_left = src_buf_left; + if ((n = tdefl_flush_block(d, 0)) != 0) + return (n < 0) ? MZ_FALSE : MZ_TRUE; + } + } + + d->m_pSrc = pSrc; + d->m_src_buf_left = src_buf_left; + return MZ_TRUE; +} + +static tdefl_status tdefl_flush_output_buffer(tdefl_compressor *d) +{ + if (d->m_pIn_buf_size) + { + *d->m_pIn_buf_size = d->m_pSrc - (const mz_uint8 *)d->m_pIn_buf; + } + + if (d->m_pOut_buf_size) + { + size_t n = MZ_MIN(*d->m_pOut_buf_size - d->m_out_buf_ofs, d->m_output_flush_remaining); + memcpy((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs, d->m_output_buf + d->m_output_flush_ofs, n); + d->m_output_flush_ofs += (mz_uint)n; + d->m_output_flush_remaining -= (mz_uint)n; + d->m_out_buf_ofs += n; + + *d->m_pOut_buf_size = d->m_out_buf_ofs; + } + + return (d->m_finished && !d->m_output_flush_remaining) ? TDEFL_STATUS_DONE : TDEFL_STATUS_OKAY; +} + +tdefl_status tdefl_compress(tdefl_compressor *d, const void *pIn_buf, size_t *pIn_buf_size, void *pOut_buf, size_t *pOut_buf_size, tdefl_flush flush) +{ + if (!d) + { + if (pIn_buf_size) + *pIn_buf_size = 0; + if (pOut_buf_size) + *pOut_buf_size = 0; + return TDEFL_STATUS_BAD_PARAM; + } + + d->m_pIn_buf = pIn_buf; + d->m_pIn_buf_size = pIn_buf_size; + d->m_pOut_buf = pOut_buf; + d->m_pOut_buf_size = pOut_buf_size; + d->m_pSrc = (const mz_uint8 *)(pIn_buf); + d->m_src_buf_left = pIn_buf_size ? *pIn_buf_size : 0; + d->m_out_buf_ofs = 0; + d->m_flush = flush; + + if (((d->m_pPut_buf_func != NULL) == ((pOut_buf != NULL) || (pOut_buf_size != NULL))) || (d->m_prev_return_status != TDEFL_STATUS_OKAY) || + (d->m_wants_to_finish && (flush != TDEFL_FINISH)) || (pIn_buf_size && *pIn_buf_size && !pIn_buf) || (pOut_buf_size && *pOut_buf_size && !pOut_buf)) + { + if (pIn_buf_size) + *pIn_buf_size = 0; + if (pOut_buf_size) + *pOut_buf_size = 0; + return (d->m_prev_return_status = TDEFL_STATUS_BAD_PARAM); + } + d->m_wants_to_finish |= (flush == TDEFL_FINISH); + + if ((d->m_output_flush_remaining) || (d->m_finished)) + return (d->m_prev_return_status = tdefl_flush_output_buffer(d)); + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN + if (((d->m_flags & TDEFL_MAX_PROBES_MASK) == 1) && + ((d->m_flags & TDEFL_GREEDY_PARSING_FLAG) != 0) && + ((d->m_flags & (TDEFL_FILTER_MATCHES | TDEFL_FORCE_ALL_RAW_BLOCKS | TDEFL_RLE_MATCHES)) == 0)) + { + if (!tdefl_compress_fast(d)) + return d->m_prev_return_status; + } + else +#endif /* #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN */ + { + if (!tdefl_compress_normal(d)) + return d->m_prev_return_status; + } + + if ((d->m_flags & (TDEFL_WRITE_ZLIB_HEADER | TDEFL_COMPUTE_ADLER32)) && (pIn_buf)) + d->m_adler32 = (mz_uint32)mz_adler32(d->m_adler32, (const mz_uint8 *)pIn_buf, d->m_pSrc - (const mz_uint8 *)pIn_buf); + + if ((flush) && (!d->m_lookahead_size) && (!d->m_src_buf_left) && (!d->m_output_flush_remaining)) + { + if (tdefl_flush_block(d, flush) < 0) + return d->m_prev_return_status; + d->m_finished = (flush == TDEFL_FINISH); + if (flush == TDEFL_FULL_FLUSH) + { + MZ_CLEAR_ARR(d->m_hash); + MZ_CLEAR_ARR(d->m_next); + d->m_dict_size = 0; + } + } + + return (d->m_prev_return_status = tdefl_flush_output_buffer(d)); +} + +tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, size_t in_buf_size, tdefl_flush flush) +{ + MZ_ASSERT(d->m_pPut_buf_func); + return tdefl_compress(d, pIn_buf, &in_buf_size, NULL, NULL, flush); +} + +tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) +{ + d->m_pPut_buf_func = pPut_buf_func; + d->m_pPut_buf_user = pPut_buf_user; + d->m_flags = (mz_uint)(flags); + d->m_max_probes[0] = 1 + ((flags & 0xFFF) + 2) / 3; + d->m_greedy_parsing = (flags & TDEFL_GREEDY_PARSING_FLAG) != 0; + d->m_max_probes[1] = 1 + (((flags & 0xFFF) >> 2) + 2) / 3; + if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG)) + MZ_CLEAR_ARR(d->m_hash); + d->m_lookahead_pos = d->m_lookahead_size = d->m_dict_size = d->m_total_lz_bytes = d->m_lz_code_buf_dict_pos = d->m_bits_in = 0; + d->m_output_flush_ofs = d->m_output_flush_remaining = d->m_finished = d->m_block_index = d->m_bit_buffer = d->m_wants_to_finish = 0; + d->m_pLZ_code_buf = d->m_lz_code_buf + 1; + d->m_pLZ_flags = d->m_lz_code_buf; + *d->m_pLZ_flags = 0; + d->m_num_flags_left = 8; + d->m_pOutput_buf = d->m_output_buf; + d->m_pOutput_buf_end = d->m_output_buf; + d->m_prev_return_status = TDEFL_STATUS_OKAY; + d->m_saved_match_dist = d->m_saved_match_len = d->m_saved_lit = 0; + d->m_adler32 = 1; + d->m_pIn_buf = NULL; + d->m_pOut_buf = NULL; + d->m_pIn_buf_size = NULL; + d->m_pOut_buf_size = NULL; + d->m_flush = TDEFL_NO_FLUSH; + d->m_pSrc = NULL; + d->m_src_buf_left = 0; + d->m_out_buf_ofs = 0; + if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG)) + MZ_CLEAR_ARR(d->m_dict); + memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0); + memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1); + return TDEFL_STATUS_OKAY; +} + +tdefl_status tdefl_get_prev_return_status(tdefl_compressor *d) +{ + return d->m_prev_return_status; +} + +mz_uint32 tdefl_get_adler32(tdefl_compressor *d) +{ + return d->m_adler32; +} + +mz_bool tdefl_compress_mem_to_output(const void *pBuf, size_t buf_len, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) +{ + tdefl_compressor *pComp; + mz_bool succeeded; + if (((buf_len) && (!pBuf)) || (!pPut_buf_func)) + return MZ_FALSE; + pComp = (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor)); + if (!pComp) + return MZ_FALSE; + succeeded = (tdefl_init(pComp, pPut_buf_func, pPut_buf_user, flags) == TDEFL_STATUS_OKAY); + succeeded = succeeded && (tdefl_compress_buffer(pComp, pBuf, buf_len, TDEFL_FINISH) == TDEFL_STATUS_DONE); + MZ_FREE(pComp); + return succeeded; +} + +typedef struct +{ + size_t m_size, m_capacity; + mz_uint8 *m_pBuf; + mz_bool m_expandable; +} tdefl_output_buffer; + +static mz_bool tdefl_output_buffer_putter(const void *pBuf, int len, void *pUser) +{ + tdefl_output_buffer *p = (tdefl_output_buffer *)pUser; + size_t new_size = p->m_size + len; + if (new_size > p->m_capacity) + { + size_t new_capacity = p->m_capacity; + mz_uint8 *pNew_buf; + if (!p->m_expandable) + return MZ_FALSE; + do + { + new_capacity = MZ_MAX(128U, new_capacity << 1U); + } while (new_size > new_capacity); + pNew_buf = (mz_uint8 *)MZ_REALLOC(p->m_pBuf, new_capacity); + if (!pNew_buf) + return MZ_FALSE; + p->m_pBuf = pNew_buf; + p->m_capacity = new_capacity; + } + memcpy((mz_uint8 *)p->m_pBuf + p->m_size, pBuf, len); + p->m_size = new_size; + return MZ_TRUE; +} + +void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags) +{ + tdefl_output_buffer out_buf; + MZ_CLEAR_OBJ(out_buf); + if (!pOut_len) + return MZ_FALSE; + else + *pOut_len = 0; + out_buf.m_expandable = MZ_TRUE; + if (!tdefl_compress_mem_to_output(pSrc_buf, src_buf_len, tdefl_output_buffer_putter, &out_buf, flags)) + return NULL; + *pOut_len = out_buf.m_size; + return out_buf.m_pBuf; +} + +size_t tdefl_compress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags) +{ + tdefl_output_buffer out_buf; + MZ_CLEAR_OBJ(out_buf); + if (!pOut_buf) + return 0; + out_buf.m_pBuf = (mz_uint8 *)pOut_buf; + out_buf.m_capacity = out_buf_len; + if (!tdefl_compress_mem_to_output(pSrc_buf, src_buf_len, tdefl_output_buffer_putter, &out_buf, flags)) + return 0; + return out_buf.m_size; +} + +static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 }; + +/* level may actually range from [0,10] (10 is a "hidden" max level, where we want a bit more compression and it's fine if throughput to fall off a cliff on some files). */ +mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy) +{ + mz_uint comp_flags = s_tdefl_num_probes[(level >= 0) ? MZ_MIN(10, level) : MZ_DEFAULT_LEVEL] | ((level <= 3) ? TDEFL_GREEDY_PARSING_FLAG : 0); + if (window_bits > 0) + comp_flags |= TDEFL_WRITE_ZLIB_HEADER; + + if (!level) + comp_flags |= TDEFL_FORCE_ALL_RAW_BLOCKS; + else if (strategy == MZ_FILTERED) + comp_flags |= TDEFL_FILTER_MATCHES; + else if (strategy == MZ_HUFFMAN_ONLY) + comp_flags &= ~TDEFL_MAX_PROBES_MASK; + else if (strategy == MZ_FIXED) + comp_flags |= TDEFL_FORCE_ALL_STATIC_BLOCKS; + else if (strategy == MZ_RLE) + comp_flags |= TDEFL_RLE_MATCHES; + + return comp_flags; +} + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4204) /* nonstandard extension used : non-constant aggregate initializer (also supported by GNU C and C99, so no big deal) */ +#endif + +/* Simple PNG writer function by Alex Evans, 2011. Released into the public domain: https://gist.github.com/908299, more context at + http://altdevblogaday.org/2011/04/06/a-smaller-jpg-encoder/. + This is actually a modification of Alex's original code so PNG files generated by this function pass pngcheck. */ +void *tdefl_write_image_to_png_file_in_memory_ex(const void *pImage, int w, int h, int num_chans, size_t *pLen_out, mz_uint level, mz_bool flip) +{ + /* Using a local copy of this array here in case MINIZ_NO_ZLIB_APIS was defined. */ + static const mz_uint s_tdefl_png_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 }; + tdefl_compressor *pComp = (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor)); + tdefl_output_buffer out_buf; + int i, bpl = w * num_chans, y, z; + mz_uint32 c; + *pLen_out = 0; + if (!pComp) + return NULL; + MZ_CLEAR_OBJ(out_buf); + out_buf.m_expandable = MZ_TRUE; + out_buf.m_capacity = 57 + MZ_MAX(64, (1 + bpl) * h); + if (NULL == (out_buf.m_pBuf = (mz_uint8 *)MZ_MALLOC(out_buf.m_capacity))) + { + MZ_FREE(pComp); + return NULL; + } + /* write dummy header */ + for (z = 41; z; --z) + tdefl_output_buffer_putter(&z, 1, &out_buf); + /* compress image data */ + tdefl_init(pComp, tdefl_output_buffer_putter, &out_buf, s_tdefl_png_num_probes[MZ_MIN(10, level)] | TDEFL_WRITE_ZLIB_HEADER); + for (y = 0; y < h; ++y) + { + tdefl_compress_buffer(pComp, &z, 1, TDEFL_NO_FLUSH); + tdefl_compress_buffer(pComp, (mz_uint8 *)pImage + (flip ? (h - 1 - y) : y) * bpl, bpl, TDEFL_NO_FLUSH); + } + if (tdefl_compress_buffer(pComp, NULL, 0, TDEFL_FINISH) != TDEFL_STATUS_DONE) + { + MZ_FREE(pComp); + MZ_FREE(out_buf.m_pBuf); + return NULL; + } + /* write real header */ + *pLen_out = out_buf.m_size - 41; + { + static const mz_uint8 chans[] = { 0x00, 0x00, 0x04, 0x02, 0x06 }; + mz_uint8 pnghdr[41] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, + 0x0a, 0x1a, 0x0a, 0x00, 0x00, + 0x00, 0x0d, 0x49, 0x48, 0x44, + 0x52, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x49, 0x44, 0x41, + 0x54 }; + pnghdr[18] = (mz_uint8)(w >> 8); + pnghdr[19] = (mz_uint8)w; + pnghdr[22] = (mz_uint8)(h >> 8); + pnghdr[23] = (mz_uint8)h; + pnghdr[25] = chans[num_chans]; + pnghdr[33] = (mz_uint8)(*pLen_out >> 24); + pnghdr[34] = (mz_uint8)(*pLen_out >> 16); + pnghdr[35] = (mz_uint8)(*pLen_out >> 8); + pnghdr[36] = (mz_uint8)*pLen_out; + c = (mz_uint32)mz_crc32(MZ_CRC32_INIT, pnghdr + 12, 17); + for (i = 0; i < 4; ++i, c <<= 8) + ((mz_uint8 *)(pnghdr + 29))[i] = (mz_uint8)(c >> 24); + memcpy(out_buf.m_pBuf, pnghdr, 41); + } + /* write footer (IDAT CRC-32, followed by IEND chunk) */ + if (!tdefl_output_buffer_putter("\0\0\0\0\0\0\0\0\x49\x45\x4e\x44\xae\x42\x60\x82", 16, &out_buf)) + { + *pLen_out = 0; + MZ_FREE(pComp); + MZ_FREE(out_buf.m_pBuf); + return NULL; + } + c = (mz_uint32)mz_crc32(MZ_CRC32_INIT, out_buf.m_pBuf + 41 - 4, *pLen_out + 4); + for (i = 0; i < 4; ++i, c <<= 8) + (out_buf.m_pBuf + out_buf.m_size - 16)[i] = (mz_uint8)(c >> 24); + /* compute final size of file, grab compressed data buffer and return */ + *pLen_out += 57; + MZ_FREE(pComp); + return out_buf.m_pBuf; +} +void *tdefl_write_image_to_png_file_in_memory(const void *pImage, int w, int h, int num_chans, size_t *pLen_out) +{ + /* Level 6 corresponds to TDEFL_DEFAULT_MAX_PROBES or MZ_DEFAULT_LEVEL (but we can't depend on MZ_DEFAULT_LEVEL being available in case the zlib API's where #defined out) */ + return tdefl_write_image_to_png_file_in_memory_ex(pImage, w, h, num_chans, pLen_out, 6, MZ_FALSE); +} + +#ifndef MINIZ_NO_MALLOC +/* Allocate the tdefl_compressor and tinfl_decompressor structures in C so that */ +/* non-C language bindings to tdefL_ and tinfl_ API don't need to worry about */ +/* structure size and allocation mechanism. */ +tdefl_compressor *tdefl_compressor_alloc(void) +{ + return (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor)); +} + +void tdefl_compressor_free(tdefl_compressor *pComp) +{ + MZ_FREE(pComp); +} +#endif + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + /************************************************************************** + * + * Copyright 2013-2014 RAD Game Tools and Valve Software + * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + * 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 + * 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. + * + **************************************************************************/ + + + +#ifndef MINIZ_NO_INFLATE_APIS + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------- Low-level Decompression (completely independent from all compression API's) */ + +#define TINFL_MEMCPY(d, s, l) memcpy(d, s, l) +#define TINFL_MEMSET(p, c, l) memset(p, c, l) + +#define TINFL_CR_BEGIN \ + switch (r->m_state) \ + { \ + case 0: +#define TINFL_CR_RETURN(state_index, result) \ + do \ + { \ + status = result; \ + r->m_state = state_index; \ + goto common_exit; \ + case state_index:; \ + } \ + MZ_MACRO_END +#define TINFL_CR_RETURN_FOREVER(state_index, result) \ + do \ + { \ + for (;;) \ + { \ + TINFL_CR_RETURN(state_index, result); \ + } \ + } \ + MZ_MACRO_END +#define TINFL_CR_FINISH } + +#define TINFL_GET_BYTE(state_index, c) \ + do \ + { \ + while (pIn_buf_cur >= pIn_buf_end) \ + { \ + TINFL_CR_RETURN(state_index, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS); \ + } \ + c = *pIn_buf_cur++; \ + } \ + MZ_MACRO_END + +#define TINFL_NEED_BITS(state_index, n) \ + do \ + { \ + mz_uint c; \ + TINFL_GET_BYTE(state_index, c); \ + bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \ + num_bits += 8; \ + } while (num_bits < (mz_uint)(n)) +#define TINFL_SKIP_BITS(state_index, n) \ + do \ + { \ + if (num_bits < (mz_uint)(n)) \ + { \ + TINFL_NEED_BITS(state_index, n); \ + } \ + bit_buf >>= (n); \ + num_bits -= (n); \ + } \ + MZ_MACRO_END +#define TINFL_GET_BITS(state_index, b, n) \ + do \ + { \ + if (num_bits < (mz_uint)(n)) \ + { \ + TINFL_NEED_BITS(state_index, n); \ + } \ + b = bit_buf & ((1 << (n)) - 1); \ + bit_buf >>= (n); \ + num_bits -= (n); \ + } \ + MZ_MACRO_END + +/* TINFL_HUFF_BITBUF_FILL() is only used rarely, when the number of bytes remaining in the input buffer falls below 2. */ +/* It reads just enough bytes from the input stream that are needed to decode the next Huffman code (and absolutely no more). It works by trying to fully decode a */ +/* Huffman code by using whatever bits are currently present in the bit buffer. If this fails, it reads another byte, and tries again until it succeeds or until the */ +/* bit buffer contains >=15 bits (deflate's max. Huffman code size). */ +#define TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree) \ + do \ + { \ + temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]; \ + if (temp >= 0) \ + { \ + code_len = temp >> 9; \ + if ((code_len) && (num_bits >= code_len)) \ + break; \ + } \ + else if (num_bits > TINFL_FAST_LOOKUP_BITS) \ + { \ + code_len = TINFL_FAST_LOOKUP_BITS; \ + do \ + { \ + temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \ + } while ((temp < 0) && (num_bits >= (code_len + 1))); \ + if (temp >= 0) \ + break; \ + } \ + TINFL_GET_BYTE(state_index, c); \ + bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \ + num_bits += 8; \ + } while (num_bits < 15); + +/* TINFL_HUFF_DECODE() decodes the next Huffman coded symbol. It's more complex than you would initially expect because the zlib API expects the decompressor to never read */ +/* beyond the final byte of the deflate stream. (In other words, when this macro wants to read another byte from the input, it REALLY needs another byte in order to fully */ +/* decode the next Huffman code.) Handling this properly is particularly important on raw deflate (non-zlib) streams, which aren't followed by a byte aligned adler-32. */ +/* The slow path is only executed at the very end of the input buffer. */ +/* v1.16: The original macro handled the case at the very end of the passed-in input buffer, but we also need to handle the case where the user passes in 1+zillion bytes */ +/* following the deflate data and our non-conservative read-ahead path won't kick in here on this code. This is much trickier. */ +#define TINFL_HUFF_DECODE(state_index, sym, pLookUp, pTree) \ + do \ + { \ + int temp; \ + mz_uint code_len, c; \ + if (num_bits < 15) \ + { \ + if ((pIn_buf_end - pIn_buf_cur) < 2) \ + { \ + TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree); \ + } \ + else \ + { \ + bit_buf |= (((tinfl_bit_buf_t)pIn_buf_cur[0]) << num_bits) | (((tinfl_bit_buf_t)pIn_buf_cur[1]) << (num_bits + 8)); \ + pIn_buf_cur += 2; \ + num_bits += 16; \ + } \ + } \ + if ((temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) \ + code_len = temp >> 9, temp &= 511; \ + else \ + { \ + code_len = TINFL_FAST_LOOKUP_BITS; \ + do \ + { \ + temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \ + } while (temp < 0); \ + } \ + sym = temp; \ + bit_buf >>= code_len; \ + num_bits -= code_len; \ + } \ + MZ_MACRO_END + +static void tinfl_clear_tree(tinfl_decompressor *r) +{ + if (r->m_type == 0) + MZ_CLEAR_ARR(r->m_tree_0); + else if (r->m_type == 1) + MZ_CLEAR_ARR(r->m_tree_1); + else + MZ_CLEAR_ARR(r->m_tree_2); +} + +tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags) +{ + static const mz_uint16 s_length_base[31] = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 }; + static const mz_uint8 s_length_extra[31] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0 }; + static const mz_uint16 s_dist_base[32] = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 0, 0 }; + static const mz_uint8 s_dist_extra[32] = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }; + static const mz_uint8 s_length_dezigzag[19] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + static const mz_uint16 s_min_table_sizes[3] = { 257, 1, 4 }; + + mz_int16 *pTrees[3]; + mz_uint8 *pCode_sizes[3]; + + tinfl_status status = TINFL_STATUS_FAILED; + mz_uint32 num_bits, dist, counter, num_extra; + tinfl_bit_buf_t bit_buf; + const mz_uint8 *pIn_buf_cur = pIn_buf_next, *const pIn_buf_end = pIn_buf_next + *pIn_buf_size; + mz_uint8 *pOut_buf_cur = pOut_buf_next, *const pOut_buf_end = pOut_buf_next ? pOut_buf_next + *pOut_buf_size : NULL; + size_t out_buf_size_mask = (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF) ? (size_t)-1 : ((pOut_buf_next - pOut_buf_start) + *pOut_buf_size) - 1, dist_from_out_buf_start; + + /* Ensure the output buffer's size is a power of 2, unless the output buffer is large enough to hold the entire output file (in which case it doesn't matter). */ + if (((out_buf_size_mask + 1) & out_buf_size_mask) || (pOut_buf_next < pOut_buf_start)) + { + *pIn_buf_size = *pOut_buf_size = 0; + return TINFL_STATUS_BAD_PARAM; + } + + pTrees[0] = r->m_tree_0; + pTrees[1] = r->m_tree_1; + pTrees[2] = r->m_tree_2; + pCode_sizes[0] = r->m_code_size_0; + pCode_sizes[1] = r->m_code_size_1; + pCode_sizes[2] = r->m_code_size_2; + + num_bits = r->m_num_bits; + bit_buf = r->m_bit_buf; + dist = r->m_dist; + counter = r->m_counter; + num_extra = r->m_num_extra; + dist_from_out_buf_start = r->m_dist_from_out_buf_start; + TINFL_CR_BEGIN + + bit_buf = num_bits = dist = counter = num_extra = r->m_zhdr0 = r->m_zhdr1 = 0; + r->m_z_adler32 = r->m_check_adler32 = 1; + if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) + { + TINFL_GET_BYTE(1, r->m_zhdr0); + TINFL_GET_BYTE(2, r->m_zhdr1); + counter = (((r->m_zhdr0 * 256 + r->m_zhdr1) % 31 != 0) || (r->m_zhdr1 & 32) || ((r->m_zhdr0 & 15) != 8)); + if (!(decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)) + counter |= (((1U << (8U + (r->m_zhdr0 >> 4))) > 32768U) || ((out_buf_size_mask + 1) < (size_t)((size_t)1 << (8U + (r->m_zhdr0 >> 4))))); + if (counter) + { + TINFL_CR_RETURN_FOREVER(36, TINFL_STATUS_FAILED); + } + } + + do + { + TINFL_GET_BITS(3, r->m_final, 3); + r->m_type = r->m_final >> 1; + if (r->m_type == 0) + { + TINFL_SKIP_BITS(5, num_bits & 7); + for (counter = 0; counter < 4; ++counter) + { + if (num_bits) + TINFL_GET_BITS(6, r->m_raw_header[counter], 8); + else + TINFL_GET_BYTE(7, r->m_raw_header[counter]); + } + if ((counter = (r->m_raw_header[0] | (r->m_raw_header[1] << 8))) != (mz_uint)(0xFFFF ^ (r->m_raw_header[2] | (r->m_raw_header[3] << 8)))) + { + TINFL_CR_RETURN_FOREVER(39, TINFL_STATUS_FAILED); + } + while ((counter) && (num_bits)) + { + TINFL_GET_BITS(51, dist, 8); + while (pOut_buf_cur >= pOut_buf_end) + { + TINFL_CR_RETURN(52, TINFL_STATUS_HAS_MORE_OUTPUT); + } + *pOut_buf_cur++ = (mz_uint8)dist; + counter--; + } + while (counter) + { + size_t n; + while (pOut_buf_cur >= pOut_buf_end) + { + TINFL_CR_RETURN(9, TINFL_STATUS_HAS_MORE_OUTPUT); + } + while (pIn_buf_cur >= pIn_buf_end) + { + TINFL_CR_RETURN(38, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS); + } + n = MZ_MIN(MZ_MIN((size_t)(pOut_buf_end - pOut_buf_cur), (size_t)(pIn_buf_end - pIn_buf_cur)), counter); + TINFL_MEMCPY(pOut_buf_cur, pIn_buf_cur, n); + pIn_buf_cur += n; + pOut_buf_cur += n; + counter -= (mz_uint)n; + } + } + else if (r->m_type == 3) + { + TINFL_CR_RETURN_FOREVER(10, TINFL_STATUS_FAILED); + } + else + { + if (r->m_type == 1) + { + mz_uint8 *p = r->m_code_size_0; + mz_uint i; + r->m_table_sizes[0] = 288; + r->m_table_sizes[1] = 32; + TINFL_MEMSET(r->m_code_size_1, 5, 32); + for (i = 0; i <= 143; ++i) + *p++ = 8; + for (; i <= 255; ++i) + *p++ = 9; + for (; i <= 279; ++i) + *p++ = 7; + for (; i <= 287; ++i) + *p++ = 8; + } + else + { + for (counter = 0; counter < 3; counter++) + { + TINFL_GET_BITS(11, r->m_table_sizes[counter], "\05\05\04"[counter]); + r->m_table_sizes[counter] += s_min_table_sizes[counter]; + } + MZ_CLEAR_ARR(r->m_code_size_2); + for (counter = 0; counter < r->m_table_sizes[2]; counter++) + { + mz_uint s; + TINFL_GET_BITS(14, s, 3); + r->m_code_size_2[s_length_dezigzag[counter]] = (mz_uint8)s; + } + r->m_table_sizes[2] = 19; + } + for (; (int)r->m_type >= 0; r->m_type--) + { + int tree_next, tree_cur; + mz_int16 *pLookUp; + mz_int16 *pTree; + mz_uint8 *pCode_size; + mz_uint i, j, used_syms, total, sym_index, next_code[17], total_syms[16]; + pLookUp = r->m_look_up[r->m_type]; + pTree = pTrees[r->m_type]; + pCode_size = pCode_sizes[r->m_type]; + MZ_CLEAR_ARR(total_syms); + TINFL_MEMSET(pLookUp, 0, sizeof(r->m_look_up[0])); + tinfl_clear_tree(r); + for (i = 0; i < r->m_table_sizes[r->m_type]; ++i) + total_syms[pCode_size[i]]++; + used_syms = 0, total = 0; + next_code[0] = next_code[1] = 0; + for (i = 1; i <= 15; ++i) + { + used_syms += total_syms[i]; + next_code[i + 1] = (total = ((total + total_syms[i]) << 1)); + } + if ((65536 != total) && (used_syms > 1)) + { + TINFL_CR_RETURN_FOREVER(35, TINFL_STATUS_FAILED); + } + for (tree_next = -1, sym_index = 0; sym_index < r->m_table_sizes[r->m_type]; ++sym_index) + { + mz_uint rev_code = 0, l, cur_code, code_size = pCode_size[sym_index]; + if (!code_size) + continue; + cur_code = next_code[code_size]++; + for (l = code_size; l > 0; l--, cur_code >>= 1) + rev_code = (rev_code << 1) | (cur_code & 1); + if (code_size <= TINFL_FAST_LOOKUP_BITS) + { + mz_int16 k = (mz_int16)((code_size << 9) | sym_index); + while (rev_code < TINFL_FAST_LOOKUP_SIZE) + { + pLookUp[rev_code] = k; + rev_code += (1 << code_size); + } + continue; + } + if (0 == (tree_cur = pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)])) + { + pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)] = (mz_int16)tree_next; + tree_cur = tree_next; + tree_next -= 2; + } + rev_code >>= (TINFL_FAST_LOOKUP_BITS - 1); + for (j = code_size; j > (TINFL_FAST_LOOKUP_BITS + 1); j--) + { + tree_cur -= ((rev_code >>= 1) & 1); + if (!pTree[-tree_cur - 1]) + { + pTree[-tree_cur - 1] = (mz_int16)tree_next; + tree_cur = tree_next; + tree_next -= 2; + } + else + tree_cur = pTree[-tree_cur - 1]; + } + tree_cur -= ((rev_code >>= 1) & 1); + pTree[-tree_cur - 1] = (mz_int16)sym_index; + } + if (r->m_type == 2) + { + for (counter = 0; counter < (r->m_table_sizes[0] + r->m_table_sizes[1]);) + { + mz_uint s; + TINFL_HUFF_DECODE(16, dist, r->m_look_up[2], r->m_tree_2); + if (dist < 16) + { + r->m_len_codes[counter++] = (mz_uint8)dist; + continue; + } + if ((dist == 16) && (!counter)) + { + TINFL_CR_RETURN_FOREVER(17, TINFL_STATUS_FAILED); + } + num_extra = "\02\03\07"[dist - 16]; + TINFL_GET_BITS(18, s, num_extra); + s += "\03\03\013"[dist - 16]; + TINFL_MEMSET(r->m_len_codes + counter, (dist == 16) ? r->m_len_codes[counter - 1] : 0, s); + counter += s; + } + if ((r->m_table_sizes[0] + r->m_table_sizes[1]) != counter) + { + TINFL_CR_RETURN_FOREVER(21, TINFL_STATUS_FAILED); + } + TINFL_MEMCPY(r->m_code_size_0, r->m_len_codes, r->m_table_sizes[0]); + TINFL_MEMCPY(r->m_code_size_1, r->m_len_codes + r->m_table_sizes[0], r->m_table_sizes[1]); + } + } + for (;;) + { + mz_uint8 *pSrc; + for (;;) + { + if (((pIn_buf_end - pIn_buf_cur) < 4) || ((pOut_buf_end - pOut_buf_cur) < 2)) + { + TINFL_HUFF_DECODE(23, counter, r->m_look_up[0], r->m_tree_0); + if (counter >= 256) + break; + while (pOut_buf_cur >= pOut_buf_end) + { + TINFL_CR_RETURN(24, TINFL_STATUS_HAS_MORE_OUTPUT); + } + *pOut_buf_cur++ = (mz_uint8)counter; + } + else + { + int sym2; + mz_uint code_len; +#if TINFL_USE_64BIT_BITBUF + if (num_bits < 30) + { + bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE32(pIn_buf_cur)) << num_bits); + pIn_buf_cur += 4; + num_bits += 32; + } +#else + if (num_bits < 15) + { + bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits); + pIn_buf_cur += 2; + num_bits += 16; + } +#endif + if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) + code_len = sym2 >> 9; + else + { + code_len = TINFL_FAST_LOOKUP_BITS; + do + { + sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)]; + } while (sym2 < 0); + } + counter = sym2; + bit_buf >>= code_len; + num_bits -= code_len; + if (counter & 256) + break; + +#if !TINFL_USE_64BIT_BITBUF + if (num_bits < 15) + { + bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits); + pIn_buf_cur += 2; + num_bits += 16; + } +#endif + if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) + code_len = sym2 >> 9; + else + { + code_len = TINFL_FAST_LOOKUP_BITS; + do + { + sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)]; + } while (sym2 < 0); + } + bit_buf >>= code_len; + num_bits -= code_len; + + pOut_buf_cur[0] = (mz_uint8)counter; + if (sym2 & 256) + { + pOut_buf_cur++; + counter = sym2; + break; + } + pOut_buf_cur[1] = (mz_uint8)sym2; + pOut_buf_cur += 2; + } + } + if ((counter &= 511) == 256) + break; + + num_extra = s_length_extra[counter - 257]; + counter = s_length_base[counter - 257]; + if (num_extra) + { + mz_uint extra_bits; + TINFL_GET_BITS(25, extra_bits, num_extra); + counter += extra_bits; + } + + TINFL_HUFF_DECODE(26, dist, r->m_look_up[1], r->m_tree_1); + num_extra = s_dist_extra[dist]; + dist = s_dist_base[dist]; + if (num_extra) + { + mz_uint extra_bits; + TINFL_GET_BITS(27, extra_bits, num_extra); + dist += extra_bits; + } + + dist_from_out_buf_start = pOut_buf_cur - pOut_buf_start; + if ((dist == 0 || dist > dist_from_out_buf_start || dist_from_out_buf_start == 0) && (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)) + { + TINFL_CR_RETURN_FOREVER(37, TINFL_STATUS_FAILED); + } + + pSrc = pOut_buf_start + ((dist_from_out_buf_start - dist) & out_buf_size_mask); + + if ((MZ_MAX(pOut_buf_cur, pSrc) + counter) > pOut_buf_end) + { + while (counter--) + { + while (pOut_buf_cur >= pOut_buf_end) + { + TINFL_CR_RETURN(53, TINFL_STATUS_HAS_MORE_OUTPUT); + } + *pOut_buf_cur++ = pOut_buf_start[(dist_from_out_buf_start++ - dist) & out_buf_size_mask]; + } + continue; + } +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES + else if ((counter >= 9) && (counter <= dist)) + { + const mz_uint8 *pSrc_end = pSrc + (counter & ~7); + do + { +#ifdef MINIZ_UNALIGNED_USE_MEMCPY + memcpy(pOut_buf_cur, pSrc, sizeof(mz_uint32)*2); +#else + ((mz_uint32 *)pOut_buf_cur)[0] = ((const mz_uint32 *)pSrc)[0]; + ((mz_uint32 *)pOut_buf_cur)[1] = ((const mz_uint32 *)pSrc)[1]; +#endif + pOut_buf_cur += 8; + } while ((pSrc += 8) < pSrc_end); + if ((counter &= 7) < 3) + { + if (counter) + { + pOut_buf_cur[0] = pSrc[0]; + if (counter > 1) + pOut_buf_cur[1] = pSrc[1]; + pOut_buf_cur += counter; + } + continue; + } + } +#endif + while(counter>2) + { + pOut_buf_cur[0] = pSrc[0]; + pOut_buf_cur[1] = pSrc[1]; + pOut_buf_cur[2] = pSrc[2]; + pOut_buf_cur += 3; + pSrc += 3; + counter -= 3; + } + if (counter > 0) + { + pOut_buf_cur[0] = pSrc[0]; + if (counter > 1) + pOut_buf_cur[1] = pSrc[1]; + pOut_buf_cur += counter; + } + } + } + } while (!(r->m_final & 1)); + + /* Ensure byte alignment and put back any bytes from the bitbuf if we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */ + /* I'm being super conservative here. A number of simplifications can be made to the byte alignment part, and the Adler32 check shouldn't ever need to worry about reading from the bitbuf now. */ + TINFL_SKIP_BITS(32, num_bits & 7); + while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8)) + { + --pIn_buf_cur; + num_bits -= 8; + } + bit_buf &= ~(~(tinfl_bit_buf_t)0 << num_bits); + MZ_ASSERT(!num_bits); /* if this assert fires then we've read beyond the end of non-deflate/zlib streams with following data (such as gzip streams). */ + + if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) + { + for (counter = 0; counter < 4; ++counter) + { + mz_uint s; + if (num_bits) + TINFL_GET_BITS(41, s, 8); + else + TINFL_GET_BYTE(42, s); + r->m_z_adler32 = (r->m_z_adler32 << 8) | s; + } + } + TINFL_CR_RETURN_FOREVER(34, TINFL_STATUS_DONE); + + TINFL_CR_FINISH + +common_exit: + /* As long as we aren't telling the caller that we NEED more input to make forward progress: */ + /* Put back any bytes from the bitbuf in case we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */ + /* We need to be very careful here to NOT push back any bytes we definitely know we need to make forward progress, though, or we'll lock the caller up into an inf loop. */ + if ((status != TINFL_STATUS_NEEDS_MORE_INPUT) && (status != TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS)) + { + while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8)) + { + --pIn_buf_cur; + num_bits -= 8; + } + } + r->m_num_bits = num_bits; + r->m_bit_buf = bit_buf & ~(~(tinfl_bit_buf_t)0 << num_bits); + r->m_dist = dist; + r->m_counter = counter; + r->m_num_extra = num_extra; + r->m_dist_from_out_buf_start = dist_from_out_buf_start; + *pIn_buf_size = pIn_buf_cur - pIn_buf_next; + *pOut_buf_size = pOut_buf_cur - pOut_buf_next; + if ((decomp_flags & (TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_COMPUTE_ADLER32)) && (status >= 0)) + { + const mz_uint8 *ptr = pOut_buf_next; + size_t buf_len = *pOut_buf_size; + mz_uint32 i, s1 = r->m_check_adler32 & 0xffff, s2 = r->m_check_adler32 >> 16; + size_t block_len = buf_len % 5552; + while (buf_len) + { + for (i = 0; i + 7 < block_len; i += 8, ptr += 8) + { + s1 += ptr[0], s2 += s1; + s1 += ptr[1], s2 += s1; + s1 += ptr[2], s2 += s1; + s1 += ptr[3], s2 += s1; + s1 += ptr[4], s2 += s1; + s1 += ptr[5], s2 += s1; + s1 += ptr[6], s2 += s1; + s1 += ptr[7], s2 += s1; + } + for (; i < block_len; ++i) + s1 += *ptr++, s2 += s1; + s1 %= 65521U, s2 %= 65521U; + buf_len -= block_len; + block_len = 5552; + } + r->m_check_adler32 = (s2 << 16) + s1; + if ((status == TINFL_STATUS_DONE) && (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) && (r->m_check_adler32 != r->m_z_adler32)) + status = TINFL_STATUS_ADLER32_MISMATCH; + } + return status; +} + +/* Higher level helper functions. */ +void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags) +{ + tinfl_decompressor decomp; + void *pBuf = NULL, *pNew_buf; + size_t src_buf_ofs = 0, out_buf_capacity = 0; + *pOut_len = 0; + tinfl_init(&decomp); + for (;;) + { + size_t src_buf_size = src_buf_len - src_buf_ofs, dst_buf_size = out_buf_capacity - *pOut_len, new_out_buf_capacity; + tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf + src_buf_ofs, &src_buf_size, (mz_uint8 *)pBuf, pBuf ? (mz_uint8 *)pBuf + *pOut_len : NULL, &dst_buf_size, + (flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF); + if ((status < 0) || (status == TINFL_STATUS_NEEDS_MORE_INPUT)) + { + MZ_FREE(pBuf); + *pOut_len = 0; + return NULL; + } + src_buf_ofs += src_buf_size; + *pOut_len += dst_buf_size; + if (status == TINFL_STATUS_DONE) + break; + new_out_buf_capacity = out_buf_capacity * 2; + if (new_out_buf_capacity < 128) + new_out_buf_capacity = 128; + pNew_buf = MZ_REALLOC(pBuf, new_out_buf_capacity); + if (!pNew_buf) + { + MZ_FREE(pBuf); + *pOut_len = 0; + return NULL; + } + pBuf = pNew_buf; + out_buf_capacity = new_out_buf_capacity; + } + return pBuf; +} + +size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags) +{ + tinfl_decompressor decomp; + tinfl_status status; + tinfl_init(&decomp); + status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf, &src_buf_len, (mz_uint8 *)pOut_buf, (mz_uint8 *)pOut_buf, &out_buf_len, (flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF); + return (status != TINFL_STATUS_DONE) ? TINFL_DECOMPRESS_MEM_TO_MEM_FAILED : out_buf_len; +} + +int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) +{ + int result = 0; + tinfl_decompressor decomp; + mz_uint8 *pDict = (mz_uint8 *)MZ_MALLOC(TINFL_LZ_DICT_SIZE); + size_t in_buf_ofs = 0, dict_ofs = 0; + if (!pDict) + return TINFL_STATUS_FAILED; + memset(pDict,0,TINFL_LZ_DICT_SIZE); + tinfl_init(&decomp); + for (;;) + { + size_t in_buf_size = *pIn_buf_size - in_buf_ofs, dst_buf_size = TINFL_LZ_DICT_SIZE - dict_ofs; + tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pIn_buf + in_buf_ofs, &in_buf_size, pDict, pDict + dict_ofs, &dst_buf_size, + (flags & ~(TINFL_FLAG_HAS_MORE_INPUT | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF))); + in_buf_ofs += in_buf_size; + if ((dst_buf_size) && (!(*pPut_buf_func)(pDict + dict_ofs, (int)dst_buf_size, pPut_buf_user))) + break; + if (status != TINFL_STATUS_HAS_MORE_OUTPUT) + { + result = (status == TINFL_STATUS_DONE); + break; + } + dict_ofs = (dict_ofs + dst_buf_size) & (TINFL_LZ_DICT_SIZE - 1); + } + MZ_FREE(pDict); + *pIn_buf_size = in_buf_ofs; + return result; +} + +#ifndef MINIZ_NO_MALLOC +tinfl_decompressor *tinfl_decompressor_alloc(void) +{ + tinfl_decompressor *pDecomp = (tinfl_decompressor *)MZ_MALLOC(sizeof(tinfl_decompressor)); + if (pDecomp) + tinfl_init(pDecomp); + return pDecomp; +} + +void tinfl_decompressor_free(tinfl_decompressor *pDecomp) +{ + MZ_FREE(pDecomp); +} +#endif + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + /************************************************************************** + * + * Copyright 2013-2014 RAD Game Tools and Valve Software + * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + * Copyright 2016 Martin Raiber + * 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 + * 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. + * + **************************************************************************/ + + +#ifndef MINIZ_NO_ARCHIVE_APIS + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------- .ZIP archive reading */ + +#ifdef MINIZ_NO_STDIO +#define MZ_FILE void * +#else +#include + +#if defined(_MSC_VER) || defined(__MINGW64__) + +#define WIN32_LEAN_AND_MEAN +#include + +static WCHAR* mz_utf8z_to_widechar(const char* str) +{ + int reqChars = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + WCHAR* wStr = (WCHAR*)malloc(reqChars * sizeof(WCHAR)); + MultiByteToWideChar(CP_UTF8, 0, str, -1, wStr, reqChars); + return wStr; +} + +static FILE *mz_fopen(const char *pFilename, const char *pMode) +{ + WCHAR* wFilename = mz_utf8z_to_widechar(pFilename); + WCHAR* wMode = mz_utf8z_to_widechar(pMode); + FILE* pFile = NULL; + errno_t err = _wfopen_s(&pFile, wFilename, wMode); + free(wFilename); + free(wMode); + return err ? NULL : pFile; +} + +static FILE *mz_freopen(const char *pPath, const char *pMode, FILE *pStream) +{ + WCHAR* wPath = mz_utf8z_to_widechar(pPath); + WCHAR* wMode = mz_utf8z_to_widechar(pMode); + FILE* pFile = NULL; + errno_t err = _wfreopen_s(&pFile, wPath, wMode, pStream); + free(wPath); + free(wMode); + return err ? NULL : pFile; +} + +static int mz_stat64(const char *path, struct __stat64 *buffer) +{ + WCHAR* wPath = mz_utf8z_to_widechar(path); + int res = _wstat64(wPath, buffer); + free(wPath); + return res; +} + +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN mz_fopen +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 _ftelli64 +#define MZ_FSEEK64 _fseeki64 +#define MZ_FILE_STAT_STRUCT _stat64 +#define MZ_FILE_STAT mz_stat64 +#define MZ_FFLUSH fflush +#define MZ_FREOPEN mz_freopen +#define MZ_DELETE_FILE remove + +#elif defined(__MINGW32__) || defined(__WATCOMC__) +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 _ftelli64 +#define MZ_FSEEK64 _fseeki64 +#define MZ_FILE_STAT_STRUCT stat +#define MZ_FILE_STAT stat +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(f, m, s) freopen(f, m, s) +#define MZ_DELETE_FILE remove + +#elif defined(__TINYC__) +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 ftell +#define MZ_FSEEK64 fseek +#define MZ_FILE_STAT_STRUCT stat +#define MZ_FILE_STAT stat +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(f, m, s) freopen(f, m, s) +#define MZ_DELETE_FILE remove + +#elif defined(__USE_LARGEFILE64) /* gcc, clang */ +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen64(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 ftello64 +#define MZ_FSEEK64 fseeko64 +#define MZ_FILE_STAT_STRUCT stat64 +#define MZ_FILE_STAT stat64 +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(p, m, s) freopen64(p, m, s) +#define MZ_DELETE_FILE remove + +#elif defined(__APPLE__) || defined(__FreeBSD__) +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 ftello +#define MZ_FSEEK64 fseeko +#define MZ_FILE_STAT_STRUCT stat +#define MZ_FILE_STAT stat +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(p, m, s) freopen(p, m, s) +#define MZ_DELETE_FILE remove + +#else +#pragma message("Using fopen, ftello, fseeko, stat() etc. path for file I/O - this path may not support large files.") +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#ifdef __STRICT_ANSI__ +#define MZ_FTELL64 ftell +#define MZ_FSEEK64 fseek +#else +#define MZ_FTELL64 ftello +#define MZ_FSEEK64 fseeko +#endif +#define MZ_FILE_STAT_STRUCT stat +#define MZ_FILE_STAT stat +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(f, m, s) freopen(f, m, s) +#define MZ_DELETE_FILE remove +#endif /* #ifdef _MSC_VER */ +#endif /* #ifdef MINIZ_NO_STDIO */ + +#define MZ_TOLOWER(c) ((((c) >= 'A') && ((c) <= 'Z')) ? ((c) - 'A' + 'a') : (c)) + +/* Various ZIP archive enums. To completely avoid cross platform compiler alignment and platform endian issues, miniz.c doesn't use structs for any of this stuff. */ +enum +{ + /* ZIP archive identifiers and record sizes */ + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG = 0x06054b50, + MZ_ZIP_CENTRAL_DIR_HEADER_SIG = 0x02014b50, + MZ_ZIP_LOCAL_DIR_HEADER_SIG = 0x04034b50, + MZ_ZIP_LOCAL_DIR_HEADER_SIZE = 30, + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE = 46, + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE = 22, + + /* ZIP64 archive identifier and record sizes */ + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG = 0x06064b50, + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG = 0x07064b50, + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE = 56, + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE = 20, + MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID = 0x0001, + MZ_ZIP_DATA_DESCRIPTOR_ID = 0x08074b50, + MZ_ZIP_DATA_DESCRIPTER_SIZE64 = 24, + MZ_ZIP_DATA_DESCRIPTER_SIZE32 = 16, + + /* Central directory header record offsets */ + MZ_ZIP_CDH_SIG_OFS = 0, + MZ_ZIP_CDH_VERSION_MADE_BY_OFS = 4, + MZ_ZIP_CDH_VERSION_NEEDED_OFS = 6, + MZ_ZIP_CDH_BIT_FLAG_OFS = 8, + MZ_ZIP_CDH_METHOD_OFS = 10, + MZ_ZIP_CDH_FILE_TIME_OFS = 12, + MZ_ZIP_CDH_FILE_DATE_OFS = 14, + MZ_ZIP_CDH_CRC32_OFS = 16, + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS = 20, + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS = 24, + MZ_ZIP_CDH_FILENAME_LEN_OFS = 28, + MZ_ZIP_CDH_EXTRA_LEN_OFS = 30, + MZ_ZIP_CDH_COMMENT_LEN_OFS = 32, + MZ_ZIP_CDH_DISK_START_OFS = 34, + MZ_ZIP_CDH_INTERNAL_ATTR_OFS = 36, + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS = 38, + MZ_ZIP_CDH_LOCAL_HEADER_OFS = 42, + + /* Local directory header offsets */ + MZ_ZIP_LDH_SIG_OFS = 0, + MZ_ZIP_LDH_VERSION_NEEDED_OFS = 4, + MZ_ZIP_LDH_BIT_FLAG_OFS = 6, + MZ_ZIP_LDH_METHOD_OFS = 8, + MZ_ZIP_LDH_FILE_TIME_OFS = 10, + MZ_ZIP_LDH_FILE_DATE_OFS = 12, + MZ_ZIP_LDH_CRC32_OFS = 14, + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS = 18, + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS = 22, + MZ_ZIP_LDH_FILENAME_LEN_OFS = 26, + MZ_ZIP_LDH_EXTRA_LEN_OFS = 28, + MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR = 1 << 3, + + /* End of central directory offsets */ + MZ_ZIP_ECDH_SIG_OFS = 0, + MZ_ZIP_ECDH_NUM_THIS_DISK_OFS = 4, + MZ_ZIP_ECDH_NUM_DISK_CDIR_OFS = 6, + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS = 8, + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS = 10, + MZ_ZIP_ECDH_CDIR_SIZE_OFS = 12, + MZ_ZIP_ECDH_CDIR_OFS_OFS = 16, + MZ_ZIP_ECDH_COMMENT_SIZE_OFS = 20, + + /* ZIP64 End of central directory locator offsets */ + MZ_ZIP64_ECDL_SIG_OFS = 0, /* 4 bytes */ + MZ_ZIP64_ECDL_NUM_DISK_CDIR_OFS = 4, /* 4 bytes */ + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS = 8, /* 8 bytes */ + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS = 16, /* 4 bytes */ + + /* ZIP64 End of central directory header offsets */ + MZ_ZIP64_ECDH_SIG_OFS = 0, /* 4 bytes */ + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS = 4, /* 8 bytes */ + MZ_ZIP64_ECDH_VERSION_MADE_BY_OFS = 12, /* 2 bytes */ + MZ_ZIP64_ECDH_VERSION_NEEDED_OFS = 14, /* 2 bytes */ + MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS = 16, /* 4 bytes */ + MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS = 20, /* 4 bytes */ + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS = 24, /* 8 bytes */ + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS = 32, /* 8 bytes */ + MZ_ZIP64_ECDH_CDIR_SIZE_OFS = 40, /* 8 bytes */ + MZ_ZIP64_ECDH_CDIR_OFS_OFS = 48, /* 8 bytes */ + MZ_ZIP_VERSION_MADE_BY_DOS_FILESYSTEM_ID = 0, + MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG = 0x10, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED = 1, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG = 32, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION = 64, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED = 8192, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8 = 1 << 11 +}; + +typedef struct +{ + void *m_p; + size_t m_size, m_capacity; + mz_uint m_element_size; +} mz_zip_array; + +struct mz_zip_internal_state_tag +{ + mz_zip_array m_central_dir; + mz_zip_array m_central_dir_offsets; + mz_zip_array m_sorted_central_dir_offsets; + + /* The flags passed in when the archive is initially opened. */ + mz_uint32 m_init_flags; + + /* MZ_TRUE if the archive has a zip64 end of central directory headers, etc. */ + mz_bool m_zip64; + + /* MZ_TRUE if we found zip64 extended info in the central directory (m_zip64 will also be slammed to true too, even if we didn't find a zip64 end of central dir header, etc.) */ + mz_bool m_zip64_has_extended_info_fields; + + /* These fields are used by the file, FILE, memory, and memory/heap read/write helpers. */ + MZ_FILE *m_pFile; + mz_uint64 m_file_archive_start_ofs; + + void *m_pMem; + size_t m_mem_size; + size_t m_mem_capacity; +}; + +#define MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(array_ptr, element_size) (array_ptr)->m_element_size = element_size + +#if defined(DEBUG) || defined(_DEBUG) +static MZ_FORCEINLINE mz_uint mz_zip_array_range_check(const mz_zip_array *pArray, mz_uint index) +{ + MZ_ASSERT(index < pArray->m_size); + return index; +} +#define MZ_ZIP_ARRAY_ELEMENT(array_ptr, element_type, index) ((element_type *)((array_ptr)->m_p))[mz_zip_array_range_check(array_ptr, index)] +#else +#define MZ_ZIP_ARRAY_ELEMENT(array_ptr, element_type, index) ((element_type *)((array_ptr)->m_p))[index] +#endif + +static MZ_FORCEINLINE void mz_zip_array_init(mz_zip_array *pArray, mz_uint32 element_size) +{ + memset(pArray, 0, sizeof(mz_zip_array)); + pArray->m_element_size = element_size; +} + +static MZ_FORCEINLINE void mz_zip_array_clear(mz_zip_archive *pZip, mz_zip_array *pArray) +{ + pZip->m_pFree(pZip->m_pAlloc_opaque, pArray->m_p); + memset(pArray, 0, sizeof(mz_zip_array)); +} + +static mz_bool mz_zip_array_ensure_capacity(mz_zip_archive *pZip, mz_zip_array *pArray, size_t min_new_capacity, mz_uint growing) +{ + void *pNew_p; + size_t new_capacity = min_new_capacity; + MZ_ASSERT(pArray->m_element_size); + if (pArray->m_capacity >= min_new_capacity) + return MZ_TRUE; + if (growing) + { + new_capacity = MZ_MAX(1, pArray->m_capacity); + while (new_capacity < min_new_capacity) + new_capacity *= 2; + } + if (NULL == (pNew_p = pZip->m_pRealloc(pZip->m_pAlloc_opaque, pArray->m_p, pArray->m_element_size, new_capacity))) + return MZ_FALSE; + pArray->m_p = pNew_p; + pArray->m_capacity = new_capacity; + return MZ_TRUE; +} + +static MZ_FORCEINLINE mz_bool mz_zip_array_reserve(mz_zip_archive *pZip, mz_zip_array *pArray, size_t new_capacity, mz_uint growing) +{ + if (new_capacity > pArray->m_capacity) + { + if (!mz_zip_array_ensure_capacity(pZip, pArray, new_capacity, growing)) + return MZ_FALSE; + } + return MZ_TRUE; +} + +static MZ_FORCEINLINE mz_bool mz_zip_array_resize(mz_zip_archive *pZip, mz_zip_array *pArray, size_t new_size, mz_uint growing) +{ + if (new_size > pArray->m_capacity) + { + if (!mz_zip_array_ensure_capacity(pZip, pArray, new_size, growing)) + return MZ_FALSE; + } + pArray->m_size = new_size; + return MZ_TRUE; +} + +static MZ_FORCEINLINE mz_bool mz_zip_array_ensure_room(mz_zip_archive *pZip, mz_zip_array *pArray, size_t n) +{ + return mz_zip_array_reserve(pZip, pArray, pArray->m_size + n, MZ_TRUE); +} + +static MZ_FORCEINLINE mz_bool mz_zip_array_push_back(mz_zip_archive *pZip, mz_zip_array *pArray, const void *pElements, size_t n) +{ + size_t orig_size = pArray->m_size; + if (!mz_zip_array_resize(pZip, pArray, orig_size + n, MZ_TRUE)) + return MZ_FALSE; + if (n > 0) + memcpy((mz_uint8 *)pArray->m_p + orig_size * pArray->m_element_size, pElements, n * pArray->m_element_size); + return MZ_TRUE; +} + +#ifndef MINIZ_NO_TIME +static MZ_TIME_T mz_zip_dos_to_time_t(int dos_time, int dos_date) +{ + struct tm tm; + memset(&tm, 0, sizeof(tm)); + tm.tm_isdst = -1; + tm.tm_year = ((dos_date >> 9) & 127) + 1980 - 1900; + tm.tm_mon = ((dos_date >> 5) & 15) - 1; + tm.tm_mday = dos_date & 31; + tm.tm_hour = (dos_time >> 11) & 31; + tm.tm_min = (dos_time >> 5) & 63; + tm.tm_sec = (dos_time << 1) & 62; + return mktime(&tm); +} + +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS +static void mz_zip_time_t_to_dos_time(MZ_TIME_T time, mz_uint16 *pDOS_time, mz_uint16 *pDOS_date) +{ +#ifdef _MSC_VER + struct tm tm_struct; + struct tm *tm = &tm_struct; + errno_t err = localtime_s(tm, &time); + if (err) + { + *pDOS_date = 0; + *pDOS_time = 0; + return; + } +#else + struct tm *tm = localtime(&time); +#endif /* #ifdef _MSC_VER */ + + *pDOS_time = (mz_uint16)(((tm->tm_hour) << 11) + ((tm->tm_min) << 5) + ((tm->tm_sec) >> 1)); + *pDOS_date = (mz_uint16)(((tm->tm_year + 1900 - 1980) << 9) + ((tm->tm_mon + 1) << 5) + tm->tm_mday); +} +#endif /* MINIZ_NO_ARCHIVE_WRITING_APIS */ + +#ifndef MINIZ_NO_STDIO +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS +static mz_bool mz_zip_get_file_modified_time(const char *pFilename, MZ_TIME_T *pTime) +{ + struct MZ_FILE_STAT_STRUCT file_stat; + + /* On Linux with x86 glibc, this call will fail on large files (I think >= 0x80000000 bytes) unless you compiled with _LARGEFILE64_SOURCE. Argh. */ + if (MZ_FILE_STAT(pFilename, &file_stat) != 0) + return MZ_FALSE; + + *pTime = file_stat.st_mtime; + + return MZ_TRUE; +} +#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS*/ + +static mz_bool mz_zip_set_file_times(const char *pFilename, MZ_TIME_T access_time, MZ_TIME_T modified_time) +{ + struct utimbuf t; + + memset(&t, 0, sizeof(t)); + t.actime = access_time; + t.modtime = modified_time; + + return !utime(pFilename, &t); +} +#endif /* #ifndef MINIZ_NO_STDIO */ +#endif /* #ifndef MINIZ_NO_TIME */ + +static MZ_FORCEINLINE mz_bool mz_zip_set_error(mz_zip_archive *pZip, mz_zip_error err_num) +{ + if (pZip) + pZip->m_last_error = err_num; + return MZ_FALSE; +} + +static mz_bool mz_zip_reader_init_internal(mz_zip_archive *pZip, mz_uint flags) +{ + (void)flags; + if ((!pZip) || (pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_INVALID)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!pZip->m_pAlloc) + pZip->m_pAlloc = miniz_def_alloc_func; + if (!pZip->m_pFree) + pZip->m_pFree = miniz_def_free_func; + if (!pZip->m_pRealloc) + pZip->m_pRealloc = miniz_def_realloc_func; + + pZip->m_archive_size = 0; + pZip->m_central_directory_file_ofs = 0; + pZip->m_total_files = 0; + pZip->m_last_error = MZ_ZIP_NO_ERROR; + + if (NULL == (pZip->m_pState = (mz_zip_internal_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_internal_state)))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + memset(pZip->m_pState, 0, sizeof(mz_zip_internal_state)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir, sizeof(mz_uint8)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir_offsets, sizeof(mz_uint32)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_sorted_central_dir_offsets, sizeof(mz_uint32)); + pZip->m_pState->m_init_flags = flags; + pZip->m_pState->m_zip64 = MZ_FALSE; + pZip->m_pState->m_zip64_has_extended_info_fields = MZ_FALSE; + + pZip->m_zip_mode = MZ_ZIP_MODE_READING; + + return MZ_TRUE; +} + +static MZ_FORCEINLINE mz_bool mz_zip_reader_filename_less(const mz_zip_array *pCentral_dir_array, const mz_zip_array *pCentral_dir_offsets, mz_uint l_index, mz_uint r_index) +{ + const mz_uint8 *pL = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, l_index)), *pE; + const mz_uint8 *pR = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, r_index)); + mz_uint l_len = MZ_READ_LE16(pL + MZ_ZIP_CDH_FILENAME_LEN_OFS), r_len = MZ_READ_LE16(pR + MZ_ZIP_CDH_FILENAME_LEN_OFS); + mz_uint8 l = 0, r = 0; + pL += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; + pR += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; + pE = pL + MZ_MIN(l_len, r_len); + while (pL < pE) + { + if ((l = MZ_TOLOWER(*pL)) != (r = MZ_TOLOWER(*pR))) + break; + pL++; + pR++; + } + return (pL == pE) ? (l_len < r_len) : (l < r); +} + +#define MZ_SWAP_UINT32(a, b) \ + do \ + { \ + mz_uint32 t = a; \ + a = b; \ + b = t; \ + } \ + MZ_MACRO_END + +/* Heap sort of lowercased filenames, used to help accelerate plain central directory searches by mz_zip_reader_locate_file(). (Could also use qsort(), but it could allocate memory.) */ +static void mz_zip_reader_sort_central_dir_offsets_by_filename(mz_zip_archive *pZip) +{ + mz_zip_internal_state *pState = pZip->m_pState; + const mz_zip_array *pCentral_dir_offsets = &pState->m_central_dir_offsets; + const mz_zip_array *pCentral_dir = &pState->m_central_dir; + mz_uint32 *pIndices; + mz_uint32 start, end; + const mz_uint32 size = pZip->m_total_files; + + if (size <= 1U) + return; + + pIndices = &MZ_ZIP_ARRAY_ELEMENT(&pState->m_sorted_central_dir_offsets, mz_uint32, 0); + + start = (size - 2U) >> 1U; + for (;;) + { + mz_uint64 child, root = start; + for (;;) + { + if ((child = (root << 1U) + 1U) >= size) + break; + child += (((child + 1U) < size) && (mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[child], pIndices[child + 1U]))); + if (!mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[root], pIndices[child])) + break; + MZ_SWAP_UINT32(pIndices[root], pIndices[child]); + root = child; + } + if (!start) + break; + start--; + } + + end = size - 1; + while (end > 0) + { + mz_uint64 child, root = 0; + MZ_SWAP_UINT32(pIndices[end], pIndices[0]); + for (;;) + { + if ((child = (root << 1U) + 1U) >= end) + break; + child += (((child + 1U) < end) && mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[child], pIndices[child + 1U])); + if (!mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[root], pIndices[child])) + break; + MZ_SWAP_UINT32(pIndices[root], pIndices[child]); + root = child; + } + end--; + } +} + +static mz_bool mz_zip_reader_locate_header_sig(mz_zip_archive *pZip, mz_uint32 record_sig, mz_uint32 record_size, mz_int64 *pOfs) +{ + mz_int64 cur_file_ofs; + mz_uint32 buf_u32[4096 / sizeof(mz_uint32)]; + mz_uint8 *pBuf = (mz_uint8 *)buf_u32; + + /* Basic sanity checks - reject files which are too small */ + if (pZip->m_archive_size < record_size) + return MZ_FALSE; + + /* Find the record by scanning the file from the end towards the beginning. */ + cur_file_ofs = MZ_MAX((mz_int64)pZip->m_archive_size - (mz_int64)sizeof(buf_u32), 0); + for (;;) + { + int i, n = (int)MZ_MIN(sizeof(buf_u32), pZip->m_archive_size - cur_file_ofs); + + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, n) != (mz_uint)n) + return MZ_FALSE; + + for (i = n - 4; i >= 0; --i) + { + mz_uint s = MZ_READ_LE32(pBuf + i); + if (s == record_sig) + { + if ((pZip->m_archive_size - (cur_file_ofs + i)) >= record_size) + break; + } + } + + if (i >= 0) + { + cur_file_ofs += i; + break; + } + + /* Give up if we've searched the entire file, or we've gone back "too far" (~64kb) */ + if ((!cur_file_ofs) || ((pZip->m_archive_size - cur_file_ofs) >= (MZ_UINT16_MAX + record_size))) + return MZ_FALSE; + + cur_file_ofs = MZ_MAX(cur_file_ofs - (sizeof(buf_u32) - 3), 0); + } + + *pOfs = cur_file_ofs; + return MZ_TRUE; +} + +static mz_bool mz_zip_reader_read_central_dir(mz_zip_archive *pZip, mz_uint flags) +{ + mz_uint cdir_size = 0, cdir_entries_on_this_disk = 0, num_this_disk = 0, cdir_disk_index = 0; + mz_uint64 cdir_ofs = 0; + mz_int64 cur_file_ofs = 0; + const mz_uint8 *p; + + mz_uint32 buf_u32[4096 / sizeof(mz_uint32)]; + mz_uint8 *pBuf = (mz_uint8 *)buf_u32; + mz_bool sort_central_dir = ((flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0); + mz_uint32 zip64_end_of_central_dir_locator_u32[(MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pZip64_locator = (mz_uint8 *)zip64_end_of_central_dir_locator_u32; + + mz_uint32 zip64_end_of_central_dir_header_u32[(MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pZip64_end_of_central_dir = (mz_uint8 *)zip64_end_of_central_dir_header_u32; + + mz_uint64 zip64_end_of_central_dir_ofs = 0; + + /* Basic sanity checks - reject files which are too small, and check the first 4 bytes of the file to make sure a local header is there. */ + if (pZip->m_archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (!mz_zip_reader_locate_header_sig(pZip, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE, &cur_file_ofs)) + return mz_zip_set_error(pZip, MZ_ZIP_FAILED_FINDING_CENTRAL_DIR); + + /* Read and verify the end of central directory record. */ + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_SIG_OFS) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (cur_file_ofs >= (MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)) + { + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs - MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE, pZip64_locator, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) == MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) + { + if (MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_SIG_OFS) == MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG) + { + zip64_end_of_central_dir_ofs = MZ_READ_LE64(pZip64_locator + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS); + if (zip64_end_of_central_dir_ofs > (pZip->m_archive_size - MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (pZip->m_pRead(pZip->m_pIO_opaque, zip64_end_of_central_dir_ofs, pZip64_end_of_central_dir, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) == MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) + { + if (MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_SIG_OFS) == MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG) + { + pZip->m_pState->m_zip64 = MZ_TRUE; + } + } + } + } + } + + pZip->m_total_files = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS); + cdir_entries_on_this_disk = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS); + num_this_disk = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_THIS_DISK_OFS); + cdir_disk_index = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_DISK_CDIR_OFS); + cdir_size = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_SIZE_OFS); + cdir_ofs = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_OFS_OFS); + + if (pZip->m_pState->m_zip64) + { + mz_uint32 zip64_total_num_of_disks = MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS); + mz_uint64 zip64_cdir_total_entries = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS); + mz_uint64 zip64_cdir_total_entries_on_this_disk = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS); + mz_uint64 zip64_size_of_end_of_central_dir_record = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS); + mz_uint64 zip64_size_of_central_directory = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_SIZE_OFS); + + if (zip64_size_of_end_of_central_dir_record < (MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE - 12)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (zip64_total_num_of_disks != 1U) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + /* Check for miniz's practical limits */ + if (zip64_cdir_total_entries > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + + pZip->m_total_files = (mz_uint32)zip64_cdir_total_entries; + + if (zip64_cdir_total_entries_on_this_disk > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + + cdir_entries_on_this_disk = (mz_uint32)zip64_cdir_total_entries_on_this_disk; + + /* Check for miniz's current practical limits (sorry, this should be enough for millions of files) */ + if (zip64_size_of_central_directory > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + cdir_size = (mz_uint32)zip64_size_of_central_directory; + + num_this_disk = MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS); + + cdir_disk_index = MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS); + + cdir_ofs = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_OFS_OFS); + } + + if (pZip->m_total_files != cdir_entries_on_this_disk) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + if (((num_this_disk | cdir_disk_index) != 0) && ((num_this_disk != 1) || (cdir_disk_index != 1))) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + if (cdir_size < (mz_uint64)pZip->m_total_files * MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if ((cdir_ofs + (mz_uint64)cdir_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + pZip->m_central_directory_file_ofs = cdir_ofs; + + if (pZip->m_total_files) + { + mz_uint i, n; + /* Read the entire central directory into a heap block, and allocate another heap block to hold the unsorted central dir file record offsets, and possibly another to hold the sorted indices. */ + if ((!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir, cdir_size, MZ_FALSE)) || + (!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir_offsets, pZip->m_total_files, MZ_FALSE))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + if (sort_central_dir) + { + if (!mz_zip_array_resize(pZip, &pZip->m_pState->m_sorted_central_dir_offsets, pZip->m_total_files, MZ_FALSE)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (pZip->m_pRead(pZip->m_pIO_opaque, cdir_ofs, pZip->m_pState->m_central_dir.m_p, cdir_size) != cdir_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + /* Now create an index into the central directory file records, do some basic sanity checking on each record */ + p = (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p; + for (n = cdir_size, i = 0; i < pZip->m_total_files; ++i) + { + mz_uint total_header_size, disk_index, bit_flags, filename_size, ext_data_size; + mz_uint64 comp_size, decomp_size, local_header_ofs; + + if ((n < MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) || (MZ_READ_LE32(p) != MZ_ZIP_CENTRAL_DIR_HEADER_SIG)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, i) = (mz_uint32)(p - (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p); + + if (sort_central_dir) + MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_sorted_central_dir_offsets, mz_uint32, i) = i; + + comp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS); + decomp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS); + local_header_ofs = MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS); + filename_size = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + ext_data_size = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS); + + if ((!pZip->m_pState->m_zip64_has_extended_info_fields) && + (ext_data_size) && + (MZ_MAX(MZ_MAX(comp_size, decomp_size), local_header_ofs) == MZ_UINT32_MAX)) + { + /* Attempt to find zip64 extended information field in the entry's extra data */ + mz_uint32 extra_size_remaining = ext_data_size; + + if (extra_size_remaining) + { + const mz_uint8 *pExtra_data; + void* buf = NULL; + + if (MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size + ext_data_size > n) + { + buf = MZ_MALLOC(ext_data_size); + if(buf==NULL) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + if (pZip->m_pRead(pZip->m_pIO_opaque, cdir_ofs + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size, buf, ext_data_size) != ext_data_size) + { + MZ_FREE(buf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + + pExtra_data = (mz_uint8*)buf; + } + else + { + pExtra_data = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size; + } + + do + { + mz_uint32 field_id; + mz_uint32 field_data_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + { + MZ_FREE(buf); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + + if ((field_data_size + sizeof(mz_uint16) * 2) > extra_size_remaining) + { + MZ_FREE(buf); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + /* Ok, the archive didn't have any zip64 headers but it uses a zip64 extended information field so mark it as zip64 anyway (this can occur with infozip's zip util when it reads compresses files from stdin). */ + pZip->m_pState->m_zip64 = MZ_TRUE; + pZip->m_pState->m_zip64_has_extended_info_fields = MZ_TRUE; + break; + } + + pExtra_data += sizeof(mz_uint16) * 2 + field_data_size; + extra_size_remaining = extra_size_remaining - sizeof(mz_uint16) * 2 - field_data_size; + } while (extra_size_remaining); + + MZ_FREE(buf); + } + } + + /* I've seen archives that aren't marked as zip64 that uses zip64 ext data, argh */ + if ((comp_size != MZ_UINT32_MAX) && (decomp_size != MZ_UINT32_MAX)) + { + if (((!MZ_READ_LE32(p + MZ_ZIP_CDH_METHOD_OFS)) && (decomp_size != comp_size)) || (decomp_size && !comp_size)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + disk_index = MZ_READ_LE16(p + MZ_ZIP_CDH_DISK_START_OFS); + if ((disk_index == MZ_UINT16_MAX) || ((disk_index != num_this_disk) && (disk_index != 1))) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + if (comp_size != MZ_UINT32_MAX) + { + if (((mz_uint64)MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS) + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + comp_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + bit_flags = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); + if (bit_flags & MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + + if ((total_header_size = MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_COMMENT_LEN_OFS)) > n) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + n -= total_header_size; + p += total_header_size; + } + } + + if (sort_central_dir) + mz_zip_reader_sort_central_dir_offsets_by_filename(pZip); + + return MZ_TRUE; +} + +void mz_zip_zero_struct(mz_zip_archive *pZip) +{ + if (pZip) + MZ_CLEAR_PTR(pZip); +} + +static mz_bool mz_zip_reader_end_internal(mz_zip_archive *pZip, mz_bool set_last_error) +{ + mz_bool status = MZ_TRUE; + + if (!pZip) + return MZ_FALSE; + + if ((!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (pZip->m_zip_mode != MZ_ZIP_MODE_READING)) + { + if (set_last_error) + pZip->m_last_error = MZ_ZIP_INVALID_PARAMETER; + + return MZ_FALSE; + } + + if (pZip->m_pState) + { + mz_zip_internal_state *pState = pZip->m_pState; + pZip->m_pState = NULL; + + mz_zip_array_clear(pZip, &pState->m_central_dir); + mz_zip_array_clear(pZip, &pState->m_central_dir_offsets); + mz_zip_array_clear(pZip, &pState->m_sorted_central_dir_offsets); + +#ifndef MINIZ_NO_STDIO + if (pState->m_pFile) + { + if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE) + { + if (MZ_FCLOSE(pState->m_pFile) == EOF) + { + if (set_last_error) + pZip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED; + status = MZ_FALSE; + } + } + pState->m_pFile = NULL; + } +#endif /* #ifndef MINIZ_NO_STDIO */ + + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + } + pZip->m_zip_mode = MZ_ZIP_MODE_INVALID; + + return status; +} + +mz_bool mz_zip_reader_end(mz_zip_archive *pZip) +{ + return mz_zip_reader_end_internal(pZip, MZ_TRUE); +} +mz_bool mz_zip_reader_init(mz_zip_archive *pZip, mz_uint64 size, mz_uint flags) +{ + if ((!pZip) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_reader_init_internal(pZip, flags)) + return MZ_FALSE; + + pZip->m_zip_type = MZ_ZIP_TYPE_USER; + pZip->m_archive_size = size; + + if (!mz_zip_reader_read_central_dir(pZip, flags)) + { + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return MZ_FALSE; + } + + return MZ_TRUE; +} + +static size_t mz_zip_mem_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n) +{ + mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; + size_t s = (file_ofs >= pZip->m_archive_size) ? 0 : (size_t)MZ_MIN(pZip->m_archive_size - file_ofs, n); + memcpy(pBuf, (const mz_uint8 *)pZip->m_pState->m_pMem + file_ofs, s); + return s; +} + +mz_bool mz_zip_reader_init_mem(mz_zip_archive *pZip, const void *pMem, size_t size, mz_uint flags) +{ + if (!pMem) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (!mz_zip_reader_init_internal(pZip, flags)) + return MZ_FALSE; + + pZip->m_zip_type = MZ_ZIP_TYPE_MEMORY; + pZip->m_archive_size = size; + pZip->m_pRead = mz_zip_mem_read_func; + pZip->m_pIO_opaque = pZip; + pZip->m_pNeeds_keepalive = NULL; + +#ifdef __cplusplus + pZip->m_pState->m_pMem = const_cast(pMem); +#else + pZip->m_pState->m_pMem = (void *)pMem; +#endif + + pZip->m_pState->m_mem_size = size; + + if (!mz_zip_reader_read_central_dir(pZip, flags)) + { + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return MZ_FALSE; + } + + return MZ_TRUE; +} + +#ifndef MINIZ_NO_STDIO +static size_t mz_zip_file_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n) +{ + mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; + mz_int64 cur_ofs = MZ_FTELL64(pZip->m_pState->m_pFile); + + file_ofs += pZip->m_pState->m_file_archive_start_ofs; + + if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pZip->m_pState->m_pFile, (mz_int64)file_ofs, SEEK_SET)))) + return 0; + + return MZ_FREAD(pBuf, 1, n, pZip->m_pState->m_pFile); +} + +mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint32 flags) +{ + return mz_zip_reader_init_file_v2(pZip, pFilename, flags, 0, 0); +} + +mz_bool mz_zip_reader_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags, mz_uint64 file_start_ofs, mz_uint64 archive_size) +{ + mz_uint64 file_size; + MZ_FILE *pFile; + + if ((!pZip) || (!pFilename) || ((archive_size) && (archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE))) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pFile = MZ_FOPEN(pFilename, "rb"); + if (!pFile) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + + file_size = archive_size; + if (!file_size) + { + if (MZ_FSEEK64(pFile, 0, SEEK_END)) + { + MZ_FCLOSE(pFile); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED); + } + + file_size = MZ_FTELL64(pFile); + } + + /* TODO: Better sanity check archive_size and the # of actual remaining bytes */ + + if (file_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + { + MZ_FCLOSE(pFile); + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + } + + if (!mz_zip_reader_init_internal(pZip, flags)) + { + MZ_FCLOSE(pFile); + return MZ_FALSE; + } + + pZip->m_zip_type = MZ_ZIP_TYPE_FILE; + pZip->m_pRead = mz_zip_file_read_func; + pZip->m_pIO_opaque = pZip; + pZip->m_pState->m_pFile = pFile; + pZip->m_archive_size = file_size; + pZip->m_pState->m_file_archive_start_ofs = file_start_ofs; + + if (!mz_zip_reader_read_central_dir(pZip, flags)) + { + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return MZ_FALSE; + } + + return MZ_TRUE; +} + +mz_bool mz_zip_reader_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint64 archive_size, mz_uint flags) +{ + mz_uint64 cur_file_ofs; + + if ((!pZip) || (!pFile)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + + cur_file_ofs = MZ_FTELL64(pFile); + + if (!archive_size) + { + if (MZ_FSEEK64(pFile, 0, SEEK_END)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED); + + archive_size = MZ_FTELL64(pFile) - cur_file_ofs; + + if (archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + } + + if (!mz_zip_reader_init_internal(pZip, flags)) + return MZ_FALSE; + + pZip->m_zip_type = MZ_ZIP_TYPE_CFILE; + pZip->m_pRead = mz_zip_file_read_func; + + pZip->m_pIO_opaque = pZip; + pZip->m_pState->m_pFile = pFile; + pZip->m_archive_size = archive_size; + pZip->m_pState->m_file_archive_start_ofs = cur_file_ofs; + + if (!mz_zip_reader_read_central_dir(pZip, flags)) + { + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return MZ_FALSE; + } + + return MZ_TRUE; +} + +#endif /* #ifndef MINIZ_NO_STDIO */ + +static MZ_FORCEINLINE const mz_uint8 *mz_zip_get_cdh(mz_zip_archive *pZip, mz_uint file_index) +{ + if ((!pZip) || (!pZip->m_pState) || (file_index >= pZip->m_total_files)) + return NULL; + return &MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index)); +} + +mz_bool mz_zip_reader_is_file_encrypted(mz_zip_archive *pZip, mz_uint file_index) +{ + mz_uint m_bit_flag; + const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return MZ_FALSE; + } + + m_bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); + return (m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION)) != 0; +} + +mz_bool mz_zip_reader_is_file_supported(mz_zip_archive *pZip, mz_uint file_index) +{ + mz_uint bit_flag; + mz_uint method; + + const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return MZ_FALSE; + } + + method = MZ_READ_LE16(p + MZ_ZIP_CDH_METHOD_OFS); + bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); + + if ((method != 0) && (method != MZ_DEFLATED)) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + return MZ_FALSE; + } + + if (bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION)) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + return MZ_FALSE; + } + + if (bit_flag & MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); + return MZ_FALSE; + } + + return MZ_TRUE; +} + +mz_bool mz_zip_reader_is_file_a_directory(mz_zip_archive *pZip, mz_uint file_index) +{ + mz_uint filename_len, attribute_mapping_id, external_attr; + const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return MZ_FALSE; + } + + filename_len = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + if (filename_len) + { + if (*(p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_len - 1) == '/') + return MZ_TRUE; + } + + /* Bugfix: This code was also checking if the internal attribute was non-zero, which wasn't correct. */ + /* Most/all zip writers (hopefully) set DOS file/directory attributes in the low 16-bits, so check for the DOS directory flag and ignore the source OS ID in the created by field. */ + /* FIXME: Remove this check? Is it necessary - we already check the filename. */ + attribute_mapping_id = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_MADE_BY_OFS) >> 8; + (void)attribute_mapping_id; + + external_attr = MZ_READ_LE32(p + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS); + if ((external_attr & MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG) != 0) + { + return MZ_TRUE; + } + + return MZ_FALSE; +} + +static mz_bool mz_zip_file_stat_internal(mz_zip_archive *pZip, mz_uint file_index, const mz_uint8 *pCentral_dir_header, mz_zip_archive_file_stat *pStat, mz_bool *pFound_zip64_extra_data) +{ + mz_uint n; + const mz_uint8 *p = pCentral_dir_header; + + if (pFound_zip64_extra_data) + *pFound_zip64_extra_data = MZ_FALSE; + + if ((!p) || (!pStat)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* Extract fields from the central directory record. */ + pStat->m_file_index = file_index; + pStat->m_central_dir_ofs = MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index); + pStat->m_version_made_by = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_MADE_BY_OFS); + pStat->m_version_needed = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_NEEDED_OFS); + pStat->m_bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); + pStat->m_method = MZ_READ_LE16(p + MZ_ZIP_CDH_METHOD_OFS); +#ifndef MINIZ_NO_TIME + pStat->m_time = mz_zip_dos_to_time_t(MZ_READ_LE16(p + MZ_ZIP_CDH_FILE_TIME_OFS), MZ_READ_LE16(p + MZ_ZIP_CDH_FILE_DATE_OFS)); +#endif + pStat->m_crc32 = MZ_READ_LE32(p + MZ_ZIP_CDH_CRC32_OFS); + pStat->m_comp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS); + pStat->m_uncomp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS); + pStat->m_internal_attr = MZ_READ_LE16(p + MZ_ZIP_CDH_INTERNAL_ATTR_OFS); + pStat->m_external_attr = MZ_READ_LE32(p + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS); + pStat->m_local_header_ofs = MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS); + + /* Copy as much of the filename and comment as possible. */ + n = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + n = MZ_MIN(n, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE - 1); + memcpy(pStat->m_filename, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, n); + pStat->m_filename[n] = '\0'; + + n = MZ_READ_LE16(p + MZ_ZIP_CDH_COMMENT_LEN_OFS); + n = MZ_MIN(n, MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE - 1); + pStat->m_comment_size = n; + memcpy(pStat->m_comment, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS), n); + pStat->m_comment[n] = '\0'; + + /* Set some flags for convienance */ + pStat->m_is_directory = mz_zip_reader_is_file_a_directory(pZip, file_index); + pStat->m_is_encrypted = mz_zip_reader_is_file_encrypted(pZip, file_index); + pStat->m_is_supported = mz_zip_reader_is_file_supported(pZip, file_index); + + /* See if we need to read any zip64 extended information fields. */ + /* Confusingly, these zip64 fields can be present even on non-zip64 archives (Debian zip on a huge files from stdin piped to stdout creates them). */ + if (MZ_MAX(MZ_MAX(pStat->m_comp_size, pStat->m_uncomp_size), pStat->m_local_header_ofs) == MZ_UINT32_MAX) + { + /* Attempt to find zip64 extended information field in the entry's extra data */ + mz_uint32 extra_size_remaining = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS); + + if (extra_size_remaining) + { + const mz_uint8 *pExtra_data = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + + do + { + mz_uint32 field_id; + mz_uint32 field_data_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + + if ((field_data_size + sizeof(mz_uint16) * 2) > extra_size_remaining) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + const mz_uint8 *pField_data = pExtra_data + sizeof(mz_uint16) * 2; + mz_uint32 field_data_remaining = field_data_size; + + if (pFound_zip64_extra_data) + *pFound_zip64_extra_data = MZ_TRUE; + + if (pStat->m_uncomp_size == MZ_UINT32_MAX) + { + if (field_data_remaining < sizeof(mz_uint64)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + pStat->m_uncomp_size = MZ_READ_LE64(pField_data); + pField_data += sizeof(mz_uint64); + field_data_remaining -= sizeof(mz_uint64); + } + + if (pStat->m_comp_size == MZ_UINT32_MAX) + { + if (field_data_remaining < sizeof(mz_uint64)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + pStat->m_comp_size = MZ_READ_LE64(pField_data); + pField_data += sizeof(mz_uint64); + field_data_remaining -= sizeof(mz_uint64); + } + + if (pStat->m_local_header_ofs == MZ_UINT32_MAX) + { + if (field_data_remaining < sizeof(mz_uint64)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + pStat->m_local_header_ofs = MZ_READ_LE64(pField_data); + pField_data += sizeof(mz_uint64); + field_data_remaining -= sizeof(mz_uint64); + } + + break; + } + + pExtra_data += sizeof(mz_uint16) * 2 + field_data_size; + extra_size_remaining = extra_size_remaining - sizeof(mz_uint16) * 2 - field_data_size; + } while (extra_size_remaining); + } + } + + return MZ_TRUE; +} + +static MZ_FORCEINLINE mz_bool mz_zip_string_equal(const char *pA, const char *pB, mz_uint len, mz_uint flags) +{ + mz_uint i; + if (flags & MZ_ZIP_FLAG_CASE_SENSITIVE) + return 0 == memcmp(pA, pB, len); + for (i = 0; i < len; ++i) + if (MZ_TOLOWER(pA[i]) != MZ_TOLOWER(pB[i])) + return MZ_FALSE; + return MZ_TRUE; +} + +static MZ_FORCEINLINE int mz_zip_filename_compare(const mz_zip_array *pCentral_dir_array, const mz_zip_array *pCentral_dir_offsets, mz_uint l_index, const char *pR, mz_uint r_len) +{ + const mz_uint8 *pL = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, l_index)), *pE; + mz_uint l_len = MZ_READ_LE16(pL + MZ_ZIP_CDH_FILENAME_LEN_OFS); + mz_uint8 l = 0, r = 0; + pL += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; + pE = pL + MZ_MIN(l_len, r_len); + while (pL < pE) + { + if ((l = MZ_TOLOWER(*pL)) != (r = MZ_TOLOWER(*pR))) + break; + pL++; + pR++; + } + return (pL == pE) ? (int)(l_len - r_len) : (l - r); +} + +static mz_bool mz_zip_locate_file_binary_search(mz_zip_archive *pZip, const char *pFilename, mz_uint32 *pIndex) +{ + mz_zip_internal_state *pState = pZip->m_pState; + const mz_zip_array *pCentral_dir_offsets = &pState->m_central_dir_offsets; + const mz_zip_array *pCentral_dir = &pState->m_central_dir; + mz_uint32 *pIndices = &MZ_ZIP_ARRAY_ELEMENT(&pState->m_sorted_central_dir_offsets, mz_uint32, 0); + const mz_uint32 size = pZip->m_total_files; + const mz_uint filename_len = (mz_uint)strlen(pFilename); + + if (pIndex) + *pIndex = 0; + + if (size) + { + /* yes I could use uint32_t's, but then we would have to add some special case checks in the loop, argh, and */ + /* honestly the major expense here on 32-bit CPU's will still be the filename compare */ + mz_int64 l = 0, h = (mz_int64)size - 1; + + while (l <= h) + { + mz_int64 m = l + ((h - l) >> 1); + mz_uint32 file_index = pIndices[(mz_uint32)m]; + + int comp = mz_zip_filename_compare(pCentral_dir, pCentral_dir_offsets, file_index, pFilename, filename_len); + if (!comp) + { + if (pIndex) + *pIndex = file_index; + return MZ_TRUE; + } + else if (comp < 0) + l = m + 1; + else + h = m - 1; + } + } + + return mz_zip_set_error(pZip, MZ_ZIP_FILE_NOT_FOUND); +} + +int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags) +{ + mz_uint32 index; + if (!mz_zip_reader_locate_file_v2(pZip, pName, pComment, flags, &index)) + return -1; + else + return (int)index; +} + +mz_bool mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *pIndex) +{ + mz_uint file_index; + size_t name_len, comment_len; + + if (pIndex) + *pIndex = 0; + + if ((!pZip) || (!pZip->m_pState) || (!pName)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* See if we can use a binary search */ + if (((pZip->m_pState->m_init_flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0) && + (pZip->m_zip_mode == MZ_ZIP_MODE_READING) && + ((flags & (MZ_ZIP_FLAG_IGNORE_PATH | MZ_ZIP_FLAG_CASE_SENSITIVE)) == 0) && (!pComment) && (pZip->m_pState->m_sorted_central_dir_offsets.m_size)) + { + return mz_zip_locate_file_binary_search(pZip, pName, pIndex); + } + + /* Locate the entry by scanning the entire central directory */ + name_len = strlen(pName); + if (name_len > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + comment_len = pComment ? strlen(pComment) : 0; + if (comment_len > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + for (file_index = 0; file_index < pZip->m_total_files; file_index++) + { + const mz_uint8 *pHeader = &MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index)); + mz_uint filename_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_FILENAME_LEN_OFS); + const char *pFilename = (const char *)pHeader + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; + if (filename_len < name_len) + continue; + if (comment_len) + { + mz_uint file_extra_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_EXTRA_LEN_OFS), file_comment_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_COMMENT_LEN_OFS); + const char *pFile_comment = pFilename + filename_len + file_extra_len; + if ((file_comment_len != comment_len) || (!mz_zip_string_equal(pComment, pFile_comment, file_comment_len, flags))) + continue; + } + if ((flags & MZ_ZIP_FLAG_IGNORE_PATH) && (filename_len)) + { + int ofs = filename_len - 1; + do + { + if ((pFilename[ofs] == '/') || (pFilename[ofs] == '\\') || (pFilename[ofs] == ':')) + break; + } while (--ofs >= 0); + ofs++; + pFilename += ofs; + filename_len -= ofs; + } + if ((filename_len == name_len) && (mz_zip_string_equal(pName, pFilename, filename_len, flags))) + { + if (pIndex) + *pIndex = file_index; + return MZ_TRUE; + } + } + + return mz_zip_set_error(pZip, MZ_ZIP_FILE_NOT_FOUND); +} + +static +mz_bool mz_zip_reader_extract_to_mem_no_alloc1(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size, const mz_zip_archive_file_stat *st) +{ + int status = TINFL_STATUS_DONE; + mz_uint64 needed_size, cur_file_ofs, comp_remaining, out_buf_ofs = 0, read_buf_size, read_buf_ofs = 0, read_buf_avail; + mz_zip_archive_file_stat file_stat; + void *pRead_buf; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + tinfl_decompressor inflator; + + if ((!pZip) || (!pZip->m_pState) || ((buf_size) && (!pBuf)) || ((user_read_buf_size) && (!pUser_read_buf)) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (st) { + file_stat = *st; + } else + if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return MZ_FALSE; + + /* A directory or zero length file */ + if ((file_stat.m_is_directory) || (!file_stat.m_comp_size)) + return MZ_TRUE; + + /* Encryption and patch files are not supported. */ + if (file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + + /* This function only supports decompressing stored and deflate. */ + if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + + /* Ensure supplied output buffer is large enough. */ + needed_size = (flags & MZ_ZIP_FLAG_COMPRESSED_DATA) ? file_stat.m_comp_size : file_stat.m_uncomp_size; + if (buf_size < needed_size) + return mz_zip_set_error(pZip, MZ_ZIP_BUF_TOO_SMALL); + + /* Read and parse the local directory entry. */ + cur_file_ofs = file_stat.m_local_header_ofs; + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + cur_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + if ((cur_file_ofs + file_stat.m_comp_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!file_stat.m_method)) + { + /* The file is stored or the caller has requested the compressed data. */ + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, (size_t)needed_size) != needed_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) == 0) + { + if (mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, (size_t)file_stat.m_uncomp_size) != file_stat.m_crc32) + return mz_zip_set_error(pZip, MZ_ZIP_CRC_CHECK_FAILED); + } +#endif + + return MZ_TRUE; + } + + /* Decompress the file either directly from memory or from a file input buffer. */ + tinfl_init(&inflator); + + if (pZip->m_pState->m_pMem) + { + /* Read directly from the archive in memory. */ + pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + cur_file_ofs; + read_buf_size = read_buf_avail = file_stat.m_comp_size; + comp_remaining = 0; + } + else if (pUser_read_buf) + { + /* Use a user provided read buffer. */ + if (!user_read_buf_size) + return MZ_FALSE; + pRead_buf = (mz_uint8 *)pUser_read_buf; + read_buf_size = user_read_buf_size; + read_buf_avail = 0; + comp_remaining = file_stat.m_comp_size; + } + else + { + /* Temporarily allocate a read buffer. */ + read_buf_size = MZ_MIN(file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE); + if (((sizeof(size_t) == sizeof(mz_uint32))) && (read_buf_size > 0x7FFFFFFF)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (NULL == (pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)read_buf_size))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + read_buf_avail = 0; + comp_remaining = file_stat.m_comp_size; + } + + do + { + /* The size_t cast here should be OK because we've verified that the output buffer is >= file_stat.m_uncomp_size above */ + size_t in_buf_size, out_buf_size = (size_t)(file_stat.m_uncomp_size - out_buf_ofs); + if ((!read_buf_avail) && (!pZip->m_pState->m_pMem)) + { + read_buf_avail = MZ_MIN(read_buf_size, comp_remaining); + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) + { + status = TINFL_STATUS_FAILED; + mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED); + break; + } + cur_file_ofs += read_buf_avail; + comp_remaining -= read_buf_avail; + read_buf_ofs = 0; + } + in_buf_size = (size_t)read_buf_avail; + status = tinfl_decompress(&inflator, (mz_uint8 *)pRead_buf + read_buf_ofs, &in_buf_size, (mz_uint8 *)pBuf, (mz_uint8 *)pBuf + out_buf_ofs, &out_buf_size, TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF | (comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0)); + read_buf_avail -= in_buf_size; + read_buf_ofs += in_buf_size; + out_buf_ofs += out_buf_size; + } while (status == TINFL_STATUS_NEEDS_MORE_INPUT); + + if (status == TINFL_STATUS_DONE) + { + /* Make sure the entire file was decompressed, and check its CRC. */ + if (out_buf_ofs != file_stat.m_uncomp_size) + { + mz_zip_set_error(pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE); + status = TINFL_STATUS_FAILED; + } +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + else if (mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, (size_t)file_stat.m_uncomp_size) != file_stat.m_crc32) + { + mz_zip_set_error(pZip, MZ_ZIP_CRC_CHECK_FAILED); + status = TINFL_STATUS_FAILED; + } +#endif + } + + if ((!pZip->m_pState->m_pMem) && (!pUser_read_buf)) + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + + return status == TINFL_STATUS_DONE; +} + +mz_bool mz_zip_reader_extract_to_mem_no_alloc(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size) +{ + return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, pUser_read_buf, user_read_buf_size, NULL); +} + +mz_bool mz_zip_reader_extract_file_to_mem_no_alloc(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size) +{ + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) + return MZ_FALSE; + return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, pUser_read_buf, user_read_buf_size, NULL); +} + +mz_bool mz_zip_reader_extract_to_mem(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags) +{ + return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, NULL, 0, NULL); +} + +mz_bool mz_zip_reader_extract_file_to_mem(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags) +{ + return mz_zip_reader_extract_file_to_mem_no_alloc(pZip, pFilename, pBuf, buf_size, flags, NULL, 0); +} + +void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags) +{ + mz_zip_archive_file_stat file_stat; + mz_uint64 alloc_size; + void *pBuf; + + if (pSize) + *pSize = 0; + + if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return NULL; + + alloc_size = (flags & MZ_ZIP_FLAG_COMPRESSED_DATA) ? file_stat.m_comp_size : file_stat.m_uncomp_size; + if (((sizeof(size_t) == sizeof(mz_uint32))) && (alloc_size > 0x7FFFFFFF)) + { + mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + return NULL; + } + + if (NULL == (pBuf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)alloc_size))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + return NULL; + } + + if (!mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, (size_t)alloc_size, flags, NULL, 0, &file_stat)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return NULL; + } + + if (pSize) + *pSize = (size_t)alloc_size; + return pBuf; +} + +void *mz_zip_reader_extract_file_to_heap(mz_zip_archive *pZip, const char *pFilename, size_t *pSize, mz_uint flags) +{ + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) + { + if (pSize) + *pSize = 0; + return MZ_FALSE; + } + return mz_zip_reader_extract_to_heap(pZip, file_index, pSize, flags); +} + +mz_bool mz_zip_reader_extract_to_callback(mz_zip_archive *pZip, mz_uint file_index, mz_file_write_func pCallback, void *pOpaque, mz_uint flags) +{ + int status = TINFL_STATUS_DONE; +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + mz_uint file_crc32 = MZ_CRC32_INIT; +#endif + mz_uint64 read_buf_size, read_buf_ofs = 0, read_buf_avail, comp_remaining, out_buf_ofs = 0, cur_file_ofs; + mz_zip_archive_file_stat file_stat; + void *pRead_buf = NULL; + void *pWrite_buf = NULL; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + + if ((!pZip) || (!pZip->m_pState) || (!pCallback) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return MZ_FALSE; + + /* A directory or zero length file */ + if ((file_stat.m_is_directory) || (!file_stat.m_comp_size)) + return MZ_TRUE; + + /* Encryption and patch files are not supported. */ + if (file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + + /* This function only supports decompressing stored and deflate. */ + if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + + /* Read and do some minimal validation of the local directory entry (this doesn't crack the zip64 stuff, which we already have from the central dir) */ + cur_file_ofs = file_stat.m_local_header_ofs; + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + cur_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + if ((cur_file_ofs + file_stat.m_comp_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + /* Decompress the file either directly from memory or from a file input buffer. */ + if (pZip->m_pState->m_pMem) + { + pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + cur_file_ofs; + read_buf_size = read_buf_avail = file_stat.m_comp_size; + comp_remaining = 0; + } + else + { + read_buf_size = MZ_MIN(file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE); + if (NULL == (pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)read_buf_size))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + read_buf_avail = 0; + comp_remaining = file_stat.m_comp_size; + } + + if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!file_stat.m_method)) + { + /* The file is stored or the caller has requested the compressed data. */ + if (pZip->m_pState->m_pMem) + { + if (((sizeof(size_t) == sizeof(mz_uint32))) && (file_stat.m_comp_size > MZ_UINT32_MAX)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pCallback(pOpaque, out_buf_ofs, pRead_buf, (size_t)file_stat.m_comp_size) != file_stat.m_comp_size) + { + mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED); + status = TINFL_STATUS_FAILED; + } + else if (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + { +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + file_crc32 = (mz_uint32)mz_crc32(file_crc32, (const mz_uint8 *)pRead_buf, (size_t)file_stat.m_comp_size); +#endif + } + + cur_file_ofs += file_stat.m_comp_size; + out_buf_ofs += file_stat.m_comp_size; + comp_remaining = 0; + } + else + { + while (comp_remaining) + { + read_buf_avail = MZ_MIN(read_buf_size, comp_remaining); + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + if (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + { + file_crc32 = (mz_uint32)mz_crc32(file_crc32, (const mz_uint8 *)pRead_buf, (size_t)read_buf_avail); + } +#endif + + if (pCallback(pOpaque, out_buf_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) + { + mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + + cur_file_ofs += read_buf_avail; + out_buf_ofs += read_buf_avail; + comp_remaining -= read_buf_avail; + } + } + } + else + { + tinfl_decompressor inflator; + tinfl_init(&inflator); + + if (NULL == (pWrite_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, TINFL_LZ_DICT_SIZE))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + status = TINFL_STATUS_FAILED; + } + else + { + do + { + mz_uint8 *pWrite_buf_cur = (mz_uint8 *)pWrite_buf + (out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); + size_t in_buf_size, out_buf_size = TINFL_LZ_DICT_SIZE - (out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); + if ((!read_buf_avail) && (!pZip->m_pState->m_pMem)) + { + read_buf_avail = MZ_MIN(read_buf_size, comp_remaining); + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + cur_file_ofs += read_buf_avail; + comp_remaining -= read_buf_avail; + read_buf_ofs = 0; + } + + in_buf_size = (size_t)read_buf_avail; + status = tinfl_decompress(&inflator, (const mz_uint8 *)pRead_buf + read_buf_ofs, &in_buf_size, (mz_uint8 *)pWrite_buf, pWrite_buf_cur, &out_buf_size, comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0); + read_buf_avail -= in_buf_size; + read_buf_ofs += in_buf_size; + + if (out_buf_size) + { + if (pCallback(pOpaque, out_buf_ofs, pWrite_buf_cur, out_buf_size) != out_buf_size) + { + mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + file_crc32 = (mz_uint32)mz_crc32(file_crc32, pWrite_buf_cur, out_buf_size); +#endif + if ((out_buf_ofs += out_buf_size) > file_stat.m_uncomp_size) + { + mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + } + } while ((status == TINFL_STATUS_NEEDS_MORE_INPUT) || (status == TINFL_STATUS_HAS_MORE_OUTPUT)); + } + } + + if ((status == TINFL_STATUS_DONE) && (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA))) + { + /* Make sure the entire file was decompressed, and check its CRC. */ + if (out_buf_ofs != file_stat.m_uncomp_size) + { + mz_zip_set_error(pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE); + status = TINFL_STATUS_FAILED; + } +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + else if (file_crc32 != file_stat.m_crc32) + { + mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED); + status = TINFL_STATUS_FAILED; + } +#endif + } + + if (!pZip->m_pState->m_pMem) + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + + if (pWrite_buf) + pZip->m_pFree(pZip->m_pAlloc_opaque, pWrite_buf); + + return status == TINFL_STATUS_DONE; +} + +mz_bool mz_zip_reader_extract_file_to_callback(mz_zip_archive *pZip, const char *pFilename, mz_file_write_func pCallback, void *pOpaque, mz_uint flags) +{ + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) + return MZ_FALSE; + + return mz_zip_reader_extract_to_callback(pZip, file_index, pCallback, pOpaque, flags); +} + +mz_zip_reader_extract_iter_state* mz_zip_reader_extract_iter_new(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags) +{ + mz_zip_reader_extract_iter_state *pState; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + + /* Argument sanity check */ + if ((!pZip) || (!pZip->m_pState)) + return NULL; + + /* Allocate an iterator status structure */ + pState = (mz_zip_reader_extract_iter_state*)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_reader_extract_iter_state)); + if (!pState) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + return NULL; + } + + /* Fetch file details */ + if (!mz_zip_reader_file_stat(pZip, file_index, &pState->file_stat)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + /* Encryption and patch files are not supported. */ + if (pState->file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + /* This function only supports decompressing stored and deflate. */ + if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (pState->file_stat.m_method != 0) && (pState->file_stat.m_method != MZ_DEFLATED)) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + /* Init state - save args */ + pState->pZip = pZip; + pState->flags = flags; + + /* Init state - reset variables to defaults */ + pState->status = TINFL_STATUS_DONE; +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + pState->file_crc32 = MZ_CRC32_INIT; +#endif + pState->read_buf_ofs = 0; + pState->out_buf_ofs = 0; + pState->pRead_buf = NULL; + pState->pWrite_buf = NULL; + pState->out_blk_remain = 0; + + /* Read and parse the local directory entry. */ + pState->cur_file_ofs = pState->file_stat.m_local_header_ofs; + if (pZip->m_pRead(pZip->m_pIO_opaque, pState->cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + pState->cur_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + if ((pState->cur_file_ofs + pState->file_stat.m_comp_size) > pZip->m_archive_size) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + /* Decompress the file either directly from memory or from a file input buffer. */ + if (pZip->m_pState->m_pMem) + { + pState->pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + pState->cur_file_ofs; + pState->read_buf_size = pState->read_buf_avail = pState->file_stat.m_comp_size; + pState->comp_remaining = pState->file_stat.m_comp_size; + } + else + { + if (!((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method))) + { + /* Decompression required, therefore intermediate read buffer required */ + pState->read_buf_size = MZ_MIN(pState->file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE); + if (NULL == (pState->pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)pState->read_buf_size))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + } + else + { + /* Decompression not required - we will be reading directly into user buffer, no temp buf required */ + pState->read_buf_size = 0; + } + pState->read_buf_avail = 0; + pState->comp_remaining = pState->file_stat.m_comp_size; + } + + if (!((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method))) + { + /* Decompression required, init decompressor */ + tinfl_init( &pState->inflator ); + + /* Allocate write buffer */ + if (NULL == (pState->pWrite_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, TINFL_LZ_DICT_SIZE))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + if (pState->pRead_buf) + pZip->m_pFree(pZip->m_pAlloc_opaque, pState->pRead_buf); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + } + + return pState; +} + +mz_zip_reader_extract_iter_state* mz_zip_reader_extract_file_iter_new(mz_zip_archive *pZip, const char *pFilename, mz_uint flags) +{ + mz_uint32 file_index; + + /* Locate file index by name */ + if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) + return NULL; + + /* Construct iterator */ + return mz_zip_reader_extract_iter_new(pZip, file_index, flags); +} + +size_t mz_zip_reader_extract_iter_read(mz_zip_reader_extract_iter_state* pState, void* pvBuf, size_t buf_size) +{ + size_t copied_to_caller = 0; + + /* Argument sanity check */ + if ((!pState) || (!pState->pZip) || (!pState->pZip->m_pState) || (!pvBuf)) + return 0; + + if ((pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method)) + { + /* The file is stored or the caller has requested the compressed data, calc amount to return. */ + copied_to_caller = (size_t)MZ_MIN( buf_size, pState->comp_remaining ); + + /* Zip is in memory....or requires reading from a file? */ + if (pState->pZip->m_pState->m_pMem) + { + /* Copy data to caller's buffer */ + memcpy( pvBuf, pState->pRead_buf, copied_to_caller ); + pState->pRead_buf = ((mz_uint8*)pState->pRead_buf) + copied_to_caller; + } + else + { + /* Read directly into caller's buffer */ + if (pState->pZip->m_pRead(pState->pZip->m_pIO_opaque, pState->cur_file_ofs, pvBuf, copied_to_caller) != copied_to_caller) + { + /* Failed to read all that was asked for, flag failure and alert user */ + mz_zip_set_error(pState->pZip, MZ_ZIP_FILE_READ_FAILED); + pState->status = TINFL_STATUS_FAILED; + copied_to_caller = 0; + } + } + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + /* Compute CRC if not returning compressed data only */ + if (!(pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + pState->file_crc32 = (mz_uint32)mz_crc32(pState->file_crc32, (const mz_uint8 *)pvBuf, copied_to_caller); +#endif + + /* Advance offsets, dec counters */ + pState->cur_file_ofs += copied_to_caller; + pState->out_buf_ofs += copied_to_caller; + pState->comp_remaining -= copied_to_caller; + } + else + { + do + { + /* Calc ptr to write buffer - given current output pos and block size */ + mz_uint8 *pWrite_buf_cur = (mz_uint8 *)pState->pWrite_buf + (pState->out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); + + /* Calc max output size - given current output pos and block size */ + size_t in_buf_size, out_buf_size = TINFL_LZ_DICT_SIZE - (pState->out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); + + if (!pState->out_blk_remain) + { + /* Read more data from file if none available (and reading from file) */ + if ((!pState->read_buf_avail) && (!pState->pZip->m_pState->m_pMem)) + { + /* Calc read size */ + pState->read_buf_avail = MZ_MIN(pState->read_buf_size, pState->comp_remaining); + if (pState->pZip->m_pRead(pState->pZip->m_pIO_opaque, pState->cur_file_ofs, pState->pRead_buf, (size_t)pState->read_buf_avail) != pState->read_buf_avail) + { + mz_zip_set_error(pState->pZip, MZ_ZIP_FILE_READ_FAILED); + pState->status = TINFL_STATUS_FAILED; + break; + } + + /* Advance offsets, dec counters */ + pState->cur_file_ofs += pState->read_buf_avail; + pState->comp_remaining -= pState->read_buf_avail; + pState->read_buf_ofs = 0; + } + + /* Perform decompression */ + in_buf_size = (size_t)pState->read_buf_avail; + pState->status = tinfl_decompress(&pState->inflator, (const mz_uint8 *)pState->pRead_buf + pState->read_buf_ofs, &in_buf_size, (mz_uint8 *)pState->pWrite_buf, pWrite_buf_cur, &out_buf_size, pState->comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0); + pState->read_buf_avail -= in_buf_size; + pState->read_buf_ofs += in_buf_size; + + /* Update current output block size remaining */ + pState->out_blk_remain = out_buf_size; + } + + if (pState->out_blk_remain) + { + /* Calc amount to return. */ + size_t to_copy = MZ_MIN( (buf_size - copied_to_caller), pState->out_blk_remain ); + + /* Copy data to caller's buffer */ + memcpy( (mz_uint8*)pvBuf + copied_to_caller, pWrite_buf_cur, to_copy ); + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + /* Perform CRC */ + pState->file_crc32 = (mz_uint32)mz_crc32(pState->file_crc32, pWrite_buf_cur, to_copy); +#endif + + /* Decrement data consumed from block */ + pState->out_blk_remain -= to_copy; + + /* Inc output offset, while performing sanity check */ + if ((pState->out_buf_ofs += to_copy) > pState->file_stat.m_uncomp_size) + { + mz_zip_set_error(pState->pZip, MZ_ZIP_DECOMPRESSION_FAILED); + pState->status = TINFL_STATUS_FAILED; + break; + } + + /* Increment counter of data copied to caller */ + copied_to_caller += to_copy; + } + } while ( (copied_to_caller < buf_size) && ((pState->status == TINFL_STATUS_NEEDS_MORE_INPUT) || (pState->status == TINFL_STATUS_HAS_MORE_OUTPUT)) ); + } + + /* Return how many bytes were copied into user buffer */ + return copied_to_caller; +} + +mz_bool mz_zip_reader_extract_iter_free(mz_zip_reader_extract_iter_state* pState) +{ + int status; + + /* Argument sanity check */ + if ((!pState) || (!pState->pZip) || (!pState->pZip->m_pState)) + return MZ_FALSE; + + /* Was decompression completed and requested? */ + if ((pState->status == TINFL_STATUS_DONE) && (!(pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA))) + { + /* Make sure the entire file was decompressed, and check its CRC. */ + if (pState->out_buf_ofs != pState->file_stat.m_uncomp_size) + { + mz_zip_set_error(pState->pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE); + pState->status = TINFL_STATUS_FAILED; + } +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + else if (pState->file_crc32 != pState->file_stat.m_crc32) + { + mz_zip_set_error(pState->pZip, MZ_ZIP_DECOMPRESSION_FAILED); + pState->status = TINFL_STATUS_FAILED; + } +#endif + } + + /* Free buffers */ + if (!pState->pZip->m_pState->m_pMem) + pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState->pRead_buf); + if (pState->pWrite_buf) + pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState->pWrite_buf); + + /* Save status */ + status = pState->status; + + /* Free context */ + pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState); + + return status == TINFL_STATUS_DONE; +} + +#ifndef MINIZ_NO_STDIO +static size_t mz_zip_file_write_callback(void *pOpaque, mz_uint64 ofs, const void *pBuf, size_t n) +{ + (void)ofs; + + return MZ_FWRITE(pBuf, 1, n, (MZ_FILE *)pOpaque); +} + +mz_bool mz_zip_reader_extract_to_file(mz_zip_archive *pZip, mz_uint file_index, const char *pDst_filename, mz_uint flags) +{ + mz_bool status; + mz_zip_archive_file_stat file_stat; + MZ_FILE *pFile; + + if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return MZ_FALSE; + + if ((file_stat.m_is_directory) || (!file_stat.m_is_supported)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); + + pFile = MZ_FOPEN(pDst_filename, "wb"); + if (!pFile) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + + status = mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_file_write_callback, pFile, flags); + + if (MZ_FCLOSE(pFile) == EOF) + { + if (status) + mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED); + + status = MZ_FALSE; + } + +#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_STDIO) + if (status) + mz_zip_set_file_times(pDst_filename, file_stat.m_time, file_stat.m_time); +#endif + + return status; +} + +mz_bool mz_zip_reader_extract_file_to_file(mz_zip_archive *pZip, const char *pArchive_filename, const char *pDst_filename, mz_uint flags) +{ + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pArchive_filename, NULL, flags, &file_index)) + return MZ_FALSE; + + return mz_zip_reader_extract_to_file(pZip, file_index, pDst_filename, flags); +} + +mz_bool mz_zip_reader_extract_to_cfile(mz_zip_archive *pZip, mz_uint file_index, MZ_FILE *pFile, mz_uint flags) +{ + mz_zip_archive_file_stat file_stat; + + if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return MZ_FALSE; + + if ((file_stat.m_is_directory) || (!file_stat.m_is_supported)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); + + return mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_file_write_callback, pFile, flags); +} + +mz_bool mz_zip_reader_extract_file_to_cfile(mz_zip_archive *pZip, const char *pArchive_filename, MZ_FILE *pFile, mz_uint flags) +{ + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pArchive_filename, NULL, flags, &file_index)) + return MZ_FALSE; + + return mz_zip_reader_extract_to_cfile(pZip, file_index, pFile, flags); +} +#endif /* #ifndef MINIZ_NO_STDIO */ + +static size_t mz_zip_compute_crc32_callback(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n) +{ + mz_uint32 *p = (mz_uint32 *)pOpaque; + (void)file_ofs; + *p = (mz_uint32)mz_crc32(*p, (const mz_uint8 *)pBuf, n); + return n; +} + +mz_bool mz_zip_validate_file(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags) +{ + mz_zip_archive_file_stat file_stat; + mz_zip_internal_state *pState; + const mz_uint8 *pCentral_dir_header; + mz_bool found_zip64_ext_data_in_cdir = MZ_FALSE; + mz_bool found_zip64_ext_data_in_ldir = MZ_FALSE; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + mz_uint64 local_header_ofs = 0; + mz_uint32 local_header_filename_len, local_header_extra_len, local_header_crc32; + mz_uint64 local_header_comp_size, local_header_uncomp_size; + mz_uint32 uncomp_crc32 = MZ_CRC32_INIT; + mz_bool has_data_descriptor; + mz_uint32 local_header_bit_flags; + + mz_zip_array file_data_array; + mz_zip_array_init(&file_data_array, 1); + + if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (file_index > pZip->m_total_files) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + pCentral_dir_header = mz_zip_get_cdh(pZip, file_index); + + if (!mz_zip_file_stat_internal(pZip, file_index, pCentral_dir_header, &file_stat, &found_zip64_ext_data_in_cdir)) + return MZ_FALSE; + + /* A directory or zero length file */ + if ((file_stat.m_is_directory) || (!file_stat.m_uncomp_size)) + return MZ_TRUE; + + /* Encryption and patch files are not supported. */ + if (file_stat.m_is_encrypted) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + + /* This function only supports stored and deflate. */ + if ((file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + + if (!file_stat.m_is_supported) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); + + /* Read and parse the local directory entry. */ + local_header_ofs = file_stat.m_local_header_ofs; + if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + local_header_filename_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS); + local_header_extra_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + local_header_comp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS); + local_header_uncomp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS); + local_header_crc32 = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_CRC32_OFS); + local_header_bit_flags = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_BIT_FLAG_OFS); + has_data_descriptor = (local_header_bit_flags & 8) != 0; + + if (local_header_filename_len != strlen(file_stat.m_filename)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if ((local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len + local_header_extra_len + file_stat.m_comp_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (!mz_zip_array_resize(pZip, &file_data_array, MZ_MAX(local_header_filename_len, local_header_extra_len), MZ_FALSE)) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + goto handle_failure; + } + + if (local_header_filename_len) + { + if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE, file_data_array.m_p, local_header_filename_len) != local_header_filename_len) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + goto handle_failure; + } + + /* I've seen 1 archive that had the same pathname, but used backslashes in the local dir and forward slashes in the central dir. Do we care about this? For now, this case will fail validation. */ + if (memcmp(file_stat.m_filename, file_data_array.m_p, local_header_filename_len) != 0) + { + mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + goto handle_failure; + } + } + + if ((local_header_extra_len) && ((local_header_comp_size == MZ_UINT32_MAX) || (local_header_uncomp_size == MZ_UINT32_MAX))) + { + mz_uint32 extra_size_remaining = local_header_extra_len; + const mz_uint8 *pExtra_data = (const mz_uint8 *)file_data_array.m_p; + + if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len, file_data_array.m_p, local_header_extra_len) != local_header_extra_len) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + goto handle_failure; + } + + do + { + mz_uint32 field_id, field_data_size, field_total_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + goto handle_failure; + } + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + field_total_size = field_data_size + sizeof(mz_uint16) * 2; + + if (field_total_size > extra_size_remaining) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + goto handle_failure; + } + + if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + const mz_uint8 *pSrc_field_data = pExtra_data + sizeof(mz_uint32); + + if (field_data_size < sizeof(mz_uint64) * 2) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + goto handle_failure; + } + + local_header_uncomp_size = MZ_READ_LE64(pSrc_field_data); + local_header_comp_size = MZ_READ_LE64(pSrc_field_data + sizeof(mz_uint64)); + + found_zip64_ext_data_in_ldir = MZ_TRUE; + break; + } + + pExtra_data += field_total_size; + extra_size_remaining -= field_total_size; + } while (extra_size_remaining); + } + + /* TODO: parse local header extra data when local_header_comp_size is 0xFFFFFFFF! (big_descriptor.zip) */ + /* I've seen zips in the wild with the data descriptor bit set, but proper local header values and bogus data descriptors */ + if ((has_data_descriptor) && (!local_header_comp_size) && (!local_header_crc32)) + { + mz_uint8 descriptor_buf[32]; + mz_bool has_id; + const mz_uint8 *pSrc; + mz_uint32 file_crc32; + mz_uint64 comp_size = 0, uncomp_size = 0; + + mz_uint32 num_descriptor_uint32s = ((pState->m_zip64) || (found_zip64_ext_data_in_ldir)) ? 6 : 4; + + if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len + local_header_extra_len + file_stat.m_comp_size, descriptor_buf, sizeof(mz_uint32) * num_descriptor_uint32s) != (sizeof(mz_uint32) * num_descriptor_uint32s)) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + goto handle_failure; + } + + has_id = (MZ_READ_LE32(descriptor_buf) == MZ_ZIP_DATA_DESCRIPTOR_ID); + pSrc = has_id ? (descriptor_buf + sizeof(mz_uint32)) : descriptor_buf; + + file_crc32 = MZ_READ_LE32(pSrc); + + if ((pState->m_zip64) || (found_zip64_ext_data_in_ldir)) + { + comp_size = MZ_READ_LE64(pSrc + sizeof(mz_uint32)); + uncomp_size = MZ_READ_LE64(pSrc + sizeof(mz_uint32) + sizeof(mz_uint64)); + } + else + { + comp_size = MZ_READ_LE32(pSrc + sizeof(mz_uint32)); + uncomp_size = MZ_READ_LE32(pSrc + sizeof(mz_uint32) + sizeof(mz_uint32)); + } + + if ((file_crc32 != file_stat.m_crc32) || (comp_size != file_stat.m_comp_size) || (uncomp_size != file_stat.m_uncomp_size)) + { + mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + goto handle_failure; + } + } + else + { + if ((local_header_crc32 != file_stat.m_crc32) || (local_header_comp_size != file_stat.m_comp_size) || (local_header_uncomp_size != file_stat.m_uncomp_size)) + { + mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + goto handle_failure; + } + } + + mz_zip_array_clear(pZip, &file_data_array); + + if ((flags & MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY) == 0) + { + if (!mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_compute_crc32_callback, &uncomp_crc32, 0)) + return MZ_FALSE; + + /* 1 more check to be sure, although the extract checks too. */ + if (uncomp_crc32 != file_stat.m_crc32) + { + mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + return MZ_FALSE; + } + } + + return MZ_TRUE; + +handle_failure: + mz_zip_array_clear(pZip, &file_data_array); + return MZ_FALSE; +} + +mz_bool mz_zip_validate_archive(mz_zip_archive *pZip, mz_uint flags) +{ + mz_zip_internal_state *pState; + mz_uint32 i; + + if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + /* Basic sanity checks */ + if (!pState->m_zip64) + { + if (pZip->m_total_files > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + if (pZip->m_archive_size > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + } + else + { + if (pState->m_central_dir.m_size >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + } + + for (i = 0; i < pZip->m_total_files; i++) + { + if (MZ_ZIP_FLAG_VALIDATE_LOCATE_FILE_FLAG & flags) + { + mz_uint32 found_index; + mz_zip_archive_file_stat stat; + + if (!mz_zip_reader_file_stat(pZip, i, &stat)) + return MZ_FALSE; + + if (!mz_zip_reader_locate_file_v2(pZip, stat.m_filename, NULL, 0, &found_index)) + return MZ_FALSE; + + /* This check can fail if there are duplicate filenames in the archive (which we don't check for when writing - that's up to the user) */ + if (found_index != i) + return mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + } + + if (!mz_zip_validate_file(pZip, i, flags)) + return MZ_FALSE; + } + + return MZ_TRUE; +} + +mz_bool mz_zip_validate_mem_archive(const void *pMem, size_t size, mz_uint flags, mz_zip_error *pErr) +{ + mz_bool success = MZ_TRUE; + mz_zip_archive zip; + mz_zip_error actual_err = MZ_ZIP_NO_ERROR; + + if ((!pMem) || (!size)) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_PARAMETER; + return MZ_FALSE; + } + + mz_zip_zero_struct(&zip); + + if (!mz_zip_reader_init_mem(&zip, pMem, size, flags)) + { + if (pErr) + *pErr = zip.m_last_error; + return MZ_FALSE; + } + + if (!mz_zip_validate_archive(&zip, flags)) + { + actual_err = zip.m_last_error; + success = MZ_FALSE; + } + + if (!mz_zip_reader_end_internal(&zip, success)) + { + if (!actual_err) + actual_err = zip.m_last_error; + success = MZ_FALSE; + } + + if (pErr) + *pErr = actual_err; + + return success; +} + +#ifndef MINIZ_NO_STDIO +mz_bool mz_zip_validate_file_archive(const char *pFilename, mz_uint flags, mz_zip_error *pErr) +{ + mz_bool success = MZ_TRUE; + mz_zip_archive zip; + mz_zip_error actual_err = MZ_ZIP_NO_ERROR; + + if (!pFilename) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_PARAMETER; + return MZ_FALSE; + } + + mz_zip_zero_struct(&zip); + + if (!mz_zip_reader_init_file_v2(&zip, pFilename, flags, 0, 0)) + { + if (pErr) + *pErr = zip.m_last_error; + return MZ_FALSE; + } + + if (!mz_zip_validate_archive(&zip, flags)) + { + actual_err = zip.m_last_error; + success = MZ_FALSE; + } + + if (!mz_zip_reader_end_internal(&zip, success)) + { + if (!actual_err) + actual_err = zip.m_last_error; + success = MZ_FALSE; + } + + if (pErr) + *pErr = actual_err; + + return success; +} +#endif /* #ifndef MINIZ_NO_STDIO */ + +/* ------------------- .ZIP archive writing */ + +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS + +static MZ_FORCEINLINE void mz_write_le16(mz_uint8 *p, mz_uint16 v) +{ + p[0] = (mz_uint8)v; + p[1] = (mz_uint8)(v >> 8); +} +static MZ_FORCEINLINE void mz_write_le32(mz_uint8 *p, mz_uint32 v) +{ + p[0] = (mz_uint8)v; + p[1] = (mz_uint8)(v >> 8); + p[2] = (mz_uint8)(v >> 16); + p[3] = (mz_uint8)(v >> 24); +} +static MZ_FORCEINLINE void mz_write_le64(mz_uint8 *p, mz_uint64 v) +{ + mz_write_le32(p, (mz_uint32)v); + mz_write_le32(p + sizeof(mz_uint32), (mz_uint32)(v >> 32)); +} + +#define MZ_WRITE_LE16(p, v) mz_write_le16((mz_uint8 *)(p), (mz_uint16)(v)) +#define MZ_WRITE_LE32(p, v) mz_write_le32((mz_uint8 *)(p), (mz_uint32)(v)) +#define MZ_WRITE_LE64(p, v) mz_write_le64((mz_uint8 *)(p), (mz_uint64)(v)) + +static size_t mz_zip_heap_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n) +{ + mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; + mz_zip_internal_state *pState = pZip->m_pState; + mz_uint64 new_size = MZ_MAX(file_ofs + n, pState->m_mem_size); + + if (!n) + return 0; + + /* An allocation this big is likely to just fail on 32-bit systems, so don't even go there. */ + if ((sizeof(size_t) == sizeof(mz_uint32)) && (new_size > 0x7FFFFFFF)) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE); + return 0; + } + + if (new_size > pState->m_mem_capacity) + { + void *pNew_block; + size_t new_capacity = MZ_MAX(64, pState->m_mem_capacity); + + while (new_capacity < new_size) + new_capacity *= 2; + + if (NULL == (pNew_block = pZip->m_pRealloc(pZip->m_pAlloc_opaque, pState->m_pMem, 1, new_capacity))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + return 0; + } + + pState->m_pMem = pNew_block; + pState->m_mem_capacity = new_capacity; + } + memcpy((mz_uint8 *)pState->m_pMem + file_ofs, pBuf, n); + pState->m_mem_size = (size_t)new_size; + return n; +} + +static mz_bool mz_zip_writer_end_internal(mz_zip_archive *pZip, mz_bool set_last_error) +{ + mz_zip_internal_state *pState; + mz_bool status = MZ_TRUE; + + if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || ((pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) && (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED))) + { + if (set_last_error) + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return MZ_FALSE; + } + + pState = pZip->m_pState; + pZip->m_pState = NULL; + mz_zip_array_clear(pZip, &pState->m_central_dir); + mz_zip_array_clear(pZip, &pState->m_central_dir_offsets); + mz_zip_array_clear(pZip, &pState->m_sorted_central_dir_offsets); + +#ifndef MINIZ_NO_STDIO + if (pState->m_pFile) + { + if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE) + { + if (MZ_FCLOSE(pState->m_pFile) == EOF) + { + if (set_last_error) + mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED); + status = MZ_FALSE; + } + } + + pState->m_pFile = NULL; + } +#endif /* #ifndef MINIZ_NO_STDIO */ + + if ((pZip->m_pWrite == mz_zip_heap_write_func) && (pState->m_pMem)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pState->m_pMem); + pState->m_pMem = NULL; + } + + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + pZip->m_zip_mode = MZ_ZIP_MODE_INVALID; + return status; +} + +mz_bool mz_zip_writer_init_v2(mz_zip_archive *pZip, mz_uint64 existing_size, mz_uint flags) +{ + mz_bool zip64 = (flags & MZ_ZIP_FLAG_WRITE_ZIP64) != 0; + + if ((!pZip) || (pZip->m_pState) || (!pZip->m_pWrite) || (pZip->m_zip_mode != MZ_ZIP_MODE_INVALID)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) + { + if (!pZip->m_pRead) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + } + + if (pZip->m_file_offset_alignment) + { + /* Ensure user specified file offset alignment is a power of 2. */ + if (pZip->m_file_offset_alignment & (pZip->m_file_offset_alignment - 1)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + } + + if (!pZip->m_pAlloc) + pZip->m_pAlloc = miniz_def_alloc_func; + if (!pZip->m_pFree) + pZip->m_pFree = miniz_def_free_func; + if (!pZip->m_pRealloc) + pZip->m_pRealloc = miniz_def_realloc_func; + + pZip->m_archive_size = existing_size; + pZip->m_central_directory_file_ofs = 0; + pZip->m_total_files = 0; + + if (NULL == (pZip->m_pState = (mz_zip_internal_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_internal_state)))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + memset(pZip->m_pState, 0, sizeof(mz_zip_internal_state)); + + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir, sizeof(mz_uint8)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir_offsets, sizeof(mz_uint32)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_sorted_central_dir_offsets, sizeof(mz_uint32)); + + pZip->m_pState->m_zip64 = zip64; + pZip->m_pState->m_zip64_has_extended_info_fields = zip64; + + pZip->m_zip_type = MZ_ZIP_TYPE_USER; + pZip->m_zip_mode = MZ_ZIP_MODE_WRITING; + + return MZ_TRUE; +} + +mz_bool mz_zip_writer_init(mz_zip_archive *pZip, mz_uint64 existing_size) +{ + return mz_zip_writer_init_v2(pZip, existing_size, 0); +} + +mz_bool mz_zip_writer_init_heap_v2(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size, mz_uint flags) +{ + pZip->m_pWrite = mz_zip_heap_write_func; + pZip->m_pNeeds_keepalive = NULL; + + if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) + pZip->m_pRead = mz_zip_mem_read_func; + + pZip->m_pIO_opaque = pZip; + + if (!mz_zip_writer_init_v2(pZip, size_to_reserve_at_beginning, flags)) + return MZ_FALSE; + + pZip->m_zip_type = MZ_ZIP_TYPE_HEAP; + + if (0 != (initial_allocation_size = MZ_MAX(initial_allocation_size, size_to_reserve_at_beginning))) + { + if (NULL == (pZip->m_pState->m_pMem = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, initial_allocation_size))) + { + mz_zip_writer_end_internal(pZip, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + pZip->m_pState->m_mem_capacity = initial_allocation_size; + } + + return MZ_TRUE; +} + +mz_bool mz_zip_writer_init_heap(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size) +{ + return mz_zip_writer_init_heap_v2(pZip, size_to_reserve_at_beginning, initial_allocation_size, 0); +} + +#ifndef MINIZ_NO_STDIO +static size_t mz_zip_file_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n) +{ + mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; + mz_int64 cur_ofs = MZ_FTELL64(pZip->m_pState->m_pFile); + + file_ofs += pZip->m_pState->m_file_archive_start_ofs; + + if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pZip->m_pState->m_pFile, (mz_int64)file_ofs, SEEK_SET)))) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED); + return 0; + } + + return MZ_FWRITE(pBuf, 1, n, pZip->m_pState->m_pFile); +} + +mz_bool mz_zip_writer_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning) +{ + return mz_zip_writer_init_file_v2(pZip, pFilename, size_to_reserve_at_beginning, 0); +} + +mz_bool mz_zip_writer_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning, mz_uint flags) +{ + MZ_FILE *pFile; + + pZip->m_pWrite = mz_zip_file_write_func; + pZip->m_pNeeds_keepalive = NULL; + + if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) + pZip->m_pRead = mz_zip_file_read_func; + + pZip->m_pIO_opaque = pZip; + + if (!mz_zip_writer_init_v2(pZip, size_to_reserve_at_beginning, flags)) + return MZ_FALSE; + + if (NULL == (pFile = MZ_FOPEN(pFilename, (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) ? "w+b" : "wb"))) + { + mz_zip_writer_end(pZip); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + } + + pZip->m_pState->m_pFile = pFile; + pZip->m_zip_type = MZ_ZIP_TYPE_FILE; + + if (size_to_reserve_at_beginning) + { + mz_uint64 cur_ofs = 0; + char buf[4096]; + + MZ_CLEAR_ARR(buf); + + do + { + size_t n = (size_t)MZ_MIN(sizeof(buf), size_to_reserve_at_beginning); + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_ofs, buf, n) != n) + { + mz_zip_writer_end(pZip); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + cur_ofs += n; + size_to_reserve_at_beginning -= n; + } while (size_to_reserve_at_beginning); + } + + return MZ_TRUE; +} + +mz_bool mz_zip_writer_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint flags) +{ + pZip->m_pWrite = mz_zip_file_write_func; + pZip->m_pNeeds_keepalive = NULL; + + if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) + pZip->m_pRead = mz_zip_file_read_func; + + pZip->m_pIO_opaque = pZip; + + if (!mz_zip_writer_init_v2(pZip, 0, flags)) + return MZ_FALSE; + + pZip->m_pState->m_pFile = pFile; + pZip->m_pState->m_file_archive_start_ofs = MZ_FTELL64(pZip->m_pState->m_pFile); + pZip->m_zip_type = MZ_ZIP_TYPE_CFILE; + + return MZ_TRUE; +} +#endif /* #ifndef MINIZ_NO_STDIO */ + +mz_bool mz_zip_writer_init_from_reader_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags) +{ + mz_zip_internal_state *pState; + + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_READING)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (flags & MZ_ZIP_FLAG_WRITE_ZIP64) + { + /* We don't support converting a non-zip64 file to zip64 - this seems like more trouble than it's worth. (What about the existing 32-bit data descriptors that could follow the compressed data?) */ + if (!pZip->m_pState->m_zip64) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + } + + /* No sense in trying to write to an archive that's already at the support max size */ + if (pZip->m_pState->m_zip64) + { + if (pZip->m_total_files == MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + if (pZip->m_total_files == MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + + if ((pZip->m_archive_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_ZIP_LOCAL_DIR_HEADER_SIZE) > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE); + } + + pState = pZip->m_pState; + + if (pState->m_pFile) + { +#ifdef MINIZ_NO_STDIO + (void)pFilename; + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); +#else + if (pZip->m_pIO_opaque != pZip) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE) + { + if (!pFilename) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* Archive is being read from stdio and was originally opened only for reading. Try to reopen as writable. */ + if (NULL == (pState->m_pFile = MZ_FREOPEN(pFilename, "r+b", pState->m_pFile))) + { + /* The mz_zip_archive is now in a bogus state because pState->m_pFile is NULL, so just close it. */ + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + } + } + + pZip->m_pWrite = mz_zip_file_write_func; + pZip->m_pNeeds_keepalive = NULL; +#endif /* #ifdef MINIZ_NO_STDIO */ + } + else if (pState->m_pMem) + { + /* Archive lives in a memory block. Assume it's from the heap that we can resize using the realloc callback. */ + if (pZip->m_pIO_opaque != pZip) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState->m_mem_capacity = pState->m_mem_size; + pZip->m_pWrite = mz_zip_heap_write_func; + pZip->m_pNeeds_keepalive = NULL; + } + /* Archive is being read via a user provided read function - make sure the user has specified a write function too. */ + else if (!pZip->m_pWrite) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* Start writing new files at the archive's current central directory location. */ + /* TODO: We could add a flag that lets the user start writing immediately AFTER the existing central dir - this would be safer. */ + pZip->m_archive_size = pZip->m_central_directory_file_ofs; + pZip->m_central_directory_file_ofs = 0; + + /* Clear the sorted central dir offsets, they aren't useful or maintained now. */ + /* Even though we're now in write mode, files can still be extracted and verified, but file locates will be slow. */ + /* TODO: We could easily maintain the sorted central directory offsets. */ + mz_zip_array_clear(pZip, &pZip->m_pState->m_sorted_central_dir_offsets); + + pZip->m_zip_mode = MZ_ZIP_MODE_WRITING; + + return MZ_TRUE; +} + +mz_bool mz_zip_writer_init_from_reader(mz_zip_archive *pZip, const char *pFilename) +{ + return mz_zip_writer_init_from_reader_v2(pZip, pFilename, 0); +} + +/* TODO: pArchive_name is a terrible name here! */ +mz_bool mz_zip_writer_add_mem(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, mz_uint level_and_flags) +{ + return mz_zip_writer_add_mem_ex(pZip, pArchive_name, pBuf, buf_size, NULL, 0, level_and_flags, 0, 0); +} + +typedef struct +{ + mz_zip_archive *m_pZip; + mz_uint64 m_cur_archive_file_ofs; + mz_uint64 m_comp_size; +} mz_zip_writer_add_state; + +static mz_bool mz_zip_writer_add_put_buf_callback(const void *pBuf, int len, void *pUser) +{ + mz_zip_writer_add_state *pState = (mz_zip_writer_add_state *)pUser; + if ((int)pState->m_pZip->m_pWrite(pState->m_pZip->m_pIO_opaque, pState->m_cur_archive_file_ofs, pBuf, len) != len) + return MZ_FALSE; + + pState->m_cur_archive_file_ofs += len; + pState->m_comp_size += len; + return MZ_TRUE; +} + +#define MZ_ZIP64_MAX_LOCAL_EXTRA_FIELD_SIZE (sizeof(mz_uint16) * 2 + sizeof(mz_uint64) * 2) +#define MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE (sizeof(mz_uint16) * 2 + sizeof(mz_uint64) * 3) +static mz_uint32 mz_zip_writer_create_zip64_extra_data(mz_uint8 *pBuf, mz_uint64 *pUncomp_size, mz_uint64 *pComp_size, mz_uint64 *pLocal_header_ofs) +{ + mz_uint8 *pDst = pBuf; + mz_uint32 field_size = 0; + + MZ_WRITE_LE16(pDst + 0, MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID); + MZ_WRITE_LE16(pDst + 2, 0); + pDst += sizeof(mz_uint16) * 2; + + if (pUncomp_size) + { + MZ_WRITE_LE64(pDst, *pUncomp_size); + pDst += sizeof(mz_uint64); + field_size += sizeof(mz_uint64); + } + + if (pComp_size) + { + MZ_WRITE_LE64(pDst, *pComp_size); + pDst += sizeof(mz_uint64); + field_size += sizeof(mz_uint64); + } + + if (pLocal_header_ofs) + { + MZ_WRITE_LE64(pDst, *pLocal_header_ofs); + pDst += sizeof(mz_uint64); + field_size += sizeof(mz_uint64); + } + + MZ_WRITE_LE16(pBuf + 2, field_size); + + return (mz_uint32)(pDst - pBuf); +} + +static mz_bool mz_zip_writer_create_local_dir_header(mz_zip_archive *pZip, mz_uint8 *pDst, mz_uint16 filename_size, mz_uint16 extra_size, mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date) +{ + (void)pZip; + memset(pDst, 0, MZ_ZIP_LOCAL_DIR_HEADER_SIZE); + MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_SIG_OFS, MZ_ZIP_LOCAL_DIR_HEADER_SIG); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_VERSION_NEEDED_OFS, method ? 20 : 0); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_BIT_FLAG_OFS, bit_flags); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_METHOD_OFS, method); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILE_TIME_OFS, dos_time); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILE_DATE_OFS, dos_date); + MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_CRC32_OFS, uncomp_crc32); + MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS, MZ_MIN(comp_size, MZ_UINT32_MAX)); + MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS, MZ_MIN(uncomp_size, MZ_UINT32_MAX)); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILENAME_LEN_OFS, filename_size); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_EXTRA_LEN_OFS, extra_size); + return MZ_TRUE; +} + +static mz_bool mz_zip_writer_create_central_dir_header(mz_zip_archive *pZip, mz_uint8 *pDst, + mz_uint16 filename_size, mz_uint16 extra_size, mz_uint16 comment_size, + mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, + mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date, + mz_uint64 local_header_ofs, mz_uint32 ext_attributes) +{ + (void)pZip; + memset(pDst, 0, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_SIG_OFS, MZ_ZIP_CENTRAL_DIR_HEADER_SIG); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_VERSION_NEEDED_OFS, method ? 20 : 0); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_BIT_FLAG_OFS, bit_flags); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_METHOD_OFS, method); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILE_TIME_OFS, dos_time); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILE_DATE_OFS, dos_date); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_CRC32_OFS, uncomp_crc32); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS, MZ_MIN(comp_size, MZ_UINT32_MAX)); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS, MZ_MIN(uncomp_size, MZ_UINT32_MAX)); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILENAME_LEN_OFS, filename_size); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_EXTRA_LEN_OFS, extra_size); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_COMMENT_LEN_OFS, comment_size); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS, ext_attributes); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_LOCAL_HEADER_OFS, MZ_MIN(local_header_ofs, MZ_UINT32_MAX)); + return MZ_TRUE; +} + +static mz_bool mz_zip_writer_add_to_central_dir(mz_zip_archive *pZip, const char *pFilename, mz_uint16 filename_size, + const void *pExtra, mz_uint16 extra_size, const void *pComment, mz_uint16 comment_size, + mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, + mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date, + mz_uint64 local_header_ofs, mz_uint32 ext_attributes, + const char *user_extra_data, mz_uint user_extra_data_len) +{ + mz_zip_internal_state *pState = pZip->m_pState; + mz_uint32 central_dir_ofs = (mz_uint32)pState->m_central_dir.m_size; + size_t orig_central_dir_size = pState->m_central_dir.m_size; + mz_uint8 central_dir_header[MZ_ZIP_CENTRAL_DIR_HEADER_SIZE]; + + if (!pZip->m_pState->m_zip64) + { + if (local_header_ofs > 0xFFFFFFFF) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE); + } + + /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */ + if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size + extra_size + user_extra_data_len + comment_size) >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + if (!mz_zip_writer_create_central_dir_header(pZip, central_dir_header, filename_size, (mz_uint16)(extra_size + user_extra_data_len), comment_size, uncomp_size, comp_size, uncomp_crc32, method, bit_flags, dos_time, dos_date, local_header_ofs, ext_attributes)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if ((!mz_zip_array_push_back(pZip, &pState->m_central_dir, central_dir_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pFilename, filename_size)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pExtra, extra_size)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir, user_extra_data, user_extra_data_len)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pComment, comment_size)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir_offsets, ¢ral_dir_ofs, 1))) + { + /* Try to resize the central directory array back into its original state. */ + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + return MZ_TRUE; +} + +static mz_bool mz_zip_writer_validate_archive_name(const char *pArchive_name) +{ + /* Basic ZIP archive filename validity checks: Valid filenames cannot start with a forward slash, cannot contain a drive letter, and cannot use DOS-style backward slashes. */ + if (*pArchive_name == '/') + return MZ_FALSE; + + /* Making sure the name does not contain drive letters or DOS style backward slashes is the responsibility of the program using miniz*/ + + return MZ_TRUE; +} + +static mz_uint mz_zip_writer_compute_padding_needed_for_file_alignment(mz_zip_archive *pZip) +{ + mz_uint32 n; + if (!pZip->m_file_offset_alignment) + return 0; + n = (mz_uint32)(pZip->m_archive_size & (pZip->m_file_offset_alignment - 1)); + return (mz_uint)((pZip->m_file_offset_alignment - n) & (pZip->m_file_offset_alignment - 1)); +} + +static mz_bool mz_zip_writer_write_zeros(mz_zip_archive *pZip, mz_uint64 cur_file_ofs, mz_uint32 n) +{ + char buf[4096]; + memset(buf, 0, MZ_MIN(sizeof(buf), n)); + while (n) + { + mz_uint32 s = MZ_MIN(sizeof(buf), n); + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_file_ofs, buf, s) != s) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_file_ofs += s; + n -= s; + } + return MZ_TRUE; +} + +mz_bool mz_zip_writer_add_mem_ex(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + mz_uint64 uncomp_size, mz_uint32 uncomp_crc32) +{ + return mz_zip_writer_add_mem_ex_v2(pZip, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, uncomp_size, uncomp_crc32, NULL, NULL, 0, NULL, 0); +} + +mz_bool mz_zip_writer_add_mem_ex_v2(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, + mz_uint level_and_flags, mz_uint64 uncomp_size, mz_uint32 uncomp_crc32, MZ_TIME_T *last_modified, + const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len) +{ + mz_uint16 method = 0, dos_time = 0, dos_date = 0; + mz_uint level, ext_attributes = 0, num_alignment_padding_bytes; + mz_uint64 local_dir_header_ofs = pZip->m_archive_size, cur_archive_file_ofs = pZip->m_archive_size, comp_size = 0; + size_t archive_name_size; + mz_uint8 local_dir_header[MZ_ZIP_LOCAL_DIR_HEADER_SIZE]; + tdefl_compressor *pComp = NULL; + mz_bool store_data_uncompressed; + mz_zip_internal_state *pState; + mz_uint8 *pExtra_data = NULL; + mz_uint32 extra_size = 0; + mz_uint8 extra_data[MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE]; + mz_uint16 bit_flags = 0; + + if ((int)level_and_flags < 0) + level_and_flags = MZ_DEFAULT_LEVEL; + + if (uncomp_size || (buf_size && !(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA))) + bit_flags |= MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR; + + if (!(level_and_flags & MZ_ZIP_FLAG_ASCII_FILENAME)) + bit_flags |= MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8; + + level = level_and_flags & 0xF; + store_data_uncompressed = ((!level) || (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)); + + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || ((buf_size) && (!pBuf)) || (!pArchive_name) || ((comment_size) && (!pComment)) || (level > MZ_UBER_COMPRESSION)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + if (pState->m_zip64) + { + if (pZip->m_total_files == MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + if (pZip->m_total_files == MZ_UINT16_MAX) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); */ + } + if (((mz_uint64)buf_size > 0xFFFFFFFF) || (uncomp_size > 0xFFFFFFFF)) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ + } + } + + if ((!(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (uncomp_size)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_writer_validate_archive_name(pArchive_name)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); + +#ifndef MINIZ_NO_TIME + if (last_modified != NULL) + { + mz_zip_time_t_to_dos_time(*last_modified, &dos_time, &dos_date); + } + else + { + MZ_TIME_T cur_time; + time(&cur_time); + mz_zip_time_t_to_dos_time(cur_time, &dos_time, &dos_date); + } +#endif /* #ifndef MINIZ_NO_TIME */ + + if (!(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + { + uncomp_crc32 = (mz_uint32)mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, buf_size); + uncomp_size = buf_size; + if (uncomp_size <= 3) + { + level = 0; + store_data_uncompressed = MZ_TRUE; + } + } + + archive_name_size = strlen(pArchive_name); + if (archive_name_size > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); + + num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip); + + /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */ + if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE + comment_size) >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + if (!pState->m_zip64) + { + /* Bail early if the archive would obviously become too large */ + if ((pZip->m_archive_size + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + archive_name_size + + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + user_extra_data_len + + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + user_extra_data_central_len + + MZ_ZIP_DATA_DESCRIPTER_SIZE32) > 0xFFFFFFFF) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ + } + } + + if ((archive_name_size) && (pArchive_name[archive_name_size - 1] == '/')) + { + /* Set DOS Subdirectory attribute bit. */ + ext_attributes |= MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG; + + /* Subdirectories cannot contain data. */ + if ((buf_size) || (uncomp_size)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + } + + /* Try to do any allocations before writing to the archive, so if an allocation fails the file remains unmodified. (A good idea if we're doing an in-place modification.) */ + if ((!mz_zip_array_ensure_room(pZip, &pState->m_central_dir, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + (pState->m_zip64 ? MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE : 0))) || (!mz_zip_array_ensure_room(pZip, &pState->m_central_dir_offsets, 1))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + if ((!store_data_uncompressed) && (buf_size)) + { + if (NULL == (pComp = (tdefl_compressor *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(tdefl_compressor)))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!mz_zip_writer_write_zeros(pZip, cur_archive_file_ofs, num_alignment_padding_bytes)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return MZ_FALSE; + } + + local_dir_header_ofs += num_alignment_padding_bytes; + if (pZip->m_file_offset_alignment) + { + MZ_ASSERT((local_dir_header_ofs & (pZip->m_file_offset_alignment - 1)) == 0); + } + cur_archive_file_ofs += num_alignment_padding_bytes; + + MZ_CLEAR_ARR(local_dir_header); + + if (!store_data_uncompressed || (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + { + method = MZ_DEFLATED; + } + + if (pState->m_zip64) + { + if (uncomp_size >= MZ_UINT32_MAX || local_dir_header_ofs >= MZ_UINT32_MAX) + { + pExtra_data = extra_data; + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), 0, 0, 0, method, bit_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, local_dir_header_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + cur_archive_file_ofs += archive_name_size; + + if (pExtra_data != NULL) + { + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, extra_data, extra_size) != extra_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += extra_size; + } + } + else + { + if ((comp_size > MZ_UINT32_MAX) || (cur_archive_file_ofs > MZ_UINT32_MAX)) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)user_extra_data_len, 0, 0, 0, method, bit_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, local_dir_header_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + cur_archive_file_ofs += archive_name_size; + } + + if (user_extra_data_len > 0) + { + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, user_extra_data, user_extra_data_len) != user_extra_data_len) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += user_extra_data_len; + } + + if (store_data_uncompressed) + { + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pBuf, buf_size) != buf_size) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_file_ofs += buf_size; + comp_size = buf_size; + } + else if (buf_size) + { + mz_zip_writer_add_state state; + + state.m_pZip = pZip; + state.m_cur_archive_file_ofs = cur_archive_file_ofs; + state.m_comp_size = 0; + + if ((tdefl_init(pComp, mz_zip_writer_add_put_buf_callback, &state, tdefl_create_comp_flags_from_zip_params(level, -15, MZ_DEFAULT_STRATEGY)) != TDEFL_STATUS_OKAY) || + (tdefl_compress_buffer(pComp, pBuf, buf_size, TDEFL_FINISH) != TDEFL_STATUS_DONE)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return mz_zip_set_error(pZip, MZ_ZIP_COMPRESSION_FAILED); + } + + comp_size = state.m_comp_size; + cur_archive_file_ofs = state.m_cur_archive_file_ofs; + } + + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + pComp = NULL; + + if (uncomp_size) + { + mz_uint8 local_dir_footer[MZ_ZIP_DATA_DESCRIPTER_SIZE64]; + mz_uint32 local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE32; + + MZ_ASSERT(bit_flags & MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR); + + MZ_WRITE_LE32(local_dir_footer + 0, MZ_ZIP_DATA_DESCRIPTOR_ID); + MZ_WRITE_LE32(local_dir_footer + 4, uncomp_crc32); + if (pExtra_data == NULL) + { + if (comp_size > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + MZ_WRITE_LE32(local_dir_footer + 8, comp_size); + MZ_WRITE_LE32(local_dir_footer + 12, uncomp_size); + } + else + { + MZ_WRITE_LE64(local_dir_footer + 8, comp_size); + MZ_WRITE_LE64(local_dir_footer + 16, uncomp_size); + local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE64; + } + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_footer, local_dir_footer_size) != local_dir_footer_size) + return MZ_FALSE; + + cur_archive_file_ofs += local_dir_footer_size; + } + + if (pExtra_data != NULL) + { + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_add_to_central_dir(pZip, pArchive_name, (mz_uint16)archive_name_size, pExtra_data, (mz_uint16)extra_size, pComment, + comment_size, uncomp_size, comp_size, uncomp_crc32, method, bit_flags, dos_time, dos_date, local_dir_header_ofs, ext_attributes, + user_extra_data_central, user_extra_data_central_len)) + return MZ_FALSE; + + pZip->m_total_files++; + pZip->m_archive_size = cur_archive_file_ofs; + + return MZ_TRUE; +} + +mz_bool mz_zip_writer_add_read_buf_callback(mz_zip_archive *pZip, const char *pArchive_name, mz_file_read_func read_callback, void* callback_opaque, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len) +{ + mz_uint16 gen_flags; + mz_uint uncomp_crc32 = MZ_CRC32_INIT, level, num_alignment_padding_bytes; + mz_uint16 method = 0, dos_time = 0, dos_date = 0, ext_attributes = 0; + mz_uint64 local_dir_header_ofs, cur_archive_file_ofs = pZip->m_archive_size, uncomp_size = 0, comp_size = 0; + size_t archive_name_size; + mz_uint8 local_dir_header[MZ_ZIP_LOCAL_DIR_HEADER_SIZE]; + mz_uint8 *pExtra_data = NULL; + mz_uint32 extra_size = 0; + mz_uint8 extra_data[MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE]; + mz_zip_internal_state *pState; + mz_uint64 file_ofs = 0, cur_archive_header_file_ofs; + + if ((int)level_and_flags < 0) + level_and_flags = MZ_DEFAULT_LEVEL; + level = level_and_flags & 0xF; + + gen_flags = (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) ? 0 : MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR; + + if (!(level_and_flags & MZ_ZIP_FLAG_ASCII_FILENAME)) + gen_flags |= MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8; + + /* Sanity checks */ + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || (!pArchive_name) || ((comment_size) && (!pComment)) || (level > MZ_UBER_COMPRESSION)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + if ((!pState->m_zip64) && (max_size > MZ_UINT32_MAX)) + { + /* Source file is too large for non-zip64 */ + /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ + pState->m_zip64 = MZ_TRUE; + } + + /* We could support this, but why? */ + if (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_writer_validate_archive_name(pArchive_name)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); + + if (pState->m_zip64) + { + if (pZip->m_total_files == MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + if (pZip->m_total_files == MZ_UINT16_MAX) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); */ + } + } + + archive_name_size = strlen(pArchive_name); + if (archive_name_size > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); + + num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip); + + /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */ + if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE + comment_size) >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + if (!pState->m_zip64) + { + /* Bail early if the archive would obviously become too large */ + if ((pZip->m_archive_size + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + + archive_name_size + comment_size + user_extra_data_len + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + 1024 + + MZ_ZIP_DATA_DESCRIPTER_SIZE32 + user_extra_data_central_len) > 0xFFFFFFFF) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ + } + } + +#ifndef MINIZ_NO_TIME + if (pFile_time) + { + mz_zip_time_t_to_dos_time(*pFile_time, &dos_time, &dos_date); + } +#endif + + if (max_size <= 3) + level = 0; + + if (!mz_zip_writer_write_zeros(pZip, cur_archive_file_ofs, num_alignment_padding_bytes)) + { + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_file_ofs += num_alignment_padding_bytes; + local_dir_header_ofs = cur_archive_file_ofs; + + if (pZip->m_file_offset_alignment) + { + MZ_ASSERT((cur_archive_file_ofs & (pZip->m_file_offset_alignment - 1)) == 0); + } + + if (max_size && level) + { + method = MZ_DEFLATED; + } + + MZ_CLEAR_ARR(local_dir_header); + if (pState->m_zip64) + { + if (max_size >= MZ_UINT32_MAX || local_dir_header_ofs >= MZ_UINT32_MAX) + { + pExtra_data = extra_data; + if (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (max_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (max_size >= MZ_UINT32_MAX) ? &comp_size : NULL, + (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + else + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, NULL, + NULL, + (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), 0, 0, 0, method, gen_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_file_ofs += archive_name_size; + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, extra_data, extra_size) != extra_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += extra_size; + } + else + { + if ((comp_size > MZ_UINT32_MAX) || (cur_archive_file_ofs > MZ_UINT32_MAX)) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)user_extra_data_len, 0, 0, 0, method, gen_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_file_ofs += archive_name_size; + } + + if (user_extra_data_len > 0) + { + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, user_extra_data, user_extra_data_len) != user_extra_data_len) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += user_extra_data_len; + } + + if (max_size) + { + void *pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, MZ_ZIP_MAX_IO_BUF_SIZE); + if (!pRead_buf) + { + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!level) + { + while (1) + { + size_t n = read_callback(callback_opaque, file_ofs, pRead_buf, MZ_ZIP_MAX_IO_BUF_SIZE); + if (n == 0) + break; + + if ((n > MZ_ZIP_MAX_IO_BUF_SIZE) || (file_ofs + n > max_size)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pRead_buf, n) != n) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + file_ofs += n; + uncomp_crc32 = (mz_uint32)mz_crc32(uncomp_crc32, (const mz_uint8 *)pRead_buf, n); + cur_archive_file_ofs += n; + } + uncomp_size = file_ofs; + comp_size = uncomp_size; + } + else + { + mz_bool result = MZ_FALSE; + mz_zip_writer_add_state state; + tdefl_compressor *pComp = (tdefl_compressor *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(tdefl_compressor)); + if (!pComp) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + state.m_pZip = pZip; + state.m_cur_archive_file_ofs = cur_archive_file_ofs; + state.m_comp_size = 0; + + if (tdefl_init(pComp, mz_zip_writer_add_put_buf_callback, &state, tdefl_create_comp_flags_from_zip_params(level, -15, MZ_DEFAULT_STRATEGY)) != TDEFL_STATUS_OKAY) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + } + + for (;;) + { + tdefl_status status; + tdefl_flush flush = TDEFL_NO_FLUSH; + + size_t n = read_callback(callback_opaque, file_ofs, pRead_buf, MZ_ZIP_MAX_IO_BUF_SIZE); + if ((n > MZ_ZIP_MAX_IO_BUF_SIZE) || (file_ofs + n > max_size)) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + break; + } + + file_ofs += n; + uncomp_crc32 = (mz_uint32)mz_crc32(uncomp_crc32, (const mz_uint8 *)pRead_buf, n); + + if (pZip->m_pNeeds_keepalive != NULL && pZip->m_pNeeds_keepalive(pZip->m_pIO_opaque)) + flush = TDEFL_FULL_FLUSH; + + if (n == 0) + flush = TDEFL_FINISH; + + status = tdefl_compress_buffer(pComp, pRead_buf, n, flush); + if (status == TDEFL_STATUS_DONE) + { + result = MZ_TRUE; + break; + } + else if (status != TDEFL_STATUS_OKAY) + { + mz_zip_set_error(pZip, MZ_ZIP_COMPRESSION_FAILED); + break; + } + } + + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + + if (!result) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return MZ_FALSE; + } + + uncomp_size = file_ofs; + comp_size = state.m_comp_size; + cur_archive_file_ofs = state.m_cur_archive_file_ofs; + } + + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + } + + if (!(level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE)) + { + mz_uint8 local_dir_footer[MZ_ZIP_DATA_DESCRIPTER_SIZE64]; + mz_uint32 local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE32; + + MZ_WRITE_LE32(local_dir_footer + 0, MZ_ZIP_DATA_DESCRIPTOR_ID); + MZ_WRITE_LE32(local_dir_footer + 4, uncomp_crc32); + if (pExtra_data == NULL) + { + if (comp_size > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + MZ_WRITE_LE32(local_dir_footer + 8, comp_size); + MZ_WRITE_LE32(local_dir_footer + 12, uncomp_size); + } + else + { + MZ_WRITE_LE64(local_dir_footer + 8, comp_size); + MZ_WRITE_LE64(local_dir_footer + 16, uncomp_size); + local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE64; + } + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_footer, local_dir_footer_size) != local_dir_footer_size) + return MZ_FALSE; + + cur_archive_file_ofs += local_dir_footer_size; + } + + if (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) + { + if (pExtra_data != NULL) + { + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (max_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (max_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, + (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), + (max_size >= MZ_UINT32_MAX) ? MZ_UINT32_MAX : uncomp_size, + (max_size >= MZ_UINT32_MAX) ? MZ_UINT32_MAX : comp_size, + uncomp_crc32, method, gen_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + cur_archive_header_file_ofs = local_dir_header_ofs; + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + if (pExtra_data != NULL) + { + cur_archive_header_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_header_file_ofs += archive_name_size; + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, extra_data, extra_size) != extra_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_header_file_ofs += extra_size; + } + } + + if (pExtra_data != NULL) + { + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_add_to_central_dir(pZip, pArchive_name, (mz_uint16)archive_name_size, pExtra_data, (mz_uint16)extra_size, pComment, comment_size, + uncomp_size, comp_size, uncomp_crc32, method, gen_flags, dos_time, dos_date, local_dir_header_ofs, ext_attributes, + user_extra_data_central, user_extra_data_central_len)) + return MZ_FALSE; + + pZip->m_total_files++; + pZip->m_archive_size = cur_archive_file_ofs; + + return MZ_TRUE; +} + +#ifndef MINIZ_NO_STDIO + +static size_t mz_file_read_func_stdio(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n) +{ + MZ_FILE *pSrc_file = (MZ_FILE *)pOpaque; + mz_int64 cur_ofs = MZ_FTELL64(pSrc_file); + + if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pSrc_file, (mz_int64)file_ofs, SEEK_SET)))) + return 0; + + return MZ_FREAD(pBuf, 1, n, pSrc_file); +} + +mz_bool mz_zip_writer_add_cfile(mz_zip_archive *pZip, const char *pArchive_name, MZ_FILE *pSrc_file, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len) +{ + return mz_zip_writer_add_read_buf_callback(pZip, pArchive_name, mz_file_read_func_stdio, pSrc_file, max_size, pFile_time, pComment, comment_size, level_and_flags, + user_extra_data, user_extra_data_len, user_extra_data_central, user_extra_data_central_len); +} + +mz_bool mz_zip_writer_add_file(mz_zip_archive *pZip, const char *pArchive_name, const char *pSrc_filename, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags) +{ + MZ_FILE *pSrc_file = NULL; + mz_uint64 uncomp_size = 0; + MZ_TIME_T file_modified_time; + MZ_TIME_T *pFile_time = NULL; + mz_bool status; + + memset(&file_modified_time, 0, sizeof(file_modified_time)); + +#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_STDIO) + pFile_time = &file_modified_time; + if (!mz_zip_get_file_modified_time(pSrc_filename, &file_modified_time)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_STAT_FAILED); +#endif + + pSrc_file = MZ_FOPEN(pSrc_filename, "rb"); + if (!pSrc_file) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + + MZ_FSEEK64(pSrc_file, 0, SEEK_END); + uncomp_size = MZ_FTELL64(pSrc_file); + MZ_FSEEK64(pSrc_file, 0, SEEK_SET); + + status = mz_zip_writer_add_cfile(pZip, pArchive_name, pSrc_file, uncomp_size, pFile_time, pComment, comment_size, level_and_flags, NULL, 0, NULL, 0); + + MZ_FCLOSE(pSrc_file); + + return status; +} +#endif /* #ifndef MINIZ_NO_STDIO */ + +static mz_bool mz_zip_writer_update_zip64_extension_block(mz_zip_array *pNew_ext, mz_zip_archive *pZip, const mz_uint8 *pExt, mz_uint32 ext_len, mz_uint64 *pComp_size, mz_uint64 *pUncomp_size, mz_uint64 *pLocal_header_ofs, mz_uint32 *pDisk_start) +{ + /* + 64 should be enough for any new zip64 data */ + if (!mz_zip_array_reserve(pZip, pNew_ext, ext_len + 64, MZ_FALSE)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + mz_zip_array_resize(pZip, pNew_ext, 0, MZ_FALSE); + + if ((pUncomp_size) || (pComp_size) || (pLocal_header_ofs) || (pDisk_start)) + { + mz_uint8 new_ext_block[64]; + mz_uint8 *pDst = new_ext_block; + mz_write_le16(pDst, MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID); + mz_write_le16(pDst + sizeof(mz_uint16), 0); + pDst += sizeof(mz_uint16) * 2; + + if (pUncomp_size) + { + mz_write_le64(pDst, *pUncomp_size); + pDst += sizeof(mz_uint64); + } + + if (pComp_size) + { + mz_write_le64(pDst, *pComp_size); + pDst += sizeof(mz_uint64); + } + + if (pLocal_header_ofs) + { + mz_write_le64(pDst, *pLocal_header_ofs); + pDst += sizeof(mz_uint64); + } + + if (pDisk_start) + { + mz_write_le32(pDst, *pDisk_start); + pDst += sizeof(mz_uint32); + } + + mz_write_le16(new_ext_block + sizeof(mz_uint16), (mz_uint16)((pDst - new_ext_block) - sizeof(mz_uint16) * 2)); + + if (!mz_zip_array_push_back(pZip, pNew_ext, new_ext_block, pDst - new_ext_block)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if ((pExt) && (ext_len)) + { + mz_uint32 extra_size_remaining = ext_len; + const mz_uint8 *pExtra_data = pExt; + + do + { + mz_uint32 field_id, field_data_size, field_total_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + field_total_size = field_data_size + sizeof(mz_uint16) * 2; + + if (field_total_size > extra_size_remaining) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (field_id != MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + if (!mz_zip_array_push_back(pZip, pNew_ext, pExtra_data, field_total_size)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + pExtra_data += field_total_size; + extra_size_remaining -= field_total_size; + } while (extra_size_remaining); + } + + return MZ_TRUE; +} + +/* TODO: This func is now pretty freakin complex due to zip64, split it up? */ +mz_bool mz_zip_writer_add_from_zip_reader(mz_zip_archive *pZip, mz_zip_archive *pSource_zip, mz_uint src_file_index) +{ + mz_uint n, bit_flags, num_alignment_padding_bytes, src_central_dir_following_data_size; + mz_uint64 src_archive_bytes_remaining, local_dir_header_ofs; + mz_uint64 cur_src_file_ofs, cur_dst_file_ofs; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + mz_uint8 new_central_header[MZ_ZIP_CENTRAL_DIR_HEADER_SIZE]; + size_t orig_central_dir_size; + mz_zip_internal_state *pState; + void *pBuf; + const mz_uint8 *pSrc_central_header; + mz_zip_archive_file_stat src_file_stat; + mz_uint32 src_filename_len, src_comment_len, src_ext_len; + mz_uint32 local_header_filename_size, local_header_extra_len; + mz_uint64 local_header_comp_size, local_header_uncomp_size; + mz_bool found_zip64_ext_data_in_ldir = MZ_FALSE; + + /* Sanity checks */ + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || (!pSource_zip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + /* Don't support copying files from zip64 archives to non-zip64, even though in some cases this is possible */ + if ((pSource_zip->m_pState->m_zip64) && (!pZip->m_pState->m_zip64)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* Get pointer to the source central dir header and crack it */ + if (NULL == (pSrc_central_header = mz_zip_get_cdh(pSource_zip, src_file_index))) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (MZ_READ_LE32(pSrc_central_header + MZ_ZIP_CDH_SIG_OFS) != MZ_ZIP_CENTRAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + src_filename_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_FILENAME_LEN_OFS); + src_comment_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_COMMENT_LEN_OFS); + src_ext_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_EXTRA_LEN_OFS); + src_central_dir_following_data_size = src_filename_len + src_ext_len + src_comment_len; + + /* TODO: We don't support central dir's >= MZ_UINT32_MAX bytes right now (+32 fudge factor in case we need to add more extra data) */ + if ((pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_central_dir_following_data_size + 32) >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip); + + if (!pState->m_zip64) + { + if (pZip->m_total_files == MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + /* TODO: Our zip64 support still has some 32-bit limits that may not be worth fixing. */ + if (pZip->m_total_files == MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + + if (!mz_zip_file_stat_internal(pSource_zip, src_file_index, pSrc_central_header, &src_file_stat, NULL)) + return MZ_FALSE; + + cur_src_file_ofs = src_file_stat.m_local_header_ofs; + cur_dst_file_ofs = pZip->m_archive_size; + + /* Read the source archive's local dir header */ + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + cur_src_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE; + + /* Compute the total size we need to copy (filename+extra data+compressed data) */ + local_header_filename_size = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS); + local_header_extra_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + local_header_comp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS); + local_header_uncomp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS); + src_archive_bytes_remaining = local_header_filename_size + local_header_extra_len + src_file_stat.m_comp_size; + + /* Try to find a zip64 extended information field */ + if ((local_header_extra_len) && ((local_header_comp_size == MZ_UINT32_MAX) || (local_header_uncomp_size == MZ_UINT32_MAX))) + { + mz_zip_array file_data_array; + const mz_uint8 *pExtra_data; + mz_uint32 extra_size_remaining = local_header_extra_len; + + mz_zip_array_init(&file_data_array, 1); + if (!mz_zip_array_resize(pZip, &file_data_array, local_header_extra_len, MZ_FALSE)) + { + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, src_file_stat.m_local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_size, file_data_array.m_p, local_header_extra_len) != local_header_extra_len) + { + mz_zip_array_clear(pZip, &file_data_array); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + + pExtra_data = (const mz_uint8 *)file_data_array.m_p; + + do + { + mz_uint32 field_id, field_data_size, field_total_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + { + mz_zip_array_clear(pZip, &file_data_array); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + field_total_size = field_data_size + sizeof(mz_uint16) * 2; + + if (field_total_size > extra_size_remaining) + { + mz_zip_array_clear(pZip, &file_data_array); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + const mz_uint8 *pSrc_field_data = pExtra_data + sizeof(mz_uint32); + + if (field_data_size < sizeof(mz_uint64) * 2) + { + mz_zip_array_clear(pZip, &file_data_array); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + local_header_uncomp_size = MZ_READ_LE64(pSrc_field_data); + local_header_comp_size = MZ_READ_LE64(pSrc_field_data + sizeof(mz_uint64)); /* may be 0 if there's a descriptor */ + + found_zip64_ext_data_in_ldir = MZ_TRUE; + break; + } + + pExtra_data += field_total_size; + extra_size_remaining -= field_total_size; + } while (extra_size_remaining); + + mz_zip_array_clear(pZip, &file_data_array); + } + + if (!pState->m_zip64) + { + /* Try to detect if the new archive will most likely wind up too big and bail early (+(sizeof(mz_uint32) * 4) is for the optional descriptor which could be present, +64 is a fudge factor). */ + /* We also check when the archive is finalized so this doesn't need to be perfect. */ + mz_uint64 approx_new_archive_size = cur_dst_file_ofs + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + src_archive_bytes_remaining + (sizeof(mz_uint32) * 4) + + pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_central_dir_following_data_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + 64; + + if (approx_new_archive_size >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + } + + /* Write dest archive padding */ + if (!mz_zip_writer_write_zeros(pZip, cur_dst_file_ofs, num_alignment_padding_bytes)) + return MZ_FALSE; + + cur_dst_file_ofs += num_alignment_padding_bytes; + + local_dir_header_ofs = cur_dst_file_ofs; + if (pZip->m_file_offset_alignment) + { + MZ_ASSERT((local_dir_header_ofs & (pZip->m_file_offset_alignment - 1)) == 0); + } + + /* The original zip's local header+ext block doesn't change, even with zip64, so we can just copy it over to the dest zip */ + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_dst_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE; + + /* Copy over the source archive bytes to the dest archive, also ensure we have enough buf space to handle optional data descriptor */ + if (NULL == (pBuf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)MZ_MAX(32U, MZ_MIN((mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE, src_archive_bytes_remaining))))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + while (src_archive_bytes_remaining) + { + n = (mz_uint)MZ_MIN((mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE, src_archive_bytes_remaining); + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, n) != n) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + cur_src_file_ofs += n; + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pBuf, n) != n) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + cur_dst_file_ofs += n; + + src_archive_bytes_remaining -= n; + } + + /* Now deal with the optional data descriptor */ + bit_flags = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_BIT_FLAG_OFS); + if (bit_flags & 8) + { + /* Copy data descriptor */ + if ((pSource_zip->m_pState->m_zip64) || (found_zip64_ext_data_in_ldir)) + { + /* src is zip64, dest must be zip64 */ + + /* name uint32_t's */ + /* id 1 (optional in zip64?) */ + /* crc 1 */ + /* comp_size 2 */ + /* uncomp_size 2 */ + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, (sizeof(mz_uint32) * 6)) != (sizeof(mz_uint32) * 6)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + + n = sizeof(mz_uint32) * ((MZ_READ_LE32(pBuf) == MZ_ZIP_DATA_DESCRIPTOR_ID) ? 6 : 5); + } + else + { + /* src is NOT zip64 */ + mz_bool has_id; + + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, sizeof(mz_uint32) * 4) != sizeof(mz_uint32) * 4) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + + has_id = (MZ_READ_LE32(pBuf) == MZ_ZIP_DATA_DESCRIPTOR_ID); + + if (pZip->m_pState->m_zip64) + { + /* dest is zip64, so upgrade the data descriptor */ + const mz_uint8 *pSrc_descriptor = (const mz_uint8 *)pBuf + (has_id ? sizeof(mz_uint32) : 0); + const mz_uint32 src_crc32 = MZ_READ_LE32(pSrc_descriptor); + const mz_uint64 src_comp_size = MZ_READ_LE32(pSrc_descriptor + sizeof(mz_uint32)); + const mz_uint64 src_uncomp_size = MZ_READ_LE32(pSrc_descriptor + 2*sizeof(mz_uint32)); + + mz_write_le32((mz_uint8 *)pBuf, MZ_ZIP_DATA_DESCRIPTOR_ID); + mz_write_le32((mz_uint8 *)pBuf + sizeof(mz_uint32) * 1, src_crc32); + mz_write_le64((mz_uint8 *)pBuf + sizeof(mz_uint32) * 2, src_comp_size); + mz_write_le64((mz_uint8 *)pBuf + sizeof(mz_uint32) * 4, src_uncomp_size); + + n = sizeof(mz_uint32) * 6; + } + else + { + /* dest is NOT zip64, just copy it as-is */ + n = sizeof(mz_uint32) * (has_id ? 4 : 3); + } + } + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pBuf, n) != n) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_src_file_ofs += n; + cur_dst_file_ofs += n; + } + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + + /* Finally, add the new central dir header */ + orig_central_dir_size = pState->m_central_dir.m_size; + + memcpy(new_central_header, pSrc_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE); + + if (pState->m_zip64) + { + /* This is the painful part: We need to write a new central dir header + ext block with updated zip64 fields, and ensure the old fields (if any) are not included. */ + const mz_uint8 *pSrc_ext = pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_filename_len; + mz_zip_array new_ext_block; + + mz_zip_array_init(&new_ext_block, sizeof(mz_uint8)); + + MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS, MZ_UINT32_MAX); + MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS, MZ_UINT32_MAX); + MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_LOCAL_HEADER_OFS, MZ_UINT32_MAX); + + if (!mz_zip_writer_update_zip64_extension_block(&new_ext_block, pZip, pSrc_ext, src_ext_len, &src_file_stat.m_comp_size, &src_file_stat.m_uncomp_size, &local_dir_header_ofs, NULL)) + { + mz_zip_array_clear(pZip, &new_ext_block); + return MZ_FALSE; + } + + MZ_WRITE_LE16(new_central_header + MZ_ZIP_CDH_EXTRA_LEN_OFS, new_ext_block.m_size); + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) + { + mz_zip_array_clear(pZip, &new_ext_block); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, src_filename_len)) + { + mz_zip_array_clear(pZip, &new_ext_block); + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_ext_block.m_p, new_ext_block.m_size)) + { + mz_zip_array_clear(pZip, &new_ext_block); + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_filename_len + src_ext_len, src_comment_len)) + { + mz_zip_array_clear(pZip, &new_ext_block); + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + mz_zip_array_clear(pZip, &new_ext_block); + } + else + { + /* sanity checks */ + if (cur_dst_file_ofs > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + if (local_dir_header_ofs >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_LOCAL_HEADER_OFS, local_dir_header_ofs); + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, src_central_dir_following_data_size)) + { + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + } + + /* This shouldn't trigger unless we screwed up during the initial sanity checks */ + if (pState->m_central_dir.m_size >= MZ_UINT32_MAX) + { + /* TODO: Support central dirs >= 32-bits in size */ + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + } + + n = (mz_uint32)orig_central_dir_size; + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir_offsets, &n, 1)) + { + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + pZip->m_total_files++; + pZip->m_archive_size = cur_dst_file_ofs; + + return MZ_TRUE; +} + +mz_bool mz_zip_writer_finalize_archive(mz_zip_archive *pZip) +{ + mz_zip_internal_state *pState; + mz_uint64 central_dir_ofs, central_dir_size; + mz_uint8 hdr[256]; + + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + if (pState->m_zip64) + { + if ((mz_uint64)pState->m_central_dir.m_size >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + if ((pZip->m_total_files > MZ_UINT16_MAX) || ((pZip->m_archive_size + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) > MZ_UINT32_MAX)) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + + central_dir_ofs = 0; + central_dir_size = 0; + if (pZip->m_total_files) + { + /* Write central directory */ + central_dir_ofs = pZip->m_archive_size; + central_dir_size = pState->m_central_dir.m_size; + pZip->m_central_directory_file_ofs = central_dir_ofs; + if (pZip->m_pWrite(pZip->m_pIO_opaque, central_dir_ofs, pState->m_central_dir.m_p, (size_t)central_dir_size) != central_dir_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + pZip->m_archive_size += central_dir_size; + } + + if (pState->m_zip64) + { + /* Write zip64 end of central directory header */ + mz_uint64 rel_ofs_to_zip64_ecdr = pZip->m_archive_size; + + MZ_CLEAR_ARR(hdr); + MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDH_SIG_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE - sizeof(mz_uint32) - sizeof(mz_uint64)); + MZ_WRITE_LE16(hdr + MZ_ZIP64_ECDH_VERSION_MADE_BY_OFS, 0x031E); /* TODO: always Unix */ + MZ_WRITE_LE16(hdr + MZ_ZIP64_ECDH_VERSION_NEEDED_OFS, 0x002D); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS, pZip->m_total_files); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS, pZip->m_total_files); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_SIZE_OFS, central_dir_size); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_OFS_OFS, central_dir_ofs); + if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + pZip->m_archive_size += MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE; + + /* Write zip64 end of central directory locator */ + MZ_CLEAR_ARR(hdr); + MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDL_SIG_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS, rel_ofs_to_zip64_ecdr); + MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS, 1); + if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) != MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + pZip->m_archive_size += MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE; + } + + /* Write end of central directory record */ + MZ_CLEAR_ARR(hdr); + MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_SIG_OFS, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG); + MZ_WRITE_LE16(hdr + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS, MZ_MIN(MZ_UINT16_MAX, pZip->m_total_files)); + MZ_WRITE_LE16(hdr + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS, MZ_MIN(MZ_UINT16_MAX, pZip->m_total_files)); + MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_CDIR_SIZE_OFS, MZ_MIN(MZ_UINT32_MAX, central_dir_size)); + MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_CDIR_OFS_OFS, MZ_MIN(MZ_UINT32_MAX, central_dir_ofs)); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + +#ifndef MINIZ_NO_STDIO + if ((pState->m_pFile) && (MZ_FFLUSH(pState->m_pFile) == EOF)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED); +#endif /* #ifndef MINIZ_NO_STDIO */ + + pZip->m_archive_size += MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE; + + pZip->m_zip_mode = MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED; + return MZ_TRUE; +} + +mz_bool mz_zip_writer_finalize_heap_archive(mz_zip_archive *pZip, void **ppBuf, size_t *pSize) +{ + if ((!ppBuf) || (!pSize)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + *ppBuf = NULL; + *pSize = 0; + + if ((!pZip) || (!pZip->m_pState)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (pZip->m_pWrite != mz_zip_heap_write_func) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_writer_finalize_archive(pZip)) + return MZ_FALSE; + + *ppBuf = pZip->m_pState->m_pMem; + *pSize = pZip->m_pState->m_mem_size; + pZip->m_pState->m_pMem = NULL; + pZip->m_pState->m_mem_size = pZip->m_pState->m_mem_capacity = 0; + + return MZ_TRUE; +} + +mz_bool mz_zip_writer_end(mz_zip_archive *pZip) +{ + return mz_zip_writer_end_internal(pZip, MZ_TRUE); +} + +#ifndef MINIZ_NO_STDIO +mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags) +{ + return mz_zip_add_mem_to_archive_file_in_place_v2(pZip_filename, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, NULL); +} + +mz_bool mz_zip_add_mem_to_archive_file_in_place_v2(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_zip_error *pErr) +{ + mz_bool status, created_new_archive = MZ_FALSE; + mz_zip_archive zip_archive; + struct MZ_FILE_STAT_STRUCT file_stat; + mz_zip_error actual_err = MZ_ZIP_NO_ERROR; + + mz_zip_zero_struct(&zip_archive); + if ((int)level_and_flags < 0) + level_and_flags = MZ_DEFAULT_LEVEL; + + if ((!pZip_filename) || (!pArchive_name) || ((buf_size) && (!pBuf)) || ((comment_size) && (!pComment)) || ((level_and_flags & 0xF) > MZ_UBER_COMPRESSION)) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_PARAMETER; + return MZ_FALSE; + } + + if (!mz_zip_writer_validate_archive_name(pArchive_name)) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_FILENAME; + return MZ_FALSE; + } + + /* Important: The regular non-64 bit version of stat() can fail here if the file is very large, which could cause the archive to be overwritten. */ + /* So be sure to compile with _LARGEFILE64_SOURCE 1 */ + if (MZ_FILE_STAT(pZip_filename, &file_stat) != 0) + { + /* Create a new archive. */ + if (!mz_zip_writer_init_file_v2(&zip_archive, pZip_filename, 0, level_and_flags)) + { + if (pErr) + *pErr = zip_archive.m_last_error; + return MZ_FALSE; + } + + created_new_archive = MZ_TRUE; + } + else + { + /* Append to an existing archive. */ + if (!mz_zip_reader_init_file_v2(&zip_archive, pZip_filename, level_and_flags | MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY, 0, 0)) + { + if (pErr) + *pErr = zip_archive.m_last_error; + return MZ_FALSE; + } + + if (!mz_zip_writer_init_from_reader_v2(&zip_archive, pZip_filename, level_and_flags)) + { + if (pErr) + *pErr = zip_archive.m_last_error; + + mz_zip_reader_end_internal(&zip_archive, MZ_FALSE); + + return MZ_FALSE; + } + } + + status = mz_zip_writer_add_mem_ex(&zip_archive, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, 0, 0); + actual_err = zip_archive.m_last_error; + + /* Always finalize, even if adding failed for some reason, so we have a valid central directory. (This may not always succeed, but we can try.) */ + if (!mz_zip_writer_finalize_archive(&zip_archive)) + { + if (!actual_err) + actual_err = zip_archive.m_last_error; + + status = MZ_FALSE; + } + + if (!mz_zip_writer_end_internal(&zip_archive, status)) + { + if (!actual_err) + actual_err = zip_archive.m_last_error; + + status = MZ_FALSE; + } + + if ((!status) && (created_new_archive)) + { + /* It's a new archive and something went wrong, so just delete it. */ + int ignoredStatus = MZ_DELETE_FILE(pZip_filename); + (void)ignoredStatus; + } + + if (pErr) + *pErr = actual_err; + + return status; +} + +void *mz_zip_extract_archive_file_to_heap_v2(const char *pZip_filename, const char *pArchive_name, const char *pComment, size_t *pSize, mz_uint flags, mz_zip_error *pErr) +{ + mz_uint32 file_index; + mz_zip_archive zip_archive; + void *p = NULL; + + if (pSize) + *pSize = 0; + + if ((!pZip_filename) || (!pArchive_name)) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_PARAMETER; + + return NULL; + } + + mz_zip_zero_struct(&zip_archive); + if (!mz_zip_reader_init_file_v2(&zip_archive, pZip_filename, flags | MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY, 0, 0)) + { + if (pErr) + *pErr = zip_archive.m_last_error; + + return NULL; + } + + if (mz_zip_reader_locate_file_v2(&zip_archive, pArchive_name, pComment, flags, &file_index)) + { + p = mz_zip_reader_extract_to_heap(&zip_archive, file_index, pSize, flags); + } + + mz_zip_reader_end_internal(&zip_archive, p != NULL); + + if (pErr) + *pErr = zip_archive.m_last_error; + + return p; +} + +void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint flags) +{ + return mz_zip_extract_archive_file_to_heap_v2(pZip_filename, pArchive_name, NULL, pSize, flags, NULL); +} + +#endif /* #ifndef MINIZ_NO_STDIO */ + +#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS */ + +/* ------------------- Misc utils */ + +mz_zip_mode mz_zip_get_mode(mz_zip_archive *pZip) +{ + return pZip ? pZip->m_zip_mode : MZ_ZIP_MODE_INVALID; +} + +mz_zip_type mz_zip_get_type(mz_zip_archive *pZip) +{ + return pZip ? pZip->m_zip_type : MZ_ZIP_TYPE_INVALID; +} + +mz_zip_error mz_zip_set_last_error(mz_zip_archive *pZip, mz_zip_error err_num) +{ + mz_zip_error prev_err; + + if (!pZip) + return MZ_ZIP_INVALID_PARAMETER; + + prev_err = pZip->m_last_error; + + pZip->m_last_error = err_num; + return prev_err; +} + +mz_zip_error mz_zip_peek_last_error(mz_zip_archive *pZip) +{ + if (!pZip) + return MZ_ZIP_INVALID_PARAMETER; + + return pZip->m_last_error; +} + +mz_zip_error mz_zip_clear_last_error(mz_zip_archive *pZip) +{ + return mz_zip_set_last_error(pZip, MZ_ZIP_NO_ERROR); +} + +mz_zip_error mz_zip_get_last_error(mz_zip_archive *pZip) +{ + mz_zip_error prev_err; + + if (!pZip) + return MZ_ZIP_INVALID_PARAMETER; + + prev_err = pZip->m_last_error; + + pZip->m_last_error = MZ_ZIP_NO_ERROR; + return prev_err; +} + +const char *mz_zip_get_error_string(mz_zip_error mz_err) +{ + switch (mz_err) + { + case MZ_ZIP_NO_ERROR: + return "no error"; + case MZ_ZIP_UNDEFINED_ERROR: + return "undefined error"; + case MZ_ZIP_TOO_MANY_FILES: + return "too many files"; + case MZ_ZIP_FILE_TOO_LARGE: + return "file too large"; + case MZ_ZIP_UNSUPPORTED_METHOD: + return "unsupported method"; + case MZ_ZIP_UNSUPPORTED_ENCRYPTION: + return "unsupported encryption"; + case MZ_ZIP_UNSUPPORTED_FEATURE: + return "unsupported feature"; + case MZ_ZIP_FAILED_FINDING_CENTRAL_DIR: + return "failed finding central directory"; + case MZ_ZIP_NOT_AN_ARCHIVE: + return "not a ZIP archive"; + case MZ_ZIP_INVALID_HEADER_OR_CORRUPTED: + return "invalid header or archive is corrupted"; + case MZ_ZIP_UNSUPPORTED_MULTIDISK: + return "unsupported multidisk archive"; + case MZ_ZIP_DECOMPRESSION_FAILED: + return "decompression failed or archive is corrupted"; + case MZ_ZIP_COMPRESSION_FAILED: + return "compression failed"; + case MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE: + return "unexpected decompressed size"; + case MZ_ZIP_CRC_CHECK_FAILED: + return "CRC-32 check failed"; + case MZ_ZIP_UNSUPPORTED_CDIR_SIZE: + return "unsupported central directory size"; + case MZ_ZIP_ALLOC_FAILED: + return "allocation failed"; + case MZ_ZIP_FILE_OPEN_FAILED: + return "file open failed"; + case MZ_ZIP_FILE_CREATE_FAILED: + return "file create failed"; + case MZ_ZIP_FILE_WRITE_FAILED: + return "file write failed"; + case MZ_ZIP_FILE_READ_FAILED: + return "file read failed"; + case MZ_ZIP_FILE_CLOSE_FAILED: + return "file close failed"; + case MZ_ZIP_FILE_SEEK_FAILED: + return "file seek failed"; + case MZ_ZIP_FILE_STAT_FAILED: + return "file stat failed"; + case MZ_ZIP_INVALID_PARAMETER: + return "invalid parameter"; + case MZ_ZIP_INVALID_FILENAME: + return "invalid filename"; + case MZ_ZIP_BUF_TOO_SMALL: + return "buffer too small"; + case MZ_ZIP_INTERNAL_ERROR: + return "internal error"; + case MZ_ZIP_FILE_NOT_FOUND: + return "file not found"; + case MZ_ZIP_ARCHIVE_TOO_LARGE: + return "archive is too large"; + case MZ_ZIP_VALIDATION_FAILED: + return "validation failed"; + case MZ_ZIP_WRITE_CALLBACK_FAILED: + return "write callback failed"; + case MZ_ZIP_TOTAL_ERRORS: + return "total errors"; + default: + break; + } + + return "unknown error"; +} + +/* Note: Just because the archive is not zip64 doesn't necessarily mean it doesn't have Zip64 extended information extra field, argh. */ +mz_bool mz_zip_is_zip64(mz_zip_archive *pZip) +{ + if ((!pZip) || (!pZip->m_pState)) + return MZ_FALSE; + + return pZip->m_pState->m_zip64; +} + +size_t mz_zip_get_central_dir_size(mz_zip_archive *pZip) +{ + if ((!pZip) || (!pZip->m_pState)) + return 0; + + return pZip->m_pState->m_central_dir.m_size; +} + +mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip) +{ + return pZip ? pZip->m_total_files : 0; +} + +mz_uint64 mz_zip_get_archive_size(mz_zip_archive *pZip) +{ + if (!pZip) + return 0; + return pZip->m_archive_size; +} + +mz_uint64 mz_zip_get_archive_file_start_offset(mz_zip_archive *pZip) +{ + if ((!pZip) || (!pZip->m_pState)) + return 0; + return pZip->m_pState->m_file_archive_start_ofs; +} + +MZ_FILE *mz_zip_get_cfile(mz_zip_archive *pZip) +{ + if ((!pZip) || (!pZip->m_pState)) + return 0; + return pZip->m_pState->m_pFile; +} + +size_t mz_zip_read_archive_data(mz_zip_archive *pZip, mz_uint64 file_ofs, void *pBuf, size_t n) +{ + if ((!pZip) || (!pZip->m_pState) || (!pBuf) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + return pZip->m_pRead(pZip->m_pIO_opaque, file_ofs, pBuf, n); +} + +mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size) +{ + mz_uint n; + const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + if (filename_buf_size) + pFilename[0] = '\0'; + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return 0; + } + n = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + if (filename_buf_size) + { + n = MZ_MIN(n, filename_buf_size - 1); + memcpy(pFilename, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, n); + pFilename[n] = '\0'; + } + return n + 1; +} + +mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat) +{ + return mz_zip_file_stat_internal(pZip, file_index, mz_zip_get_cdh(pZip, file_index), pStat, NULL); +} + +mz_bool mz_zip_end(mz_zip_archive *pZip) +{ + if (!pZip) + return MZ_FALSE; + + if (pZip->m_zip_mode == MZ_ZIP_MODE_READING) + return mz_zip_reader_end(pZip); +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS + else if ((pZip->m_zip_mode == MZ_ZIP_MODE_WRITING) || (pZip->m_zip_mode == MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED)) + return mz_zip_writer_end(pZip); +#endif + + return MZ_FALSE; +} + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_ARCHIVE_APIS*/ diff --git a/Apps/Maps/host/third_party/miniz/miniz.h b/Apps/Maps/host/third_party/miniz/miniz.h new file mode 100644 index 0000000..9fcfffc --- /dev/null +++ b/Apps/Maps/host/third_party/miniz/miniz.h @@ -0,0 +1,1422 @@ +#ifndef MINIZ_EXPORT +#define MINIZ_EXPORT +#endif +/* miniz.c 3.0.0 - public domain deflate/inflate, zlib-subset, ZIP reading/writing/appending, PNG writing + See "unlicense" statement at the end of this file. + Rich Geldreich , last updated Oct. 13, 2013 + Implements RFC 1950: http://www.ietf.org/rfc/rfc1950.txt and RFC 1951: http://www.ietf.org/rfc/rfc1951.txt + + Most API's defined in miniz.c are optional. For example, to disable the archive related functions just define + MINIZ_NO_ARCHIVE_APIS, or to get rid of all stdio usage define MINIZ_NO_STDIO (see the list below for more macros). + + * Low-level Deflate/Inflate implementation notes: + + Compression: Use the "tdefl" API's. The compressor supports raw, static, and dynamic blocks, lazy or + greedy parsing, match length filtering, RLE-only, and Huffman-only streams. It performs and compresses + approximately as well as zlib. + + Decompression: Use the "tinfl" API's. The entire decompressor is implemented as a single function + coroutine: see tinfl_decompress(). It supports decompression into a 32KB (or larger power of 2) wrapping buffer, or into a memory + block large enough to hold the entire file. + + The low-level tdefl/tinfl API's do not make any use of dynamic memory allocation. + + * zlib-style API notes: + + miniz.c implements a fairly large subset of zlib. There's enough functionality present for it to be a drop-in + zlib replacement in many apps: + The z_stream struct, optional memory allocation callbacks + deflateInit/deflateInit2/deflate/deflateReset/deflateEnd/deflateBound + inflateInit/inflateInit2/inflate/inflateReset/inflateEnd + compress, compress2, compressBound, uncompress + CRC-32, Adler-32 - Using modern, minimal code size, CPU cache friendly routines. + Supports raw deflate streams or standard zlib streams with adler-32 checking. + + Limitations: + The callback API's are not implemented yet. No support for gzip headers or zlib static dictionaries. + I've tried to closely emulate zlib's various flavors of stream flushing and return status codes, but + there are no guarantees that miniz.c pulls this off perfectly. + + * PNG writing: See the tdefl_write_image_to_png_file_in_memory() function, originally written by + Alex Evans. Supports 1-4 bytes/pixel images. + + * ZIP archive API notes: + + The ZIP archive API's where designed with simplicity and efficiency in mind, with just enough abstraction to + get the job done with minimal fuss. There are simple API's to retrieve file information, read files from + existing archives, create new archives, append new files to existing archives, or clone archive data from + one archive to another. It supports archives located in memory or the heap, on disk (using stdio.h), + or you can specify custom file read/write callbacks. + + - Archive reading: Just call this function to read a single file from a disk archive: + + void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, + size_t *pSize, mz_uint zip_flags); + + For more complex cases, use the "mz_zip_reader" functions. Upon opening an archive, the entire central + directory is located and read as-is into memory, and subsequent file access only occurs when reading individual files. + + - Archives file scanning: The simple way is to use this function to scan a loaded archive for a specific file: + + int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags); + + The locate operation can optionally check file comments too, which (as one example) can be used to identify + multiple versions of the same file in an archive. This function uses a simple linear search through the central + directory, so it's not very fast. + + Alternately, you can iterate through all the files in an archive (using mz_zip_reader_get_num_files()) and + retrieve detailed info on each file by calling mz_zip_reader_file_stat(). + + - Archive creation: Use the "mz_zip_writer" functions. The ZIP writer immediately writes compressed file data + to disk and builds an exact image of the central directory in memory. The central directory image is written + all at once at the end of the archive file when the archive is finalized. + + The archive writer can optionally align each file's local header and file data to any power of 2 alignment, + which can be useful when the archive will be read from optical media. Also, the writer supports placing + arbitrary data blobs at the very beginning of ZIP archives. Archives written using either feature are still + readable by any ZIP tool. + + - Archive appending: The simple way to add a single file to an archive is to call this function: + + mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, + const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags); + + The archive will be created if it doesn't already exist, otherwise it'll be appended to. + Note the appending is done in-place and is not an atomic operation, so if something goes wrong + during the operation it's possible the archive could be left without a central directory (although the local + file headers and file data will be fine, so the archive will be recoverable). + + For more complex archive modification scenarios: + 1. The safest way is to use a mz_zip_reader to read the existing archive, cloning only those bits you want to + preserve into a new archive using using the mz_zip_writer_add_from_zip_reader() function (which compiles the + compressed file data as-is). When you're done, delete the old archive and rename the newly written archive, and + you're done. This is safe but requires a bunch of temporary disk space or heap memory. + + 2. Or, you can convert an mz_zip_reader in-place to an mz_zip_writer using mz_zip_writer_init_from_reader(), + append new files as needed, then finalize the archive which will write an updated central directory to the + original archive. (This is basically what mz_zip_add_mem_to_archive_file_in_place() does.) There's a + possibility that the archive's central directory could be lost with this method if anything goes wrong, though. + + - ZIP archive support limitations: + No spanning support. Extraction functions can only handle unencrypted, stored or deflated files. + Requires streams capable of seeking. + + * This is a header file library, like stb_image.c. To get only a header file, either cut and paste the + below header, or create miniz.h, #define MINIZ_HEADER_FILE_ONLY, and then include miniz.c from it. + + * Important: For best perf. be sure to customize the below macros for your target platform: + #define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 1 + #define MINIZ_LITTLE_ENDIAN 1 + #define MINIZ_HAS_64BIT_REGISTERS 1 + + * On platforms using glibc, Be sure to "#define _LARGEFILE64_SOURCE 1" before including miniz.c to ensure miniz + uses the 64-bit variants: fopen64(), stat64(), etc. Otherwise you won't be able to process large files + (i.e. 32-bit stat() fails for me on files > 0x7FFFFFFF bytes). +*/ +#pragma once + + + +/* Defines to completely disable specific portions of miniz.c: + If all macros here are defined the only functionality remaining will be CRC-32 and adler-32. */ + +/* Define MINIZ_NO_STDIO to disable all usage and any functions which rely on stdio for file I/O. */ +/*#define MINIZ_NO_STDIO */ + +/* If MINIZ_NO_TIME is specified then the ZIP archive functions will not be able to get the current time, or */ +/* get/set file times, and the C run-time funcs that get/set times won't be called. */ +/* The current downside is the times written to your archives will be from 1979. */ +/*#define MINIZ_NO_TIME */ + +/* Define MINIZ_NO_DEFLATE_APIS to disable all compression API's. */ +/*#define MINIZ_NO_DEFLATE_APIS */ + +/* Define MINIZ_NO_INFLATE_APIS to disable all decompression API's. */ +/*#define MINIZ_NO_INFLATE_APIS */ + +/* Define MINIZ_NO_ARCHIVE_APIS to disable all ZIP archive API's. */ +/*#define MINIZ_NO_ARCHIVE_APIS */ + +/* Define MINIZ_NO_ARCHIVE_WRITING_APIS to disable all writing related ZIP archive API's. */ +/*#define MINIZ_NO_ARCHIVE_WRITING_APIS */ + +/* Define MINIZ_NO_ZLIB_APIS to remove all ZLIB-style compression/decompression API's. */ +/*#define MINIZ_NO_ZLIB_APIS */ + +/* Define MINIZ_NO_ZLIB_COMPATIBLE_NAME to disable zlib names, to prevent conflicts against stock zlib. */ +/*#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES */ + +/* Define MINIZ_NO_MALLOC to disable all calls to malloc, free, and realloc. + Note if MINIZ_NO_MALLOC is defined then the user must always provide custom user alloc/free/realloc + callbacks to the zlib and archive API's, and a few stand-alone helper API's which don't provide custom user + functions (such as tdefl_compress_mem_to_heap() and tinfl_decompress_mem_to_heap()) won't work. */ +/*#define MINIZ_NO_MALLOC */ + +#ifdef MINIZ_NO_INFLATE_APIS +#define MINIZ_NO_ARCHIVE_APIS +#endif + +#ifdef MINIZ_NO_DEFLATE_APIS +#define MINIZ_NO_ARCHIVE_WRITING_APIS +#endif + +#if defined(__TINYC__) && (defined(__linux) || defined(__linux__)) +/* TODO: Work around "error: include file 'sys\utime.h' when compiling with tcc on Linux */ +#define MINIZ_NO_TIME +#endif + +#include + +#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_ARCHIVE_APIS) +#include +#endif + +#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__i386) || defined(__i486__) || defined(__i486) || defined(i386) || defined(__ia64__) || defined(__x86_64__) +/* MINIZ_X86_OR_X64_CPU is only used to help set the below macros. */ +#define MINIZ_X86_OR_X64_CPU 1 +#else +#define MINIZ_X86_OR_X64_CPU 0 +#endif + +/* Set MINIZ_LITTLE_ENDIAN only if not set */ +#if !defined(MINIZ_LITTLE_ENDIAN) +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) + +#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +/* Set MINIZ_LITTLE_ENDIAN to 1 if the processor is little endian. */ +#define MINIZ_LITTLE_ENDIAN 1 +#else +#define MINIZ_LITTLE_ENDIAN 0 +#endif + +#else + +#if MINIZ_X86_OR_X64_CPU +#define MINIZ_LITTLE_ENDIAN 1 +#else +#define MINIZ_LITTLE_ENDIAN 0 +#endif + +#endif +#endif + +/* Using unaligned loads and stores causes errors when using UBSan */ +#if defined(__has_feature) +#if __has_feature(undefined_behavior_sanitizer) +#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0 +#endif +#endif + +/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES only if not set */ +#if !defined(MINIZ_USE_UNALIGNED_LOADS_AND_STORES) +#if MINIZ_X86_OR_X64_CPU +/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES to 1 on CPU's that permit efficient integer loads and stores from unaligned addresses. */ +#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0 +#define MINIZ_UNALIGNED_USE_MEMCPY +#else +#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0 +#endif +#endif + +#if defined(_M_X64) || defined(_WIN64) || defined(__MINGW64__) || defined(_LP64) || defined(__LP64__) || defined(__ia64__) || defined(__x86_64__) +/* Set MINIZ_HAS_64BIT_REGISTERS to 1 if operations on 64-bit integers are reasonably fast (and don't involve compiler generated calls to helper functions). */ +#define MINIZ_HAS_64BIT_REGISTERS 1 +#else +#define MINIZ_HAS_64BIT_REGISTERS 0 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------- zlib-style API Definitions. */ + +/* For more compatibility with zlib, miniz.c uses unsigned long for some parameters/struct members. Beware: mz_ulong can be either 32 or 64-bits! */ +typedef unsigned long mz_ulong; + +/* mz_free() internally uses the MZ_FREE() macro (which by default calls free() unless you've modified the MZ_MALLOC macro) to release a block allocated from the heap. */ +MINIZ_EXPORT void mz_free(void *p); + +#define MZ_ADLER32_INIT (1) +/* mz_adler32() returns the initial adler-32 value to use when called with ptr==NULL. */ +MINIZ_EXPORT mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len); + +#define MZ_CRC32_INIT (0) +/* mz_crc32() returns the initial CRC-32 value to use when called with ptr==NULL. */ +MINIZ_EXPORT mz_ulong mz_crc32(mz_ulong crc, const unsigned char *ptr, size_t buf_len); + +/* Compression strategies. */ +enum +{ + MZ_DEFAULT_STRATEGY = 0, + MZ_FILTERED = 1, + MZ_HUFFMAN_ONLY = 2, + MZ_RLE = 3, + MZ_FIXED = 4 +}; + +/* Method */ +#define MZ_DEFLATED 8 + +/* Heap allocation callbacks. +Note that mz_alloc_func parameter types purposely differ from zlib's: items/size is size_t, not unsigned long. */ +typedef void *(*mz_alloc_func)(void *opaque, size_t items, size_t size); +typedef void (*mz_free_func)(void *opaque, void *address); +typedef void *(*mz_realloc_func)(void *opaque, void *address, size_t items, size_t size); + +/* Compression levels: 0-9 are the standard zlib-style levels, 10 is best possible compression (not zlib compatible, and may be very slow), MZ_DEFAULT_COMPRESSION=MZ_DEFAULT_LEVEL. */ +enum +{ + MZ_NO_COMPRESSION = 0, + MZ_BEST_SPEED = 1, + MZ_BEST_COMPRESSION = 9, + MZ_UBER_COMPRESSION = 10, + MZ_DEFAULT_LEVEL = 6, + MZ_DEFAULT_COMPRESSION = -1 +}; + +#define MZ_VERSION "11.0.2" +#define MZ_VERNUM 0xB002 +#define MZ_VER_MAJOR 11 +#define MZ_VER_MINOR 2 +#define MZ_VER_REVISION 0 +#define MZ_VER_SUBREVISION 0 + +#ifndef MINIZ_NO_ZLIB_APIS + +/* Flush values. For typical usage you only need MZ_NO_FLUSH and MZ_FINISH. The other values are for advanced use (refer to the zlib docs). */ +enum +{ + MZ_NO_FLUSH = 0, + MZ_PARTIAL_FLUSH = 1, + MZ_SYNC_FLUSH = 2, + MZ_FULL_FLUSH = 3, + MZ_FINISH = 4, + MZ_BLOCK = 5 +}; + +/* Return status codes. MZ_PARAM_ERROR is non-standard. */ +enum +{ + MZ_OK = 0, + MZ_STREAM_END = 1, + MZ_NEED_DICT = 2, + MZ_ERRNO = -1, + MZ_STREAM_ERROR = -2, + MZ_DATA_ERROR = -3, + MZ_MEM_ERROR = -4, + MZ_BUF_ERROR = -5, + MZ_VERSION_ERROR = -6, + MZ_PARAM_ERROR = -10000 +}; + +/* Window bits */ +#define MZ_DEFAULT_WINDOW_BITS 15 + +struct mz_internal_state; + +/* Compression/decompression stream struct. */ +typedef struct mz_stream_s +{ + const unsigned char *next_in; /* pointer to next byte to read */ + unsigned int avail_in; /* number of bytes available at next_in */ + mz_ulong total_in; /* total number of bytes consumed so far */ + + unsigned char *next_out; /* pointer to next byte to write */ + unsigned int avail_out; /* number of bytes that can be written to next_out */ + mz_ulong total_out; /* total number of bytes produced so far */ + + char *msg; /* error msg (unused) */ + struct mz_internal_state *state; /* internal state, allocated by zalloc/zfree */ + + mz_alloc_func zalloc; /* optional heap allocation function (defaults to malloc) */ + mz_free_func zfree; /* optional heap free function (defaults to free) */ + void *opaque; /* heap alloc function user pointer */ + + int data_type; /* data_type (unused) */ + mz_ulong adler; /* adler32 of the source or uncompressed data */ + mz_ulong reserved; /* not used */ +} mz_stream; + +typedef mz_stream *mz_streamp; + +/* Returns the version string of miniz.c. */ +MINIZ_EXPORT const char *mz_version(void); + +#ifndef MINIZ_NO_DEFLATE_APIS + +/* mz_deflateInit() initializes a compressor with default options: */ +/* Parameters: */ +/* pStream must point to an initialized mz_stream struct. */ +/* level must be between [MZ_NO_COMPRESSION, MZ_BEST_COMPRESSION]. */ +/* level 1 enables a specially optimized compression function that's been optimized purely for performance, not ratio. */ +/* (This special func. is currently only enabled when MINIZ_USE_UNALIGNED_LOADS_AND_STORES and MINIZ_LITTLE_ENDIAN are defined.) */ +/* Return values: */ +/* MZ_OK on success. */ +/* MZ_STREAM_ERROR if the stream is bogus. */ +/* MZ_PARAM_ERROR if the input parameters are bogus. */ +/* MZ_MEM_ERROR on out of memory. */ +MINIZ_EXPORT int mz_deflateInit(mz_streamp pStream, int level); + +/* mz_deflateInit2() is like mz_deflate(), except with more control: */ +/* Additional parameters: */ +/* method must be MZ_DEFLATED */ +/* window_bits must be MZ_DEFAULT_WINDOW_BITS (to wrap the deflate stream with zlib header/adler-32 footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate/no header or footer) */ +/* mem_level must be between [1, 9] (it's checked but ignored by miniz.c) */ +MINIZ_EXPORT int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy); + +/* Quickly resets a compressor without having to reallocate anything. Same as calling mz_deflateEnd() followed by mz_deflateInit()/mz_deflateInit2(). */ +MINIZ_EXPORT int mz_deflateReset(mz_streamp pStream); + +/* mz_deflate() compresses the input to output, consuming as much of the input and producing as much output as possible. */ +/* Parameters: */ +/* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */ +/* flush may be MZ_NO_FLUSH, MZ_PARTIAL_FLUSH/MZ_SYNC_FLUSH, MZ_FULL_FLUSH, or MZ_FINISH. */ +/* Return values: */ +/* MZ_OK on success (when flushing, or if more input is needed but not available, and/or there's more output to be written but the output buffer is full). */ +/* MZ_STREAM_END if all input has been consumed and all output bytes have been written. Don't call mz_deflate() on the stream anymore. */ +/* MZ_STREAM_ERROR if the stream is bogus. */ +/* MZ_PARAM_ERROR if one of the parameters is invalid. */ +/* MZ_BUF_ERROR if no forward progress is possible because the input and/or output buffers are empty. (Fill up the input buffer or free up some output space and try again.) */ +MINIZ_EXPORT int mz_deflate(mz_streamp pStream, int flush); + +/* mz_deflateEnd() deinitializes a compressor: */ +/* Return values: */ +/* MZ_OK on success. */ +/* MZ_STREAM_ERROR if the stream is bogus. */ +MINIZ_EXPORT int mz_deflateEnd(mz_streamp pStream); + +/* mz_deflateBound() returns a (very) conservative upper bound on the amount of data that could be generated by deflate(), assuming flush is set to only MZ_NO_FLUSH or MZ_FINISH. */ +MINIZ_EXPORT mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len); + +/* Single-call compression functions mz_compress() and mz_compress2(): */ +/* Returns MZ_OK on success, or one of the error codes from mz_deflate() on failure. */ +MINIZ_EXPORT int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len); +MINIZ_EXPORT int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level); + +/* mz_compressBound() returns a (very) conservative upper bound on the amount of data that could be generated by calling mz_compress(). */ +MINIZ_EXPORT mz_ulong mz_compressBound(mz_ulong source_len); + +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + +#ifndef MINIZ_NO_INFLATE_APIS + +/* Initializes a decompressor. */ +MINIZ_EXPORT int mz_inflateInit(mz_streamp pStream); + +/* mz_inflateInit2() is like mz_inflateInit() with an additional option that controls the window size and whether or not the stream has been wrapped with a zlib header/footer: */ +/* window_bits must be MZ_DEFAULT_WINDOW_BITS (to parse zlib header/footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate). */ +MINIZ_EXPORT int mz_inflateInit2(mz_streamp pStream, int window_bits); + +/* Quickly resets a compressor without having to reallocate anything. Same as calling mz_inflateEnd() followed by mz_inflateInit()/mz_inflateInit2(). */ +MINIZ_EXPORT int mz_inflateReset(mz_streamp pStream); + +/* Decompresses the input stream to the output, consuming only as much of the input as needed, and writing as much to the output as possible. */ +/* Parameters: */ +/* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */ +/* flush may be MZ_NO_FLUSH, MZ_SYNC_FLUSH, or MZ_FINISH. */ +/* On the first call, if flush is MZ_FINISH it's assumed the input and output buffers are both sized large enough to decompress the entire stream in a single call (this is slightly faster). */ +/* MZ_FINISH implies that there are no more source bytes available beside what's already in the input buffer, and that the output buffer is large enough to hold the rest of the decompressed data. */ +/* Return values: */ +/* MZ_OK on success. Either more input is needed but not available, and/or there's more output to be written but the output buffer is full. */ +/* MZ_STREAM_END if all needed input has been consumed and all output bytes have been written. For zlib streams, the adler-32 of the decompressed data has also been verified. */ +/* MZ_STREAM_ERROR if the stream is bogus. */ +/* MZ_DATA_ERROR if the deflate stream is invalid. */ +/* MZ_PARAM_ERROR if one of the parameters is invalid. */ +/* MZ_BUF_ERROR if no forward progress is possible because the input buffer is empty but the inflater needs more input to continue, or if the output buffer is not large enough. Call mz_inflate() again */ +/* with more input data, or with more room in the output buffer (except when using single call decompression, described above). */ +MINIZ_EXPORT int mz_inflate(mz_streamp pStream, int flush); + +/* Deinitializes a decompressor. */ +MINIZ_EXPORT int mz_inflateEnd(mz_streamp pStream); + +/* Single-call decompression. */ +/* Returns MZ_OK on success, or one of the error codes from mz_inflate() on failure. */ +MINIZ_EXPORT int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len); +MINIZ_EXPORT int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len); +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + +/* Returns a string description of the specified error code, or NULL if the error code is invalid. */ +MINIZ_EXPORT const char *mz_error(int err); + +/* Redefine zlib-compatible names to miniz equivalents, so miniz.c can be used as a drop-in replacement for the subset of zlib that miniz.c supports. */ +/* Define MINIZ_NO_ZLIB_COMPATIBLE_NAMES to disable zlib-compatibility if you use zlib in the same project. */ +#ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES +typedef unsigned char Byte; +typedef unsigned int uInt; +typedef mz_ulong uLong; +typedef Byte Bytef; +typedef uInt uIntf; +typedef char charf; +typedef int intf; +typedef void *voidpf; +typedef uLong uLongf; +typedef void *voidp; +typedef void *const voidpc; +#define Z_NULL 0 +#define Z_NO_FLUSH MZ_NO_FLUSH +#define Z_PARTIAL_FLUSH MZ_PARTIAL_FLUSH +#define Z_SYNC_FLUSH MZ_SYNC_FLUSH +#define Z_FULL_FLUSH MZ_FULL_FLUSH +#define Z_FINISH MZ_FINISH +#define Z_BLOCK MZ_BLOCK +#define Z_OK MZ_OK +#define Z_STREAM_END MZ_STREAM_END +#define Z_NEED_DICT MZ_NEED_DICT +#define Z_ERRNO MZ_ERRNO +#define Z_STREAM_ERROR MZ_STREAM_ERROR +#define Z_DATA_ERROR MZ_DATA_ERROR +#define Z_MEM_ERROR MZ_MEM_ERROR +#define Z_BUF_ERROR MZ_BUF_ERROR +#define Z_VERSION_ERROR MZ_VERSION_ERROR +#define Z_PARAM_ERROR MZ_PARAM_ERROR +#define Z_NO_COMPRESSION MZ_NO_COMPRESSION +#define Z_BEST_SPEED MZ_BEST_SPEED +#define Z_BEST_COMPRESSION MZ_BEST_COMPRESSION +#define Z_DEFAULT_COMPRESSION MZ_DEFAULT_COMPRESSION +#define Z_DEFAULT_STRATEGY MZ_DEFAULT_STRATEGY +#define Z_FILTERED MZ_FILTERED +#define Z_HUFFMAN_ONLY MZ_HUFFMAN_ONLY +#define Z_RLE MZ_RLE +#define Z_FIXED MZ_FIXED +#define Z_DEFLATED MZ_DEFLATED +#define Z_DEFAULT_WINDOW_BITS MZ_DEFAULT_WINDOW_BITS +#define alloc_func mz_alloc_func +#define free_func mz_free_func +#define internal_state mz_internal_state +#define z_stream mz_stream + +#ifndef MINIZ_NO_DEFLATE_APIS +#define deflateInit mz_deflateInit +#define deflateInit2 mz_deflateInit2 +#define deflateReset mz_deflateReset +#define deflate mz_deflate +#define deflateEnd mz_deflateEnd +#define deflateBound mz_deflateBound +#define compress mz_compress +#define compress2 mz_compress2 +#define compressBound mz_compressBound +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + +#ifndef MINIZ_NO_INFLATE_APIS +#define inflateInit mz_inflateInit +#define inflateInit2 mz_inflateInit2 +#define inflateReset mz_inflateReset +#define inflate mz_inflate +#define inflateEnd mz_inflateEnd +#define uncompress mz_uncompress +#define uncompress2 mz_uncompress2 +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + +#define crc32 mz_crc32 +#define adler32 mz_adler32 +#define MAX_WBITS 15 +#define MAX_MEM_LEVEL 9 +#define zError mz_error +#define ZLIB_VERSION MZ_VERSION +#define ZLIB_VERNUM MZ_VERNUM +#define ZLIB_VER_MAJOR MZ_VER_MAJOR +#define ZLIB_VER_MINOR MZ_VER_MINOR +#define ZLIB_VER_REVISION MZ_VER_REVISION +#define ZLIB_VER_SUBREVISION MZ_VER_SUBREVISION +#define zlibVersion mz_version +#define zlib_version mz_version() +#endif /* #ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES */ + +#endif /* MINIZ_NO_ZLIB_APIS */ + +#ifdef __cplusplus +} +#endif + + + + + +#pragma once +#include +#include +#include +#include + + + +/* ------------------- Types and macros */ +typedef unsigned char mz_uint8; +typedef signed short mz_int16; +typedef unsigned short mz_uint16; +typedef unsigned int mz_uint32; +typedef unsigned int mz_uint; +typedef int64_t mz_int64; +typedef uint64_t mz_uint64; +typedef int mz_bool; + +#define MZ_FALSE (0) +#define MZ_TRUE (1) + +/* Works around MSVC's spammy "warning C4127: conditional expression is constant" message. */ +#ifdef _MSC_VER +#define MZ_MACRO_END while (0, 0) +#else +#define MZ_MACRO_END while (0) +#endif + +#ifdef MINIZ_NO_STDIO +#define MZ_FILE void * +#else +#include +#define MZ_FILE FILE +#endif /* #ifdef MINIZ_NO_STDIO */ + +#ifdef MINIZ_NO_TIME +typedef struct mz_dummy_time_t_tag +{ + mz_uint32 m_dummy1; + mz_uint32 m_dummy2; +} mz_dummy_time_t; +#define MZ_TIME_T mz_dummy_time_t +#else +#define MZ_TIME_T time_t +#endif + +#define MZ_ASSERT(x) assert(x) + +#ifdef MINIZ_NO_MALLOC +#define MZ_MALLOC(x) NULL +#define MZ_FREE(x) (void)x, ((void)0) +#define MZ_REALLOC(p, x) NULL +#else +#define MZ_MALLOC(x) malloc(x) +#define MZ_FREE(x) free(x) +#define MZ_REALLOC(p, x) realloc(p, x) +#endif + +#define MZ_MAX(a, b) (((a) > (b)) ? (a) : (b)) +#define MZ_MIN(a, b) (((a) < (b)) ? (a) : (b)) +#define MZ_CLEAR_OBJ(obj) memset(&(obj), 0, sizeof(obj)) +#define MZ_CLEAR_ARR(obj) memset((obj), 0, sizeof(obj)) +#define MZ_CLEAR_PTR(obj) memset((obj), 0, sizeof(*obj)) + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN +#define MZ_READ_LE16(p) *((const mz_uint16 *)(p)) +#define MZ_READ_LE32(p) *((const mz_uint32 *)(p)) +#else +#define MZ_READ_LE16(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U)) +#define MZ_READ_LE32(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U) | ((mz_uint32)(((const mz_uint8 *)(p))[2]) << 16U) | ((mz_uint32)(((const mz_uint8 *)(p))[3]) << 24U)) +#endif + +#define MZ_READ_LE64(p) (((mz_uint64)MZ_READ_LE32(p)) | (((mz_uint64)MZ_READ_LE32((const mz_uint8 *)(p) + sizeof(mz_uint32))) << 32U)) + +#ifdef _MSC_VER +#define MZ_FORCEINLINE __forceinline +#elif defined(__GNUC__) +#define MZ_FORCEINLINE __inline__ __attribute__((__always_inline__)) +#else +#define MZ_FORCEINLINE inline +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +extern MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size); +extern MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address); +extern MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size); + +#define MZ_UINT16_MAX (0xFFFFU) +#define MZ_UINT32_MAX (0xFFFFFFFFU) + +#ifdef __cplusplus +} +#endif + #pragma once + + +#ifndef MINIZ_NO_DEFLATE_APIS + +#ifdef __cplusplus +extern "C" { +#endif +/* ------------------- Low-level Compression API Definitions */ + +/* Set TDEFL_LESS_MEMORY to 1 to use less memory (compression will be slightly slower, and raw/dynamic blocks will be output more frequently). */ +#define TDEFL_LESS_MEMORY 0 + +/* tdefl_init() compression flags logically OR'd together (low 12 bits contain the max. number of probes per dictionary search): */ +/* TDEFL_DEFAULT_MAX_PROBES: The compressor defaults to 128 dictionary probes per dictionary search. 0=Huffman only, 1=Huffman+LZ (fastest/crap compression), 4095=Huffman+LZ (slowest/best compression). */ +enum +{ + TDEFL_HUFFMAN_ONLY = 0, + TDEFL_DEFAULT_MAX_PROBES = 128, + TDEFL_MAX_PROBES_MASK = 0xFFF +}; + +/* TDEFL_WRITE_ZLIB_HEADER: If set, the compressor outputs a zlib header before the deflate data, and the Adler-32 of the source data at the end. Otherwise, you'll get raw deflate data. */ +/* TDEFL_COMPUTE_ADLER32: Always compute the adler-32 of the input data (even when not writing zlib headers). */ +/* TDEFL_GREEDY_PARSING_FLAG: Set to use faster greedy parsing, instead of more efficient lazy parsing. */ +/* TDEFL_NONDETERMINISTIC_PARSING_FLAG: Enable to decrease the compressor's initialization time to the minimum, but the output may vary from run to run given the same input (depending on the contents of memory). */ +/* TDEFL_RLE_MATCHES: Only look for RLE matches (matches with a distance of 1) */ +/* TDEFL_FILTER_MATCHES: Discards matches <= 5 chars if enabled. */ +/* TDEFL_FORCE_ALL_STATIC_BLOCKS: Disable usage of optimized Huffman tables. */ +/* TDEFL_FORCE_ALL_RAW_BLOCKS: Only use raw (uncompressed) deflate blocks. */ +/* The low 12 bits are reserved to control the max # of hash probes per dictionary lookup (see TDEFL_MAX_PROBES_MASK). */ +enum +{ + TDEFL_WRITE_ZLIB_HEADER = 0x01000, + TDEFL_COMPUTE_ADLER32 = 0x02000, + TDEFL_GREEDY_PARSING_FLAG = 0x04000, + TDEFL_NONDETERMINISTIC_PARSING_FLAG = 0x08000, + TDEFL_RLE_MATCHES = 0x10000, + TDEFL_FILTER_MATCHES = 0x20000, + TDEFL_FORCE_ALL_STATIC_BLOCKS = 0x40000, + TDEFL_FORCE_ALL_RAW_BLOCKS = 0x80000 +}; + +/* High level compression functions: */ +/* tdefl_compress_mem_to_heap() compresses a block in memory to a heap block allocated via malloc(). */ +/* On entry: */ +/* pSrc_buf, src_buf_len: Pointer and size of source block to compress. */ +/* flags: The max match finder probes (default is 128) logically OR'd against the above flags. Higher probes are slower but improve compression. */ +/* On return: */ +/* Function returns a pointer to the compressed data, or NULL on failure. */ +/* *pOut_len will be set to the compressed data's size, which could be larger than src_buf_len on uncompressible data. */ +/* The caller must free() the returned block when it's no longer needed. */ +MINIZ_EXPORT void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags); + +/* tdefl_compress_mem_to_mem() compresses a block in memory to another block in memory. */ +/* Returns 0 on failure. */ +MINIZ_EXPORT size_t tdefl_compress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags); + +/* Compresses an image to a compressed PNG file in memory. */ +/* On entry: */ +/* pImage, w, h, and num_chans describe the image to compress. num_chans may be 1, 2, 3, or 4. */ +/* The image pitch in bytes per scanline will be w*num_chans. The leftmost pixel on the top scanline is stored first in memory. */ +/* level may range from [0,10], use MZ_NO_COMPRESSION, MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc. or a decent default is MZ_DEFAULT_LEVEL */ +/* If flip is true, the image will be flipped on the Y axis (useful for OpenGL apps). */ +/* On return: */ +/* Function returns a pointer to the compressed data, or NULL on failure. */ +/* *pLen_out will be set to the size of the PNG image file. */ +/* The caller must mz_free() the returned heap block (which will typically be larger than *pLen_out) when it's no longer needed. */ +MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory_ex(const void *pImage, int w, int h, int num_chans, size_t *pLen_out, mz_uint level, mz_bool flip); +MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory(const void *pImage, int w, int h, int num_chans, size_t *pLen_out); + +/* Output stream interface. The compressor uses this interface to write compressed data. It'll typically be called TDEFL_OUT_BUF_SIZE at a time. */ +typedef mz_bool (*tdefl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser); + +/* tdefl_compress_mem_to_output() compresses a block to an output stream. The above helpers use this function internally. */ +MINIZ_EXPORT mz_bool tdefl_compress_mem_to_output(const void *pBuf, size_t buf_len, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags); + +enum +{ + TDEFL_MAX_HUFF_TABLES = 3, + TDEFL_MAX_HUFF_SYMBOLS_0 = 288, + TDEFL_MAX_HUFF_SYMBOLS_1 = 32, + TDEFL_MAX_HUFF_SYMBOLS_2 = 19, + TDEFL_LZ_DICT_SIZE = 32768, + TDEFL_LZ_DICT_SIZE_MASK = TDEFL_LZ_DICT_SIZE - 1, + TDEFL_MIN_MATCH_LEN = 3, + TDEFL_MAX_MATCH_LEN = 258 +}; + +/* TDEFL_OUT_BUF_SIZE MUST be large enough to hold a single entire compressed output block (using static/fixed Huffman codes). */ +#if TDEFL_LESS_MEMORY +enum +{ + TDEFL_LZ_CODE_BUF_SIZE = 24 * 1024, + TDEFL_OUT_BUF_SIZE = (TDEFL_LZ_CODE_BUF_SIZE * 13) / 10, + TDEFL_MAX_HUFF_SYMBOLS = 288, + TDEFL_LZ_HASH_BITS = 12, + TDEFL_LEVEL1_HASH_SIZE_MASK = 4095, + TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3, + TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS +}; +#else +enum +{ + TDEFL_LZ_CODE_BUF_SIZE = 64 * 1024, + TDEFL_OUT_BUF_SIZE = (TDEFL_LZ_CODE_BUF_SIZE * 13) / 10, + TDEFL_MAX_HUFF_SYMBOLS = 288, + TDEFL_LZ_HASH_BITS = 15, + TDEFL_LEVEL1_HASH_SIZE_MASK = 4095, + TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3, + TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS +}; +#endif + +/* The low-level tdefl functions below may be used directly if the above helper functions aren't flexible enough. The low-level functions don't make any heap allocations, unlike the above helper functions. */ +typedef enum { + TDEFL_STATUS_BAD_PARAM = -2, + TDEFL_STATUS_PUT_BUF_FAILED = -1, + TDEFL_STATUS_OKAY = 0, + TDEFL_STATUS_DONE = 1 +} tdefl_status; + +/* Must map to MZ_NO_FLUSH, MZ_SYNC_FLUSH, etc. enums */ +typedef enum { + TDEFL_NO_FLUSH = 0, + TDEFL_SYNC_FLUSH = 2, + TDEFL_FULL_FLUSH = 3, + TDEFL_FINISH = 4 +} tdefl_flush; + +/* tdefl's compression state structure. */ +typedef struct +{ + tdefl_put_buf_func_ptr m_pPut_buf_func; + void *m_pPut_buf_user; + mz_uint m_flags, m_max_probes[2]; + int m_greedy_parsing; + mz_uint m_adler32, m_lookahead_pos, m_lookahead_size, m_dict_size; + mz_uint8 *m_pLZ_code_buf, *m_pLZ_flags, *m_pOutput_buf, *m_pOutput_buf_end; + mz_uint m_num_flags_left, m_total_lz_bytes, m_lz_code_buf_dict_pos, m_bits_in, m_bit_buffer; + mz_uint m_saved_match_dist, m_saved_match_len, m_saved_lit, m_output_flush_ofs, m_output_flush_remaining, m_finished, m_block_index, m_wants_to_finish; + tdefl_status m_prev_return_status; + const void *m_pIn_buf; + void *m_pOut_buf; + size_t *m_pIn_buf_size, *m_pOut_buf_size; + tdefl_flush m_flush; + const mz_uint8 *m_pSrc; + size_t m_src_buf_left, m_out_buf_ofs; + mz_uint8 m_dict[TDEFL_LZ_DICT_SIZE + TDEFL_MAX_MATCH_LEN - 1]; + mz_uint16 m_huff_count[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS]; + mz_uint16 m_huff_codes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS]; + mz_uint8 m_huff_code_sizes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS]; + mz_uint8 m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE]; + mz_uint16 m_next[TDEFL_LZ_DICT_SIZE]; + mz_uint16 m_hash[TDEFL_LZ_HASH_SIZE]; + mz_uint8 m_output_buf[TDEFL_OUT_BUF_SIZE]; +} tdefl_compressor; + +/* Initializes the compressor. */ +/* There is no corresponding deinit() function because the tdefl API's do not dynamically allocate memory. */ +/* pBut_buf_func: If NULL, output data will be supplied to the specified callback. In this case, the user should call the tdefl_compress_buffer() API for compression. */ +/* If pBut_buf_func is NULL the user should always call the tdefl_compress() API. */ +/* flags: See the above enums (TDEFL_HUFFMAN_ONLY, TDEFL_WRITE_ZLIB_HEADER, etc.) */ +MINIZ_EXPORT tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags); + +/* Compresses a block of data, consuming as much of the specified input buffer as possible, and writing as much compressed data to the specified output buffer as possible. */ +MINIZ_EXPORT tdefl_status tdefl_compress(tdefl_compressor *d, const void *pIn_buf, size_t *pIn_buf_size, void *pOut_buf, size_t *pOut_buf_size, tdefl_flush flush); + +/* tdefl_compress_buffer() is only usable when the tdefl_init() is called with a non-NULL tdefl_put_buf_func_ptr. */ +/* tdefl_compress_buffer() always consumes the entire input buffer. */ +MINIZ_EXPORT tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, size_t in_buf_size, tdefl_flush flush); + +MINIZ_EXPORT tdefl_status tdefl_get_prev_return_status(tdefl_compressor *d); +MINIZ_EXPORT mz_uint32 tdefl_get_adler32(tdefl_compressor *d); + +/* Create tdefl_compress() flags given zlib-style compression parameters. */ +/* level may range from [0,10] (where 10 is absolute max compression, but may be much slower on some files) */ +/* window_bits may be -15 (raw deflate) or 15 (zlib) */ +/* strategy may be either MZ_DEFAULT_STRATEGY, MZ_FILTERED, MZ_HUFFMAN_ONLY, MZ_RLE, or MZ_FIXED */ +MINIZ_EXPORT mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy); + +#ifndef MINIZ_NO_MALLOC +/* Allocate the tdefl_compressor structure in C so that */ +/* non-C language bindings to tdefl_ API don't need to worry about */ +/* structure size and allocation mechanism. */ +MINIZ_EXPORT tdefl_compressor *tdefl_compressor_alloc(void); +MINIZ_EXPORT void tdefl_compressor_free(tdefl_compressor *pComp); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + #pragma once + +/* ------------------- Low-level Decompression API Definitions */ + +#ifndef MINIZ_NO_INFLATE_APIS + +#ifdef __cplusplus +extern "C" { +#endif +/* Decompression flags used by tinfl_decompress(). */ +/* TINFL_FLAG_PARSE_ZLIB_HEADER: If set, the input has a valid zlib header and ends with an adler32 checksum (it's a valid zlib stream). Otherwise, the input is a raw deflate stream. */ +/* TINFL_FLAG_HAS_MORE_INPUT: If set, there are more input bytes available beyond the end of the supplied input buffer. If clear, the input buffer contains all remaining input. */ +/* TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF: If set, the output buffer is large enough to hold the entire decompressed stream. If clear, the output buffer is at least the size of the dictionary (typically 32KB). */ +/* TINFL_FLAG_COMPUTE_ADLER32: Force adler-32 checksum computation of the decompressed bytes. */ +enum +{ + TINFL_FLAG_PARSE_ZLIB_HEADER = 1, + TINFL_FLAG_HAS_MORE_INPUT = 2, + TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF = 4, + TINFL_FLAG_COMPUTE_ADLER32 = 8 +}; + +/* High level decompression functions: */ +/* tinfl_decompress_mem_to_heap() decompresses a block in memory to a heap block allocated via malloc(). */ +/* On entry: */ +/* pSrc_buf, src_buf_len: Pointer and size of the Deflate or zlib source data to decompress. */ +/* On return: */ +/* Function returns a pointer to the decompressed data, or NULL on failure. */ +/* *pOut_len will be set to the decompressed data's size, which could be larger than src_buf_len on uncompressible data. */ +/* The caller must call mz_free() on the returned block when it's no longer needed. */ +MINIZ_EXPORT void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags); + +/* tinfl_decompress_mem_to_mem() decompresses a block in memory to another block in memory. */ +/* Returns TINFL_DECOMPRESS_MEM_TO_MEM_FAILED on failure, or the number of bytes written on success. */ +#define TINFL_DECOMPRESS_MEM_TO_MEM_FAILED ((size_t)(-1)) +MINIZ_EXPORT size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags); + +/* tinfl_decompress_mem_to_callback() decompresses a block in memory to an internal 32KB buffer, and a user provided callback function will be called to flush the buffer. */ +/* Returns 1 on success or 0 on failure. */ +typedef int (*tinfl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser); +MINIZ_EXPORT int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags); + +struct tinfl_decompressor_tag; +typedef struct tinfl_decompressor_tag tinfl_decompressor; + +#ifndef MINIZ_NO_MALLOC +/* Allocate the tinfl_decompressor structure in C so that */ +/* non-C language bindings to tinfl_ API don't need to worry about */ +/* structure size and allocation mechanism. */ +MINIZ_EXPORT tinfl_decompressor *tinfl_decompressor_alloc(void); +MINIZ_EXPORT void tinfl_decompressor_free(tinfl_decompressor *pDecomp); +#endif + +/* Max size of LZ dictionary. */ +#define TINFL_LZ_DICT_SIZE 32768 + +/* Return status. */ +typedef enum { + /* This flags indicates the inflator needs 1 or more input bytes to make forward progress, but the caller is indicating that no more are available. The compressed data */ + /* is probably corrupted. If you call the inflator again with more bytes it'll try to continue processing the input but this is a BAD sign (either the data is corrupted or you called it incorrectly). */ + /* If you call it again with no input you'll just get TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS again. */ + TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS = -4, + + /* This flag indicates that one or more of the input parameters was obviously bogus. (You can try calling it again, but if you get this error the calling code is wrong.) */ + TINFL_STATUS_BAD_PARAM = -3, + + /* This flags indicate the inflator is finished but the adler32 check of the uncompressed data didn't match. If you call it again it'll return TINFL_STATUS_DONE. */ + TINFL_STATUS_ADLER32_MISMATCH = -2, + + /* This flags indicate the inflator has somehow failed (bad code, corrupted input, etc.). If you call it again without resetting via tinfl_init() it it'll just keep on returning the same status failure code. */ + TINFL_STATUS_FAILED = -1, + + /* Any status code less than TINFL_STATUS_DONE must indicate a failure. */ + + /* This flag indicates the inflator has returned every byte of uncompressed data that it can, has consumed every byte that it needed, has successfully reached the end of the deflate stream, and */ + /* if zlib headers and adler32 checking enabled that it has successfully checked the uncompressed data's adler32. If you call it again you'll just get TINFL_STATUS_DONE over and over again. */ + TINFL_STATUS_DONE = 0, + + /* This flag indicates the inflator MUST have more input data (even 1 byte) before it can make any more forward progress, or you need to clear the TINFL_FLAG_HAS_MORE_INPUT */ + /* flag on the next call if you don't have any more source data. If the source data was somehow corrupted it's also possible (but unlikely) for the inflator to keep on demanding input to */ + /* proceed, so be sure to properly set the TINFL_FLAG_HAS_MORE_INPUT flag. */ + TINFL_STATUS_NEEDS_MORE_INPUT = 1, + + /* This flag indicates the inflator definitely has 1 or more bytes of uncompressed data available, but it cannot write this data into the output buffer. */ + /* Note if the source compressed data was corrupted it's possible for the inflator to return a lot of uncompressed data to the caller. I've been assuming you know how much uncompressed data to expect */ + /* (either exact or worst case) and will stop calling the inflator and fail after receiving too much. In pure streaming scenarios where you have no idea how many bytes to expect this may not be possible */ + /* so I may need to add some code to address this. */ + TINFL_STATUS_HAS_MORE_OUTPUT = 2 +} tinfl_status; + +/* Initializes the decompressor to its initial state. */ +#define tinfl_init(r) \ + do \ + { \ + (r)->m_state = 0; \ + } \ + MZ_MACRO_END +#define tinfl_get_adler32(r) (r)->m_check_adler32 + +/* Main low-level decompressor coroutine function. This is the only function actually needed for decompression. All the other functions are just high-level helpers for improved usability. */ +/* This is a universal API, i.e. it can be used as a building block to build any desired higher level decompression API. In the limit case, it can be called once per every byte input or output. */ +MINIZ_EXPORT tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags); + +/* Internal/private bits follow. */ +enum +{ + TINFL_MAX_HUFF_TABLES = 3, + TINFL_MAX_HUFF_SYMBOLS_0 = 288, + TINFL_MAX_HUFF_SYMBOLS_1 = 32, + TINFL_MAX_HUFF_SYMBOLS_2 = 19, + TINFL_FAST_LOOKUP_BITS = 10, + TINFL_FAST_LOOKUP_SIZE = 1 << TINFL_FAST_LOOKUP_BITS +}; + +#if MINIZ_HAS_64BIT_REGISTERS +#define TINFL_USE_64BIT_BITBUF 1 +#else +#define TINFL_USE_64BIT_BITBUF 0 +#endif + +#if TINFL_USE_64BIT_BITBUF +typedef mz_uint64 tinfl_bit_buf_t; +#define TINFL_BITBUF_SIZE (64) +#else +typedef mz_uint32 tinfl_bit_buf_t; +#define TINFL_BITBUF_SIZE (32) +#endif + +struct tinfl_decompressor_tag +{ + mz_uint32 m_state, m_num_bits, m_zhdr0, m_zhdr1, m_z_adler32, m_final, m_type, m_check_adler32, m_dist, m_counter, m_num_extra, m_table_sizes[TINFL_MAX_HUFF_TABLES]; + tinfl_bit_buf_t m_bit_buf; + size_t m_dist_from_out_buf_start; + mz_int16 m_look_up[TINFL_MAX_HUFF_TABLES][TINFL_FAST_LOOKUP_SIZE]; + mz_int16 m_tree_0[TINFL_MAX_HUFF_SYMBOLS_0 * 2]; + mz_int16 m_tree_1[TINFL_MAX_HUFF_SYMBOLS_1 * 2]; + mz_int16 m_tree_2[TINFL_MAX_HUFF_SYMBOLS_2 * 2]; + mz_uint8 m_code_size_0[TINFL_MAX_HUFF_SYMBOLS_0]; + mz_uint8 m_code_size_1[TINFL_MAX_HUFF_SYMBOLS_1]; + mz_uint8 m_code_size_2[TINFL_MAX_HUFF_SYMBOLS_2]; + mz_uint8 m_raw_header[4], m_len_codes[TINFL_MAX_HUFF_SYMBOLS_0 + TINFL_MAX_HUFF_SYMBOLS_1 + 137]; +}; + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + +#pragma once + + +/* ------------------- ZIP archive reading/writing */ + +#ifndef MINIZ_NO_ARCHIVE_APIS + +#ifdef __cplusplus +extern "C" { +#endif + +enum +{ + /* Note: These enums can be reduced as needed to save memory or stack space - they are pretty conservative. */ + MZ_ZIP_MAX_IO_BUF_SIZE = 64 * 1024, + MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE = 512, + MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE = 512 +}; + +typedef struct +{ + /* Central directory file index. */ + mz_uint32 m_file_index; + + /* Byte offset of this entry in the archive's central directory. Note we currently only support up to UINT_MAX or less bytes in the central dir. */ + mz_uint64 m_central_dir_ofs; + + /* These fields are copied directly from the zip's central dir. */ + mz_uint16 m_version_made_by; + mz_uint16 m_version_needed; + mz_uint16 m_bit_flag; + mz_uint16 m_method; + + /* CRC-32 of uncompressed data. */ + mz_uint32 m_crc32; + + /* File's compressed size. */ + mz_uint64 m_comp_size; + + /* File's uncompressed size. Note, I've seen some old archives where directory entries had 512 bytes for their uncompressed sizes, but when you try to unpack them you actually get 0 bytes. */ + mz_uint64 m_uncomp_size; + + /* Zip internal and external file attributes. */ + mz_uint16 m_internal_attr; + mz_uint32 m_external_attr; + + /* Entry's local header file offset in bytes. */ + mz_uint64 m_local_header_ofs; + + /* Size of comment in bytes. */ + mz_uint32 m_comment_size; + + /* MZ_TRUE if the entry appears to be a directory. */ + mz_bool m_is_directory; + + /* MZ_TRUE if the entry uses encryption/strong encryption (which miniz_zip doesn't support) */ + mz_bool m_is_encrypted; + + /* MZ_TRUE if the file is not encrypted, a patch file, and if it uses a compression method we support. */ + mz_bool m_is_supported; + + /* Filename. If string ends in '/' it's a subdirectory entry. */ + /* Guaranteed to be zero terminated, may be truncated to fit. */ + char m_filename[MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE]; + + /* Comment field. */ + /* Guaranteed to be zero terminated, may be truncated to fit. */ + char m_comment[MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE]; + +#ifdef MINIZ_NO_TIME + MZ_TIME_T m_padding; +#else + MZ_TIME_T m_time; +#endif +} mz_zip_archive_file_stat; + +typedef size_t (*mz_file_read_func)(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n); +typedef size_t (*mz_file_write_func)(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n); +typedef mz_bool (*mz_file_needs_keepalive)(void *pOpaque); + +struct mz_zip_internal_state_tag; +typedef struct mz_zip_internal_state_tag mz_zip_internal_state; + +typedef enum { + MZ_ZIP_MODE_INVALID = 0, + MZ_ZIP_MODE_READING = 1, + MZ_ZIP_MODE_WRITING = 2, + MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED = 3 +} mz_zip_mode; + +typedef enum { + MZ_ZIP_FLAG_CASE_SENSITIVE = 0x0100, + MZ_ZIP_FLAG_IGNORE_PATH = 0x0200, + MZ_ZIP_FLAG_COMPRESSED_DATA = 0x0400, + MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY = 0x0800, + MZ_ZIP_FLAG_VALIDATE_LOCATE_FILE_FLAG = 0x1000, /* if enabled, mz_zip_reader_locate_file() will be called on each file as its validated to ensure the func finds the file in the central dir (intended for testing) */ + MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY = 0x2000, /* validate the local headers, but don't decompress the entire file and check the crc32 */ + MZ_ZIP_FLAG_WRITE_ZIP64 = 0x4000, /* always use the zip64 file format, instead of the original zip file format with automatic switch to zip64. Use as flags parameter with mz_zip_writer_init*_v2 */ + MZ_ZIP_FLAG_WRITE_ALLOW_READING = 0x8000, + MZ_ZIP_FLAG_ASCII_FILENAME = 0x10000, + /*After adding a compressed file, seek back + to local file header and set the correct sizes*/ + MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE = 0x20000 +} mz_zip_flags; + +typedef enum { + MZ_ZIP_TYPE_INVALID = 0, + MZ_ZIP_TYPE_USER, + MZ_ZIP_TYPE_MEMORY, + MZ_ZIP_TYPE_HEAP, + MZ_ZIP_TYPE_FILE, + MZ_ZIP_TYPE_CFILE, + MZ_ZIP_TOTAL_TYPES +} mz_zip_type; + +/* miniz error codes. Be sure to update mz_zip_get_error_string() if you add or modify this enum. */ +typedef enum { + MZ_ZIP_NO_ERROR = 0, + MZ_ZIP_UNDEFINED_ERROR, + MZ_ZIP_TOO_MANY_FILES, + MZ_ZIP_FILE_TOO_LARGE, + MZ_ZIP_UNSUPPORTED_METHOD, + MZ_ZIP_UNSUPPORTED_ENCRYPTION, + MZ_ZIP_UNSUPPORTED_FEATURE, + MZ_ZIP_FAILED_FINDING_CENTRAL_DIR, + MZ_ZIP_NOT_AN_ARCHIVE, + MZ_ZIP_INVALID_HEADER_OR_CORRUPTED, + MZ_ZIP_UNSUPPORTED_MULTIDISK, + MZ_ZIP_DECOMPRESSION_FAILED, + MZ_ZIP_COMPRESSION_FAILED, + MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE, + MZ_ZIP_CRC_CHECK_FAILED, + MZ_ZIP_UNSUPPORTED_CDIR_SIZE, + MZ_ZIP_ALLOC_FAILED, + MZ_ZIP_FILE_OPEN_FAILED, + MZ_ZIP_FILE_CREATE_FAILED, + MZ_ZIP_FILE_WRITE_FAILED, + MZ_ZIP_FILE_READ_FAILED, + MZ_ZIP_FILE_CLOSE_FAILED, + MZ_ZIP_FILE_SEEK_FAILED, + MZ_ZIP_FILE_STAT_FAILED, + MZ_ZIP_INVALID_PARAMETER, + MZ_ZIP_INVALID_FILENAME, + MZ_ZIP_BUF_TOO_SMALL, + MZ_ZIP_INTERNAL_ERROR, + MZ_ZIP_FILE_NOT_FOUND, + MZ_ZIP_ARCHIVE_TOO_LARGE, + MZ_ZIP_VALIDATION_FAILED, + MZ_ZIP_WRITE_CALLBACK_FAILED, + MZ_ZIP_TOTAL_ERRORS +} mz_zip_error; + +typedef struct +{ + mz_uint64 m_archive_size; + mz_uint64 m_central_directory_file_ofs; + + /* We only support up to UINT32_MAX files in zip64 mode. */ + mz_uint32 m_total_files; + mz_zip_mode m_zip_mode; + mz_zip_type m_zip_type; + mz_zip_error m_last_error; + + mz_uint64 m_file_offset_alignment; + + mz_alloc_func m_pAlloc; + mz_free_func m_pFree; + mz_realloc_func m_pRealloc; + void *m_pAlloc_opaque; + + mz_file_read_func m_pRead; + mz_file_write_func m_pWrite; + mz_file_needs_keepalive m_pNeeds_keepalive; + void *m_pIO_opaque; + + mz_zip_internal_state *m_pState; + +} mz_zip_archive; + +typedef struct +{ + mz_zip_archive *pZip; + mz_uint flags; + + int status; + + mz_uint64 read_buf_size, read_buf_ofs, read_buf_avail, comp_remaining, out_buf_ofs, cur_file_ofs; + mz_zip_archive_file_stat file_stat; + void *pRead_buf; + void *pWrite_buf; + + size_t out_blk_remain; + + tinfl_decompressor inflator; + +#ifdef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + mz_uint padding; +#else + mz_uint file_crc32; +#endif + +} mz_zip_reader_extract_iter_state; + +/* -------- ZIP reading */ + +/* Inits a ZIP archive reader. */ +/* These functions read and validate the archive's central directory. */ +MINIZ_EXPORT mz_bool mz_zip_reader_init(mz_zip_archive *pZip, mz_uint64 size, mz_uint flags); + +MINIZ_EXPORT mz_bool mz_zip_reader_init_mem(mz_zip_archive *pZip, const void *pMem, size_t size, mz_uint flags); + +#ifndef MINIZ_NO_STDIO +/* Read a archive from a disk file. */ +/* file_start_ofs is the file offset where the archive actually begins, or 0. */ +/* actual_archive_size is the true total size of the archive, which may be smaller than the file's actual size on disk. If zero the entire file is treated as the archive. */ +MINIZ_EXPORT mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint32 flags); +MINIZ_EXPORT mz_bool mz_zip_reader_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags, mz_uint64 file_start_ofs, mz_uint64 archive_size); + +/* Read an archive from an already opened FILE, beginning at the current file position. */ +/* The archive is assumed to be archive_size bytes long. If archive_size is 0, then the entire rest of the file is assumed to contain the archive. */ +/* The FILE will NOT be closed when mz_zip_reader_end() is called. */ +MINIZ_EXPORT mz_bool mz_zip_reader_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint64 archive_size, mz_uint flags); +#endif + +/* Ends archive reading, freeing all allocations, and closing the input archive file if mz_zip_reader_init_file() was used. */ +MINIZ_EXPORT mz_bool mz_zip_reader_end(mz_zip_archive *pZip); + +/* -------- ZIP reading or writing */ + +/* Clears a mz_zip_archive struct to all zeros. */ +/* Important: This must be done before passing the struct to any mz_zip functions. */ +MINIZ_EXPORT void mz_zip_zero_struct(mz_zip_archive *pZip); + +MINIZ_EXPORT mz_zip_mode mz_zip_get_mode(mz_zip_archive *pZip); +MINIZ_EXPORT mz_zip_type mz_zip_get_type(mz_zip_archive *pZip); + +/* Returns the total number of files in the archive. */ +MINIZ_EXPORT mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip); + +MINIZ_EXPORT mz_uint64 mz_zip_get_archive_size(mz_zip_archive *pZip); +MINIZ_EXPORT mz_uint64 mz_zip_get_archive_file_start_offset(mz_zip_archive *pZip); +MINIZ_EXPORT MZ_FILE *mz_zip_get_cfile(mz_zip_archive *pZip); + +/* Reads n bytes of raw archive data, starting at file offset file_ofs, to pBuf. */ +MINIZ_EXPORT size_t mz_zip_read_archive_data(mz_zip_archive *pZip, mz_uint64 file_ofs, void *pBuf, size_t n); + +/* All mz_zip funcs set the m_last_error field in the mz_zip_archive struct. These functions retrieve/manipulate this field. */ +/* Note that the m_last_error functionality is not thread safe. */ +MINIZ_EXPORT mz_zip_error mz_zip_set_last_error(mz_zip_archive *pZip, mz_zip_error err_num); +MINIZ_EXPORT mz_zip_error mz_zip_peek_last_error(mz_zip_archive *pZip); +MINIZ_EXPORT mz_zip_error mz_zip_clear_last_error(mz_zip_archive *pZip); +MINIZ_EXPORT mz_zip_error mz_zip_get_last_error(mz_zip_archive *pZip); +MINIZ_EXPORT const char *mz_zip_get_error_string(mz_zip_error mz_err); + +/* MZ_TRUE if the archive file entry is a directory entry. */ +MINIZ_EXPORT mz_bool mz_zip_reader_is_file_a_directory(mz_zip_archive *pZip, mz_uint file_index); + +/* MZ_TRUE if the file is encrypted/strong encrypted. */ +MINIZ_EXPORT mz_bool mz_zip_reader_is_file_encrypted(mz_zip_archive *pZip, mz_uint file_index); + +/* MZ_TRUE if the compression method is supported, and the file is not encrypted, and the file is not a compressed patch file. */ +MINIZ_EXPORT mz_bool mz_zip_reader_is_file_supported(mz_zip_archive *pZip, mz_uint file_index); + +/* Retrieves the filename of an archive file entry. */ +/* Returns the number of bytes written to pFilename, or if filename_buf_size is 0 this function returns the number of bytes needed to fully store the filename. */ +MINIZ_EXPORT mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size); + +/* Attempts to locates a file in the archive's central directory. */ +/* Valid flags: MZ_ZIP_FLAG_CASE_SENSITIVE, MZ_ZIP_FLAG_IGNORE_PATH */ +/* Returns -1 if the file cannot be found. */ +MINIZ_EXPORT int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags); +MINIZ_EXPORT mz_bool mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *file_index); + +/* Returns detailed information about an archive file entry. */ +MINIZ_EXPORT mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat); + +/* MZ_TRUE if the file is in zip64 format. */ +/* A file is considered zip64 if it contained a zip64 end of central directory marker, or if it contained any zip64 extended file information fields in the central directory. */ +MINIZ_EXPORT mz_bool mz_zip_is_zip64(mz_zip_archive *pZip); + +/* Returns the total central directory size in bytes. */ +/* The current max supported size is <= MZ_UINT32_MAX. */ +MINIZ_EXPORT size_t mz_zip_get_central_dir_size(mz_zip_archive *pZip); + +/* Extracts a archive file to a memory buffer using no memory allocation. */ +/* There must be at least enough room on the stack to store the inflator's state (~34KB or so). */ +MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem_no_alloc(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size); +MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem_no_alloc(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size); + +/* Extracts a archive file to a memory buffer. */ +MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags); +MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags); + +/* Extracts a archive file to a dynamically allocated heap buffer. */ +/* The memory will be allocated via the mz_zip_archive's alloc/realloc functions. */ +/* Returns NULL and sets the last error on failure. */ +MINIZ_EXPORT void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags); +MINIZ_EXPORT void *mz_zip_reader_extract_file_to_heap(mz_zip_archive *pZip, const char *pFilename, size_t *pSize, mz_uint flags); + +/* Extracts a archive file using a callback function to output the file's data. */ +MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_callback(mz_zip_archive *pZip, mz_uint file_index, mz_file_write_func pCallback, void *pOpaque, mz_uint flags); +MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_callback(mz_zip_archive *pZip, const char *pFilename, mz_file_write_func pCallback, void *pOpaque, mz_uint flags); + +/* Extract a file iteratively */ +MINIZ_EXPORT mz_zip_reader_extract_iter_state* mz_zip_reader_extract_iter_new(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags); +MINIZ_EXPORT mz_zip_reader_extract_iter_state* mz_zip_reader_extract_file_iter_new(mz_zip_archive *pZip, const char *pFilename, mz_uint flags); +MINIZ_EXPORT size_t mz_zip_reader_extract_iter_read(mz_zip_reader_extract_iter_state* pState, void* pvBuf, size_t buf_size); +MINIZ_EXPORT mz_bool mz_zip_reader_extract_iter_free(mz_zip_reader_extract_iter_state* pState); + +#ifndef MINIZ_NO_STDIO +/* Extracts a archive file to a disk file and sets its last accessed and modified times. */ +/* This function only extracts files, not archive directory records. */ +MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_file(mz_zip_archive *pZip, mz_uint file_index, const char *pDst_filename, mz_uint flags); +MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_file(mz_zip_archive *pZip, const char *pArchive_filename, const char *pDst_filename, mz_uint flags); + +/* Extracts a archive file starting at the current position in the destination FILE stream. */ +MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_cfile(mz_zip_archive *pZip, mz_uint file_index, MZ_FILE *File, mz_uint flags); +MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_cfile(mz_zip_archive *pZip, const char *pArchive_filename, MZ_FILE *pFile, mz_uint flags); +#endif + +#if 0 +/* TODO */ + typedef void *mz_zip_streaming_extract_state_ptr; + mz_zip_streaming_extract_state_ptr mz_zip_streaming_extract_begin(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags); + mz_uint64 mz_zip_streaming_extract_get_size(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState); + mz_uint64 mz_zip_streaming_extract_get_cur_ofs(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState); + mz_bool mz_zip_streaming_extract_seek(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, mz_uint64 new_ofs); + size_t mz_zip_streaming_extract_read(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, void *pBuf, size_t buf_size); + mz_bool mz_zip_streaming_extract_end(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState); +#endif + +/* This function compares the archive's local headers, the optional local zip64 extended information block, and the optional descriptor following the compressed data vs. the data in the central directory. */ +/* It also validates that each file can be successfully uncompressed unless the MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY is specified. */ +MINIZ_EXPORT mz_bool mz_zip_validate_file(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags); + +/* Validates an entire archive by calling mz_zip_validate_file() on each file. */ +MINIZ_EXPORT mz_bool mz_zip_validate_archive(mz_zip_archive *pZip, mz_uint flags); + +/* Misc utils/helpers, valid for ZIP reading or writing */ +MINIZ_EXPORT mz_bool mz_zip_validate_mem_archive(const void *pMem, size_t size, mz_uint flags, mz_zip_error *pErr); +#ifndef MINIZ_NO_STDIO +MINIZ_EXPORT mz_bool mz_zip_validate_file_archive(const char *pFilename, mz_uint flags, mz_zip_error *pErr); +#endif + +/* Universal end function - calls either mz_zip_reader_end() or mz_zip_writer_end(). */ +MINIZ_EXPORT mz_bool mz_zip_end(mz_zip_archive *pZip); + +/* -------- ZIP writing */ + +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS + +/* Inits a ZIP archive writer. */ +/*Set pZip->m_pWrite (and pZip->m_pIO_opaque) before calling mz_zip_writer_init or mz_zip_writer_init_v2*/ +/*The output is streamable, i.e. file_ofs in mz_file_write_func always increases only by n*/ +MINIZ_EXPORT mz_bool mz_zip_writer_init(mz_zip_archive *pZip, mz_uint64 existing_size); +MINIZ_EXPORT mz_bool mz_zip_writer_init_v2(mz_zip_archive *pZip, mz_uint64 existing_size, mz_uint flags); + +MINIZ_EXPORT mz_bool mz_zip_writer_init_heap(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size); +MINIZ_EXPORT mz_bool mz_zip_writer_init_heap_v2(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size, mz_uint flags); + +#ifndef MINIZ_NO_STDIO +MINIZ_EXPORT mz_bool mz_zip_writer_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning); +MINIZ_EXPORT mz_bool mz_zip_writer_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning, mz_uint flags); +MINIZ_EXPORT mz_bool mz_zip_writer_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint flags); +#endif + +/* Converts a ZIP archive reader object into a writer object, to allow efficient in-place file appends to occur on an existing archive. */ +/* For archives opened using mz_zip_reader_init_file, pFilename must be the archive's filename so it can be reopened for writing. If the file can't be reopened, mz_zip_reader_end() will be called. */ +/* For archives opened using mz_zip_reader_init_mem, the memory block must be growable using the realloc callback (which defaults to realloc unless you've overridden it). */ +/* Finally, for archives opened using mz_zip_reader_init, the mz_zip_archive's user provided m_pWrite function cannot be NULL. */ +/* Note: In-place archive modification is not recommended unless you know what you're doing, because if execution stops or something goes wrong before */ +/* the archive is finalized the file's central directory will be hosed. */ +MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader(mz_zip_archive *pZip, const char *pFilename); +MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags); + +/* Adds the contents of a memory buffer to an archive. These functions record the current local time into the archive. */ +/* To add a directory entry, call this method with an archive name ending in a forwardslash with an empty buffer. */ +/* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */ +MINIZ_EXPORT mz_bool mz_zip_writer_add_mem(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, mz_uint level_and_flags); + +/* Like mz_zip_writer_add_mem(), except you can specify a file comment field, and optionally supply the function with already compressed data. */ +/* uncomp_size/uncomp_crc32 are only used if the MZ_ZIP_FLAG_COMPRESSED_DATA flag is specified. */ +MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + mz_uint64 uncomp_size, mz_uint32 uncomp_crc32); + +MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex_v2(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + mz_uint64 uncomp_size, mz_uint32 uncomp_crc32, MZ_TIME_T *last_modified, const char *user_extra_data_local, mz_uint user_extra_data_local_len, + const char *user_extra_data_central, mz_uint user_extra_data_central_len); + +/* Adds the contents of a file to an archive. This function also records the disk file's modified time into the archive. */ +/* File data is supplied via a read callback function. User mz_zip_writer_add_(c)file to add a file directly.*/ +MINIZ_EXPORT mz_bool mz_zip_writer_add_read_buf_callback(mz_zip_archive *pZip, const char *pArchive_name, mz_file_read_func read_callback, void* callback_opaque, mz_uint64 max_size, + const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len, + const char *user_extra_data_central, mz_uint user_extra_data_central_len); + + +#ifndef MINIZ_NO_STDIO +/* Adds the contents of a disk file to an archive. This function also records the disk file's modified time into the archive. */ +/* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */ +MINIZ_EXPORT mz_bool mz_zip_writer_add_file(mz_zip_archive *pZip, const char *pArchive_name, const char *pSrc_filename, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags); + +/* Like mz_zip_writer_add_file(), except the file data is read from the specified FILE stream. */ +MINIZ_EXPORT mz_bool mz_zip_writer_add_cfile(mz_zip_archive *pZip, const char *pArchive_name, MZ_FILE *pSrc_file, mz_uint64 max_size, + const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len, + const char *user_extra_data_central, mz_uint user_extra_data_central_len); +#endif + +/* Adds a file to an archive by fully cloning the data from another archive. */ +/* This function fully clones the source file's compressed data (no recompression), along with its full filename, extra data (it may add or modify the zip64 local header extra data field), and the optional descriptor following the compressed data. */ +MINIZ_EXPORT mz_bool mz_zip_writer_add_from_zip_reader(mz_zip_archive *pZip, mz_zip_archive *pSource_zip, mz_uint src_file_index); + +/* Finalizes the archive by writing the central directory records followed by the end of central directory record. */ +/* After an archive is finalized, the only valid call on the mz_zip_archive struct is mz_zip_writer_end(). */ +/* An archive must be manually finalized by calling this function for it to be valid. */ +MINIZ_EXPORT mz_bool mz_zip_writer_finalize_archive(mz_zip_archive *pZip); + +/* Finalizes a heap archive, returning a pointer to the heap block and its size. */ +/* The heap block will be allocated using the mz_zip_archive's alloc/realloc callbacks. */ +MINIZ_EXPORT mz_bool mz_zip_writer_finalize_heap_archive(mz_zip_archive *pZip, void **ppBuf, size_t *pSize); + +/* Ends archive writing, freeing all allocations, and closing the output file if mz_zip_writer_init_file() was used. */ +/* Note for the archive to be valid, it *must* have been finalized before ending (this function will not do it for you). */ +MINIZ_EXPORT mz_bool mz_zip_writer_end(mz_zip_archive *pZip); + +/* -------- Misc. high-level helper functions: */ + +/* mz_zip_add_mem_to_archive_file_in_place() efficiently (but not atomically) appends a memory blob to a ZIP archive. */ +/* Note this is NOT a fully safe operation. If it crashes or dies in some way your archive can be left in a screwed up state (without a central directory). */ +/* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */ +/* TODO: Perhaps add an option to leave the existing central dir in place in case the add dies? We could then truncate the file (so the old central dir would be at the end) if something goes wrong. */ +MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags); +MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place_v2(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_zip_error *pErr); + +#ifndef MINIZ_NO_STDIO +/* Reads a single file from an archive into a heap block. */ +/* If pComment is not NULL, only the file with the specified comment will be extracted. */ +/* Returns NULL on failure. */ +MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint flags); +MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap_v2(const char *pZip_filename, const char *pArchive_name, const char *pComment, size_t *pSize, mz_uint flags, mz_zip_error *pErr); +#endif + +#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS */ + +#ifdef __cplusplus +} +#endif + +#endif /* MINIZ_NO_ARCHIVE_APIS */ diff --git a/Apps/Maps/host/ui_preview.cpp b/Apps/Maps/host/ui_preview.cpp new file mode 100644 index 0000000..b3a9cac --- /dev/null +++ b/Apps/Maps/host/ui_preview.cpp @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Draws the app's screens on the PC with the same UI code, over a real map, so layouts can be checked without +// a device. Text is drawn as blocks (FakeFont). Usage: ui_preview [--mono] + +#include "../main/Source/render/Compose.h" +#include "../main/Source/render/TileRenderer.h" +#include "../main/Source/ui/Canvas.h" +#include "../main/Source/ui/Controls.h" +#include "../main/Source/ui/DownloadFrame.h" +#include "../main/Source/ui/MapOverlay.h" +#include "../main/Source/ui/Widgets.h" +#include "FakeFont.h" +#include "Png.h" + +#include +#include +#include +#include + +using namespace maps; + +#ifndef FIXTURE_DIR +#define FIXTURE_DIR "../tests/fixtures" +#endif + +namespace { + +constexpr int WIDTH = 320; +constexpr int HEIGHT = 240; + +struct Preview { + std::vector map; + std::vector pixels; + Canvas canvas; + FakeFont font; + MapOverlay overlay; + const Palette* palette; + UiTheme theme; + bool mono; + std::string folder; + + Preview(const std::string& out, bool monochrome) : map(WIDTH * HEIGHT), pixels(WIDTH * HEIGHT), mono(monochrome), folder(out) { + canvas = { pixels.data(), WIDTH, HEIGHT }; + palette = mono ? &monochromePalette() : &colorPalette(); + theme = uiTheme(*palette, mono); + overlay.setFonts(&font, &font); + } + + void startFromMap() { pixels = map; } + + void drawMapOverlay(bool fullAttribution) { + overlay.setBottomInset(0); + overlay.drawAccuracyCircle(canvas, 150, 110, 40.0f, *palette); + overlay.drawMarker(canvas, 150, 110, true, 45.0f, *palette); + overlay.drawScaleBar(canvas, 9.5, *palette); + if (fullAttribution) { + int x; + int y; + int w; + int h; + overlay.drawAttribution(canvas, *palette, x, y, w, h); + } else { + overlay.drawAttributionMark(canvas, *palette); + } + } + + void save(const char* name) { + const std::string path = folder + "/" + name + (mono ? "-mono" : "") + ".png"; + if (writePngRgb565(path.c_str(), pixels.data(), WIDTH, HEIGHT)) { + printf("wrote %s\n", path.c_str()); + } + } +}; + +std::vector readFile(const char* path) { + std::vector bytes; + FILE* file = fopen(path, "rb"); + if (file == nullptr) { + return bytes; + } + uint8_t buffer[4096]; + size_t count; + while ((count = fread(buffer, 1, sizeof(buffer), file)) > 0) { + bytes.insert(bytes.end(), buffer, buffer + count); + } + fclose(file); + return bytes; +} + +struct OneTile { + std::vector pixels; +}; + +const uint16_t* lookup(const TileKey& tile, void* context) { + auto* one = static_cast(context); + return tile == TileKey({ 14, 9058, 5729 }) ? one->pixels.data() : nullptr; +} + +} // namespace + +int main(int argc, char* argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: ui_preview [--mono]\n"); + return 2; + } + const bool mono = argc > 2 && strcmp(argv[2], "--mono") == 0; + auto* preview = new Preview(argv[1], mono); + + // The fixture tile, drawn into the middle of the screen + const std::vector pbf = readFile(FIXTURE_DIR "/14-9058-5729.pbf"); + auto* renderer = new TileRenderer(); + OneTile tile; + tile.pixels.resize(TILE_SIZE * TILE_SIZE); + MemoryInput input(pbf.data(), pbf.size()); + if (!renderer->init() || !renderer->render(&input, { 14, 9058, 5729 }, { 14, 9058, 5729 }, *preview->palette, tile.pixels.data(), nullptr)) { + fprintf(stderr, "Could not render the fixture tile\n"); + return 1; + } + composeView(preview->map.data(), WIDTH, HEIGHT, 9058.5 * TILE_SIZE, 5729.5 * TILE_SIZE, 14, lookup, &tile, preview->palette->placeholder); + Canvas& canvas = preview->canvas; + Font& font = preview->font; + const UiTheme& theme = preview->theme; + + // The map as it opens, with the full attribution, and a toast + preview->startFromMap(); + preview->drawMapOverlay(true); + preview->save("map-start"); + preview->startFromMap(); + preview->drawMapOverlay(false); + preview->overlay.drawToast(canvas, "Location is off. Enable it in Tactility's Settings.", theme); + preview->save("map-toast"); + // An approximate (IP) position: a ring and a label, no accuracy circle + preview->startFromMap(); + preview->overlay.drawApproximateMarker(canvas, 150, 110, 25000.0f, *preview->palette); + preview->overlay.drawAttributionMark(canvas, *preview->palette); + preview->save("map-approximate"); + + // The control overlay: with key hints (T-Deck), touch only, and focus on a board with buttons only + Controls controls; + controls.layout(WIDTH, HEIGHT, preview->overlay.scaleBarTop(canvas)); + controls.setInputs({ true, true, false }); + controls.show(); + preview->startFromMap(); + preview->drawMapOverlay(false); + controls.draw(canvas, font, theme, PositionButton::Free); + preview->save("controls-keys"); + controls.setInputs({ false, true, false }); + preview->startFromMap(); + preview->drawMapOverlay(false); + controls.draw(canvas, font, theme, PositionButton::Following); + preview->save("controls-touch"); + controls.setInputs({ true, false, false }); + controls.show(); + controls.moveFocus(3); + preview->startFromMap(); + preview->drawMapOverlay(false); + controls.draw(canvas, font, theme, PositionButton::Unavailable); + preview->save("controls-focus"); + + // Download area: fixed frame, map under it, info bar with the Download button + const DownloadLayout layout = downloadLayout(WIDTH, HEIGHT, font.lineHeight()); + const double centerX = 9058.5 * TILE_SIZE; + const double centerY = 5729.5 * TILE_SIZE; + preview->startFromMap(); + drawDownloadScreen(canvas, font, theme, layout, planDownload(screenBounds(layout.frame, centerX, centerY, 14, WIDTH, HEIGHT), MAX_SOURCE_ZOOM)); + preview->save("download"); + + // Menu popup over the map + ListView menu; + for (const char* label : { "Download area", "Regions", "Settings", "About and licences" }) { + menu.add(label); + } + menu.layoutPopup(WIDTH, HEIGHT, font, 220); + menu.select(1); + preview->startFromMap(); + preview->drawMapOverlay(false); + menu.draw(canvas, font, theme); + preview->save("menu"); + + // Regions, with more rows than fit, and the delete question + ListView regions; + regions.setTitle("< Regions"); + regions.setHint("Enter: show Del: delete Esc: back"); + const char* names[] = { "area01 complete", "area02 downloading 42%", "budapest outdated", "szeged complete", "area05 paused 10%", + "vienna complete", "area07 complete", "area08 complete" }; + for (const char* name : names) { + regions.add(name, "12.3 MB 26-10-29"); + } + regions.layoutFullScreen(WIDTH, HEIGHT, font); + regions.select(6); + regions.draw(canvas, font, theme); + preview->save("regions"); + Dialog dialog; + dialog.open("Delete area07? Tiles other regions use are kept.", "Delete", true); + dialog.layout(WIDTH, HEIGHT, font); + dialog.draw(canvas, font, theme); + preview->save("regions-delete"); + + // Settings + ListView settings; + settings.setTitle("< Settings"); + settings.setHint("Enter or tap: change Esc: back"); + settings.add("SD card cache", "On"); + settings.add("Prepared tiles", "Deflated"); + settings.add("Smooth pinch", "On"); + settings.layoutFullScreen(WIDTH, HEIGHT, font); + settings.draw(canvas, font, theme); + preview->save("settings"); + + delete renderer; + delete preview; + return 0; +} diff --git a/Apps/Maps/main/CMakeLists.txt b/Apps/Maps/main/CMakeLists.txt new file mode 100644 index 0000000..b11441b --- /dev/null +++ b/Apps/Maps/main/CMakeLists.txt @@ -0,0 +1,8 @@ +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) +tactility_component_register( + SRCS ${SOURCE_FILES} + REQUIRES graphics-module binfont-module location-module http-module esp_http_client esp_rom mbedtls +) +target_compile_options(${COMPONENT_LIB} PRIVATE -O2 -Wno-format-truncation) diff --git a/Apps/Maps/main/Source/app/BinFontText.cpp b/Apps/Maps/main/Source/app/BinFontText.cpp new file mode 100644 index 0000000..52e66ac --- /dev/null +++ b/Apps/Maps/main/Source/app/BinFontText.cpp @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "BinFontText.h" + +#include "../platform/Platform.h" +#include "../render/Raster.h" + +#include + +#include +#include + +namespace maps { + +namespace { + +struct FontSearch { + const char* prefix; + char name[96]; +}; + +bool findFont(const char* name, bool isDirectory, void* context) { + auto* search = static_cast(context); + if (!isDirectory && strncmp(name, search->prefix, strlen(search->prefix)) == 0) { + snprintf(search->name, sizeof(search->name), "%s", name); + return false; + } + return true; +} + +/** Decodes one UTF-8 character and advances the pointer. */ +uint32_t nextCodepoint(const char*& text) { + const auto first = static_cast(*text++); + if (first < 0x80) { + return first; + } + if ((first & 0xE0) == 0xC0 && *text != '\0') { + return ((first & 0x1Fu) << 6) | (static_cast(*text++) & 0x3Fu); + } + // Longer sequences are skipped and shown as '?' + while ((static_cast(*text) & 0xC0) == 0x80) { + text++; + } + return '?'; +} + +} // namespace + +bool BinFontText::open(const char* folder, const char* const* prefixes, int prefixCount) { + for (int i = 0; i < prefixCount; i++) { + FontSearch search { prefixes[i], "" }; + listDirectory(folder, findFont, &search); + if (search.name[0] == '\0') { + continue; + } + char path[260]; + snprintf(path, sizeof(path), "%s/%s", folder, search.name); + if (binfont_open_file(path, &font) == ERROR_NONE) { + BinFontMetrics metrics; + binfont_get_metrics(font, &metrics); + height = metrics.line_height; + ascent = metrics.ascent; + logInfo("Font %s, line height %d", search.name, height); + return true; + } + font = nullptr; + } + return false; +} + +void BinFontText::close() { + for (auto& entry : glyphs) { + largeFree(entry.alpha); + entry = {}; + } + largeFree(copyright.alpha); + copyright = {}; + if (font != nullptr) { + binfont_close(font); + font = nullptr; + } +} + +void BinFontText::preload() { + for (uint32_t codepoint = 32; codepoint < 127; codepoint++) { + glyph(codepoint); + } + glyph(0xA9); +} + +BinFontText::Glyph* BinFontText::glyph(uint32_t codepoint) { + Glyph* entry; + if (codepoint >= 32 && codepoint < 128) { + entry = &glyphs[codepoint - 32]; + } else if (codepoint == 0xA9) { + entry = ©right; + } else { + entry = &glyphs['?' - 32]; + codepoint = '?'; + } + if (entry->loaded || font == nullptr) { + return entry; + } + entry->loaded = true; + BinFontGlyph info; + if (!binfont_get_glyph(font, codepoint, &info)) { + return entry; + } + entry->x = info.x; + entry->y = info.y; + entry->width = info.width; + entry->height = info.height; + entry->advance = static_cast((info.advance_x16 + 8) >> 4); + if (info.width > 0 && info.height > 0) { + entry->alpha = static_cast(largeAlloc(static_cast(info.width) * info.height)); + if (entry->alpha != nullptr && binfont_get_glyph_bitmap(font, &info, entry->alpha, info.width) != ERROR_NONE) { + largeFree(entry->alpha); + entry->alpha = nullptr; + } + } + return entry; +} + +int BinFontText::textWidth(const char* text) { + int width = 0; + while (*text != '\0') { + width += glyph(nextCodepoint(text))->advance; + } + return width; +} + +void BinFontText::drawText(Canvas& canvas, int x, int y, const char* text, uint16_t color) { + if (font == nullptr) { + return; + } + const int baseline = y + ascent; + while (*text != '\0') { + const Glyph* entry = glyph(nextCodepoint(text)); + if (entry->alpha != nullptr) { + const int left = x + entry->x; + const int top = baseline - (entry->y + entry->height); + for (int row = 0; row < entry->height; row++) { + for (int column = 0; column < entry->width; column++) { + const uint8_t alpha = entry->alpha[row * entry->width + column]; + if (alpha != 0) { + canvas.blendPixel(left + column, top + row, color, (alpha + 4u) >> 3); + } + } + } + } + x += entry->advance; + } +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/BinFontText.h b/Apps/Maps/main/Source/app/BinFontText.h new file mode 100644 index 0000000..919396b --- /dev/null +++ b/Apps/Maps/main/Source/app/BinFontText.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../ui/Canvas.h" + +#include + +struct BinFont; + +namespace maps { + +/** Text in one of Tactility's cached fonts (/cache/fonts), glyphs loaded once on first use. */ +class BinFontText final : public Font { +public: + ~BinFontText() override { close(); } + + /** Opens the first font whose file name starts with one of the prefixes. Call it on the I/O task. */ + bool open(const char* folder, const char* const* prefixes, int prefixCount); + void close(); + bool isOpen() const { return font != nullptr; } + /** Loads the glyphs of printable ASCII and ©, so drawing never reads the card. */ + void preload(); + + int lineHeight() const override { return font != nullptr ? height : 10; } + int textWidth(const char* text) override; + void drawText(Canvas& canvas, int x, int y, const char* text, uint16_t color) override; + +private: + struct Glyph { + int16_t x; + int16_t y; + uint16_t width; + uint16_t height; + uint16_t advance; + bool loaded; + uint8_t* alpha; + }; + + Glyph* glyph(uint32_t codepoint); + + BinFont* font = nullptr; + int height = 0; + int ascent = 0; + Glyph glyphs[96] {}; + Glyph copyright {}; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/Debug.cpp b/Apps/Maps/main/Source/app/Debug.cpp new file mode 100644 index 0000000..31132d7 --- /dev/null +++ b/Apps/Maps/main/Source/app/Debug.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Debug.h" + +#include "../platform/Platform.h" + +#include +#include + +namespace maps { + +namespace { + +void writeLittleEndian(uint8_t* out, uint32_t value, int bytes) { + for (int i = 0; i < bytes; i++) { + out[i] = static_cast(value >> (8 * i)); + } +} + +} // namespace + +bool saveBmp(const char* path, const uint16_t* pixels, int width, int height) { + FILE* file = fopen(path, "wb"); + if (file == nullptr) { + return false; + } + const int rowBytes = (width * 3 + 3) & ~3; + uint8_t header[54] = { 'B', 'M' }; + writeLittleEndian(header + 2, 54 + rowBytes * height, 4); + writeLittleEndian(header + 10, 54, 4); + writeLittleEndian(header + 14, 40, 4); + writeLittleEndian(header + 18, width, 4); + writeLittleEndian(header + 22, height, 4); + writeLittleEndian(header + 26, 1, 2); + writeLittleEndian(header + 28, 24, 2); + bool ok = fwrite(header, 1, sizeof(header), file) == sizeof(header); + auto* row = static_cast(largeAlloc(rowBytes)); + if (row == nullptr) { + fclose(file); + return false; + } + memset(row, 0, rowBytes); + for (int y = height - 1; y >= 0 && ok; y--) { + for (int x = 0; x < width; x++) { + const uint16_t c = pixels[y * width + x]; + row[x * 3 + 0] = static_cast((c & 0x1F) * 255 / 31); + row[x * 3 + 1] = static_cast(((c >> 5) & 0x3F) * 255 / 63); + row[x * 3 + 2] = static_cast(((c >> 11) & 0x1F) * 255 / 31); + } + ok = fwrite(row, 1, rowBytes, file) == static_cast(rowBytes); + } + largeFree(row); + fclose(file); + return ok; +} + +DebugScript::~DebugScript() { + largeFree(text); +} + +bool DebugScript::open(const char* root) { + char path[160]; + snprintf(path, sizeof(path), "%s/debug.txt", root); + FILE* file = fopen(path, "r"); + if (file == nullptr) { + return false; + } + constexpr size_t MAX_SCRIPT = 16 * 1024; + text = static_cast(largeAlloc(MAX_SCRIPT)); + if (text != nullptr) { + const size_t length = fread(text, 1, MAX_SCRIPT - 1, file); + text[length] = '\0'; + position = 0; + logInfo("Playing %s", path); + } + fclose(file); + return text != nullptr; +} + +DebugScript::Step DebugScript::next(uint32_t now) { + Step step {}; + step.action = Action::None; + if (text == nullptr || static_cast(now - resumeAt) < 0) { + return step; + } + char line[96]; + while (text[position] != '\0') { + size_t length = 0; + while (text[position] != '\0' && text[position] != '\n') { + if (length + 1 < sizeof(line)) { + line[length++] = text[position]; + } + position++; + } + line[length] = '\0'; + if (text[position] == '\n') { + position++; + } + char command[16] = ""; + char argument[48] = ""; + if (sscanf(line, "%15s %47[^\r\n]", command, argument) < 1 || command[0] == '#') { + continue; + } + logInfo("debug: %s %s", command, argument); + if (strcmp(command, "wait") == 0) { + resumeAt = now + static_cast(atoi(argument)); + return step; + } + if (strcmp(command, "key") == 0) { + step.action = Action::Key; + if (strcmp(argument, "esc") == 0) { + step.key = 0x1B; + } else if (strcmp(argument, "enter") == 0) { + step.key = 10; + } else if (strcmp(argument, "bs") == 0) { + step.key = '\b'; + } else { + step.key = static_cast(argument[0]); + } + return step; + } + if (strcmp(command, "shot") == 0) { + step.action = Action::Shot; + snprintf(step.name, sizeof(step.name), "%s", argument); + return step; + } + if (strcmp(command, "goto") == 0 && sscanf(argument, "%lf %lf %d", &step.lat, &step.lon, &step.zoom) == 3) { + step.action = Action::Goto; + return step; + } + if (strcmp(command, "idle") == 0) { + step.action = Action::WaitIdle; + return step; + } + if (strcmp(command, "exit") == 0) { + step.action = Action::Exit; + return step; + } + } + largeFree(text); + text = nullptr; + return step; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/Debug.h b/Apps/Maps/main/Source/app/Debug.h new file mode 100644 index 0000000..5604f8f --- /dev/null +++ b/Apps/Maps/main/Source/app/Debug.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +namespace maps { + +/** Writes an RGB565 frame as a 24-bit BMP file, for screenshots while LVGL is stopped. */ +bool saveBmp(const char* path, const uint16_t* pixels, int width, int height); + +/** + * Plays /debug.txt when it exists, so the app can be tested without touching the device. + * Lines: "key ", "wait ", "shot ", "goto ", "idle" (waits until the view is complete), "exit". + */ +class DebugScript { +public: + enum class Action { + None, + Key, + Shot, + Goto, + WaitIdle, + Exit + }; + + struct Step { + Action action; + uint32_t key; + char name[32]; + double lat; + double lon; + int zoom; + }; + + ~DebugScript(); + /** Reads the whole script into memory. Call it on the I/O task. */ + bool open(const char* root); + bool isActive() const { return text != nullptr; } + /** Returns the next step once its wait is over, or Action::None. */ + Step next(uint32_t now); + +private: + char* text = nullptr; + size_t position = 0; + uint32_t resumeAt = 0; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/DisplayOutput.cpp b/Apps/Maps/main/Source/app/DisplayOutput.cpp new file mode 100644 index 0000000..6cc16b2 --- /dev/null +++ b/Apps/Maps/main/Source/app/DisplayOutput.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "DisplayOutput.h" + +#include "../platform/Platform.h" + +#include +#include + +#include + +namespace maps { + +namespace { + +/** + * Strips are in internal DMA memory. In PSRAM the SPI driver copied every strip into a new internal DMA buffer of the + * same size, which failed while a download used most of the internal RAM, and the map flickered. + */ +constexpr int STRIP_ROWS = 12; + +} // namespace + +DisplayOutput::~DisplayOutput() { + end(); +} + +bool DisplayOutput::begin(Device* displayDevice) { + display = displayDevice; + panelWidth = display_get_resolution_x(display); + panelHeight = display_get_resolution_y(display); + format = display_get_color_format(display); + monochrome = format == DISPLAY_COLOR_FORMAT_MONOCHROME || format == DISPLAY_COLOR_FORMAT_GRAYSCALE8; + fullFrame = format == DISPLAY_COLOR_FORMAT_MONOCHROME || display_has_capability(display, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME); + + if (display_get_frame_buffer_count(display) == 2) { + void* first = nullptr; + void* second = nullptr; + display_get_frame_buffer(display, 0, &first); + display_get_frame_buffer(display, 1, &second); + if (first != nullptr && second != nullptr) { + hwBuffers[0] = pixel_buffer_wrap(first, format, panelWidth, panelHeight, 0); + hwBuffers[1] = pixel_buffer_wrap(second, format, panelWidth, panelHeight, 0); + } + if (hwBuffers[0] != nullptr && hwBuffers[1] != nullptr) { + logInfo("Display %dx%d format %d, hardware double buffer", panelWidth, panelHeight, format); + return true; + } + pixel_buffer_free(hwBuffers[0]); + pixel_buffer_free(hwBuffers[1]); + hwBuffers[0] = nullptr; + hwBuffers[1] = nullptr; + } + + if (fullFrame) { + frameBuffer = pixel_buffer_create(format, panelWidth, panelHeight); + logInfo("Display %dx%d format %d, full frames", panelWidth, panelHeight, format); + return frameBuffer != nullptr; + } + + const MemoryPolicy stripPolicy = { .required = MEMORY_CAPABILITY_INTERNAL | MEMORY_CAPABILITY_DMA, .desired = 0, .alignment = 64 }; + strips[0] = pixel_buffer_create_with_policy(format, panelWidth, STRIP_ROWS, &stripPolicy); + strips[1] = pixel_buffer_create_with_policy(format, panelWidth, STRIP_ROWS, &stripPolicy); + logInfo("Display %dx%d format %d, strips of %d rows", panelWidth, panelHeight, format, STRIP_ROWS); + return strips[0] != nullptr && strips[1] != nullptr; +} + +void DisplayOutput::end() { + for (auto*& buffer : hwBuffers) { + pixel_buffer_free(buffer); + buffer = nullptr; + } + for (auto*& buffer : strips) { + pixel_buffer_free(buffer); + buffer = nullptr; + } + pixel_buffer_free(frameBuffer); + frameBuffer = nullptr; + display = nullptr; +} + +void DisplayOutput::present(uint16_t* frame, int y, int height) { + if (display == nullptr) { + return; + } + PixelBuffer* source = pixel_buffer_wrap(frame, DISPLAY_COLOR_FORMAT_RGB565, panelWidth, panelHeight, 0); + if (source == nullptr) { + return; + } + const auto conversion = monochrome ? PIXEL_BUFFER_CONVERSION_LUMA_THRESHOLD : PIXEL_BUFFER_CONVERSION_AVERAGE; + + if (hwBuffers[0] != nullptr) { + // The whole frame goes to the back buffer, so it never shows an older frame in the other region + PixelBuffer* back = hwBuffers[backBuffer]; + pixel_buffer_blit(back, 0, 0, source, 0, 0, panelWidth, panelHeight, conversion); + pixel_buffer_msync(back, 0, 0, panelWidth, panelHeight); + display_draw_bitmap(display, 0, 0, panelWidth, panelHeight, pixel_buffer_get_data(back)); + backBuffer = 1 - backBuffer; + } else if (frameBuffer != nullptr) { + pixel_buffer_blit(frameBuffer, 0, y, source, 0, y, panelWidth, height, conversion); + pixel_buffer_msync(frameBuffer, 0, 0, panelWidth, panelHeight); + display_draw_bitmap(display, 0, 0, panelWidth, panelHeight, pixel_buffer_get_data(frameBuffer)); + } else { + for (int row = y; row < y + height; row += STRIP_ROWS) { + const int rows = std::min(STRIP_ROWS, y + height - row); + PixelBuffer* strip = strips[nextStrip]; + nextStrip = 1 - nextStrip; + pixel_buffer_blit(strip, 0, 0, source, 0, row, panelWidth, rows, conversion); + pixel_buffer_msync(strip, 0, 0, panelWidth, rows); + display_draw_bitmap(display, 0, row, panelWidth, row + rows, pixel_buffer_get_data(strip)); + } + } + pixel_buffer_free(source); +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/DisplayOutput.h b/Apps/Maps/main/Source/app/DisplayOutput.h new file mode 100644 index 0000000..eddfdef --- /dev/null +++ b/Apps/Maps/main/Source/app/DisplayOutput.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +#include + +struct PixelBuffer; + +namespace maps { + +/** + * Sends an RGB565 frame to the panel in its own colour format. Uses the hardware double buffer when the driver + * has one, a full frame buffer when the panel needs full frames, and otherwise small strips. + */ +class DisplayOutput { +public: + ~DisplayOutput(); + + bool begin(Device* displayDevice); + void end(); + + /** Sends rows y to y + height of a frame of width() x height() RGB565 pixels. */ + void present(uint16_t* frame, int y, int height); + + int width() const { return panelWidth; } + int height() const { return panelHeight; } + bool isMonochrome() const { return monochrome; } + +private: + Device* display = nullptr; + int panelWidth = 0; + int panelHeight = 0; + DisplayColorFormat format = DISPLAY_COLOR_FORMAT_RGB565; + bool monochrome = false; + bool fullFrame = false; + PixelBuffer* hwBuffers[2] = { nullptr, nullptr }; + int backBuffer = 1; + PixelBuffer* frameBuffer = nullptr; + PixelBuffer* strips[2] = { nullptr, nullptr }; + int nextStrip = 0; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/Downloader.cpp b/Apps/Maps/main/Source/app/Downloader.cpp new file mode 100644 index 0000000..5be6bae --- /dev/null +++ b/Apps/Maps/main/Source/app/Downloader.cpp @@ -0,0 +1,1015 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Downloader.h" +#include "RootCertificates.h" + +#include "../platform/Platform.h" +#include "../tiles/TileJson.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace maps { + +namespace { + +constexpr uint32_t CONTROL_STACK = 8192; +constexpr uint32_t CONNECTION_STACK = 6144; +constexpr UBaseType_t TASK_PRIORITY = 3; +/** + * Network tasks run on core 1. While a TLS handshake (2-3 s of CPU) ran on core 0, the map loop and the render lane + * there made no progress for its whole length; on core 1 no stall was seen (docs/upstream/https-stall.md). + */ +constexpr BaseType_t NETWORK_CORE = 1; + +/** + * Starts a network task with its stack in PSRAM, which keeps about 28 KB of internal RAM free for Wi-Fi, TLS and + * the display. These tasks never touch flash or the SD card themselves. The task ends by suspending itself after + * signalling its owner, who then deletes it with endNetworkTask(). + */ +bool startNetworkTask(TaskFunction_t function, const char* name, uint32_t stack, void* context, TaskHandle_t* task) { + return xTaskCreatePinnedToCoreWithCaps(function, name, stack, context, TASK_PRIORITY, task, NETWORK_CORE, MALLOC_CAP_SPIRAM) == pdPASS; +} + +void endNetworkTask(TaskHandle_t task) { + // It signalled before suspending itself, wait until it got there + TaskStatus_t status {}; + for (int i = 0; i < 100; i++) { + vTaskGetInfo(task, &status, pdFALSE, eInvalid); + if (status.eCurrentState == eSuspended) { + break; + } + vTaskDelay(pdMS_TO_TICKS(5)); + } + vTaskDeleteWithCaps(task); +} +constexpr int HTTP_TIMEOUT_MS = 15000; +constexpr int SAVE_EVERY_TILES = 20; +constexpr int MAX_ATTEMPTS = 3; + +bool wifiConnected() { + Device* wifi = nullptr; + if (device_get_first_active_by_type(&WIFI_TYPE, &wifi) != ERROR_NONE) { + return false; + } + WifiStationState state = WIFI_STATION_STATE_DISCONNECTED; + const bool connected = wifi_get_station_state(wifi, &state) == ERROR_NONE && state == WIFI_STATION_STATE_CONNECTED; + device_put(wifi); + return connected; +} + +/** Larger responses are refused, no real tile comes close. */ +constexpr size_t MAX_TILE_BYTES = 4 * 1024 * 1024; + +/** + * When the last connection was established (TCP and TLS done), in ms since boot, for the timing log. 32 bits: + * 64-bit atomics are not exported to apps on the ESP32-S3. + */ +std::atomic connectedAtMs = 0; + +uint32_t msSince(int64_t startUs, int64_t endUs) { + return static_cast((endUs - startUs) / 1000); +} + +esp_err_t onHttpEvent(esp_http_client_event_t* event) { + if (event->event_id == HTTP_EVENT_ON_CONNECTED) { + connectedAtMs = static_cast(esp_timer_get_time() / 1000); + } + if (event->event_id == HTTP_EVENT_ON_DATA && event->user_data != nullptr) { + auto* body = static_cast(event->user_data); + if (body->size() + static_cast(event->data_len) <= MAX_TILE_BYTES) { + body->putBytes(event->data, static_cast(event->data_len)); + } else { + body->reserve(MAX_TILE_BYTES + 1); + body->putBytes(event->data, 1); + } + } + return ESP_OK; +} + +esp_http_client_handle_t createClient(const char* url, ByteWriter* sink) { + esp_http_client_config_t config = {}; + config.url = url; + config.user_agent = USER_AGENT; + config.timeout_ms = HTTP_TIMEOUT_MS; + config.keep_alive_enable = true; + config.cert_pem = ROOT_CERTIFICATES; + config.cert_len = sizeof(ROOT_CERTIFICATES); + config.event_handler = onHttpEvent; + config.user_data = sink; + config.buffer_size = 4096; + esp_http_client_handle_t client = esp_http_client_init(&config); + if (client != nullptr) { + // Tiles are stored as the server compressed them + esp_http_client_set_header(client, "Accept-Encoding", "gzip"); + } + return client; +} + +void makeFolders(const char* root, const char* region, const TileKey& tile) { + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/maps/%s/%d", root, region, tile.z); + mkdir(path, 0777); + if (tileFolderPath(path, sizeof(path), root, region, tile)) { + mkdir(path, 0777); + } +} + +struct MetaJob { + char path[PATH_MAX_LENGTH]; + RegionMeta* meta; +}; + +bool readMetaJob(IoTask& io, void* context) { + (void)io; + auto* job = static_cast(context); + return readRegionMeta(job->path, *job->meta); +} + +bool writeMetaJob(IoTask& io, void* context) { + (void)io; + auto* job = static_cast(context); + return writeRegionMeta(job->path, *job->meta); +} + +struct OnCardJob { + const char* root; + const TileStore* store; + const char* region; + TileKey tile; + bool inThisRegion; +}; + +bool onCardJob(IoTask& io, void* context) { + (void)io; + auto* job = static_cast(context); + char path[PATH_MAX_LENGTH]; + job->inThisRegion = tilePath(path, sizeof(path), job->root, job->region, job->tile) && fileExists(path); + if (job->inThisRegion) { + return true; + } + // A tile of an overlapping region is shared instead of downloaded again + for (int i = 0; i < job->store->regionCount(); i++) { + const char* other = job->store->regionName(i); + if (strcmp(other, job->region) != 0 && tilePath(path, sizeof(path), job->root, other, job->tile) && fileExists(path)) { + return true; + } + } + return false; +} + +struct StoreTileJob { + const char* root; + const char* region; + TileKey tile; + const ByteWriter* body; + /** The tile column folder this connection made last, so it is not made again for every tile. */ + char* lastFolder; + bool yielded; +}; + +bool storeTileJob(IoTask& io, void* context) { + auto* job = static_cast(context); + char path[PATH_MAX_LENGTH]; + char temporary[PATH_MAX_LENGTH + 8]; + char prepared[PATH_MAX_LENGTH]; + if (!tilePath(path, sizeof(path), job->root, job->region, job->tile)) { + return false; + } + snprintf(temporary, sizeof(temporary), "%s.tmp", path); + char folder[PATH_MAX_LENGTH]; + if (tileFolderPath(folder, sizeof(folder), job->root, job->region, job->tile) && strcmp(folder, job->lastFolder) != 0) { + makeFolders(job->root, job->region, job->tile); + snprintf(job->lastFolder, PATH_MAX_LENGTH, "%s", folder); + } + // Only tiles that are not on the card are fetched, a prepared copy can only be left from a deleted original + if (preparedPath(prepared, sizeof(prepared), job->root, job->region, job->tile) && fileExists(prepared)) { + remove(prepared); + } + // A write of a few hundred KB gives way to tiles on screen, the tile stays in memory and is written again + const bool ok = io.writeFile(path, temporary, job->body->bytes(), job->body->size(), true); + job->yielded = io.yielded(); + return ok; +} + +constexpr int MAX_DELETE_COLUMNS = 4096; + +struct ColumnsJob { + char folder[PATH_MAX_LENGTH]; + int* columns; + int count; +}; + +bool listColumnsJob(IoTask& io, void* context) { + (void)io; + auto* job = static_cast(context); + job->count = 0; + return listDirectory(job->folder, [](const char* name, bool isDirectory, void* data) { + auto* columnsJob = static_cast(data); + if (isDirectory && name[0] >= '0' && name[0] <= '9' && columnsJob->count < MAX_DELETE_COLUMNS) { + columnsJob->columns[columnsJob->count++] = atoi(name); + } + return true; + }, job); +} + +struct RemoveJob { + char path[PATH_MAX_LENGTH]; + bool folder; +}; + +bool removeJob(IoTask& io, void* context) { + (void)io; + auto* job = static_cast(context); + return (job->folder ? rmdir(job->path) : remove(job->path)) == 0; +} + +} // namespace + +bool Downloader::start(const char* root, TileStore* tileStore, IoTask* ioTask) { + snprintf(rootPath, sizeof(rootPath), "%s", root); + store = tileStore; + io = ioTask; + mutex = xSemaphoreCreateMutex(); + wake = xSemaphoreCreateBinary(); + stopped = xSemaphoreCreateBinary(); + connectionsDone = xSemaphoreCreateCounting(CONNECTIONS, 0); + tileJsonLock = xSemaphoreCreateMutex(); + browseWake = xSemaphoreCreateBinary(); + browseStopped = xSemaphoreCreateBinary(); + if (mutex == nullptr || wake == nullptr || stopped == nullptr || connectionsDone == nullptr || tileJsonLock == nullptr || + browseWake == nullptr || browseStopped == nullptr) { + return false; + } + current = {}; + current.state = State::Idle; + stopRequested = false; + if (!startNetworkTask(controlMain, "maps-download", CONTROL_STACK, this, &controlTask)) { + controlTask = nullptr; + return false; + } + if (!startNetworkTask(browseMain, "maps-browse", CONTROL_STACK, this, &browseTask)) { + browseTask = nullptr; + logError("Browse task not started"); + } + return true; +} + +void Downloader::stop() { + if (controlTask != nullptr) { + stopRequested = true; + cancelRequested = true; + xSemaphoreGive(wake); + xSemaphoreTake(stopped, portMAX_DELAY); + endNetworkTask(controlTask); + controlTask = nullptr; + } + if (browseTask != nullptr) { + stopRequested = true; + xSemaphoreGive(browseWake); + xSemaphoreTake(browseStopped, portMAX_DELAY); + endNetworkTask(browseTask); + browseTask = nullptr; + } + for (SemaphoreHandle_t* handle : { &mutex, &wake, &stopped, &connectionsDone, &tileJsonLock, &browseWake, &browseStopped }) { + if (*handle != nullptr) { + vSemaphoreDelete(*handle); + *handle = nullptr; + } + } +} + +bool Downloader::queue(Job next, const char* region) { + xSemaphoreTake(mutex, portMAX_DELAY); + job = next; + snprintf(jobRegion, sizeof(jobRegion), "%s", region); + xSemaphoreGive(mutex); + // A running download is paused, it continues from its meta.json later + if (busy) { + cancelRequested = true; + } + busy = true; + xSemaphoreGive(wake); + return true; +} + +bool Downloader::download(const char* region) { + return queue(Job::Download, region); +} + +bool Downloader::remove(const char* region) { + return queue(Job::Delete, region); +} + +Downloader::Status Downloader::status() { + xSemaphoreTake(mutex, portMAX_DELAY); + Status copy = current; + xSemaphoreGive(mutex); + return copy; +} + +void Downloader::setStatus(State state, const char* message) { + xSemaphoreTake(mutex, portMAX_DELAY); + current.state = state; + snprintf(current.message, sizeof(current.message), "%s", message); + xSemaphoreGive(mutex); + if (state == State::Failed) { + logError("Download: %s", message); + } else { + logInfo("Download: %s", message); + } +} + +void Downloader::controlMain(void* context) { + auto* downloader = static_cast(context); + downloader->controlLoop(); + xSemaphoreGive(downloader->stopped); + vTaskSuspend(nullptr); +} + +void Downloader::controlLoop() { + while (!stopRequested) { + xSemaphoreTake(wake, portMAX_DELAY); + if (stopRequested) { + break; + } + cancelRequested = false; + xSemaphoreTake(mutex, portMAX_DELAY); + const Job requested = job; + job = Job::None; + snprintf(current.region, sizeof(current.region), "%s", jobRegion); + xSemaphoreGive(mutex); + if (requested == Job::Download) { + runDownload(); + } else if (requested == Job::Delete) { + runDelete(); + } + xSemaphoreTake(mutex, portMAX_DELAY); + const bool more = job != Job::None; + busy = more; + xSemaphoreGive(mutex); + } +} + +bool Downloader::fetchTileJson() { + if (timing) { + // Plain HTTP first: name lookup and TCP without TLS. The HTTPS request after it then finds the name cached. + esp_http_client_config_t config = {}; + config.url = "http://tiles.openfreemap.org/planet"; + config.user_agent = USER_AGENT; + config.timeout_ms = HTTP_TIMEOUT_MS; + config.event_handler = onHttpEvent; + esp_http_client_handle_t plain = esp_http_client_init(&config); + if (plain != nullptr) { + const int64_t startUs = esp_timer_get_time(); + connectedAtMs = 0; + const esp_err_t opened = esp_http_client_open(plain, 0); + const int64_t openedUs = esp_timer_get_time(); + const int64_t headers = opened == ESP_OK ? esp_http_client_fetch_headers(plain) : -1; + const int64_t headersUs = esp_timer_get_time(); + logInfo("Timing http: start t=%u ms, connected +%u ms (DNS and TCP), open +%u, headers +%u (%d, HTTP %d)", + static_cast(startUs / 1000), connectedAtMs > 0 ? connectedAtMs - static_cast(startUs / 1000) : 0, msSince(startUs, openedUs), + msSince(startUs, headersUs), static_cast(headers), esp_http_client_get_status_code(plain)); + esp_http_client_close(plain); + esp_http_client_cleanup(plain); + } + } + const int64_t initUs = esp_timer_get_time(); + esp_http_client_handle_t client = createClient(TILEJSON_URL, nullptr); + if (client == nullptr) { + return false; + } + // The TileJSON is read as plain text + esp_http_client_delete_header(client, "Accept-Encoding"); + bool ok = false; + const int64_t startUs = esp_timer_get_time(); + connectedAtMs = 0; + const esp_err_t opened = esp_http_client_open(client, 0); + const int64_t openedUs = esp_timer_get_time(); + const int64_t length = opened == ESP_OK ? esp_http_client_fetch_headers(client) : -1; + const int64_t headersUs = esp_timer_get_time(); + const int status = esp_http_client_get_status_code(client); + if (opened != ESP_OK || length < 0 || status != 200) { + logError("TileJSON request failed: %s, HTTP %d", esp_err_to_name(opened), status); + } else { + // The tile URL comes first in OpenFreeMap's TileJSON, the long list of layer fields after it + char* text = static_cast(largeAlloc(8192)); + if (text != nullptr) { + int received = 0; + while (received < 8191) { + const int count = esp_http_client_read(client, text + received, 8191 - received); + if (count <= 0) { + break; + } + received += count; + } + text[received] = '\0'; + if (timing) { + const int64_t readUs = esp_timer_get_time(); + logInfo("Timing https: init %u ms, start t=%u ms, connected +%u ms (DNS, TCP, TLS), open +%u, headers +%u, body +%u (%d bytes)", + msSince(initUs, startUs), static_cast(startUs / 1000), connectedAtMs > 0 ? connectedAtMs - static_cast(startUs / 1000) : 0, + msSince(startUs, openedUs), msSince(startUs, headersUs), msSince(startUs, readUs), received); + } + ok = parseTileJson(text, urlTemplate, sizeof(urlTemplate), version, sizeof(version)); + if (!ok) { + logError("TileJSON has no usable tile URL (%d bytes read)", received); + } + largeFree(text); + } + } + esp_http_client_close(client); + esp_http_client_cleanup(client); + if (ok) { + logInfo("Tile source %s (version %s)", urlTemplate, version); + } + return ok; +} + +bool Downloader::readMeta(const char* region, RegionMeta& out) { + MetaJob job {}; + snprintf(job.path, sizeof(job.path), "%s/maps/%s/meta.json", rootPath, region); + job.meta = &out; + return io->run(IoPriority::Background, readMetaJob, &job); +} + +bool Downloader::writeMeta(const RegionMeta& out) { + RegionMeta copy = out; + MetaJob job {}; + snprintf(job.path, sizeof(job.path), "%s/maps/%s/meta.json", rootPath, copy.name); + job.meta = © + return io->run(IoPriority::Background, writeMetaJob, &job); +} + +bool Downloader::tileOnCard(const char* region, const TileKey& tile, bool& inThisRegion) { + OnCardJob job { rootPath, store, region, tile, false }; + const bool found = io->run(IoPriority::Background, onCardJob, &job); + inThisRegion = job.inThisRegion; + return found; +} + +bool Downloader::downloadTile(Connection& connection, const char* region, const TileKey& tile, int& status) { + char url[200]; + if (!formatTileUrl(urlTemplate, tile, url, sizeof(url))) { + return false; + } + connection.body->clear(); + auto* client = static_cast(connection.client); + esp_http_client_set_url(client, url); + const esp_err_t result = esp_http_client_perform(client); + status = esp_http_client_get_status_code(client); + const size_t size = connection.body->size(); + if (result != ESP_OK || connection.body->failed() || size > MAX_TILE_BYTES || (status != 200 && status != 204 && status != 404)) { + return false; + } + if (status != 200 || size == 0) { + // An empty tile, the sea or empty land at this zoom level + return true; + } + // Card work waits until the map has been left alone for a moment, writes hold the display's shared bus + while (!idle && !stopRequested && !cancelRequested) { + vTaskDelay(pdMS_TO_TICKS(50)); + } + StoreTileJob job { rootPath, region, tile, connection.body, connection.lastFolder, false }; + bool stored = io->run(IoPriority::Background, storeTileJob, &job); + for (int attempt = 0; !stored && job.yielded && attempt < 20 && !stopRequested; attempt++) { + vTaskDelay(pdMS_TO_TICKS(50)); + stored = io->run(IoPriority::Background, storeTileJob, &job); + } + if (!stored) { + return false; + } + connection.received += static_cast(size); + return true; +} + +bool Downloader::claimIndex(int64_t& index, int connectionIndex) { + xSemaphoreTake(mutex, portMAX_DELAY); + const bool available = nextIndex < meta.total && !failed && !cancelRequested; + if (available) { + index = nextIndex++; + activeIndex[connectionIndex] = index; + } + xSemaphoreGive(mutex); + return available; +} + +void Downloader::finishIndex(int connectionIndex) { + xSemaphoreTake(mutex, portMAX_DELAY); + activeIndex[connectionIndex] = -1; + // Tiles finish out of order, the saved count is the first tile that is not finished yet + int64_t done = nextIndex; + for (int i = 0; i < CONNECTIONS; i++) { + if (activeIndex[i] >= 0 && activeIndex[i] < done) { + done = activeIndex[i]; + } + } + meta.done = done; + uint64_t bytes = 0; + for (int i = 0; i < CONNECTIONS; i++) { + bytes += connections[i].received; + } + current.done = done; + current.bytes = meta.bytes + bytes; + sinceSave++; + const bool save = sinceSave >= SAVE_EVERY_TILES; + if (save) { + sinceSave = 0; + } + xSemaphoreGive(mutex); + if (save) { + saveProgress(); + tilesChanged = true; + } +} + +void Downloader::saveProgress() { + xSemaphoreTake(mutex, portMAX_DELAY); + RegionMeta copy = meta; + uint64_t bytes = 0; + for (int i = 0; i < CONNECTIONS; i++) { + bytes += connections[i].received; + } + copy.bytes = meta.bytes + bytes; + xSemaphoreGive(mutex); + writeMeta(copy); +} + +void Downloader::connectionMain(void* context) { + auto* connection = static_cast(context); + connection->owner->connectionLoop(*connection); + xSemaphoreGive(connection->owner->connectionsDone); + vTaskSuspend(nullptr); +} + +void Downloader::connectionLoop(Connection& connection) { + int64_t index; + while (claimIndex(index, connection.index)) { + TileKey tile {}; + if (!tileAtIndex(meta.plan(), index, tile)) { + finishIndex(connection.index); + continue; + } + bool inThisRegion = false; + if (tileOnCard(meta.name, tile, inThisRegion)) { + finishIndex(connection.index); + continue; + } + bool ok = false; + int status = 0; + const uint32_t tileStart = nowMs(); + const uint64_t receivedBefore = connection.received; + for (int attempt = 1; attempt <= MAX_ATTEMPTS && !ok && !cancelRequested; attempt++) { + if (connection.client == nullptr) { + connection.client = createClient(TILEJSON_URL, connection.body); + } + ok = connection.client != nullptr && downloadTile(connection, meta.name, tile, status); + if (!ok) { + logError("[%d] Tile %d/%d/%d failed (HTTP %d), attempt %d", connection.index, tile.z, tile.x, tile.y, status, attempt); + if (connection.client != nullptr) { + esp_http_client_cleanup(static_cast(connection.client)); + connection.client = nullptr; + } + vTaskDelay(pdMS_TO_TICKS(1000 * attempt)); + } + } + if (!ok) { + failed = true; + } + // The limit holds for the real size too, the estimate can be lower for dense city centres + xSemaphoreTake(mutex, portMAX_DELAY); + uint64_t bytes = meta.bytes; + for (int i = 0; i < CONNECTIONS; i++) { + bytes += connections[i].received; + } + xSemaphoreGive(mutex); + if (bytes > MAX_DOWNLOAD_BYTES) { + failed = true; + } + if (ok) { + logInfo("[%d] Tile %d/%d/%d: %u bytes in %u ms (HTTP %d)", connection.index, tile.z, tile.x, tile.y, + static_cast(connection.received - receivedBefore), nowMs() - tileStart, status); + finishIndex(connection.index); + } + } + if (connection.client != nullptr) { + esp_http_client_cleanup(static_cast(connection.client)); + connection.client = nullptr; + } +} + +void Downloader::runDownload() { + if (!readMeta(current.region, meta)) { + setStatus(State::Failed, "Region information is missing"); + return; + } + snprintf(meta.name, sizeof(meta.name), "%s", current.region); + if (meta.total > MAX_DOWNLOAD_TILES) { + setStatus(State::Failed, "Area is larger than 3000 tiles"); + return; + } + if (!wifiConnected()) { + setStatus(State::Failed, "No Wi-Fi connection"); + return; + } + setStatus(State::Connecting, "Connecting to OpenFreeMap"); + logMemory("download start"); + if (!ensureTileJson()) { + setStatus(State::Failed, "OpenFreeMap is not reachable"); + return; + } + if (meta.source[0] == '\0') { + snprintf(meta.source, sizeof(meta.source), "%s", urlTemplate); + } + snprintf(meta.version, sizeof(meta.version), "%s", version); + xSemaphoreTake(mutex, portMAX_DELAY); + current.total = meta.total; + current.done = meta.done; + current.bytes = meta.bytes; + nextIndex = meta.done; + sinceSave = 0; + failed = false; + xSemaphoreGive(mutex); + setStatus(State::Downloading, "Downloading"); + + const uint32_t start = nowMs(); + int started = 0; + for (int i = 0; i < CONNECTIONS; i++) { + Connection& connection = connections[i]; + connection = { this, i, nullptr, &bodies[i], 0, nullptr, "" }; + activeIndex[i] = -1; + char name[16]; + snprintf(name, sizeof(name), "maps-http%d", i); + if (startNetworkTask(connectionMain, name, CONNECTION_STACK, &connection, &connection.task)) { + started++; + } else { + connection.task = nullptr; + } + } + for (int i = 0; i < started; i++) { + xSemaphoreTake(connectionsDone, portMAX_DELAY); + } + for (int i = 0; i < CONNECTIONS; i++) { + if (connections[i].task != nullptr) { + endNetworkTask(connections[i].task); + connections[i].task = nullptr; + } + bodies[i].release(); + } + logMemory("download end"); + + xSemaphoreTake(mutex, portMAX_DELAY); + for (int i = 0; i < CONNECTIONS; i++) { + meta.bytes += connections[i].received; + connections[i].received = 0; + } + if (!failed && !cancelRequested) { + meta.done = meta.total; + } + time_t now = time(nullptr); + struct tm local {}; + localtime_r(&now, &local); + strftime(meta.date, sizeof(meta.date), "%Y-%m-%d", &local); + current.done = meta.done; + current.bytes = meta.bytes; + xSemaphoreGive(mutex); + writeMeta(meta); + store->setRegionVersion(meta.name, meta.version); + tilesChanged = true; + + const uint32_t seconds = (nowMs() - start) / 1000; + char message[64]; + if (meta.bytes > MAX_DOWNLOAD_BYTES) { + setStatus(State::Failed, "Stopped at the 50 MB limit"); + } else if (failed) { + setStatus(State::Failed, "Download failed, it continues next time"); + } else if (cancelRequested) { + setStatus(State::Done, "Download paused"); + } else { + snprintf(message, sizeof(message), "Downloaded %d KB in %u s", static_cast(meta.bytes / 1024), static_cast(seconds)); + setStatus(State::Done, message); + } +} + +struct DeleteColumnJob { + const char* root; + const char* region; + int z; + int x; + const RegionMeta* others; + int otherCount; +}; + +namespace { + +bool deleteColumnJob(IoTask& io, void* context) { + (void)io; + auto* job = static_cast(context); + char column[PATH_MAX_LENGTH]; + snprintf(column, sizeof(column), "%s/maps/%s/%d/%d", job->root, job->region, job->z, job->x); + struct Files { + DeleteColumnJob* job; + const char* column; + } files { job, column }; + listDirectory(column, [](const char* fileName, bool fileIsDirectory, void* fileData) { + auto* f = static_cast(fileData); + if (fileIsDirectory) { + return true; + } + char file[PATH_MAX_LENGTH]; + snprintf(file, sizeof(file), "%s/%s", f->column, fileName); + // Only originals move to a region that still needs them, prepared copies are made again there + const size_t nameLength = strlen(fileName); + if (nameLength < 5 || (strcmp(fileName + nameLength - 4, ".pbf") != 0 && strcmp(fileName + nameLength - 4, ".PBF") != 0)) { + remove(file); + return true; + } + const TileKey tile { f->job->z, f->job->x, atoi(fileName) }; + for (int i = 0; i < f->job->otherCount; i++) { + const RegionMeta& other = f->job->others[i]; + const TileRange range = tileRange(other.box, tile.z); + char otherPath[PATH_MAX_LENGTH]; + if (tile.z >= other.minZoom && tile.z <= other.maxZoom && tile.x >= range.minX && tile.x <= range.maxX && + tile.y >= range.minY && tile.y <= range.maxY && tilePath(otherPath, sizeof(otherPath), f->job->root, other.name, tile) && + !fileExists(otherPath)) { + makeFolders(f->job->root, other.name, tile); + if (rename(file, otherPath) == 0) { + return true; + } + } + } + remove(file); + return true; + }, &files); + rmdir(column); + return true; +} + +} // namespace + +bool Downloader::ensureTileJson() { + // The region download and the browse task may both need it first + xSemaphoreTake(tileJsonLock, portMAX_DELAY); + const bool ok = urlTemplate[0] != '\0' || fetchTileJson(); + xSemaphoreGive(tileJsonLock); + return ok; +} + +bool Downloader::isOnline() { + return wifiConnected(); +} + +void Downloader::browse(const TileKey* tiles, int count) { + if (browseTask == nullptr) { + return; + } + xSemaphoreTake(mutex, portMAX_DELAY); + bool added = false; + for (int i = 0; i < count; i++) { + bool known = false; + for (int j = 0; j < browseCount && !known; j++) { + known = browseQueue[j] == tiles[i]; + } + for (int j = 0; j < browseEmptyCount && !known; j++) { + known = browseEmpty[j] == tiles[i]; + } + if (known || !isValidTile(tiles[i])) { + continue; + } + if (browseCount == BROWSE_QUEUE) { + // The oldest request goes, the newest view matters most + for (int j = 1; j < BROWSE_QUEUE; j++) { + browseQueue[j - 1] = browseQueue[j]; + } + browseCount--; + } + browseQueue[browseCount++] = tiles[i]; + added = true; + } + xSemaphoreGive(mutex); + if (added) { + xSemaphoreGive(browseWake); + } +} + +void Downloader::prepareBrowsing() { + if (browseTask != nullptr) { + warmUp = true; + xSemaphoreGive(browseWake); + } +} + +void Downloader::browseMain(void* context) { + auto* downloader = static_cast(context); + downloader->browseLoop(); + xSemaphoreGive(downloader->browseStopped); + vTaskSuspend(nullptr); +} + +bool Downloader::ensureBrowseRegion(const TileKey& tile) { + // The region's box grows with the tiles + const double west = worldXToLon(static_cast(tile.x) * TILE_SIZE, tile.z); + const double east = worldXToLon(static_cast(tile.x + 1) * TILE_SIZE, tile.z); + const double north = worldYToLat(static_cast(tile.y) * TILE_SIZE, tile.z); + const double south = worldYToLat(static_cast(tile.y + 1) * TILE_SIZE, tile.z); + if (!browseMetaReady) { + if (!readMeta(BROWSE_REGION, browseMeta)) { + browseMeta = {}; + browseMeta.box = { west, south, east, north }; + auto makeFolder = [&] { + char folder[PATH_MAX_LENGTH]; + snprintf(folder, sizeof(folder), "%s/maps/%s", rootPath, BROWSE_REGION); + mkdir(folder, 0777); + return true; + }; + runOnCard(*io, makeFolder, IoPriority::Background); + } + snprintf(browseMeta.name, sizeof(browseMeta.name), "%s", BROWSE_REGION); + browseMeta.minZoom = 0; + browseMeta.maxZoom = MAX_SOURCE_ZOOM; + browseMeta.total = 1; + browseMeta.done = 1; + snprintf(browseMeta.version, sizeof(browseMeta.version), "%s", version); + time_t now = time(nullptr); + struct tm local {}; + localtime_r(&now, &local); + strftime(browseMeta.date, sizeof(browseMeta.date), "%Y-%m-%d", &local); + if (!writeMeta(browseMeta)) { + return false; + } + store->addRegion(BROWSE_REGION); + store->setRegionVersion(BROWSE_REGION, version); + browseMetaReady = true; + } + browseMeta.box.west = std::min(browseMeta.box.west, west); + browseMeta.box.east = std::max(browseMeta.box.east, east); + browseMeta.box.south = std::min(browseMeta.box.south, south); + browseMeta.box.north = std::max(browseMeta.box.north, north); + store->setRegionBounds(BROWSE_REGION, browseMeta.box, browseMeta.minZoom, browseMeta.maxZoom); + return true; +} + +bool Downloader::fetchBrowseTile(const TileKey& tile) { + bool inThisRegion = false; + if (tileOnCard(BROWSE_REGION, tile, inThisRegion)) { + return true; + } + if (!ensureBrowseRegion(tile)) { + return false; + } + Connection& connection = browseConnection; + const uint64_t before = connection.received; + int status = 0; + bool ok = false; + for (int attempt = 1; attempt <= 2 && !ok && !stopRequested; attempt++) { + if (connection.client == nullptr) { + connection.client = createClient(TILEJSON_URL, connection.body); + } + ok = connection.client != nullptr && downloadTile(connection, BROWSE_REGION, tile, status); + if (!ok && connection.client != nullptr) { + esp_http_client_cleanup(static_cast(connection.client)); + connection.client = nullptr; + } + } + if (!ok) { + logError("Browse: tile %d/%d/%d failed (HTTP %d)", tile.z, tile.x, tile.y, status); + return false; + } + if (connection.received == before) { + // No data there: remembered, so it is not asked for again + xSemaphoreTake(mutex, portMAX_DELAY); + browseEmpty[browseEmptyNext] = tile; + browseEmptyNext = (browseEmptyNext + 1) % BROWSE_EMPTY; + if (browseEmptyCount < BROWSE_EMPTY) { + browseEmptyCount++; + } + xSemaphoreGive(mutex); + return true; + } + browseMeta.bytes += connection.received - before; + logInfo("Browse: tile %d/%d/%d, %u bytes", tile.z, tile.x, tile.y, static_cast(connection.received - before)); + if (++browseUnsaved >= 5) { + browseUnsaved = 0; + writeMeta(browseMeta); + } + tilesChanged = true; + return true; +} + +void Downloader::browseLoop() { + browseConnection = { this, CONNECTIONS, nullptr, &browseBody, 0, nullptr, "" }; + while (!stopRequested) { + xSemaphoreTake(browseWake, portMAX_DELAY); + if (warmUp.exchange(false) && wifiConnected()) { + ensureTileJson(); + } + while (!stopRequested) { + TileKey tile {}; + xSemaphoreTake(mutex, portMAX_DELAY); + const bool any = browseCount > 0; + if (any) { + tile = browseQueue[0]; + for (int i = 1; i < browseCount; i++) { + browseQueue[i - 1] = browseQueue[i]; + } + browseCount--; + } + xSemaphoreGive(mutex); + if (!any) { + break; + } + if (!wifiConnected() || !ensureTileJson()) { + // Offline: the map says so, the requests are dropped and come again with the next view + xSemaphoreTake(mutex, portMAX_DELAY); + browseCount = 0; + xSemaphoreGive(mutex); + break; + } + browsing = true; + if (!fetchBrowseTile(tile)) { + // A failing server is not asked again at once + vTaskDelay(pdMS_TO_TICKS(2000)); + } + } + browsing = false; + browseBody.release(); + if (browseUnsaved > 0) { + browseUnsaved = 0; + writeMeta(browseMeta); + } + } + if (browseConnection.client != nullptr) { + esp_http_client_cleanup(static_cast(browseConnection.client)); + browseConnection.client = nullptr; + } +} + +void Downloader::runDelete() { + setStatus(State::Deleting, "Deleting"); + if (strcmp(current.region, BROWSE_REGION) == 0) { + // Browsing starts the region again with its next tile + browseMetaReady = false; + } + RegionMeta target {}; + readMeta(current.region, target); + snprintf(target.name, sizeof(target.name), "%s", current.region); + + // Other regions keep the tiles they skipped because this region had them + auto* others = static_cast(largeAlloc(sizeof(RegionMeta) * TileStore::MAX_REGIONS)); + auto* columns = static_cast(largeAlloc(sizeof(int) * MAX_DELETE_COLUMNS)); + if (others == nullptr || columns == nullptr) { + largeFree(others); + largeFree(columns); + setStatus(State::Failed, "Out of memory"); + return; + } + int otherCount = 0; + for (int i = 0; i < store->regionCount() && otherCount < TileStore::MAX_REGIONS; i++) { + const char* name = store->regionName(i); + if (strcmp(name, target.name) != 0 && readMeta(name, others[otherCount])) { + snprintf(others[otherCount].name, sizeof(others[otherCount].name), "%s", name); + otherCount++; + } + } + + // One column folder per job, so the tiles on screen are read in between + for (int z = 0; z <= MAX_SOURCE_ZOOM && !cancelRequested; z++) { + ColumnsJob list {}; + snprintf(list.folder, sizeof(list.folder), "%s/maps/%s/%d", rootPath, target.name, z); + list.columns = columns; + if (!io->run(IoPriority::Background, listColumnsJob, &list)) { + continue; + } + for (int i = 0; i < list.count && !cancelRequested; i++) { + DeleteColumnJob job { rootPath, target.name, z, columns[i], others, otherCount }; + io->run(IoPriority::Background, deleteColumnJob, &job); + } + RemoveJob folder {}; + snprintf(folder.path, sizeof(folder.path), "%s", list.folder); + folder.folder = true; + io->run(IoPriority::Background, removeJob, &folder); + } + largeFree(others); + largeFree(columns); + RemoveJob metaFile {}; + snprintf(metaFile.path, sizeof(metaFile.path), "%s/maps/%s/meta.json", rootPath, target.name); + io->run(IoPriority::Background, removeJob, &metaFile); + RemoveJob folder {}; + snprintf(folder.path, sizeof(folder.path), "%s/maps/%s", rootPath, target.name); + folder.folder = true; + io->run(IoPriority::Background, removeJob, &folder); + tilesChanged = true; + setStatus(State::Done, cancelRequested ? "Delete stopped" : "Region deleted"); +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/Downloader.h b/Apps/Maps/main/Source/app/Downloader.h new file mode 100644 index 0000000..8d3fecd --- /dev/null +++ b/Apps/Maps/main/Source/app/Downloader.h @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "IoTask.h" + +#include "../render/PreparedTile.h" +#include "../tiles/RegionMeta.h" +#include "../tiles/TileStore.h" + +#include +#include +#include + +#include +#include + +namespace maps { + +/** + * Downloads the tiles of a region from OpenFreeMap in the background, and deletes regions. + * The tile URL comes from OpenFreeMap's TileJSON. Two connections run at the same time, each kept alive + * across tiles. A tile is received into PSRAM and written by the I/O task, after the tiles on screen. + * Progress is saved in the region's meta.json, so a download continues after a restart. + * + * While browsing, a third connection fetches the tiles on screen that have no detail on the card yet into the + * region "browsed", one tile at a time and only while Wi-Fi is connected. + */ +class Downloader { +public: + static constexpr int CONNECTIONS = 2; + + enum class State : uint8_t { + Idle, + Connecting, + Downloading, + Deleting, + Done, + Failed + }; + + struct Status { + State state; + char region[REGION_NAME_MAX + 1]; + int64_t done; + int64_t total; + uint64_t bytes; + char message[64]; + }; + + bool start(const char* root, TileStore* store, IoTask* io); + void stop(); + + /** Starts or continues the download of a region whose meta.json exists, pausing a running job. */ + bool download(const char* region); + /** Deletes a region, keeping the tiles another region still needs. */ + bool remove(const char* region); + void cancel() { cancelRequested = true; } + + Status status(); + bool isBusy() const { return busy.load(); } + /** The tile set version OpenFreeMap serves now, empty until a download asked for it. */ + const char* latestVersion() const { return version; } + /** True once after tiles were added to or removed from the card. */ + bool takeTilesChanged() { return tilesChanged.exchange(false); } + + /** Fetches these tiles soon, after the ones already asked for, unless the server had no data for them. */ + void browse(const TileKey* tiles, int count); + /** + * Asks OpenFreeMap for its TileJSON now, while Wi-Fi is up. The first connection of a session pauses the + * device for about 1.5 s, better right after the first view than when the user pans into new ground. + */ + void prepareBrowsing(); + /** Whether tiles for the screen are being fetched. */ + bool isBrowsing() const { return browsing.load(); } + static bool isOnline(); + /** Idle: no input for a while. Downloaded tiles are only written to the card while idle. */ + void setIdle(bool value) { idle = value; } + TaskHandle_t browseTaskHandle() const { return browseTask; } + TaskHandle_t controlTaskHandle() const { return controlTask; } + /** Logs the phases of the first TileJSON request (developer option). */ + void setTiming(bool value) { timing = value; } + +private: + enum class Job : uint8_t { + None, + Download, + Delete + }; + + struct Connection { + Downloader* owner; + int index; + void* client; + ByteWriter* body; + uint64_t received; + TaskHandle_t task; + char lastFolder[PATH_MAX_LENGTH]; + }; + + bool queue(Job next, const char* region); + static void controlMain(void* context); + static void connectionMain(void* context); + void controlLoop(); + void runDownload(); + void runDelete(); + void connectionLoop(Connection& connection); + bool fetchTileJson(); + bool downloadTile(Connection& connection, const char* region, const TileKey& tile, int& status); + bool tileOnCard(const char* region, const TileKey& tile, bool& inThisRegion); + bool ensureTileJson(); + static void browseMain(void* context); + void browseLoop(); + bool fetchBrowseTile(const TileKey& tile); + bool ensureBrowseRegion(const TileKey& tile); + void saveProgress(); + bool readMeta(const char* region, RegionMeta& out); + bool writeMeta(const RegionMeta& out); + void setStatus(State state, const char* message); + bool claimIndex(int64_t& index, int connectionIndex); + void finishIndex(int connectionIndex); + + char rootPath[PATH_MAX_LENGTH] = ""; + TileStore* store = nullptr; + IoTask* io = nullptr; + SemaphoreHandle_t mutex = nullptr; + SemaphoreHandle_t wake = nullptr; + SemaphoreHandle_t stopped = nullptr; + TaskHandle_t controlTask = nullptr; + std::atomic stopRequested = false; + std::atomic cancelRequested = false; + std::atomic busy = false; + std::atomic tilesChanged = false; + Job job = Job::None; + char jobRegion[REGION_NAME_MAX + 1] = ""; + Status current {}; + RegionMeta meta {}; + char urlTemplate[160] = ""; + char version[48] = ""; + Connection connections[CONNECTIONS] {}; + ByteWriter bodies[CONNECTIONS]; + SemaphoreHandle_t tileJsonLock = nullptr; + SemaphoreHandle_t browseWake = nullptr; + SemaphoreHandle_t browseStopped = nullptr; + TaskHandle_t browseTask = nullptr; + static constexpr int BROWSE_QUEUE = 16; + static constexpr int BROWSE_EMPTY = 64; + TileKey browseQueue[BROWSE_QUEUE] {}; + int browseCount = 0; + /** Tiles the server had no data for, the sea or empty land, so they are not asked for again. */ + TileKey browseEmpty[BROWSE_EMPTY] {}; + int browseEmptyCount = 0; + int browseEmptyNext = 0; + std::atomic browsing = false; + std::atomic idle = true; + std::atomic warmUp = false; + bool timing = false; + Connection browseConnection {}; + ByteWriter browseBody; + RegionMeta browseMeta {}; + bool browseMetaReady = false; + int browseUnsaved = 0; + SemaphoreHandle_t connectionsDone = nullptr; + int64_t nextIndex = 0; + int64_t activeIndex[CONNECTIONS] {}; + int64_t sinceSave = 0; + std::atomic failed = false; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/Input.cpp b/Apps/Maps/main/Source/app/Input.cpp new file mode 100644 index 0000000..8bc84a7 --- /dev/null +++ b/Apps/Maps/main/Source/app/Input.cpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Input.h" + +#include "../platform/Platform.h" + +#include +#include +#include + +namespace maps { + +namespace { + +constexpr uint32_t RESCAN_INTERVAL_MS = 2000; + +struct Collect { + Device** devices; + int* count; +}; + +bool collectDevice(Device* device, void* context) { + auto* collect = static_cast(context); + if (*collect->count >= Input::MAX_DEVICES || !device_is_ready(device)) { + return true; + } + for (int i = 0; i < *collect->count; i++) { + if (collect->devices[i] == device) { + return true; + } + } + if (device_get(device) == ERROR_NONE) { + collect->devices[(*collect->count)++] = device; + } + return true; +} + +} // namespace + +Input::~Input() { + end(); +} + +void Input::begin() { + rescan(); + if (device_get_first_active_by_type(&TOUCH_TYPE, &touch) != ERROR_NONE) { + touch = nullptr; + } + logInfo("Input: %d keyboards, %d mice, %s", keyboardCount, mouseCount, touch != nullptr ? "touch" : "no touch"); +} + +void Input::end() { + for (int i = 0; i < keyboardCount; i++) { + device_put(keyboards[i]); + } + keyboardCount = 0; + for (int i = 0; i < mouseCount; i++) { + device_put(mice[i]); + } + mouseCount = 0; + if (touch != nullptr) { + device_put(touch); + touch = nullptr; + } +} + +void Input::rescan() { + lastScanMs = nowMs(); + Collect keyboardCollect { keyboards, &keyboardCount }; + device_for_each_of_type(&KEYBOARD_TYPE, &keyboardCollect, collectDevice); + Collect mouseCollect { mice, &mouseCount }; + device_for_each_of_type(&MOUSE_TYPE, &mouseCollect, collectDevice); +} + +int Input::readKeys(KeyEvent* events, int maxEvents) { + // The trackball driver swaps between a keyboard and a mouse device when its settings change + if (nowMs() - lastScanMs > RESCAN_INTERVAL_MS) { + rescan(); + } + int count = 0; + for (int i = 0; i < keyboardCount && count < maxEvents; i++) { + if (!device_is_ready(keyboards[i])) { + continue; + } + KeyboardKeyData data; + while (count < maxEvents && keyboard_read_key(keyboards[i], &data) == ERROR_NONE && data.key != 0) { + events[count++] = { data.key, data.pressed }; + if (!data.continue_reading) { + break; + } + } + } + return count; +} + +bool Input::readMouse(int32_t& dx, int32_t& dy) { + dx = 0; + dy = 0; + bool pressed = false; + for (int i = 0; i < mouseCount; i++) { + if (!device_is_ready(mice[i])) { + continue; + } + int32_t x = 0; + int32_t y = 0; + if (mouse_read_delta(mice[i], &x, &y) == ERROR_NONE) { + dx += x; + dy += y; + } + bool button = false; + if (mouse_get_button_count(mice[i]) > 0 && mouse_get_button_pressed(mice[i], 0, &button) == ERROR_NONE && button) { + pressed = true; + } + } + return pressed; +} + +bool Input::readTouch(TouchState& state) { + state.count = 0; + if (touch == nullptr || touch_read_data(touch, 0) != ERROR_NONE) { + return false; + } + uint16_t strength[2]; + uint8_t count = 0; + if (!touch_get_touched_points(touch, state.x, state.y, strength, &count, 2)) { + return false; + } + state.count = count > 2 ? 2 : count; + return state.count > 0; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/Input.h b/Apps/Maps/main/Source/app/Input.h new file mode 100644 index 0000000..45df480 --- /dev/null +++ b/Apps/Maps/main/Source/app/Input.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +#include + +namespace maps { + +struct KeyEvent { + uint32_t key; + bool pressed; +}; + +struct TouchState { + uint8_t count; + uint16_t x[2]; + uint16_t y[2]; +}; + +/** + * Reads keyboards (the T-Deck keyboard and a trackball in keyboard mode), mice (a trackball in mouse mode) and + * the touch screen directly from their drivers while LVGL is stopped. + */ +class Input { +public: + static constexpr int MAX_DEVICES = 4; + + ~Input(); + + void begin(); + void end(); + + /** Returns the number of key events written to events. */ + int readKeys(KeyEvent* events, int maxEvents); + /** Sums the movement of all mice since the last call. Returns true while a mouse button is held. */ + bool readMouse(int32_t& dx, int32_t& dy); + bool readTouch(TouchState& state); + bool hasTouch() const { return touch != nullptr; } + bool hasKeyboard() const { return keyboardCount > 0; } + bool hasPointer() const { return mouseCount > 0; } + +private: + void rescan(); + + Device* keyboards[MAX_DEVICES] {}; + int keyboardCount = 0; + Device* mice[MAX_DEVICES] {}; + int mouseCount = 0; + Device* touch = nullptr; + uint32_t lastScanMs = 0; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/IoTask.cpp b/Apps/Maps/main/Source/app/IoTask.cpp new file mode 100644 index 0000000..a51830a --- /dev/null +++ b/Apps/Maps/main/Source/app/IoTask.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "IoTask.h" + +#include "../platform/Platform.h" + +#include +#include + +namespace maps { + +namespace { + +/** Jobs run on this stack: folder walks, meta.json text (1 KB) parsed with sscanf, FatFs long names, paths. */ +constexpr uint32_t TASK_STACK = 8192; +/** + * The same as the app task and the render lanes. The SD card driver keeps the CPU busy while it transfers, + * and on the T-Deck the display shares the SPI bus, so a higher priority starved drawing (frames of 1-3 s). + */ +constexpr UBaseType_t TASK_PRIORITY = 4; +/** Longer jobs are logged, they hold up everything behind them. */ +constexpr uint32_t SLOW_JOB_MS = 300; + +} // namespace + +bool IoTask::start() { + mutex = xSemaphoreCreateMutex(); + wake = xSemaphoreCreateCounting(64, 0); + stopped = xSemaphoreCreateBinary(); + if (mutex == nullptr || wake == nullptr || stopped == nullptr) { + return false; + } + stopRequested = false; + // Not pinned, so it never takes a core from one render lane only + if (xTaskCreatePinnedToCore(taskMain, "maps-io", TASK_STACK, this, TASK_PRIORITY, &task, tskNO_AFFINITY) != pdPASS) { + task = nullptr; + return false; + } + runningTask = true; + // Job addresses in the log are matched against the ELF's symbols through this one + logInfo("I/O task started, taskMain at %p", reinterpret_cast(&IoTask::taskMain)); + return true; +} + +void IoTask::stop() { + if (task != nullptr) { + stopRequested = true; + xSemaphoreGive(wake); + xSemaphoreTake(stopped, portMAX_DELAY); + task = nullptr; + } + runningTask = false; + for (SemaphoreHandle_t* handle : { &mutex, &wake, &stopped }) { + if (*handle != nullptr) { + vSemaphoreDelete(*handle); + *handle = nullptr; + } + } +} + +void IoTask::taskMain(void* context) { + auto* io = static_cast(context); + io->loop(); + xSemaphoreGive(io->stopped); + vTaskDelete(nullptr); +} + +bool IoTask::run(IoPriority priority, IoJob job, void* context) { + if (!runningTask) { + return false; + } + SemaphoreHandle_t done = xSemaphoreCreateBinary(); + if (done == nullptr) { + return false; + } + Request request { job, context, done, false, nowMs(), prefetchGeneration.load() }; + xSemaphoreTake(mutex, portMAX_DELAY); + const bool queued = queue.push(&request, priority); + xSemaphoreGive(mutex); + if (!queued) { + vSemaphoreDelete(done); + logError("I/O queue full"); + return false; + } + xSemaphoreGive(wake); + xSemaphoreTake(done, portMAX_DELAY); + vSemaphoreDelete(done); + return request.result; +} + +bool IoTask::shouldYield() const { + if (running == IoPriority::Visible) { + return false; + } + if (running == IoPriority::Prefetch && runningGeneration != prefetchGeneration.load()) { + return true; + } + xSemaphoreTake(mutex, portMAX_DELAY); + const bool urgent = queue.hasMoreUrgent(running); + xSemaphoreGive(mutex); + return urgent; +} + +void IoTask::loop() { + while (true) { + xSemaphoreTake(wake, portMAX_DELAY); + while (true) { + Request* request = nullptr; + IoPriority priority = IoPriority::Background; + xSemaphoreTake(mutex, portMAX_DELAY); + queue.pop(request, &priority); + xSemaphoreGive(mutex); + if (request == nullptr) { + break; + } + if (stopRequested) { + request->result = false; + } else { + running = priority; + runningGeneration = request->prefetchGeneration; + runningYielded = false; + if (priority == IoPriority::Visible) { + const uint32_t waited = nowMs() - request->queuedAt; + longestVisibleWait = waited > longestVisibleWait ? waited : longestVisibleWait; + } + // A prefetch cancelled while it waited does not start + if (priority == IoPriority::Prefetch && runningGeneration != prefetchGeneration.load()) { + runningYielded = true; + request->result = false; + } else { + const uint32_t started = nowMs(); + request->result = request->job(*this, request->context); + const uint32_t took = nowMs() - started; + if (took > SLOW_JOB_MS) { + logInfo("Slow I/O job %u ms (priority %d, waited %u ms, job %p)", took, static_cast(priority), started - request->queuedAt, + reinterpret_cast(request->job)); + } + } + running = IoPriority::Background; + } + xSemaphoreGive(request->done); + } + if (stopRequested) { + break; + } + } +} + +bool IoTask::readFile(const char* path, FileBuffer& buffer) { + buffer.clear(); + FILE* file = fopen(path, "rb"); + if (file == nullptr) { + return false; + } + bool ok = fseek(file, 0, SEEK_END) == 0; + const long size = ok ? ftell(file) : -1; + ok = ok && size > 0 && fseek(file, 0, SEEK_SET) == 0 && buffer.reserve(static_cast(size)); + size_t done = 0; + while (ok && done < static_cast(size)) { + if (done > 0 && shouldYield()) { + markYielded(); + ok = false; + break; + } + if (done > 0) { + // Between chunks the display gets the shared SPI bus + vTaskDelay(1); + } + const size_t wanted = static_cast(size) - done < CHUNK_BYTES ? static_cast(size) - done : CHUNK_BYTES; + const size_t count = fread(buffer.writable() + done, 1, wanted, file); + ok = count == wanted; + done += count; + } + fclose(file); + if (ok) { + buffer.setSize(done); + } + return ok; +} + +bool IoTask::writeFile(const char* path, const char* temporaryPath, const uint8_t* bytes, size_t size, bool canYield) { + FILE* file = fopen(temporaryPath, "wb"); + if (file == nullptr) { + return false; + } + bool ok = true; + size_t done = 0; + while (ok && done < size) { + if (canYield && done > 0 && shouldYield()) { + markYielded(); + ok = false; + break; + } + if (done > 0) { + vTaskDelay(1); + } + const size_t count = size - done < CHUNK_BYTES ? size - done : CHUNK_BYTES; + ok = fwrite(bytes + done, 1, count, file) == count; + done += count; + } + ok = fclose(file) == 0 && ok; + // Most files are new: the old one is only removed when the rename finds it, each FAT operation holds the card + if (ok && rename(temporaryPath, path) != 0) { + remove(path); + ok = rename(temporaryPath, path) == 0; + } + if (!ok) { + remove(temporaryPath); + } + return ok; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/IoTask.h b/Apps/Maps/main/Source/app/IoTask.h new file mode 100644 index 0000000..869a85c --- /dev/null +++ b/Apps/Maps/main/Source/app/IoTask.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../mvt/ByteInput.h" +#include "../tiles/IoQueue.h" + +#include +#include +#include + +#include +#include + +namespace maps { + +class IoTask; + +/** A piece of SD card work. It runs on the I/O task and returns whether it succeeded. */ +using IoJob = bool (*)(IoTask& io, void* context); + +/** + * The only task that reads or writes the SD card while the map runs. Render lanes, the downloader and the UI + * hand it jobs and wait for them; it runs the most urgent waiting job first. Files are read with few large + * reads into PSRAM buffers, and a long read or write of lower priority stops between chunks when a more + * urgent job arrives, so its owner can queue it again. + */ +class IoTask { +public: + /** Small enough that the display, on the same SPI bus on the T-Deck, waits at most about 40 ms. */ + static constexpr size_t CHUNK_BYTES = 16 * 1024; + + bool start(); + void stop(); + + /** Runs a job on the I/O task and waits for its result. Returns false right away when the task is not running. */ + bool run(IoPriority priority, IoJob job, void* context); + + /** For jobs: whether the running job should stop because a more urgent one waits, or prefetch was cancelled. */ + bool shouldYield() const; + /** For jobs: whether the running job stopped early through shouldYield(). Reset for each job. */ + bool yielded() const { return runningYielded; } + void markYielded() { runningYielded = true; } + IoPriority runningPriority() const { return running; } + + /** Makes the running and waiting prefetch reads stop at their next chunk, after input moved the view. */ + void cancelPrefetch() { prefetchGeneration++; } + + /** For jobs: reads a whole file into buffer in large chunks. Returns false on error or when it yielded. */ + bool readFile(const char* path, FileBuffer& buffer); + /** For jobs: writes bytes to path through path.tmp and a rename, in large chunks. */ + bool writeFile(const char* path, const char* temporaryPath, const uint8_t* bytes, size_t size, bool canYield); + + /** Statistics for the log: jobs run, and the longest wait of a visible job. */ + uint32_t longestVisibleWaitMs() const { return longestVisibleWait; } + /** The fewest bytes of stack the task had left so far. */ + TaskHandle_t taskHandle() const { return task; } + uint32_t stackLeft() const { return task != nullptr ? static_cast(uxTaskGetStackHighWaterMark(task)) : 0; } + +private: + struct Request { + IoJob job; + void* context; + SemaphoreHandle_t done; + bool result; + uint32_t queuedAt; + uint32_t prefetchGeneration; + }; + + static void taskMain(void* context); + void loop(); + + TaskHandle_t task = nullptr; + SemaphoreHandle_t mutex = nullptr; + SemaphoreHandle_t wake = nullptr; + SemaphoreHandle_t stopped = nullptr; + std::atomic stopRequested = false; + std::atomic runningTask = false; + IoQueue queue; + IoPriority running = IoPriority::Background; + uint32_t runningGeneration = 0; + bool runningYielded = false; + std::atomic prefetchGeneration = 0; + uint32_t longestVisibleWait = 0; +}; + +/** Runs a lambda on the I/O task and waits for it, for the UI's occasional file access. */ +template +bool runOnCard(IoTask& io, Function& function, IoPriority priority = IoPriority::Visible) { + return io.run(priority, [](IoTask&, void* context) { return (*static_cast(context))(); }, &function); +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/LocationSource.cpp b/Apps/Maps/main/Source/app/LocationSource.cpp new file mode 100644 index 0000000..3f6e336 --- /dev/null +++ b/Apps/Maps/main/Source/app/LocationSource.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "LocationSource.h" + +#include "../platform/Platform.h" + +namespace maps { + +bool LocationSource::subscribe(TaskEventGroup* eventGroup) { + if (subscribed) { + return true; + } + subscription = {}; + subscribed = location_subscribe(&subscription, eventGroup) == ERROR_NONE; + logInfo("Location services %s, %s", location_is_enabled() ? "on" : "off", subscribed ? "subscribed" : "not subscribed"); + return subscribed; +} + +void LocationSource::unsubscribe() { + if (subscribed) { + location_unsubscribe(&subscription); + subscribed = false; + } +} + +PositionRead LocationSource::read(PositionFix& fix) { + if (!location_is_enabled()) { + return PositionRead::Off; + } + Location location {}; + if (location_get(&location) != ERROR_NONE) { + return PositionRead::NoFix; + } + fix.latitude = location.latitude; + fix.longitude = location.longitude; + fix.accuracy = location.horizontal_accuracy; + fix.course = location.course; + fix.speed = location.speed; + fix.timeMs = location.time_ms; + return PositionRead::Fix; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/LocationSource.h b/Apps/Maps/main/Source/app/LocationSource.h new file mode 100644 index 0000000..0542704 --- /dev/null +++ b/Apps/Maps/main/Source/app/LocationSource.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../platform/Position.h" + +#include + +namespace maps { + +/** + * The position from Tactility's location module, the app's only position source. Subscribing makes the + * location service run its providers (GPS, IP lookup) while the map is open, and signals a bit of the app's + * event group for each new fix. Location settings belong to the user: this never enables location or + * changes its precision. + */ +class LocationSource final : public PositionSource { +public: + ~LocationSource() override { unsubscribe(); } + + bool subscribe(TaskEventGroup* eventGroup); + void unsubscribe(); + /** The event group bit that is signalled for a new fix, 0 when not subscribed. */ + uint32_t bit() const { return subscribed ? subscription.bit : 0; } + + PositionRead read(PositionFix& fix) override; + +private: + LocationSubscription subscription {}; + bool subscribed = false; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/MapApp.cpp b/Apps/Maps/main/Source/app/MapApp.cpp new file mode 100644 index 0000000..a875795 --- /dev/null +++ b/Apps/Maps/main/Source/app/MapApp.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "MapView.h" + +#include "../platform/Platform.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace maps { + +namespace { + +constexpr const char* APP_ID = "tactility.maps"; + +void runFullscreen(Device* display, const char* root, const char* dataPath) { + const bool lvglActive = module_is_started(&lvgl_module); + if (lvglActive) { + const uint32_t start = nowMs(); + module_stop(&lvgl_module); + logInfo("LVGL stopped in %u ms", nowMs() - start); + } + + // Above the render lanes and the I/O task (4): the loop sleeps between frames, so it only takes the CPU to + // answer input and draw. At the same priority frames took 150-270 ms while tiles were loading. + const UBaseType_t appPriority = uxTaskPriorityGet(nullptr); + vTaskPrioritySet(nullptr, appPriority + 1); + + // The view is large, it lives in external RAM where the device has it + void* memory = largeAlloc(sizeof(MapView)); + if (memory != nullptr) { + auto* view = new (memory) MapView(); + if (view->begin(display, root, dataPath)) { + view->run(); + } + view->end(); + view->~MapView(); + largeFree(memory); + } else { + logError("Out of memory"); + } + vTaskPrioritySet(nullptr, appPriority); + + // LVGL comes back on every exit path, also after errors + if (lvglActive && !module_is_started(&lvgl_module)) { + const uint32_t start = nowMs(); + module_start(&lvgl_module); + logInfo("LVGL started in %u ms", nowMs() - start); + } +} + +} // namespace + +} // namespace maps + +extern "C" int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + maps::logMemory("start"); + char root[maps::PATH_MAX_LENGTH]; + if (app_paths_get_user_data_directory(maps::APP_ID, root, sizeof(root)) != ERROR_NONE) { + maps::logError("No user data directory"); + return 0; + } + mkdir(root, 0777); + char mapsFolder[maps::PATH_MAX_LENGTH]; + snprintf(mapsFolder, sizeof(mapsFolder), "%s/maps", root); + mkdir(mapsFolder, 0777); + char dataPath[maps::PATH_MAX_LENGTH] = ""; + paths_get_data_path(dataPath, sizeof(dataPath)); + + Device* display = nullptr; + if (device_get_first_active_by_type(&DISPLAY_TYPE, &display) != ERROR_NONE) { + maps::logError("No display"); + return 0; + } + maps::runFullscreen(display, root, dataPath); + device_put(display); + maps::logMemory("exit"); + return 0; +} diff --git a/Apps/Maps/main/Source/app/MapInput.cpp b/Apps/Maps/main/Source/app/MapInput.cpp new file mode 100644 index 0000000..1a3af0f --- /dev/null +++ b/Apps/Maps/main/Source/app/MapInput.cpp @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "MapView.h" + +#include "../platform/Platform.h" +#include "../render/Compose.h" + +#include +#include +#include + +namespace maps { + +namespace { + +constexpr int KEY_PAN_PIXELS = 48; +constexpr int TRACKBALL_PAN_PIXELS = 16; +constexpr int MOUSE_PAN_SCALE = 4; +/** Trackball movement in mouse mode that moves a list by one row. */ +constexpr int MOUSE_ROW_STEP = 6; +constexpr uint32_t LONG_PRESS_MS = 600; +constexpr int CURSOR_SCALE = 3; + +} // namespace + +void MapView::markInput(uint32_t now) { + lastInputAt = now; + // Prefetch and other idle work stop at once, so the card and the lanes serve what is on screen + if (idle) { + idle = false; + worker.cancelPrefetch(); + downloader.setIdle(false); + } +} + +void MapView::handleKey(uint32_t key) { + dirty = true; + if (dialog.isOpen()) { + if (dialog.handleKey(key) == UiAction::Chosen) { + deleteRegion(); + } + return; + } + if (menuOpen) { + if (key == 'm' || key == 'M') { + closeMenu(); + return; + } + handleListAction(menu.handleKey(key)); + return; + } + switch (screen) { + case Screen::Map: + handleMapKey(key); + break; + case Screen::Download: + handleDownloadKey(key); + break; + case Screen::About: + if (key == 'w' || key == 'W' || key == keys::UP) { + scrollAbout(-1); + } else if (key == 's' || key == 'S' || key == keys::DOWN) { + scrollAbout(1); + } else if (key == keys::ESCAPE || key == keys::BACKSPACE || key == keys::ENTER || key == ' ') { + closeScreen(); + } + break; + default: + handleListAction(activeList()->handleKey(key)); + break; + } +} + +void MapView::pressControl(Control control) { + dirty = true; + switch (control) { + case Control::Menu: + openMenu(); + break; + case Control::ZoomIn: + setZoom(zoom + 1, width / 2, height / 2); + break; + case Control::ZoomOut: + setZoom(zoom - 1, width / 2, height / 2); + break; + case Control::Position: + startFollowing(); + break; + default: + break; + } +} + +bool MapView::pressAt(int x, int y) { + const Control control = controls.hit(x, y); + if (control != Control::None) { + pressControl(control); + return true; + } + switch (attribution.tap(x, y, attributionBar, attributionMark(), nowMs())) { + case Attribution::Tap::Expanded: + dirty = true; + return true; + case Attribution::Tap::OpenAbout: + openAbout(); + return true; + default: + return false; + } +} + +void MapView::handleMapKey(uint32_t key) { + // Boards with buttons only move a focus through the visible controls + if (controls.isVisible() && controls.usesFocus()) { + switch (key) { + case keys::UP: + case keys::LEFT: + case keys::FOCUS_PREVIOUS: + controls.moveFocus(-1); + return; + case keys::DOWN: + case keys::RIGHT: + case keys::FOCUS_NEXT: + case keys::TAB: + controls.moveFocus(1); + return; + case keys::ESCAPE: + case keys::BACKSPACE: + controls.hide(); + return; + default: + break; + } + } + switch (key) { + case ' ': + controls.toggle(); + cursorShown = false; + break; + case keys::ESCAPE: + case keys::BACKSPACE: + exitRequested = true; + break; + case 'w': + case 'W': + pan(0, -KEY_PAN_PIXELS); + break; + case 's': + case 'S': + pan(0, KEY_PAN_PIXELS); + break; + case 'a': + case 'A': + pan(-KEY_PAN_PIXELS, 0); + break; + case 'd': + pan(KEY_PAN_PIXELS, 0); + break; + case keys::UP: + pan(0, -TRACKBALL_PAN_PIXELS); + break; + case keys::DOWN: + pan(0, TRACKBALL_PAN_PIXELS); + break; + case keys::LEFT: + pan(-TRACKBALL_PAN_PIXELS, 0); + break; + case keys::RIGHT: + pan(TRACKBALL_PAN_PIXELS, 0); + break; + case 'i': + case 'I': + setZoom(zoom + 1, width / 2, height / 2); + break; + case 'o': + case 'O': + setZoom(zoom - 1, width / 2, height / 2); + break; + case 'g': + case 'G': + startFollowing(); + break; + case 'm': + case 'M': + openMenu(); + break; + case 'D': + // Lower case d pans, so the download frame is Shift and D + screen = Screen::Download; + controls.hide(); + cursorShown = false; + break; + default: + break; + } +} + +void MapView::click(bool fromPointer) { + dirty = true; + if (dialog.isOpen()) { + if (dialog.press() == UiAction::Chosen) { + deleteRegion(); + } + } else if (menuOpen || screen != Screen::Map) { + handleKey(keys::ENTER); + } else if (fromPointer && cursorShown) { + // A click on a button presses it, a click on empty map hides the controls + if (!pressAt(cursorX, cursorY)) { + controls.hide(); + cursorShown = false; + } + } else if (controls.isVisible() && controls.usesFocus()) { + pressControl(controls.focused()); + } else { + controls.toggle(); + // With a pointer the controls are reached with a cursor, it starts in the middle of the screen + cursorShown = fromPointer && controls.isVisible(); + cursorX = width / 2; + cursorY = height / 2; + } +} + +void MapView::longClick() { + // The menu opens from the map, a long click elsewhere goes back + if (dialog.isOpen()) { + dialog.close(); + } else if (menuOpen) { + closeMenu(); + } else if (screen == Screen::Map) { + openMenu(); + } else { + closeScreen(); + } + dirty = true; +} + +void MapView::handleEnter(bool pressed, uint32_t now) { + // The trackball repeats its press while held, the keyboard only sends one press + if (pressed) { + if (!enterHeld) { + enterHeld = true; + enterLong = false; + enterDownAt = now; + } + enterSeenAt = now; + return; + } + if (enterHeld) { + enterHeld = false; + if (!enterLong) { + click(false); + } + } +} + +void MapView::handleMouse(uint32_t now) { + int32_t dx; + int32_t dy; + const bool pressed = input.readMouse(dx, dy); + if (pressed || dx != 0 || dy != 0) { + markInput(now); + } + ListView* list = menuOpen ? &menu : activeList(); + if (dialog.isOpen()) { + if (dx != 0 || dy != 0) { + mouseRowAccumulator += dx + dy; + if (std::abs(mouseRowAccumulator) >= MOUSE_ROW_STEP) { + dialog.handleKey(keys::TAB); + mouseRowAccumulator = 0; + dirty = true; + } + } + } else if (list != nullptr || screen == Screen::About) { + // In lists the trackball steps through the rows + mouseRowAccumulator += dy; + while (std::abs(mouseRowAccumulator) >= MOUSE_ROW_STEP) { + const int step = mouseRowAccumulator > 0 ? 1 : -1; + mouseRowAccumulator -= step * MOUSE_ROW_STEP; + if (list != nullptr) { + list->move(step); + } else { + scrollAbout(step); + } + dirty = true; + } + } else if (controls.isVisible() && screen == Screen::Map) { + // While the controls show, the trackball moves the pointer instead of the map + if (dx != 0 || dy != 0) { + if (!cursorShown) { + cursorShown = true; + cursorX = width / 2; + cursorY = height / 2; + } + cursorX = std::clamp(cursorX + static_cast(dx) * CURSOR_SCALE, 0, width - 1); + cursorY = std::clamp(cursorY + static_cast(dy) * CURSOR_SCALE, 0, height - 1); + dirty = true; + } + } else { + pan(dx * MOUSE_PAN_SCALE, dy * MOUSE_PAN_SCALE); + } + if (pressed && !mouseHeld) { + mouseHeld = true; + mouseLong = false; + mouseDownAt = now; + } else if (pressed && mouseHeld && !mouseLong && now - mouseDownAt >= LONG_PRESS_MS) { + mouseLong = true; + longClick(); + } else if (!pressed && mouseHeld) { + mouseHeld = false; + if (!mouseLong) { + click(true); + } + } +} + +void MapView::handleTouch(uint32_t now) { + TouchState state; + input.readTouch(state); + if (state.count > 0) { + markInput(now); + } + const TouchSample sample { state.count, { state.x[0], state.x[1] }, { state.y[0], state.y[1] } }; + Gesture found[Gestures::MAX_GESTURES]; + const int count = gestures.update(sample, now, found); + for (int i = 0; i < count; i++) { + handleGesture(found[i]); + } +} + +void MapView::handleTap(int x, int y) { + // Taps on something answer at once, taps on the map wait to tell a single from a double tap + dirty = true; + if (dialog.isOpen()) { + if (dialog.handleTap(x, y) == UiAction::Chosen) { + deleteRegion(); + } + gestures.consumeTap(); + return; + } + if (menuOpen) { + handleListAction(menu.handleTap(x, y)); + gestures.consumeTap(); + return; + } + if (screen == Screen::About) { + // The title bar goes back, the page itself scrolls by dragging + if (y < normalFont.lineHeight() + 8) { + closeScreen(); + } + gestures.consumeTap(); + return; + } + if (ListView* list = activeList()) { + handleListAction(list->handleTap(x, y)); + gestures.consumeTap(); + return; + } + // Buttons and the attribution answer at once, the map itself only to a single tap + if (screen == Screen::Download) { + if (downloadView.button.contains(x, y)) { + startDownload(); + gestures.consumeTap(); + } else if (const Attribution::Tap result = attribution.tap(x, y, attributionBar, attributionMark(), nowMs()); result != Attribution::Tap::Missed) { + if (result == Attribution::Tap::OpenAbout) { + openAbout(); + } + gestures.consumeTap(); + } else if (downloadView.bar.contains(x, y)) { + gestures.consumeTap(); + } + return; + } + if (screen == Screen::Map && pressAt(x, y)) { + gestures.consumeTap(); + } +} + +void MapView::handleLongPress(int x, int y) { + dirty = true; + if (screen == Screen::Regions && !dialog.isOpen()) { + const int row = regionList.rowAt(x, y); + if (row >= 0) { + regionList.select(row); + askDeleteRegion(); + } + return; + } + if (screen == Screen::Map && !dialog.isOpen()) { + if (menuOpen) { + closeMenu(); + } else { + openMenu(); + } + } +} + +void MapView::handleGesture(const Gesture& gesture) { + const bool onMap = !menuOpen && !dialog.isOpen() && !isFullScreen(); + switch (gesture.type) { + case GestureType::Tap: + handleTap(gesture.x, gesture.y); + break; + case GestureType::SingleTap: + // A tap on the map shows or hides the controls, dragging, pinching and double taps never do + if (onMap && screen == Screen::Map) { + controls.toggle(); + cursorShown = false; + dirty = true; + } + break; + case GestureType::DoubleTap: + if (onMap) { + setZoom(zoom + 1, gesture.x, gesture.y); + } + break; + case GestureType::LongPress: + handleLongPress(gesture.x, gesture.y); + break; + case GestureType::Drag: + if (onMap) { + pan(-gesture.dx, -gesture.dy); + } else if (!dialog.isOpen()) { + // Lists and the About page scroll with the finger, a row at a time + ListView* list = menuOpen ? &menu : activeList(); + const int rowHeight = list != nullptr ? list->rowHeight() : normalFont.lineHeight(); + dragRowAccumulator -= gesture.dy; + while (std::abs(dragRowAccumulator) >= rowHeight) { + const int step = dragRowAccumulator > 0 ? 1 : -1; + dragRowAccumulator -= step * rowHeight; + if (list != nullptr) { + list->move(step); + } else { + scrollAbout(step); + } + dirty = true; + } + } + break; + case GestureType::PinchStart: + if (!onMap) { + break; + } + pinchStepped = false; + if (pinchBase != nullptr) { + // The map as it is now, scaled live while the fingers move, without drawing vectors again + worker.lock(); + composeView(pinchBase, width, height, centerX, centerY, zoom, lookupTile, this, palette->placeholder); + worker.unlock(); + pinching = true; + pinchScale = 1.0f; + pinchStartX = gesture.startX; + pinchStartY = gesture.startY; + pinchX = gesture.x; + pinchY = gesture.y; + following = false; + dirty = true; + } + break; + case GestureType::PinchMove: + if (pinching) { + pinchScale = gesture.scale; + pinchX = gesture.x; + pinchY = gesture.y; + dirty = true; + } else if (!pinchStepped && onMap) { + // Without live scaling (e-paper, or switched off) one zoom step per pinch + if (gesture.scale > 1.5f) { + setZoom(zoom + 1, gesture.x, gesture.y); + pinchStepped = true; + } else if (gesture.scale < 0.67f) { + setZoom(zoom - 1, gesture.x, gesture.y); + pinchStepped = true; + } + } + break; + case GestureType::PinchEnd: + if (pinching) { + endPinch(gesture.scale, gesture.startX, gesture.startY, gesture.x, gesture.y); + } + break; + default: + break; + } +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/MapScreens.cpp b/Apps/Maps/main/Source/app/MapScreens.cpp new file mode 100644 index 0000000..6aba5f0 --- /dev/null +++ b/Apps/Maps/main/Source/app/MapScreens.cpp @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "MapView.h" + +#include "../platform/Platform.h" + +#include + +#include +#include +#include +#include +#include + +namespace maps { + +namespace { + +constexpr int MENU_WIDTH = 220; +constexpr int ABOUT_LINES = 40; +constexpr int ABOUT_LINE_LENGTH = 64; + +const char* const MENU_LABELS[] = { "Download area", "Regions", "Settings", "About and licences", "Exit" }; + +const char* const ABOUT_PARAGRAPHS[] = { + "Tactility Maps " APP_VERSION, + "Offline vector maps for Tactility.", + "", + "Map data \xC2\xA9 OpenStreetMap contributors, available under the Open Database Licence (ODbL): openstreetmap.org/copyright", + "", + "Vector tiles \xC2\xA9 OpenMapTiles (openmaptiles.org), served by OpenFreeMap (openfreemap.org).", + "", + "Root certificates of Google Trust Services and Let's Encrypt are included to reach OpenFreeMap over HTTPS.", + "", + "W/S or drag to scroll. Esc, Backspace or tap the title to go back.", +}; + +} // namespace + +void MapView::setListHint(ListView& list, const char* keyHint, const char* touchHint) { + // One line per input the board has, keys first + char text[128]; + const bool touch = input.hasTouch(); + const bool keys = input.hasKeyboard() || !touch; + snprintf(text, sizeof(text), "%s%s%s", keys ? keyHint : "", keys && touch ? "\n" : "", touch ? touchHint : ""); + list.setHint(text); +} + +ListView* MapView::activeList() { + switch (screen) { + case Screen::Regions: + return ®ionList; + case Screen::Settings: + return &settingList; + default: + return nullptr; + } +} + +void MapView::closeScreen() { + screen = Screen::Map; + dialog.close(); + dirty = true; + updateWanted(); +} + +// --------------------------------------------------------------------------------------------------------------- +// Menu + +void MapView::openMenu() { + menu.clear(); + for (const char* label : MENU_LABELS) { + menu.add(label); + } + menu.layoutPopup(width, height, overlay.font(), std::min(MENU_WIDTH, width - 16)); + menuOpen = true; + dirty = true; +} + +void MapView::closeMenu() { + menuOpen = false; + dirty = true; +} + +void MapView::chooseMenu(MenuItem item) { + menuOpen = false; + dirty = true; + controls.hide(); + cursorShown = false; + switch (item) { + case MenuItem::Download: + screen = Screen::Download; + break; + case MenuItem::Regions: + openRegions(); + break; + case MenuItem::Settings: + openSettings(); + break; + case MenuItem::About: + openAbout(); + break; + case MenuItem::Exit: + // The only way out by trackball or touch, LVGL's swipe back does not run while the map has the screen + exitRequested = true; + break; + default: + break; + } +} + +void MapView::handleListAction(UiAction action) { + dirty = true; + if (menuOpen) { + if (action == UiAction::Chosen) { + chooseMenu(static_cast(menu.selected())); + } else if (action == UiAction::Back) { + closeMenu(); + } + return; + } + if (action == UiAction::Back) { + closeScreen(); + return; + } + if (screen == Screen::Regions) { + if (action == UiAction::Chosen) { + chooseRegion(); + } else if (action == UiAction::Delete) { + askDeleteRegion(); + } + } else if (screen == Screen::Settings && action == UiAction::Chosen) { + toggleSetting(static_cast(settingList.selected())); + } +} + +// --------------------------------------------------------------------------------------------------------------- +// Regions + +void MapView::openRegions() { + regionCount = 0; + auto read = [&] { + for (int i = 0; i < store.regionCount() && regionCount < TileStore::MAX_REGIONS; i++) { + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/maps/%s/meta.json", rootPath, store.regionName(i)); + RegionMeta& meta = regions[regionCount]; + if (readRegionMeta(path, meta)) { + snprintf(meta.name, sizeof(meta.name), "%s", store.regionName(i)); + regionCount++; + } + } + return true; + }; + runOnCard(io, read); + const int selected = screen == Screen::Regions ? regionList.selected() : 0; + fillRegionList(); + regionList.select(selected); + screen = Screen::Regions; + dirty = true; +} + +void MapView::fillRegionList() { + regionList.clear(); + regionList.setTitle("< Regions"); + if (regionCount == 0) { + regionList.setHint("No regions yet. Menu, Download area."); + } else { + setListHint(regionList, "Enter: show Del: delete Esc: back", "Tap: show Hold: delete Tap <: back"); + } + const char* latest = downloader.latestVersion(); + const Downloader::Status status = downloader.status(); + for (int i = 0; i < regionCount; i++) { + const RegionMeta& meta = regions[i]; + char label[ListView::TEXT_LENGTH]; + char detail[ListView::TEXT_LENGTH]; + const bool active = (status.state == Downloader::State::Downloading || status.state == Downloader::State::Connecting) && + strcmp(status.region, meta.name) == 0; + if (active || !meta.isComplete()) { + const int64_t done = active ? status.done : meta.done; + snprintf(label, sizeof(label), "%s %s %d%%", meta.name, active ? "downloading" : "paused", + static_cast(meta.total > 0 ? done * 100 / meta.total : 0)); + } else if (latest[0] != '\0' && meta.version[0] != '\0' && strcmp(latest, meta.version) != 0) { + snprintf(label, sizeof(label), "%s outdated", meta.name); + } else { + snprintf(label, sizeof(label), "%s %s", meta.name, strcmp(meta.name, BROWSE_REGION) == 0 ? "viewed online" : "complete"); + } + snprintf(detail, sizeof(detail), "%.1f MB %s", meta.bytes / 1e6, meta.date[0] != '\0' ? meta.date + 2 : ""); + regionList.add(label, detail); + } + regionList.layoutFullScreen(width, height, overlay.font()); +} + +void MapView::chooseRegion() { + if (regionList.selected() >= regionCount) { + return; + } + const RegionMeta& meta = regions[regionList.selected()]; + if (!meta.isComplete()) { + downloader.download(meta.name); + } + showRegion(meta); + screen = Screen::Map; +} + +void MapView::askDeleteRegion() { + if (regionList.selected() >= regionCount) { + return; + } + char message[80]; + snprintf(message, sizeof(message), "Delete %s? Tiles other regions use are kept.", regions[regionList.selected()].name); + dialog.open(message, "Delete", true); + dialog.layout(width, height, overlay.font()); + dirty = true; +} + +void MapView::deleteRegion() { + if (screen != Screen::Regions || regionList.selected() >= regionCount) { + return; + } + const char* name = regions[regionList.selected()].name; + downloader.remove(name); + store.removeRegion(name); + worker.lock(); + worker.cache().clear(); + worker.unlock(); + // The list without it, the files go in the background + for (int i = regionList.selected(); i + 1 < regionCount; i++) { + regions[i] = regions[i + 1]; + } + regionCount--; + const int selected = regionList.selected(); + fillRegionList(); + regionList.select(selected); + dirty = true; +} + +void MapView::showRegion(const RegionMeta& meta) { + // The highest zoom level at which the whole region fits on the screen + int fitZoom = meta.maxZoom; + while (fitZoom > 0) { + const double regionWidth = lonToWorldX(meta.box.east, fitZoom) - lonToWorldX(meta.box.west, fitZoom); + const double regionHeight = latToWorldY(meta.box.south, fitZoom) - latToWorldY(meta.box.north, fitZoom); + if (regionWidth <= width && regionHeight <= height) { + break; + } + fitZoom--; + } + following = false; + centerOn((meta.box.south + meta.box.north) / 2.0, (meta.box.west + meta.box.east) / 2.0, fitZoom); +} + +// --------------------------------------------------------------------------------------------------------------- +// Settings + +void MapView::openSettings() { + fillSettingList(); + settingList.select(0); + screen = Screen::Settings; + dirty = true; +} + +void MapView::fillSettingList() { + const int selected = settingList.selected(); + settingList.clear(); + settingList.setTitle("< Settings"); + setListHint(settingList, "Enter: change Esc: back", "Tap: change Tap <: back"); + settingList.add("SD card cache", settings.sdCache ? "On" : "Off"); + settingList.add("Prepared tiles", settings.deflatePrepared ? "Deflated" : "Raw"); + settingList.add("Smooth pinch", monochrome ? "Off (display)" : (settings.smoothPinch ? "On" : "Off")); + settingList.add("Download while browsing", settings.browseDownload ? "On" : "Off"); + settingList.layoutFullScreen(width, height, overlay.font()); + settingList.select(selected); +} + +void MapView::toggleSetting(SettingItem item) { + switch (item) { + case SettingItem::SdCache: + settings.sdCache = !settings.sdCache; + worker.setSdCache(settings.sdCache); + break; + case SettingItem::PreparedTiles: + // Tiles converted from now on use the new kind, both kinds are read + settings.deflatePrepared = !settings.deflatePrepared; + worker.setDeflatePrepared(settings.deflatePrepared); + break; + case SettingItem::BrowseDownload: + settings.browseDownload = !settings.browseDownload; + break; + case SettingItem::SmoothPinch: + if (monochrome) { + return; + } + settings.smoothPinch = !settings.smoothPinch; + if (settings.smoothPinch && pinchBase == nullptr) { + pinchBase = static_cast(largeAlloc(static_cast(width) * height * sizeof(uint16_t))); + } else if (!settings.smoothPinch && !pinching) { + largeFree(pinchBase); + pinchBase = nullptr; + } + break; + default: + return; + } + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/settings.txt", rootPath); + auto write = [&] { return writeSettings(path, settings); }; + if (!runOnCard(io, write)) { + showToast("Could not save the settings"); + } + fillSettingList(); + dirty = true; +} + +// --------------------------------------------------------------------------------------------------------------- +// About and licences + +void MapView::openAbout() { + controls.hide(); + cursorShown = false; + aboutScroll = 0; + aboutLines = 0; + if (aboutBuffer == nullptr) { + aboutBuffer = static_cast(largeAlloc(ABOUT_LINES * ABOUT_LINE_LENGTH)); + if (aboutBuffer == nullptr) { + return; + } + } + screen = Screen::About; + menuOpen = false; + dirty = true; +} + +void MapView::scrollAbout(int lines) { + const int visible = (height - normalFont.lineHeight() - 8) / normalFont.lineHeight(); + aboutScroll = std::clamp(aboutScroll + lines, 0, std::max(0, aboutLines - visible)); + dirty = true; +} + +void MapView::drawAbout() { + Font& font = overlay.font(); + constexpr int padding = 8; + if (aboutLines == 0) { + // Wrapped once when the page opens + for (const char* paragraph : ABOUT_PARAGRAPHS) { + if (aboutLines >= ABOUT_LINES) { + break; + } + if (paragraph[0] == '\0') { + aboutBuffer[aboutLines++ * ABOUT_LINE_LENGTH] = '\0'; + continue; + } + aboutLines += wrapText(font, paragraph, width - 2 * padding, aboutBuffer + aboutLines * ABOUT_LINE_LENGTH, ABOUT_LINE_LENGTH, + ABOUT_LINES - aboutLines); + } + } + canvas.fillRect(0, 0, width, height, theme.panel); + const int lineHeight = font.lineHeight(); + font.drawText(canvas, padding, 3, "< About and licences", theme.text); + const int top = lineHeight + padding; + canvas.fillRect(0, top - 3, width, 1, theme.border); + const int visible = (height - top) / lineHeight; + for (int i = 0; i < visible && aboutScroll + i < aboutLines; i++) { + font.drawText(canvas, padding, top + i * lineHeight, aboutBuffer + (aboutScroll + i) * ABOUT_LINE_LENGTH, theme.text); + } + if (aboutLines > visible) { + const int barHeight = std::max(8, (height - top) * visible / aboutLines); + const int barTop = top + (height - top - barHeight) * aboutScroll / std::max(1, aboutLines - visible); + canvas.fillRect(width - 3, barTop, 2, barHeight, theme.muted); + } +} + +// --------------------------------------------------------------------------------------------------------------- +// Download area + +BoundingBox MapView::frameBox() const { + return screenBounds(downloadView.frame, centerX, centerY, zoom, width, height); +} + +void MapView::drawDownloadFrame() { + drawDownloadScreen(canvas, overlay.font(), theme, downloadView, planDownload(frameBox(), MAX_SOURCE_ZOOM)); +} + +void MapView::startDownload() { + const DownloadPlan plan = planDownload(frameBox(), MAX_SOURCE_ZOOM); + if (!plan.fits) { + showToast("Too large for one download. Zoom in to cover less."); + return; + } + RegionMeta meta {}; + auto makeFolder = [&] { + for (int number = 1; number < 100; number++) { + snprintf(meta.name, sizeof(meta.name), "area%02d", number); + char folder[PATH_MAX_LENGTH]; + snprintf(folder, sizeof(folder), "%s/maps/%s", rootPath, meta.name); + struct stat info {}; + if (stat(folder, &info) != 0) { + mkdir(folder, 0777); + return true; + } + } + return false; + }; + runOnCard(io, makeFolder); + meta.box = plan.box; + meta.minZoom = 0; + meta.maxZoom = plan.maxZoom; + meta.total = plan.tiles; + meta.done = 0; + meta.bytes = 0; + time_t now = time(nullptr); + struct tm local {}; + localtime_r(&now, &local); + strftime(meta.date, sizeof(meta.date), "%Y-%m-%d", &local); + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/maps/%s/meta.json", rootPath, meta.name); + auto write = [&] { return writeRegionMeta(path, meta); }; + if (!runOnCard(io, write)) { + logError("Could not write %s", path); + return; + } + store.addRegion(meta.name); + store.setRegionBounds(meta.name, meta.box, meta.minZoom, meta.maxZoom); + logInfo("New region %s: %d tiles up to z%d, about %d KB", meta.name, static_cast(plan.tiles), plan.maxZoom, + static_cast(plan.estimatedBytes / 1024)); + downloader.download(meta.name); + screen = Screen::Map; + dirty = true; + updateWanted(); +} + +void MapView::handleDownloadKey(uint32_t key) { + switch (key) { + case keys::ENTER: + case keys::YES: + startDownload(); + break; + case keys::ESCAPE: + case keys::BACKSPACE: + case keys::NO: + closeScreen(); + break; + case ' ': + case 'm': + case 'M': + case 'g': + case 'G': + // Only moving and zooming the map under the frame here + break; + default: + handleMapKey(key); + break; + } + dirty = true; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/MapView.cpp b/Apps/Maps/main/Source/app/MapView.cpp new file mode 100644 index 0000000..ef42473 --- /dev/null +++ b/Apps/Maps/main/Source/app/MapView.cpp @@ -0,0 +1,739 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "MapView.h" + +#include "../platform/Platform.h" +#include "../render/Compose.h" + +#include + +#include +#include + +#include +#include +#include + +namespace maps { + +namespace { + +/** + * 3.5 MB of rendered tiles in PSRAM. With 32 (4 MB) a download, a conversion and panning together left as little + * as 60 KB of PSRAM free, and downloaded tiles could not be kept. + */ +constexpr int TILE_CACHE_SLOTS = 28; +constexpr uint32_t LOOP_INTERVAL_MS = 15; +/** The map opens at ESA ESTEC, Noordwijk, the first time, then where it was left. */ +constexpr double DEFAULT_LAT = 52.2186; +constexpr double DEFAULT_LON = 4.4206; +constexpr int DEFAULT_ZOOM = 14; +/** Prefetch and other idle work start after this long without input. */ +constexpr uint32_t IDLE_AFTER_MS = 1000; +/** Location is read on each signal of the location service, and every few seconds to notice it being switched off. */ +constexpr uint32_t LOCATION_CHECK_MS = 3000; +constexpr uint32_t TOAST_SHOW_MS = 3500; +constexpr uint32_t STATUS_POLL_MS = 500; +constexpr uint32_t MESSAGE_SHOW_MS = 4000; +constexpr uint32_t ENTER_RELEASE_MS = 150; +constexpr uint32_t GAP_POLL_MS = 300; +constexpr uint32_t SLOW_LOOP_MS = 150; +/** The message about missing detail comes back at most this often. */ +constexpr uint32_t GAP_HINT_MS = 30000; +constexpr uint32_t LONG_PRESS_MS = 600; +constexpr const char* NORMAL_FONTS[] = { "text_default_", "mono_mono_", "text_small_" }; +constexpr const char* SMALL_FONTS[] = { "text_small_" }; +/** Text at least 12 px high: a smaller font is only used when its lines are this tall. */ +constexpr int MIN_LINE_HEIGHT = 15; + +} // namespace + +bool MapView::begin(Device* display, const char* root, const char* dataPath) { + snprintf(rootPath, sizeof(rootPath), "%s", root); + if (!output.begin(display)) { + logError("Display output failed"); + return false; + } + width = output.width(); + height = output.height(); + monochrome = output.isMonochrome(); + palette = monochrome ? &monochromePalette() : &colorPalette(); + theme = uiTheme(*palette, monochrome); + frame = static_cast(largeAlloc(static_cast(width) * height * sizeof(uint16_t))); + preview = static_cast(largeAlloc(TileCache::TILE_BYTES)); + if (frame == nullptr || preview == nullptr) { + logError("Out of memory"); + return false; + } + canvas = { frame, width, height }; + // From here on only the I/O task touches the SD card + if (!io.start()) { + logError("I/O task not started"); + return false; + } + int regionsFound = 0; + auto loadState = [&] { + char settingsPath[PATH_MAX_LENGTH]; + snprintf(settingsPath, sizeof(settingsPath), "%s/settings.txt", root); + readSettings(settingsPath, settings); + snprintf(settingsPath, sizeof(settingsPath), "%s/view.txt", root); + // The palette can be forced to monochrome, to check its contrast on a colour display + if (settings.devMonochrome && !monochrome) { + monochrome = true; + palette = &monochromePalette(); + theme = uiTheme(*palette, monochrome); + } + hasSavedView = readSavedView(settingsPath, savedView); + store.setRoot(root); + regionsFound = store.scanRegions(); + char folder[160]; + snprintf(folder, sizeof(folder), "%s/cache/fonts", dataPath); + if (normalFont.open(folder, NORMAL_FONTS, 3)) { + normalFont.preload(); + } else { + logError("No font found in %s, the map shows no text", folder); + } + if (smallFont.open(folder, SMALL_FONTS, 1)) { + smallFont.preload(); + } + script.open(root); + return true; + }; + runOnCard(io, loadState); + logInfo("Map root %s, %d regions", root, regionsFound); + overlay.setFonts(&normalFont, smallFont.isOpen() && smallFont.lineHeight() >= MIN_LINE_HEIGHT ? &smallFont : &normalFont); + // Live pinch scaling is off on monochrome displays, e-paper could not show it + if (settings.smoothPinch && !monochrome) { + pinchBase = static_cast(largeAlloc(static_cast(width) * height * sizeof(uint16_t))); + } + worker.setDeflatePrepared(settings.deflatePrepared); + worker.setSdCache(settings.sdCache); + if (!worker.start(&store, &io, palette, TILE_CACHE_SLOTS, root)) { + return false; + } + input.begin(); + // The overlay adapts to the inputs the board has: key hints with a keyboard, focus without touch or pointer + controls.setInputs({ input.hasKeyboard(), input.hasTouch(), input.hasPointer() }); + controls.layout(width, height, overlay.scaleBarTop(canvas)); + downloadView = downloadLayout(width, height, overlay.font().lineHeight()); + downloader.setTiming(settings.devProbe); + if (!downloader.start(root, &store, &io)) { + logError("Downloader not started"); + } + if (settings.devProbe) { + probe.watch(xTaskGetCurrentTaskHandle(), "main"); + probe.watch(worker.laneTask(0), "lane0"); + probe.watch(worker.laneTask(1), "lane1"); + probe.watch(io.taskHandle(), "io"); + probe.watch(downloader.controlTaskHandle(), "download"); + probe.watch(downloader.browseTaskHandle(), "browse"); + if (!probe.start()) { + logError("Stall probes not started"); + } + } + + if (hasSavedView) { + centerOn(savedView.lat, savedView.lon, savedView.zoom); + } else { + centerOn(DEFAULT_LAT, DEFAULT_LON, DEFAULT_ZOOM); + } + viewReady = true; + + task_event_group_construct(&eventGroup); + eventGroupReady = true; + subscribed = app_event_subscribe(&appEvents, &eventGroup) == ERROR_NONE; + location.subscribe(&eventGroup); + readLocation(); + attribution.expand(nowMs()); + logMemory("map ready"); + return true; +} + +void MapView::end() { + if (viewReady) { + saveView(); + viewReady = false; + } + downloader.stop(); + worker.stop(); + io.stop(); + probe.stop(); + location.unsubscribe(); + if (subscribed) { + app_event_unsubscribe(&appEvents); + subscribed = false; + } + if (eventGroupReady) { + task_event_group_destruct(&eventGroup); + eventGroupReady = false; + } + input.end(); + normalFont.close(); + smallFont.close(); + output.end(); + largeFree(frame); + frame = nullptr; + largeFree(preview); + preview = nullptr; + largeFree(pinchBase); + pinchBase = nullptr; + largeFree(aboutBuffer); + aboutBuffer = nullptr; +} + +void MapView::centerOn(double lat, double lon, int newZoom) { + zoom = newZoom; + centerX = lonToWorldX(lon, zoom); + centerY = latToWorldY(lat, zoom); + dirty = true; + updateWanted(); +} + +void MapView::setZoom(int newZoom, int anchorX, int anchorY) { + if (newZoom < 0 || newZoom > MAX_VIEW_ZOOM || newZoom == zoom) { + return; + } + // The world point under the anchor stays where it is on the screen + const double offsetX = anchorX - width / 2.0; + const double offsetY = anchorY - height / 2.0; + const double scale = std::ldexp(1.0, newZoom - zoom); + centerX = (centerX + offsetX) * scale - offsetX; + centerY = (centerY + offsetY) * scale - offsetY; + zoom = newZoom; + dirty = true; + updateWanted(); +} + +void MapView::pan(double dx, double dy) { + if (dx == 0.0 && dy == 0.0) { + return; + } + following = false; + const double worldSize = std::ldexp(static_cast(TILE_SIZE), zoom); + centerX = std::fmod(centerX + dx + worldSize, worldSize); + centerY = std::fmin(std::fmax(centerY + dy, 0.0), worldSize); + dirty = true; + updateWanted(); +} + +void MapView::updateWanted() { + // The visible tiles first, nearest to the centre first, then a ring around them + const double left = centerX - width / 2.0; + const double top = centerY - height / 2.0; + const int firstX = static_cast(std::floor(left / TILE_SIZE)) - 1; + const int firstY = static_cast(std::floor(top / TILE_SIZE)) - 1; + const int lastX = static_cast(std::floor((left + width - 1) / TILE_SIZE)) + 1; + const int lastY = static_cast(std::floor((top + height - 1) / TILE_SIZE)) + 1; + TileKey tiles[TileWorker::MAX_WANTED]; + double distances[TileWorker::MAX_WANTED]; + int count = 0; + const int tileCount = 1 << zoom; + for (int y = firstY; y <= lastY; y++) { + if (y < 0 || y >= tileCount) { + continue; + } + for (int x = firstX; x <= lastX && count < TileWorker::MAX_WANTED; x++) { + const double tileCenterX = (x + 0.5) * TILE_SIZE; + const double tileCenterY = (y + 0.5) * TILE_SIZE; + const bool visible = x > firstX && x < lastX && y > firstY && y < lastY; + double distance = std::sqrt((tileCenterX - centerX) * (tileCenterX - centerX) + (tileCenterY - centerY) * (tileCenterY - centerY)); + if (!visible) { + distance += 1e6; + } + int slot = count; + while (slot > 0 && distances[slot - 1] > distance) { + tiles[slot] = tiles[slot - 1]; + distances[slot] = distances[slot - 1]; + slot--; + } + tiles[slot] = { zoom, wrapTileX(x, zoom), y }; + distances[slot] = distance; + count++; + } + } + int visibleCount = 0; + while (visibleCount < count && distances[visibleCount] < 1e6) { + visibleCount++; + } + worker.want(tiles, count, visibleCount); + viewChangedAt = nowMs(); + viewTimed = false; +} + +const uint16_t* MapView::lookupTile(const TileKey& tile, void* context) { + return static_cast(context)->findTile(tile); +} + +const uint16_t* MapView::findTile(const TileKey& tile) { + TileCache& cache = worker.cache(); + if (const uint16_t* pixels = cache.find(tile)) { + return pixels; + } + // A magnified part of a cached ancestor stands in until the tile is ready + for (int level = 1; level <= 4 && tile.z - level >= 0; level++) { + const TileKey parent = ancestorOf(tile, tile.z - level); + const uint16_t* pixels = cache.find(parent); + if (pixels == nullptr) { + continue; + } + const int size = TILE_SIZE >> level; + const int originX = (tile.x - (parent.x << level)) * size; + const int originY = (tile.y - (parent.y << level)) * size; + for (int y = 0; y < TILE_SIZE; y++) { + const uint16_t* source = pixels + (originY + (y >> level)) * TILE_SIZE + originX; + uint16_t* target = preview + y * TILE_SIZE; + for (int x = 0; x < TILE_SIZE; x++) { + target[x] = source[x >> level]; + } + } + return preview; + } + return nullptr; +} + +bool MapView::isViewComplete() { + const double left = centerX - width / 2.0; + const double top = centerY - height / 2.0; + const int tileCount = 1 << zoom; + worker.lock(); + bool complete = true; + for (int y = static_cast(std::floor(top / TILE_SIZE)); y <= static_cast(std::floor((top + height - 1) / TILE_SIZE)) && complete; y++) { + for (int x = static_cast(std::floor(left / TILE_SIZE)); x <= static_cast(std::floor((left + width - 1) / TILE_SIZE)) && complete; x++) { + if (y < 0 || y >= tileCount) { + continue; + } + const TileKey tile = { zoom, wrapTileX(x, zoom), y }; + complete = worker.isSettled(tile); + } + } + worker.unlock(); + return complete; +} + +bool MapView::markerScreenPosition(int& x, int& y) const { + if (!position.hasFix()) { + return false; + } + const double worldSize = std::ldexp(static_cast(TILE_SIZE), zoom); + double dx = lonToWorldX(position.fix().longitude, zoom) - centerX; + if (dx > worldSize / 2) { + dx -= worldSize; + } else if (dx < -worldSize / 2) { + dx += worldSize; + } + x = static_cast(std::floor(width / 2.0 + dx + 0.5)); + y = static_cast(std::floor(height / 2.0 + latToWorldY(position.fix().latitude, zoom) - centerY + 0.5)); + // Far outside the screen only the accuracy circle can still show + const int reach = 4 * (width + height); + return x > -reach && y > -reach && x < width + reach && y < height + reach; +} + +void MapView::drawStatus() { + char text[80]; + const Downloader::Status& status = lastStatus; + if (status.state == Downloader::State::Downloading && status.total > 0) { + snprintf(text, sizeof(text), "Downloading %s %d%%", status.region, static_cast(status.done * 100 / status.total)); + } else if (downloader.isBrowsing()) { + snprintf(text, sizeof(text), "Downloading map"); + } else if (status.state == Downloader::State::Connecting || status.state == Downloader::State::Deleting) { + snprintf(text, sizeof(text), "%s", status.message); + } else if (static_cast(nowMs() - messageUntil) < 0) { + snprintf(text, sizeof(text), "%s", status.message); + } else if (store.regionCount() == 0) { + snprintf(text, sizeof(text), "No maps yet: Menu, Download area"); + } else { + return; + } + overlay.drawPill(canvas, text, *palette); +} + +void MapView::pollDownloader(uint32_t now) { + if (downloader.takeTilesChanged()) { + worker.tilesArrived(); + dirty = true; + } + if (!resumeChecked) { + // A download that was interrupted continues when the map opens again + resumeChecked = true; + int unfinished = -1; + RegionMeta meta {}; + auto find = [&] { + for (int i = 0; i < store.regionCount() && unfinished < 0; i++) { + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/maps/%s/meta.json", rootPath, store.regionName(i)); + if (readRegionMeta(path, meta) && !meta.isComplete()) { + unfinished = i; + } + } + return true; + }; + runOnCard(io, find, IoPriority::Background); + if (unfinished >= 0) { + logInfo("Continuing the download of %s at tile %d of %d", store.regionName(unfinished), static_cast(meta.done), static_cast(meta.total)); + downloader.download(store.regionName(unfinished)); + } + } + if (now - lastStatusPoll < STATUS_POLL_MS) { + return; + } + lastStatusPoll = now; + const Downloader::Status status = downloader.status(); + if (status.state != lastStatus.state || status.done != lastStatus.done || strcmp(status.message, lastStatus.message) != 0) { + if (status.state != lastStatus.state && (status.state == Downloader::State::Done || status.state == Downloader::State::Failed)) { + messageUntil = now + MESSAGE_SHOW_MS; + } + lastStatus = status; + dirty = true; + // The Regions screen shows progress as it happens + if (screen == Screen::Regions && status.state == Downloader::State::Done) { + openRegions(); + } + } + if (static_cast(now - messageUntil) >= 0 && messageUntil != 0) { + messageUntil = 0; + dirty = true; + } +} + +void MapView::pollGaps(uint32_t now) { + if (now - lastGapPoll < GAP_POLL_MS) { + return; + } + lastGapPoll = now; + TileKey gaps[8]; + const int count = worker.takeGaps(gaps, 8); + if (count == 0) { + return; + } + if (settings.browseDownload && Downloader::isOnline()) { + downloader.browse(gaps, count); + dirty = true; + return; + } + if (lastGapHint != 0 && now - lastGapHint < GAP_HINT_MS) { + return; + } + lastGapHint = now; + if (!settings.browseDownload) { + showToast("Less detail here. Download this area for more."); + } else { + showToast("Offline: no detail here. Download this area on Wi-Fi."); + } +} + +Rect MapView::attributionMark() const { + if (screen == Screen::Download) { + return downloadView.mark; + } + return { 0, height - MapOverlay::MARK_SIZE, MapOverlay::MARK_SIZE, MapOverlay::MARK_SIZE }; +} + +PositionButton MapView::positionButton() const { + if (!position.hasFix()) { + return PositionButton::Unavailable; + } + return following ? PositionButton::Following : PositionButton::Free; +} + +void MapView::drawMap() { + const uint32_t start = nowMs(); + worker.lock(); + const uint32_t locked = nowMs(); + composeView(frame, width, height, centerX, centerY, zoom, lookupTile, this, palette->placeholder); + worker.unlock(); + frameLockMs = locked - start; + frameComposeMs = nowMs() - locked; + int markerX; + int markerY; + if (markerScreenPosition(markerX, markerY)) { + if (position.state() == PositionState::Approximate) { + overlay.drawApproximateMarker(canvas, markerX, markerY, position.fix().accuracy, *palette); + } else { + const float radius = position.accuracyRadius(metresPerPixel(position.fix().latitude, zoom)); + overlay.drawAccuracyCircle(canvas, markerX, markerY, radius, *palette); + overlay.drawMarker(canvas, markerX, markerY, position.showsArrow(), position.fix().course, *palette); + } + } +} + +void MapView::render() { + const uint32_t start = nowMs(); + if (pinching) { + // The picture under the fingers scaled live, sharp tiles follow when the fingers lift + scaleView(pinchBase, frame, width, height, pinchScale, static_cast(pinchStartX), static_cast(pinchStartY), + static_cast(pinchX), static_cast(pinchY), palette->placeholder); + if (screen == Screen::Download) { + // The frame and its bar stay put while the map scales under them + overlay.setBottomInset(downloadView.bar.height); + drawDownloadFrame(); + } else { + overlay.setBottomInset(0); + } + overlay.drawAttributionMark(canvas, *palette); + output.present(frame, 0, height); + dirty = false; + return; + } + if (isFullScreen()) { + if (screen == Screen::About) { + drawAbout(); + } else { + activeList()->draw(canvas, overlay.font(), theme); + } + } else { + drawMap(); + overlay.setBottomInset(screen == Screen::Download ? downloadView.bar.height : 0); + if (screen == Screen::Map) { + const double lat = worldYToLat(centerY, zoom); + overlay.drawScaleBar(canvas, metresPerPixel(lat, zoom), *palette); + } + // The full attribution shows for a few seconds, then collapses into a mark that opens it again + if (screen == Screen::Download) { + drawDownloadFrame(); + } + if (attribution.isExpanded()) { + overlay.drawAttribution(canvas, *palette, attributionBar.x, attributionBar.y, attributionBar.width, attributionBar.height); + } else if (screen == Screen::Map) { + overlay.drawAttributionMark(canvas, *palette); + } + if (screen == Screen::Map) { + controls.draw(canvas, overlay.font(), theme, positionButton()); + } + if (screen == Screen::Map) { + drawStatus(); + } + } + if (menuOpen) { + menu.draw(canvas, overlay.font(), theme); + } + if (dialog.isOpen()) { + dialog.draw(canvas, overlay.font(), theme); + } + if (static_cast(nowMs() - toastUntil) < 0) { + overlay.drawToast(canvas, toastText, theme); + } + if (cursorShown && screen == Screen::Map && !menuOpen) { + // An arrow pointer, dark with a light rim so it shows on any map + const auto x = static_cast(cursorX); + const auto y = static_cast(cursorY); + canvas.line(x, y, x, y + 13.0f, 4.0f, theme.panel); + canvas.line(x, y, x + 9.0f, y + 9.0f, 4.0f, theme.panel); + canvas.line(x, y, x, y + 13.0f, 2.0f, theme.text); + canvas.line(x, y, x + 9.0f, y + 9.0f, 2.0f, theme.text); + } + const uint32_t presentStart = nowMs(); + output.present(frame, 0, height); + dirty = false; + const uint32_t frameEnd = nowMs(); + if (frameEnd - start > SLOW_LOOP_MS) { + logInfo("Slow frame %u ms: lock %u, compose %u, overlays %u, display %u", frameEnd - start, frameLockMs, frameComposeMs, + presentStart - start - frameLockMs - frameComposeMs, frameEnd - presentStart); + } + frameLockMs = 0; + frameComposeMs = 0; + if (!viewTimed && !isFullScreen() && isViewComplete()) { + viewTimed = true; + // The first connection to OpenFreeMap stalls the device for about 1.5 s: after the first view, not during it + if (!browsingPrepared && settings.browseDownload) { + browsingPrepared = true; + downloader.prepareBrowsing(); + } + logInfo("View z%d complete %u ms after the change (frame %u ms)", zoom, nowMs() - viewChangedAt, nowMs() - start); + logMemory("view complete"); + // How close each task came to the end of its stack, so stack sizes can be set from measurements + logInfo("Stack left: main %u, io %u, render0 %u, render1 %u bytes", static_cast(uxTaskGetStackHighWaterMark(nullptr)), + static_cast(io.stackLeft()), static_cast(worker.stackLeft(0)), static_cast(worker.stackLeft(1))); + } +} + +void MapView::showToast(const char* text) { + snprintf(toastText, sizeof(toastText), "%s", text); + toastUntil = nowMs() + TOAST_SHOW_MS; + dirty = true; +} + +void MapView::startFollowing() { + if (!position.hasFix()) { + // Location settings are the user's, the app only says where to find them + following = false; + showToast(position.unavailableMessage()); + return; + } + following = true; + centerOn(position.fix().latitude, position.fix().longitude, zoom); + dirty = true; +} + +void MapView::endPinch(float scale, int fromX, int fromY, int toX, int toY) { + pinching = false; + // Snaps to the nearest whole zoom level, the point under the fingers at the start stays under them + const int newZoom = snapZoom(zoom, scale, MAX_VIEW_ZOOM); + const double factor = std::ldexp(1.0, newZoom - zoom); + const double worldX = centerX + (fromX - width / 2.0); + const double worldY = centerY + (fromY - height / 2.0); + const double worldSize = std::ldexp(static_cast(TILE_SIZE), newZoom); + centerX = std::fmod(worldX * factor - (toX - width / 2.0) + worldSize, worldSize); + centerY = std::fmin(std::fmax(worldY * factor - (toY - height / 2.0), 0.0), worldSize); + zoom = newZoom; + dirty = true; + updateWanted(); +} + +void MapView::readLocation() { + lastLocationCheck = nowMs(); + const bool hadFix = position.hasFix(); + const PositionState before = position.state(); + if (!position.update(location)) { + return; + } + if (!hadFix && position.hasFix()) { + logInfo("First position, accuracy %d m", static_cast(position.fix().accuracy)); + } + if (following && position.hasFix()) { + centerOn(position.fix().latitude, position.fix().longitude, zoom); + } + if (before != position.state() && !position.hasFix()) { + following = false; + } + dirty = true; +} + +void MapView::saveView() { + SavedView view; + view.lat = worldYToLat(centerY, zoom); + view.lon = worldXToLon(centerX, zoom); + view.zoom = zoom; + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/view.txt", rootPath); + auto save = [&] { return writeSavedView(path, view); }; + if (!runOnCard(io, save)) { + logError("View not saved to %s", path); + } +} + +void MapView::saveShot(const char* name) { + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/%s.bmp", rootPath, name); + auto save = [&] { return saveBmp(path, frame, width, height); }; + logInfo("Screenshot %s: %s", path, runOnCard(io, save) ? "saved" : "failed"); +} + +void MapView::run() { + while (!exitRequested) { + const uint32_t now = nowMs(); + probe.heartbeat(now); + KeyEvent events[8]; + const int count = input.readKeys(events, 8); + bool enterThisPoll = false; + if (count > 0) { + markInput(now); + } + for (int i = 0; i < count; i++) { + if (events[i].key == keys::ENTER) { + handleEnter(events[i].pressed, now); + enterThisPoll = enterThisPoll || events[i].pressed; + } else if (events[i].pressed) { + handleKey(events[i].key); + } + } + if (enterHeld && !enterThisPoll && now - enterSeenAt > ENTER_RELEASE_MS) { + handleEnter(false, now); + } + if (enterHeld && !enterLong && now - enterDownAt >= LONG_PRESS_MS) { + enterLong = true; + longClick(); + } + const uint32_t afterKeys = nowMs(); + handleMouse(now); + handleTouch(now); + const uint32_t afterInput = nowMs(); + if (!idle && now - lastInputAt >= IDLE_AFTER_MS) { + idle = true; + worker.setIdle(true); + downloader.setIdle(true); + } + if (now - lastLocationCheck >= LOCATION_CHECK_MS) { + readLocation(); + } + const uint32_t afterLocation = nowMs(); + pollDownloader(now); + const uint32_t afterDownloader = nowMs(); + pollGaps(now); + const uint32_t afterGaps = nowMs(); + + if (!waitingIdle) { + const DebugScript::Step step = script.next(now); + switch (step.action) { + case DebugScript::Action::Key: + markInput(now); + if (step.key == keys::ENTER) { + handleEnter(true, now); + handleEnter(false, now); + } else if (step.key == 'c') { + // "key c" in a debug script is a trackball click in mouse mode + click(true); + } else { + handleKey(step.key); + } + break; + case DebugScript::Action::Shot: + render(); + saveShot(step.name); + break; + case DebugScript::Action::Goto: + centerOn(step.lat, step.lon, step.zoom); + break; + case DebugScript::Action::WaitIdle: + waitingIdle = true; + break; + case DebugScript::Action::Exit: + exitRequested = true; + break; + default: + break; + } + } else if (viewTimed && !dirty) { + waitingIdle = false; + } + + if (subscribed) { + AppEvent event; + while (app_event_poll(&appEvents, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + exitRequested = true; + } + } + } + if (worker.takeChanged() && !isFullScreen()) { + dirty = true; + } + if (attribution.update(now)) { + dirty = true; + } + if (static_cast(now - toastUntil) >= 0 && toastUntil != 0) { + toastUntil = 0; + dirty = true; + } + const uint32_t beforeRender = nowMs(); + if (dirty && !exitRequested) { + render(); + } + // A pass that takes long is a freeze the user feels, the log says where the time went + const uint32_t done = nowMs(); + if (done - now > SLOW_LOOP_MS) { + logInfo("Slow loop %u ms: keys %u, mouse+touch %u, location %u, downloader %u, gaps %u, other %u, render %u", done - now, + afterKeys - now, afterInput - afterKeys, afterLocation - afterInput, afterDownloader - afterLocation, afterGaps - afterDownloader, + beforeRender - afterGaps, done - beforeRender); + } + // Sleeps until the next input poll, waking early for app events and new positions + uint32_t flags = 0; + if (!subscribed && location.bit() == 0) { + // Without a claimed bit the wait would return at once + vTaskDelay(pdMS_TO_TICKS(LOOP_INTERVAL_MS)); + } else if (task_event_group_wait_any(&eventGroup, &flags, pdMS_TO_TICKS(LOOP_INTERVAL_MS)) == ERROR_NONE && + (flags & location.bit()) != 0) { + readLocation(); + } + } +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/MapView.h b/Apps/Maps/main/Source/app/MapView.h new file mode 100644 index 0000000..a3111ef --- /dev/null +++ b/Apps/Maps/main/Source/app/MapView.h @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "BinFontText.h" +#include "Debug.h" +#include "DisplayOutput.h" +#include "Downloader.h" +#include "Input.h" +#include "IoTask.h" +#include "LocationSource.h" +#include "StallProbe.h" +#include "TileWorker.h" + +#include "../tiles/DownloadPlan.h" +#include "../tiles/RegionMeta.h" +#include "../tiles/TileStore.h" +#include "../ui/Canvas.h" +#include "../ui/Controls.h" +#include "../ui/DownloadFrame.h" +#include "../ui/Gestures.h" +#include "../ui/MapOverlay.h" +#include "../ui/PositionTracker.h" +#include "../ui/Settings.h" +#include "../ui/Widgets.h" + +#include +#include + +#include + +namespace maps { + +#define APP_VERSION "0.0.1" + +/** + * The whole app while it runs fullscreen with LVGL stopped: the map, the menu, the Regions, Settings and About + * screens, and the download frame, all drawn by the app itself. + */ +class MapView { +public: + bool begin(Device* display, const char* root, const char* dataPath); + void run(); + void end(); + +private: + enum class Screen : uint8_t { + Map, + Download, + Regions, + Settings, + About + }; + + enum class MenuItem : uint8_t { + Download, + Regions, + Settings, + About, + Exit, + Count + }; + + enum class SettingItem : uint8_t { + SdCache, + PreparedTiles, + SmoothPinch, + BrowseDownload, + Count + }; + + // MapView.cpp: the map itself + void centerOn(double lat, double lon, int zoom); + /** Remembers the centre and zoom, so the map opens there next time. */ + void saveView(); + void setZoom(int zoom, int anchorX, int anchorY); + void pan(double dx, double dy); + void updateWanted(); + void render(); + void drawMap(); + void drawStatus(); + void pollDownloader(uint32_t now); + /** Hands the tiles that lack detail to the downloader, or says why they stay coarse. */ + void pollGaps(uint32_t now); + Rect attributionMark() const; + void readLocation(); + void showToast(const char* text); + void startFollowing(); + bool markerScreenPosition(int& x, int& y) const; + void saveShot(const char* name); + bool isViewComplete(); + static const uint16_t* lookupTile(const TileKey& tile, void* context); + const uint16_t* findTile(const TileKey& tile); + void endPinch(float scale, int fromX, int fromY, int toX, int toY); + bool isFullScreen() const { return screen == Screen::Regions || screen == Screen::Settings || screen == Screen::About; } + + // MapInput.cpp: keys, trackball and touch + void handleKey(uint32_t key); + void handleMapKey(uint32_t key); + void handleEnter(bool pressed, uint32_t now); + void handleMouse(uint32_t now); + void handleTouch(uint32_t now); + void handleGesture(const Gesture& gesture); + void handleTap(int x, int y); + void handleLongPress(int x, int y); + void markInput(uint32_t now); + /** A trackball or Enter click: the same as Enter on lists, the map's own action on the map. */ + void click(bool fromPointer); + void longClick(); + void pressControl(Control control); + /** A tap or pointer click on the map: buttons and the attribution first. Returns true when something took it. */ + bool pressAt(int x, int y); + PositionButton positionButton() const; + + // MapScreens.cpp: menu, Regions, Settings, About, download frame + void openMenu(); + void closeMenu(); + void chooseMenu(MenuItem item); + void handleListAction(UiAction action); + void openRegions(); + void fillRegionList(); + void chooseRegion(); + void askDeleteRegion(); + void deleteRegion(); + void openSettings(); + void fillSettingList(); + void toggleSetting(SettingItem item); + void openAbout(); + void scrollAbout(int lines); + void drawAbout(); + void closeScreen(); + BoundingBox frameBox() const; + void drawDownloadFrame(); + void startDownload(); + void handleDownloadKey(uint32_t key); + void showRegion(const RegionMeta& meta); + ListView* activeList(); + /** Sets a list's hint for the inputs this board has. */ + void setListHint(ListView& list, const char* keyHint, const char* touchHint); + + DisplayOutput output; + IoTask io; + Input input; + BinFontText normalFont; + BinFontText smallFont; + MapOverlay overlay; + Canvas canvas; + UiTheme theme {}; + TileStore store; + TileWorker worker; + Downloader downloader; + DebugScript script; + Screen screen = Screen::Map; + ListView menu; + bool menuOpen = false; + ListView regionList; + RegionMeta regions[TileStore::MAX_REGIONS] {}; + int regionCount = 0; + ListView settingList; + Dialog dialog; + int aboutScroll = 0; + int aboutLines = 0; + char* aboutBuffer = nullptr; + DownloadLayout downloadView {}; + Downloader::Status lastStatus {}; + uint32_t lastStatusPoll = 0; + uint32_t lastGapPoll = 0; + uint32_t lastGapHint = 0; + uint32_t messageUntil = 0; + bool resumeChecked = false; + const Palette* palette = nullptr; + bool monochrome = false; + uint16_t* frame = nullptr; + uint16_t* preview = nullptr; + int width = 0; + int height = 0; + double centerX = 0.0; + double centerY = 0.0; + int zoom = 14; + bool dirty = true; + bool exitRequested = false; + char rootPath[PATH_MAX_LENGTH] = ""; + TaskEventGroup eventGroup {}; + bool eventGroupReady = false; + AppEventSubscription appEvents {}; + bool subscribed = false; + Settings settings; + StallProbe probe; + SavedView savedView; + bool hasSavedView = false; + bool viewReady = false; + LocationSource location; + PositionTracker position; + uint32_t lastLocationCheck = 0; + char toastText[96] = ""; + uint32_t toastUntil = 0; + bool following = false; + Attribution attribution; + Rect attributionBar {}; + Controls controls; + /** The pointer of a trackball in mouse mode, shown while the controls are. */ + bool cursorShown = false; + int cursorX = 0; + int cursorY = 0; + bool enterHeld = false; + bool enterLong = false; + uint32_t enterDownAt = 0; + uint32_t enterSeenAt = 0; + bool mouseHeld = false; + uint32_t mouseDownAt = 0; + bool mouseLong = false; + int mouseRowAccumulator = 0; + Gestures gestures; + int dragRowAccumulator = 0; + uint16_t* pinchBase = nullptr; + bool pinching = false; + bool pinchStepped = false; + float pinchScale = 1.0f; + int pinchStartX = 0; + int pinchStartY = 0; + int pinchX = 0; + int pinchY = 0; + uint32_t lastInputAt = 0; + bool idle = false; + uint32_t viewChangedAt = 0; + bool browsingPrepared = false; + uint32_t frameLockMs = 0; + uint32_t frameComposeMs = 0; + bool viewTimed = true; + bool waitingIdle = false; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/PlatformTactility.cpp b/Apps/Maps/main/Source/app/PlatformTactility.cpp new file mode 100644 index 0000000..f888aa6 --- /dev/null +++ b/Apps/Maps/main/Source/app/PlatformTactility.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "../platform/Platform.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +constexpr auto* TAG = "Maps"; + +namespace maps { + +void* largeAlloc(size_t size) { + const MemoryPolicy policy = { + .required = 0, + .desired = MEMORY_CAPABILITY_EXTERNAL, + .alignment = 0, + }; + return memory_alloc_with_policy(size, &policy); +} + +void* fastAlloc(size_t size) { + const MemoryPolicy policy = { + .required = 0, + .desired = MEMORY_CAPABILITY_INTERNAL, + .alignment = 0, + }; + return memory_alloc_with_policy(size, &policy); +} + +void largeFree(void* pointer) { + if (pointer != nullptr) { + memory_free(pointer); + } +} + +uint64_t nowUs() { + return static_cast(esp_timer_get_time()); +} + +uint32_t nowMs() { + return static_cast(esp_timer_get_time() / 1000); +} + +void logInfo(const char* format, ...) { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + LOG_I(TAG, "%s", message); +} + +void logError(const char* format, ...) { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + LOG_E(TAG, "%s", message); +} + +void logMemory(const char* label) { + LOG_I(TAG, "Memory %s: internal %u free (largest %u), PSRAM %u free", + label, + static_cast(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)), + static_cast(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)), + static_cast(heap_caps_get_free_size(MALLOC_CAP_SPIRAM))); +} + +bool fileExists(const char* path) { + struct stat info {}; + return stat(path, &info) == 0 && S_ISREG(info.st_mode); +} + +bool fileSize(const char* path, uint32_t& size) { + struct stat info {}; + if (stat(path, &info) != 0 || !S_ISREG(info.st_mode)) { + return false; + } + size = static_cast(info.st_size); + return true; +} + +bool removeFile(const char* path) { + return remove(path) == 0; +} + +bool removeFolder(const char* path) { + return rmdir(path) == 0; +} + +bool listDirectory(const char* path, DirectoryCallback callback, void* context) { + DIR* dir = opendir(path); + if (dir == nullptr) { + return false; + } + while (const dirent* entry = readdir(dir)) { + if (entry->d_name[0] == '.') { + continue; + } + if (!callback(entry->d_name, entry->d_type == DT_DIR, context)) { + break; + } + } + closedir(dir); + return true; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/RootCertificates.h b/Apps/Maps/main/Source/app/RootCertificates.h new file mode 100644 index 0000000..0359935 --- /dev/null +++ b/Apps/Maps/main/Source/app/RootCertificates.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +namespace maps { + +/** + * Root certificates for the HTTPS connections to OpenFreeMap, which is served through Cloudflare with + * certificates from Google Trust Services. Let's Encrypt is included in case the tile host changes CA. + * Tactility's certificate bundle is not used, because OpenFreeMap sends GTS Root R4 cross-signed by an + * older GlobalSign root, and the bundle only checks the top of the chain it receives. + */ +constexpr const char ROOT_CERTIFICATES[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw\n" + "CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\n" + "MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\n" + "MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\n" + "Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA\n" + "A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo\n" + "27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w\n" + "Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw\n" + "TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl\n" + "qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH\n" + "szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8\n" + "Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk\n" + "MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92\n" + "wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p\n" + "aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN\n" + "VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID\n" + "AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E\n" + "FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb\n" + "C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe\n" + "QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy\n" + "h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4\n" + "7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J\n" + "ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef\n" + "MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/\n" + "Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT\n" + "6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ\n" + "0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm\n" + "2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb\n" + "bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c\n" + "-----END CERTIFICATE-----\n" + "-----BEGIN CERTIFICATE-----\n" + "MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD\n" + "VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG\n" + "A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw\n" + "WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz\n" + "IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi\n" + "AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi\n" + "QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR\n" + "HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\n" + "BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D\n" + "9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8\n" + "p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD\n" + "-----END CERTIFICATE-----\n" + "-----BEGIN CERTIFICATE-----\n" + "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\n" + "-----END CERTIFICATE-----\n" + "-----BEGIN CERTIFICATE-----\n" + "MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw\n" + "CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg\n" + "R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00\n" + "MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT\n" + "ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw\n" + "EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW\n" + "+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9\n" + "ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T\n" + "AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI\n" + "zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW\n" + "tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1\n" + "/q4AaOeMSQ+2b1tbFfLn\n" + "-----END CERTIFICATE-----\n"; + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/StallProbe.cpp b/Apps/Maps/main/Source/app/StallProbe.cpp new file mode 100644 index 0000000..ef31df5 --- /dev/null +++ b/Apps/Maps/main/Source/app/StallProbe.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "StallProbe.h" + +#include "../platform/Platform.h" + +#include + +namespace maps { + +namespace { + +/** Above the app (5), the Wi-Fi driver's helpers and lwIP (18), below the Wi-Fi task and esp_timer. */ +constexpr UBaseType_t PROBE_PRIORITY = 19; +constexpr uint32_t PROBE_STACK = 3072; + +} // namespace + +bool StallProbe::start() { + stopRequested = false; + for (int core = 0; core < 2; core++) { + if (xTaskCreatePinnedToCore(probeMain, core == 0 ? "maps-probe0" : "maps-probe1", PROBE_STACK, this, PROBE_PRIORITY, + &tasks[core], core) != pdPASS) { + stop(); + return false; + } + running++; + } + logInfo("Stall probes running on both cores, priority %u, gaps over %u ms are logged", static_cast(PROBE_PRIORITY), + static_cast(GAP_MS)); + return true; +} + +void StallProbe::stop() { + stopRequested = true; + while (running > 0) { + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + +void StallProbe::watch(TaskHandle_t task, const char* name) { + if (task != nullptr && watchedCount < MAX_WATCHED) { + watched[watchedCount++] = { task, name }; + } +} + +void StallProbe::logTaskStates(uint32_t stalledMs) { + static const char* const STATES[] = { "running", "ready", "blocked", "suspended", "deleted", "invalid" }; + char line[160]; + int used = snprintf(line, sizeof(line), "Loop stalled %u ms:", static_cast(stalledMs)); + for (int i = 0; i < watchedCount && used < static_cast(sizeof(line)); i++) { + TaskStatus_t status {}; + vTaskGetInfo(watched[i].task, &status, pdFALSE, eInvalid); + const int state = status.eCurrentState <= eInvalid ? static_cast(status.eCurrentState) : 5; + // The current priority above the base one means the task holds a lock a higher task waits for + used += snprintf(line + used, sizeof(line) - used, " %s %s %u/%u", watched[i].name, STATES[state], + static_cast(status.uxCurrentPriority), static_cast(status.uxBasePriority)); + } + logInfo("%s", line); +} + +void StallProbe::probeMain(void* context) { + auto* probe = static_cast(context); + const int core = xPortGetCoreID(); + const int64_t tickUs = static_cast(portTICK_PERIOD_MS) * 1000; + int64_t last = esp_timer_get_time(); + int64_t nextMemory = last; + while (!probe->stopRequested) { + vTaskDelay(1); + const int64_t now = esp_timer_get_time(); + const int64_t lateUs = now - last - tickUs; + if (lateUs > static_cast(GAP_MS) * 1000) { + logInfo("Stall on core %d: %u ms, from t=%u ms", core, static_cast(lateUs / 1000), static_cast(last / 1000)); + } + last = now; + // Core 1 looks at the map loop: when it stops coming round, what every app task is doing + const uint32_t beat = probe->lastBeat; + const uint32_t nowMs = static_cast(now / 1000); + if (core == 1 && beat != 0 && nowMs - beat > LOOP_STALL_MS && probe->reportedBeat != beat) { + probe->reportedBeat = beat; + probe->logTaskStates(nowMs - beat); + } + // Core 0 also samples memory once a second + if (core == 0 && now >= nextMemory) { + nextMemory = now + 1000000; + logMemory("sample"); + } + } + probe->running--; + vTaskDelete(nullptr); +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/StallProbe.h b/Apps/Maps/main/Source/app/StallProbe.h new file mode 100644 index 0000000..137b8b5 --- /dev/null +++ b/Apps/Maps/main/Source/app/StallProbe.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +#include +#include + +namespace maps { + +/** + * A developer tool for finding stalls (setting dev_probe=1 in settings.txt). One small task per core, at a + * priority above every app task and the network stack, wakes every tick and logs when it was kept from running + * for longer than GAP_MS. If the probes stall together with the app, the core was held below the scheduler + * (interrupts or the flash cache off, as during a flash write); if only the app stalls, a task of higher priority + * than the app ran or the app waited for a lock. The probe on core 0 also logs free memory once a second. + */ +class StallProbe { +public: + static constexpr uint32_t GAP_MS = 40; + /** The map loop counts as stalled when it has not come round for this long. */ + static constexpr uint32_t LOOP_STALL_MS = 300; + static constexpr int MAX_WATCHED = 8; + + bool start(); + void stop(); + /** A task whose state is logged when the map loop stalls. Call before start(). */ + void watch(TaskHandle_t task, const char* name); + /** Called by the map loop on every round. */ + void heartbeat(uint32_t nowMs) { lastBeat = nowMs; } + +private: + static void probeMain(void* context); + void logTaskStates(uint32_t stalledMs); + + struct Watched { + TaskHandle_t task; + const char* name; + }; + Watched watched[MAX_WATCHED] {}; + int watchedCount = 0; + std::atomic lastBeat = 0; + uint32_t reportedBeat = 0; + + TaskHandle_t tasks[2] {}; + std::atomic stopRequested = false; + std::atomic running = 0; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/TileWorker.cpp b/Apps/Maps/main/Source/app/TileWorker.cpp new file mode 100644 index 0000000..31c92fa --- /dev/null +++ b/Apps/Maps/main/Source/app/TileWorker.cpp @@ -0,0 +1,791 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "TileWorker.h" + +#include "../platform/Platform.h" +#include "../render/TileCodec.h" + +#include + +#include +#include + +namespace maps { + +namespace { + +/** Rendering, and conversions, where ROM tdefl alone needs about 3-4 KB of stack. */ +/** tdefl, which deflates prepared tiles, keeps about 6 KB of Huffman tables on the stack: 8 KB left 600 bytes. */ +constexpr uint32_t TASK_STACK = 10240; +constexpr UBaseType_t TASK_PRIORITY = 4; +constexpr uint32_t IDLE_WAIT_MS = 30; +constexpr char CACHE_MAGIC[4] = { 'T', 'M', 'C', '2' }; +constexpr size_t CACHE_HEADER_BYTES = 12; +constexpr size_t TILE_PIXELS = static_cast(TILE_SIZE) * TILE_SIZE; +constexpr size_t ENCODE_CAPACITY = TILE_PIXELS + 256; +/** Enough entries for the 200 MB cap: a dense rendered tile codes to about 96 KB. */ +constexpr int CACHE_INDEX_ENTRIES = 6000; +constexpr int SAVE_INDEX_EVERY_WRITES = 25; + +/** Checks a rendered tile file from the SD card cache. Returns the number of coded words, 0 when unusable. */ +uint32_t checkCacheFile(const uint8_t* bytes, size_t size, int& sourceZoom) { + uint32_t version = 0; + uint32_t words = 0; + if (size < CACHE_HEADER_BYTES || memcmp(bytes, CACHE_MAGIC, 4) != 0) { + return 0; + } + memcpy(&version, bytes + 4, 4); + memcpy(&words, bytes + 8, 4); + // The top byte of the version holds the zoom level of the vector tile it was drawn from + sourceZoom = static_cast(version >> 24); + if ((version & 0xFFFFFF) != STYLE_VERSION || size != CACHE_HEADER_BYTES + static_cast(words) * 2) { + return 0; + } + return words; +} + +struct WriteCacheJob { + const char* root; + const char* style; + TileKey tile; + const uint8_t* bytes; + size_t size; +}; + +bool writeCacheJob(IoTask& io, void* context) { + auto* job = static_cast(context); + char path[PATH_MAX_LENGTH]; + char folder[PATH_MAX_LENGTH]; + char temporary[PATH_MAX_LENGTH]; + snprintf(folder, sizeof(folder), "%s/cache", job->root); + mkdir(folder, 0777); + snprintf(folder, sizeof(folder), "%s/cache/%s", job->root, job->style); + mkdir(folder, 0777); + snprintf(folder, sizeof(folder), "%s/cache/%s/%d", job->root, job->style, job->tile.z); + mkdir(folder, 0777); + if (!cacheFolderPath(folder, sizeof(folder), job->root, job->style, job->tile) || + !cachePath(path, sizeof(path), job->root, job->style, job->tile)) { + return false; + } + mkdir(folder, 0777); + snprintf(temporary, sizeof(temporary), "%s.tmp", path); + return io.writeFile(path, temporary, job->bytes, job->size, true); +} + +struct IndexFileJob { + const char* root; + const char* style; + FileBuffer* buffer; + const ByteWriter* bytes; +}; + +bool readIndexJob(IoTask& io, void* context) { + auto* job = static_cast(context); + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/cache/%s/index.bin", job->root, job->style); + return io.readFile(path, *job->buffer); +} + +bool writeIndexJob(IoTask& io, void* context) { + auto* job = static_cast(context); + char path[PATH_MAX_LENGTH]; + char temporary[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/cache", job->root); + mkdir(path, 0777); + snprintf(path, sizeof(path), "%s/cache/%s", job->root, job->style); + mkdir(path, 0777); + snprintf(path, sizeof(path), "%s/cache/%s/index.bin", job->root, job->style); + snprintf(temporary, sizeof(temporary), "%s/cache/%s/index.tmp", job->root, job->style); + return io.writeFile(path, temporary, job->bytes->bytes(), job->bytes->size(), false); +} + +struct EvictJob { + const char* root; + const char* style; + TileKey tile; +}; + +bool evictJob(IoTask& io, void* context) { + (void)io; + auto* job = static_cast(context); + char path[PATH_MAX_LENGTH]; + // Only ever a file of the cache folder, never anything under maps/ + if (!cachePath(path, sizeof(path), job->root, job->style, job->tile) || !isInsideCache(path, job->root)) { + return false; + } + removeFile(path); + return true; +} + +struct WalkJob { + CacheWalker* walker; + CacheIndex* index; + const CacheWalker::IndexLock* lock; + bool more; +}; + +bool walkJob(IoTask& io, void* context) { + (void)io; + auto* job = static_cast(context); + job->more = job->walker->step(*job->index, job->lock); + return true; +} + +struct ConvertReadJob { + const char* root; + const char* region; + TileKey tile; + FileBuffer* buffer; + uint32_t size; + bool current; + bool yielded; +}; + +bool convertReadJob(IoTask& io, void* context) { + auto* job = static_cast(context); + char path[PATH_MAX_LENGTH]; + char prepared[PATH_MAX_LENGTH]; + if (!tilePath(path, sizeof(path), job->root, job->region, job->tile) || !fileSize(path, job->size) || + !preparedPath(prepared, sizeof(prepared), job->root, job->region, job->tile)) { + return false; + } + // Queued twice, or converted by the other lane meanwhile + job->current = PreparedScanner::isCurrent(prepared, job->tile, job->size); + const bool ok = job->current || io.readFile(path, *job->buffer); + job->yielded = io.yielded(); + return ok; +} + +struct ConvertWriteJob { + const char* root; + const char* region; + TileKey tile; + const ByteWriter* bytes; + bool yielded; +}; + +bool convertWriteJob(IoTask& io, void* context) { + auto* job = static_cast(context); + char prepared[PATH_MAX_LENGTH]; + char temporary[PATH_MAX_LENGTH]; + const bool ok = preparedPath(prepared, sizeof(prepared), job->root, job->region, job->tile) && + preparedTempPath(temporary, sizeof(temporary), job->root, job->region, job->tile) && + io.writeFile(prepared, temporary, job->bytes->bytes(), job->bytes->size(), true); + job->yielded = io.yielded(); + return ok; +} + +struct ScanJob { + PreparedScanner* scanner; + const TileStore* store; + PreparedScanner::StaleCallback callback; + void* result; + bool more; +}; + +bool scanJob(IoTask& io, void* context) { + (void)io; + auto* job = static_cast(context); + job->more = job->scanner->step(*job->store, job->callback, job->result); + return true; +} + +} // namespace + +bool TileWorker::start(TileStore* tileStore, IoTask* ioTask, const Palette* tilePalette, int cacheSlots, const char* cacheRoot) { + store = tileStore; + io = ioTask; + palette = tilePalette; + snprintf(root, sizeof(root), "%s", cacheRoot); + mutex = xSemaphoreCreateMutex(); + stopped = xSemaphoreCreateCounting(LANES, 0); + if (mutex == nullptr || stopped == nullptr || tileCache.init(cacheSlots) < 4) { + logError("Tile worker: out of memory"); + return false; + } + logInfo("Tile cache: %d slots of %d KB", tileCache.slotCount(), TileCache::TILE_BYTES / 1024); + styleCacheId(palette == &monochromePalette(), styleId, sizeof(styleId)); + if (!cacheIndex.init(CACHE_INDEX_ENTRIES)) { + logError("Tile worker: no memory for the cache index"); + return false; + } + // The index of the SD card cache, rebuilt from the files in idle time when it is missing + FileBuffer indexFile; + IndexFileJob read { root, styleId, &indexFile, nullptr }; + if (io->run(IoPriority::Visible, readIndexJob, &read) && cacheIndex.load(indexFile.bytes(), indexFile.size())) { + logInfo("SD cache %s (%s): %d tiles, %u KB", styleId, sdCache ? "on" : "off", cacheIndex.size(), + static_cast(cacheIndex.totalBytes() / 1024)); + } else { + logInfo("SD cache %s (%s): no index yet", styleId, sdCache ? "on" : "off"); + } + // With the SD card cache off its files go too, they only take space + cacheWalker.begin(root, styleId, sdCache); + if (!sdCache) { + cacheIndex.clear(); + } + stopRequested = false; + laneCount = 0; + for (int i = 0; i < LANES; i++) { + Lane& lane = lanes[i]; + lane.worker = this; + lane.index = i; + lane.active = false; + lane.converting = false; + lane.prefetching = false; + lane.cancel = false; + lane.renderer.setCancelFlag(&lane.cancel); + lane.writeBuffer = static_cast(largeAlloc(TileCache::TILE_BYTES)); + lane.encodeBuffer = static_cast(largeAlloc(ENCODE_CAPACITY * sizeof(uint16_t) + CACHE_HEADER_BYTES)); + if (lane.writeBuffer == nullptr || lane.encodeBuffer == nullptr || !lane.renderer.init()) { + logError("Tile worker: no memory for render lane %d", i); + break; + } + char name[16]; + snprintf(name, sizeof(name), "maps-render%d", i); + if (xTaskCreatePinnedToCore(taskMain, name, TASK_STACK, &lane, TASK_PRIORITY, &lane.task, i) != pdPASS) { + logError("Tile worker: task %d not started", i); + break; + } + laneCount++; + } + logMemory("render lanes started"); + return laneCount > 0; +} + +void TileWorker::stop() { + stopRequested = true; + for (int i = 0; i < laneCount; i++) { + xSemaphoreTake(stopped, portMAX_DELAY); + } + if (laneCount > 0) { + saveCacheIndex(); + } + for (int i = 0; i < LANES; i++) { + largeFree(lanes[i].writeBuffer); + lanes[i].writeBuffer = nullptr; + largeFree(lanes[i].encodeBuffer); + lanes[i].encodeBuffer = nullptr; + } + laneCount = 0; + if (stopped != nullptr) { + vSemaphoreDelete(stopped); + stopped = nullptr; + } + if (mutex != nullptr) { + vSemaphoreDelete(mutex); + mutex = nullptr; + } +} + +void TileWorker::taskMain(void* context) { + auto* lane = static_cast(context); + TileWorker* worker = lane->worker; + worker->loop(*lane); + xSemaphoreGive(worker->stopped); + vTaskDelete(nullptr); +} + +void TileWorker::want(const TileKey* tiles, int count, int visibleCount) { + lock(); + wantedCount = count < MAX_WANTED ? count : MAX_WANTED; + visibleWanted = visibleCount < wantedCount ? visibleCount : wantedCount; + memcpy(wanted, tiles, sizeof(TileKey) * wantedCount); + unlock(); +} + +bool TileWorker::isSettled(const TileKey& tile) const { + if (tileCache.contains(tile)) { + return true; + } + for (int i = 0; i < missingCount; i++) { + if (missing[i] == tile) { + return true; + } + } + return false; +} + +void TileWorker::tilesArrived() { + lock(); + missingCount = 0; + missingNext = 0; + tileCache.dropMagnified(); + unlock(); + // New originals get their prepared copies in idle time + rescanRequested = true; + changed = true; +} + +bool TileWorker::queueConversion(const TileKey& tile, int region) { + for (int i = 0; i < conversionCount; i++) { + if (conversions[i].tile == tile) { + return true; + } + } + for (int i = 0; i < laneCount; i++) { + if (lanes[i].converting && lanes[i].convertingTile == tile) { + return true; + } + } + if (conversionCount >= MAX_CONVERSIONS) { + return false; + } + conversions[conversionCount++] = { tile, region }; + return true; +} + +bool TileWorker::collectFromScan(const TileKey& tile, int region, void* context) { + auto* found = static_cast(context); + if (found->count >= found->room) { + return false; + } + found->items[found->count++] = { tile, region }; + return true; +} + +bool TileWorker::convertOne(Lane& lane) { + lock(); + Conversion job {}; + bool found = false; + bool otherConverting = false; + for (int i = 0; i < laneCount; i++) { + otherConverting = otherConverting || lanes[i].converting; + } + if (conversionCount > 0 && !otherConverting) { + // One conversion at a time: each needs about 0.4 MB of PSRAM, which a download needs as well. + // The oldest first, so the tiles just drawn from originals come before the scanned ones + job = conversions[0]; + for (int i = 1; i < conversionCount; i++) { + conversions[i - 1] = conversions[i]; + } + conversionCount--; + found = true; + lane.converting = true; + lane.convertingTile = job.tile; + } else if ((rescanRequested || !scanner.isComplete()) && !scanning) { + // Only one lane walks the folders at a time, and only that lane touches the scanner + scanning = true; + } else { + unlock(); + return false; + } + char region[REGION_NAME_MAX + 1] = ""; + char version[48] = ""; + if (found && job.region < store->regionCount()) { + snprintf(region, sizeof(region), "%s", store->regionName(job.region)); + snprintf(version, sizeof(version), "%s", store->regionVersion(job.region)); + } + unlock(); + + if (!found) { + if (rescanRequested.exchange(false)) { + scanner.restart(); + } + // Kept in the worker, not on the lane's small stack, only the scanning lane uses it + ScanResult& result = scanResult; + result.count = 0; + lock(); + result.room = MAX_CONVERSIONS - conversionCount; + unlock(); + ScanJob scan { &scanner, store, collectFromScan, &result, false }; + io->run(IoPriority::Background, scanJob, &scan); + lock(); + for (int i = 0; i < result.count; i++) { + queueConversion(result.items[i].tile, result.items[i].region); + } + scanning = false; + unlock(); + return scan.more || result.count > 0; + } + + const uint32_t start = nowMs(); + ConvertReadJob read { root, region, job.tile, &lane.fileBuffer, 0, false, false }; + bool ok = region[0] != '\0' && io->run(IoPriority::Background, convertReadJob, &read); + const uint32_t loaded = nowMs(); + bool requeue = read.yielded; + if (ok && !read.current) { + ConvertStats stats {}; + MemoryInput input(lane.fileBuffer.bytes(), lane.fileBuffer.size()); + ok = lane.converter.convert(lane.renderer.tileDecoder(), &input, job.tile, read.size, version, deflatePrepared, lane.converted, &stats); + const uint32_t convertedAt = nowMs(); + ConvertWriteJob write { root, region, job.tile, &lane.converted, false }; + ok = ok && io->run(IoPriority::Background, convertWriteJob, &write); + requeue = write.yielded; + if (ok) { + logInfo("[%d] Prepared %d/%d/%d: %u -> %u bytes, read %u ms, convert %u ms, write %u ms", lane.index, job.tile.z, job.tile.x, + job.tile.y, static_cast(read.size), static_cast(lane.converted.size()), loaded - start, convertedAt - loaded, + nowMs() - convertedAt); + } + } + // A read or write that gave way to tiles on screen is tried again later + if (!ok && !requeue) { + logError("[%d] Could not prepare %d/%d/%d", lane.index, job.tile.z, job.tile.x, job.tile.y); + } + lock(); + lane.converting = false; + if (requeue) { + queueConversion(job.tile, job.region); + } + unlock(); + // Something was done, or the card is busy with more urgent work + return true; +} + +void TileWorker::addGap(const TileKey& tile) { + for (int i = 0; i < gapCount; i++) { + if (gaps[i] == tile) { + return; + } + } + if (gapCount < MAX_GAPS) { + gaps[gapCount++] = tile; + } +} + +int TileWorker::takeGaps(TileKey* out, int max) { + lock(); + const int count = gapCount < max ? gapCount : max; + for (int i = 0; i < count; i++) { + out[i] = gaps[i]; + } + // Whatever did not fit stays for the next call + for (int i = count; i < gapCount; i++) { + gaps[i - count] = gaps[i]; + } + gapCount -= count; + unlock(); + return count; +} + +void TileWorker::cancelPrefetch() { + idle = false; + lock(); + for (int i = 0; i < laneCount; i++) { + if (lanes[i].active && lanes[i].prefetching) { + lanes[i].cancel = true; + } + } + unlock(); + io->cancelPrefetch(); +} + +void TileWorker::lockIndex(void* context) { + static_cast(context)->lock(); +} + +void TileWorker::unlockIndex(void* context) { + static_cast(context)->unlock(); +} + +void TileWorker::saveCacheIndex() { + lock(); + const bool dirty = cacheIndex.isDirty(); + if (dirty) { + cacheIndex.save(indexBytes); + cacheIndex.setClean(); + } + writesSinceSave = 0; + unlock(); + if (dirty) { + IndexFileJob job { root, styleId, nullptr, &indexBytes }; + io->run(IoPriority::Background, writeIndexJob, &job); + } +} + +bool TileWorker::tidyCache() { + lock(); + TileKey tile {}; + const bool evict = sdCache && cacheIndex.nextEviction(tile); + if (evict) { + cacheIndex.erase(tile); + } + const bool walk = !evict && !walking && !cacheWalker.isComplete(); + if (walk) { + walking = true; + } + unlock(); + if (evict) { + EvictJob job { root, styleId, tile }; + io->run(IoPriority::Background, evictJob, &job); + return true; + } + if (walk) { + const CacheWalker::IndexLock indexLock { lockIndex, unlockIndex, this }; + WalkJob job { &cacheWalker, &cacheIndex, &indexLock, false }; + io->run(IoPriority::Background, walkJob, &job); + lock(); + walking = false; + unlock(); + return true; + } + return false; +} + +bool TileWorker::claimNext(Lane& lane, bool visible, TileKey& tile, uint16_t*& pixels) { + lock(); + bool found = false; + const int first = visible ? 0 : visibleWanted; + const int last = visible ? visibleWanted : wantedCount; + for (int i = first; i < last && !found; i++) { + const TileKey& candidate = wanted[i]; + if (tileCache.contains(candidate)) { + continue; + } + bool skip = false; + for (int j = 0; j < missingCount && !skip; j++) { + skip = missing[j] == candidate; + } + for (int j = 0; j < laneCount && !skip; j++) { + skip = j != lane.index && lanes[j].active && lanes[j].current == candidate; + } + if (skip) { + continue; + } + pixels = tileCache.reserve(candidate); + if (pixels != nullptr) { + tile = candidate; + lane.current = candidate; + lane.active = true; + found = true; + } + } + unlock(); + return found; +} + +bool TileWorker::writeOne(Lane& lane) { + if (!sdCache) { + return false; + } + lock(); + TileKey tile {}; + int sourceZoom = 0; + const uint16_t* pixels = nullptr; + while (pendingWriteCount > 0 && pixels == nullptr) { + pendingWriteCount--; + tile = pendingWrites[pendingWriteCount]; + sourceZoom = pendingSourceZoom[pendingWriteCount]; + pixels = tileCache.peek(tile); + } + if (pixels != nullptr) { + memcpy(lane.writeBuffer, pixels, TileCache::TILE_BYTES); + } + unlock(); + if (pixels == nullptr) { + return false; + } + const uint32_t start = nowMs(); + // Header: magic, style version with the source zoom level on top, and the number of coded 16-bit words + auto* bytes = reinterpret_cast(lane.encodeBuffer); + const size_t words = encodeTile(lane.writeBuffer, TILE_PIXELS, reinterpret_cast(bytes + CACHE_HEADER_BYTES), ENCODE_CAPACITY); + if (words == 0) { + return true; + } + const uint32_t version = STYLE_VERSION | (static_cast(sourceZoom) << 24); + const auto wordCount = static_cast(words); + memcpy(bytes, CACHE_MAGIC, 4); + memcpy(bytes + 4, &version, 4); + memcpy(bytes + 8, &wordCount, 4); + WriteCacheJob job { root, styleId, tile, bytes, CACHE_HEADER_BYTES + words * 2 }; + if (io->run(IoPriority::Background, writeCacheJob, &job)) { + lock(); + cacheIndex.put(tile, static_cast(job.size)); + const bool save = ++writesSinceSave >= SAVE_INDEX_EVERY_WRITES; + unlock(); + logInfo("[%d] Tile %d/%d/%d written to the SD cache in %u ms", lane.index, tile.z, tile.x, tile.y, nowMs() - start); + if (save) { + saveCacheIndex(); + } + } else { + // It gave way to more urgent card work, or the card failed: it is written later or never + lock(); + if (pendingWriteCount < MAX_PENDING_WRITES && tileCache.peek(tile) != nullptr) { + pendingSourceZoom[pendingWriteCount] = sourceZoom; + pendingWrites[pendingWriteCount++] = tile; + } + unlock(); + } + return true; +} + +void TileWorker::finish(Lane& lane, const TileKey& tile, bool success, int sourceZoom) { + lock(); + if (success) { + tileCache.commit(tile, sourceZoom); + } else { + tileCache.release(tile); + missing[missingNext] = tile; + missingNext = (missingNext + 1) % 64; + if (missingCount < 64) { + missingCount++; + } + } + lane.active = false; + unlock(); + changed = true; +} + +bool TileWorker::loadJob(IoTask& io, void* context) { + auto* job = static_cast(context); + TileWorker* worker = job->worker; + FileBuffer& buffer = job->lane->fileBuffer; + job->found = Found::Nothing; + job->yielded = false; + const bool hasSource = worker->store->find(job->tile, job->source); + char path[PATH_MAX_LENGTH]; + // Only tiles the index knows are read, a missing file costs a folder lookup on the card + worker->lock(); + const bool indexed = job->tryCache && worker->sdCache && (worker->cacheIndex.contains(job->tile) || !worker->cacheIndex.isComplete()); + worker->unlock(); + if (indexed && cachePath(path, sizeof(path), worker->root, worker->styleId, job->tile) && io.readFile(path, buffer)) { + // A tile drawn magnified from a lower zoom level is drawn again once its own data is on the card + int sourceZoom = 0; + if (checkCacheFile(buffer.bytes(), buffer.size(), sourceZoom) > 0 && (!hasSource || job->source.tile.z == sourceZoom)) { + job->cachedSourceZoom = sourceZoom; + job->found = Found::Cached; + worker->lock(); + worker->cacheIndex.touch(job->tile); + worker->unlock(); + return true; + } + } + if (io.yielded()) { + job->yielded = true; + return false; + } + if (!hasSource) { + return false; + } + PreparedHeader header {}; + if (!job->skipPrepared && preparedPath(path, sizeof(path), worker->root, worker->store->regionName(job->source.region), job->source.tile) && + io.readFile(path, buffer) && readPreparedHeader(buffer.bytes(), buffer.size(), header) && + isPreparedCurrent(header, job->source.tile, job->source.size)) { + job->found = Found::Prepared; + return true; + } + if (!io.yielded() && io.readFile(job->source.path, buffer)) { + job->found = Found::Original; + return true; + } + job->yielded = io.yielded(); + return false; +} + +bool TileWorker::produce(Lane& lane, const TileKey& tile, uint16_t* pixels, bool visible) { + const uint32_t start = nowMs(); + const IoPriority priority = visible ? IoPriority::Visible : IoPriority::Prefetch; + LoadJob job {}; + job.worker = this; + job.lane = &lane; + job.tile = tile; + job.tryCache = true; + io->run(priority, loadJob, &job); + if (job.yielded) { + return false; + } + const uint32_t loaded = nowMs(); + // A tile on screen without data of its own zoom level is a gap that browsing can fill + if (visible) { + const int bestZoom = tile.z < MAX_SOURCE_ZOOM ? tile.z : MAX_SOURCE_ZOOM; + int haveZoom = -1; + if (job.found == Found::Cached) { + haveZoom = job.cachedSourceZoom; + } else if (job.found != Found::Nothing) { + haveZoom = job.source.tile.z; + } + if (haveZoom < bestZoom) { + lock(); + addGap(ancestorOf(tile, bestZoom)); + unlock(); + } + } + if (job.found == Found::Cached) { + int sourceZoom = 0; + const uint32_t words = checkCacheFile(lane.fileBuffer.bytes(), lane.fileBuffer.size(), sourceZoom); + const bool decoded = decodeTile(reinterpret_cast(lane.fileBuffer.bytes() + CACHE_HEADER_BYTES), words, pixels, TILE_PIXELS); + finish(lane, tile, decoded, sourceZoom); + if (decoded) { + logInfo("[%d] Tile %d/%d/%d from SD cache: read %u ms, decode %u ms", lane.index, tile.z, tile.x, tile.y, loaded - start, nowMs() - loaded); + } + return true; + } + RenderStats stats {}; + bool rendered = false; + if (job.found == Found::Prepared) { + rendered = lane.renderer.renderPrepared(lane.fileBuffer.bytes(), lane.fileBuffer.size(), tile, *palette, pixels, &stats); + if (lane.renderer.wasCancelled()) { + return false; + } + if (!rendered) { + // A damaged prepared tile: the original is drawn and the copy made again + job.tryCache = false; + job.skipPrepared = true; + io->run(priority, loadJob, &job); + if (job.yielded) { + return false; + } + } + } + if (job.found == Found::Original) { + MemoryInput input(lane.fileBuffer.bytes(), lane.fileBuffer.size()); + rendered = lane.renderer.render(&input, job.source.tile, tile, *palette, pixels, &stats); + if (lane.renderer.wasCancelled()) { + return false; + } + // No current prepared copy: converted in idle time + lock(); + queueConversion(job.source.tile, job.source.region); + unlock(); + } + if (rendered) { + lock(); + if (pendingWriteCount < MAX_PENDING_WRITES) { + pendingSourceZoom[pendingWriteCount] = job.source.tile.z; + pendingWrites[pendingWriteCount++] = tile; + } + unlock(); + } + finish(lane, tile, rendered, job.source.tile.z); + if (rendered) { + logInfo("[%d] Tile %d/%d/%d from z%d %s: read %u ms, render %u ms (%s %u, geometry %u, fill %u, %u features)", lane.index, tile.z, + tile.x, tile.y, job.source.tile.z, job.found == Found::Prepared ? ".prt" : ".pbf", loaded - start, stats.totalMs, + job.found == Found::Prepared ? "inflate" : "scan", stats.scanMs, stats.geometryMs, stats.fillMs, stats.features); + } + return true; +} + +void TileWorker::loop(Lane& lane) { + while (!stopRequested) { + TileKey tile {}; + uint16_t* pixels = nullptr; + // Tiles on screen first, then the SD card cache, then the tiles around the screen, then conversions + bool visible = true; + if (!claimNext(lane, true, tile, pixels)) { + if (writeOne(lane)) { + continue; + } + visible = false; + // Around the screen and other idle work only after a while without input + if (!idle || !claimNext(lane, false, tile, pixels)) { + if (!idle || (!convertOne(lane) && !tidyCache())) { + vTaskDelay(pdMS_TO_TICKS(IDLE_WAIT_MS)); + } + continue; + } + } + lock(); + lane.prefetching = !visible; + lane.cancel = false; + unlock(); + busyLanes++; + if (!produce(lane, tile, pixels, visible)) { + // Gave way to more urgent card work: the slot is freed and the tile claimed again when it is still wanted + lock(); + tileCache.release(tile); + lane.active = false; + unlock(); + } + busyLanes--; + } +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/app/TileWorker.h b/Apps/Maps/main/Source/app/TileWorker.h new file mode 100644 index 0000000..2f7bdfb --- /dev/null +++ b/Apps/Maps/main/Source/app/TileWorker.h @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "IoTask.h" + +#include "../mvt/ByteInput.h" +#include "../render/CacheIndex.h" +#include "../render/CacheWalker.h" +#include "../render/PreparedScanner.h" +#include "../render/PreparedTile.h" +#include "../render/Style.h" +#include "../render/TileCache.h" +#include "../render/TileRenderer.h" +#include "../tiles/TileStore.h" + +#include +#include +#include + +#include +#include + +namespace maps { + +/** + * Fills the tile cache with one render task per core. The lanes never touch the SD card: they hand jobs to the + * I/O task, which reads whole files into the lane's buffer, so one lane renders while the card serves the other. + * The view hands over the tiles it wants, nearest first. A tile comes from the rendered tile cache on the SD + * card when it is there and that cache is switched on, otherwise it is rendered from its prepared copy (.prt), + * or from the original vector tile while there is no current prepared copy, and written to the SD card cache. + * The SD card cache is kept per style and palette under cache/, below a size cap, deleting the least + * recently used tiles first. It never touches maps/. + * Tiles around the screen are only rendered after a second without input (idle), and any input cancels them, + * so they never delay a tile on screen. In idle time the lanes also convert originals into prepared tiles, + * the ones just drawn from an original first, then every tile on the card found by a PreparedScanner. + */ +class TileWorker { +public: + static constexpr int MAX_WANTED = 32; + static constexpr int LANES = 2; + + bool start(TileStore* store, IoTask* io, const Palette* palette, int cacheSlots, const char* cacheRoot); + void stop(); + + /** Replaces the wanted tiles. The first visibleCount are on screen, the rest are fetched ahead. */ + void want(const TileKey* tiles, int count, int visibleCount); + + /** The cache is shared with the render tasks, so every access happens between lock() and unlock(). */ + void lock() { xSemaphoreTake(mutex, portMAX_DELAY); } + void unlock() { xSemaphoreGive(mutex); } + TileCache& cache() { return tileCache; } + + /** True once since the last call when a tile was added to the cache. */ + bool takeChanged() { return changed.exchange(false); } + bool isBusy() const { return busyLanes.load() > 0; } + TaskHandle_t laneTask(int lane) const { return lane < laneCount ? lanes[lane].task : nullptr; } + /** The fewest bytes of stack a render lane had left so far. */ + uint32_t stackLeft(int lane) const { + return lane < laneCount && lanes[lane].task != nullptr ? static_cast(uxTaskGetStackHighWaterMark(lanes[lane].task)) : 0; + } + /** Whether a tile is cached or known to have no data. Call between lock() and unlock(). */ + bool isSettled(const TileKey& tile) const; + /** New vector tiles are on the card: tiles without data and tiles drawn from lower zoom levels are drawn again. */ + void tilesArrived(); + /** Whether new prepared tiles are written deflated. */ + void setDeflatePrepared(bool deflate) { deflatePrepared = deflate; } + /** Whether rendered tiles are read from and written to the SD card cache. */ + void setSdCache(bool enabled) { sdCache = enabled; } + /** Idle: no input for a while, so prefetch, conversions and cache tidying may run. */ + void setIdle(bool value) { idle = value; } + /** Input arrived: prefetch reads and renders stop at once, and idle work waits. */ + void cancelPrefetch(); + /** + * Tiles on screen that were drawn from a lower zoom level or not at all, as the tile of the zoom level that + * would draw them in full detail (at most z14). Returns how many were written to out, each one once. + */ + int takeGaps(TileKey* out, int max); + +private: + struct Lane { + TileWorker* worker; + int index; + TileRenderer renderer; + FileBuffer fileBuffer; + TaskHandle_t task; + TileKey current; + bool active; + uint16_t* writeBuffer; + uint16_t* encodeBuffer; + TileConverter converter; + ByteWriter converted; + bool converting; + TileKey convertingTile; + bool prefetching; + std::atomic cancel; + }; + + struct Conversion { + TileKey tile; + int region; + }; + + static constexpr int MAX_CONVERSIONS = 32; + + struct ScanResult { + Conversion items[MAX_CONVERSIONS]; + int count; + int room; + }; + + /** What the I/O task found for a tile. */ + enum class Found : uint8_t { + Nothing, + Cached, + Prepared, + Original + }; + + struct LoadJob { + TileWorker* worker; + Lane* lane; + TileKey tile; + bool tryCache; + bool skipPrepared; + Found found; + TileFile source; + int cachedSourceZoom; + bool yielded; + }; + + static void taskMain(void* context); + void loop(Lane& lane); + /** Claims the next wanted tile that is on screen, or around the screen when visible is false. */ + bool claimNext(Lane& lane, bool visible, TileKey& tile, uint16_t*& pixels); + /** Reads what a tile is drawn from: its SD cache file, its prepared copy or its original. */ + static bool loadJob(IoTask& io, void* context); + /** Produces a claimed tile. Returns false when it yielded to more urgent card work and should be tried again. */ + bool produce(Lane& lane, const TileKey& tile, uint16_t* pixels, bool visible); + void finish(Lane& lane, const TileKey& tile, bool success, int sourceZoom); + /** Writes one rendered tile to the SD card cache, only while no tile is waiting to be drawn. */ + bool writeOne(Lane& lane); + /** Call between lock() and unlock(). */ + void addGap(const TileKey& tile); + /** Adds a tile to the conversion queue unless it is there already. Call between lock() and unlock(). */ + bool queueConversion(const TileKey& tile, int region); + static bool collectFromScan(const TileKey& tile, int region, void* context); + /** Converts one queued tile, or looks at one more folder for tiles to convert. Returns false when there is nothing to do. */ + bool convertOne(Lane& lane); + /** Deletes the least recently used SD cache tile while the cache is over its cap, or tidies one cache folder. */ + bool tidyCache(); + void saveCacheIndex(); + static void lockIndex(void* context); + static void unlockIndex(void* context); + + TileStore* store = nullptr; + IoTask* io = nullptr; + const Palette* palette = nullptr; + Lane lanes[LANES] {}; + int laneCount = 0; + TileCache tileCache; + char root[PATH_MAX_LENGTH] = ""; + SemaphoreHandle_t mutex = nullptr; + SemaphoreHandle_t stopped = nullptr; + std::atomic stopRequested = false; + std::atomic changed = false; + std::atomic busyLanes = 0; + TileKey wanted[MAX_WANTED] {}; + int wantedCount = 0; + int visibleWanted = 0; + TileKey missing[64] {}; + int missingCount = 0; + int missingNext = 0; + static constexpr int MAX_PENDING_WRITES = 32; + TileKey pendingWrites[MAX_PENDING_WRITES] {}; + int pendingSourceZoom[MAX_PENDING_WRITES] {}; + int pendingWriteCount = 0; + static constexpr int MAX_GAPS = 16; + TileKey gaps[MAX_GAPS] {}; + int gapCount = 0; + Conversion conversions[MAX_CONVERSIONS] {}; + int conversionCount = 0; + PreparedScanner scanner; + ScanResult scanResult {}; + bool scanning = false; + std::atomic rescanRequested = false; + std::atomic deflatePrepared = true; + std::atomic sdCache = true; + std::atomic idle = false; + char styleId[8] = ""; + CacheIndex cacheIndex; + CacheWalker cacheWalker; + bool walking = false; + int writesSinceSave = 0; + ByteWriter indexBytes; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/mvt/ByteInput.cpp b/Apps/Maps/main/Source/mvt/ByteInput.cpp new file mode 100644 index 0000000..08225fd --- /dev/null +++ b/Apps/Maps/main/Source/mvt/ByteInput.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "ByteInput.h" + +#include "../platform/Platform.h" + +namespace maps { + +FileBuffer::~FileBuffer() { + largeFree(data); +} + +bool FileBuffer::reserve(size_t size) { + if (size <= capacity) { + return true; + } + largeFree(data); + length = 0; + capacity = (size + 0xFFFF) & ~static_cast(0xFFFF); + data = static_cast(largeAlloc(capacity)); + if (data == nullptr) { + capacity = 0; + return false; + } + return true; +} + +bool FileBuffer::load(const char* path) { + length = 0; + FILE* file = fopen(path, "rb"); + if (file == nullptr) { + return false; + } + bool ok = fseek(file, 0, SEEK_END) == 0; + const long fileSize = ok ? ftell(file) : -1; + ok = ok && fileSize > 0 && fseek(file, 0, SEEK_SET) == 0; + ok = ok && reserve(static_cast(fileSize)); + if (ok) { + length = fread(data, 1, static_cast(fileSize), file); + ok = length == static_cast(fileSize); + } + fclose(file); + if (!ok) { + length = 0; + } + return ok; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/mvt/ByteInput.h b/Apps/Maps/main/Source/mvt/ByteInput.h new file mode 100644 index 0000000..1379892 --- /dev/null +++ b/Apps/Maps/main/Source/mvt/ByteInput.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include +#include +#include + +namespace maps { + +/** A sequential byte source that can start over from the beginning. */ +class ByteInput { +public: + virtual ~ByteInput() = default; + /** Returns the number of bytes read, 0 at the end and a negative value on error. */ + virtual int read(uint8_t* buffer, size_t size) = 0; + virtual bool rewind() = 0; +}; + +class FileInput final : public ByteInput { + FILE* file = nullptr; + +public: + ~FileInput() override { close(); } + + bool open(const char* path) { + close(); + file = fopen(path, "rb"); + return file != nullptr; + } + + void close() { + if (file != nullptr) { + fclose(file); + file = nullptr; + } + } + + int read(uint8_t* buffer, size_t size) override { + if (file == nullptr) { + return -1; + } + const size_t count = fread(buffer, 1, size, file); + if (count == 0 && ferror(file)) { + return -1; + } + return static_cast(count); + } + + bool rewind() override { return file != nullptr && fseek(file, 0, SEEK_SET) == 0; } +}; + +class MemoryInput final : public ByteInput { + const uint8_t* data = nullptr; + size_t size = 0; + size_t position = 0; + +public: + MemoryInput() = default; + MemoryInput(const uint8_t* data, size_t size) : data(data), size(size) {} + + void reset(const uint8_t* newData, size_t newSize) { + data = newData; + size = newSize; + position = 0; + } + + int read(uint8_t* buffer, size_t count) override { + const size_t left = size - position; + const size_t n = count < left ? count : left; + memcpy(buffer, data + position, n); + position += n; + return static_cast(n); + } + + bool rewind() override { + position = 0; + return true; + } +}; + +/** A whole file in one buffer that grows as needed, so a tile is read from the card only once. */ +class FileBuffer { + uint8_t* data = nullptr; + size_t capacity = 0; + size_t length = 0; + +public: + FileBuffer() = default; + ~FileBuffer(); + FileBuffer(const FileBuffer&) = delete; + FileBuffer& operator=(const FileBuffer&) = delete; + + bool load(const char* path); + const uint8_t* bytes() const { return data; } + size_t size() const { return length; } + + /** For readers that fill the buffer themselves: room for size bytes, then setSize() with what was read. */ + bool reserve(size_t size); + uint8_t* writable() { return data; } + void setSize(size_t size) { length = size <= capacity ? size : 0; } + void clear() { length = 0; } +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/mvt/InflateReader.cpp b/Apps/Maps/main/Source/mvt/InflateReader.cpp new file mode 100644 index 0000000..f3d153f --- /dev/null +++ b/Apps/Maps/main/Source/mvt/InflateReader.cpp @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "InflateReader.h" + +#include "../platform/Platform.h" + +#include +#include +#include + +namespace maps { + +namespace { + +constexpr size_t WINDOW_SIZE = TINFL_LZ_DICT_SIZE; +constexpr size_t INPUT_BUFFER_SIZE = 4096; + +tinfl_decompressor* asDecompressor(void* pointer) { + return static_cast(pointer); +} + +} // namespace + +InflateReader::~InflateReader() { + largeFree(decompressor); + largeFree(window); + largeFree(inputBuffer); +} + +bool InflateReader::init() { + if (decompressor == nullptr) { + decompressor = fastAlloc(sizeof(tinfl_decompressor)); + } + if (window == nullptr) { + window = static_cast(largeAlloc(WINDOW_SIZE)); + } + if (inputBuffer == nullptr) { + inputBuffer = static_cast(fastAlloc(INPUT_BUFFER_SIZE)); + } + return decompressor != nullptr && window != nullptr && inputBuffer != nullptr; +} + +bool InflateReader::begin(ByteInput* source) { + input = source; + inputPosition = 0; + inputSize = 0; + inputEnded = false; + windowOffset = 0; + pending = nullptr; + pendingEnd = nullptr; + position = 0; + finished = false; + error = false; + if (decompressor == nullptr || !input->rewind() || !fillInput()) { + error = true; + return false; + } + tinfl_init(asDecompressor(decompressor)); + if (inputSize >= 2 && inputBuffer[0] == 0x1F && inputBuffer[1] == 0x8B) { + streamFormat = Format::Gzip; + if (!skipGzipHeader()) { + error = true; + return false; + } + } else if (inputSize >= 2 && (inputBuffer[0] & 0x0F) == 8 && ((inputBuffer[0] << 8) | inputBuffer[1]) % 31 == 0) { + streamFormat = Format::Zlib; + } else { + streamFormat = Format::Raw; + } + return true; +} + +bool InflateReader::fillInput() { + if (inputEnded) { + return false; + } + if (inputPosition < inputSize) { + memmove(inputBuffer, inputBuffer + inputPosition, inputSize - inputPosition); + } + inputSize -= inputPosition; + inputPosition = 0; + const int count = input->read(inputBuffer + inputSize, INPUT_BUFFER_SIZE - inputSize); + if (count < 0) { + error = true; + inputEnded = true; + return false; + } + if (count == 0) { + inputEnded = true; + return inputSize > 0; + } + inputSize += static_cast(count); + return true; +} + +int InflateReader::getInputByte() { + if (inputPosition >= inputSize && !fillInput()) { + return -1; + } + return inputBuffer[inputPosition++]; +} + +bool InflateReader::skipGzipHeader() { + uint8_t header[10]; + for (unsigned char& byte : header) { + const int value = getInputByte(); + if (value < 0) { + return false; + } + byte = static_cast(value); + } + if (header[2] != 8) { + return false; + } + const uint8_t flags = header[3]; + if (flags & 0x04) { + const int low = getInputByte(); + const int high = getInputByte(); + if (low < 0 || high < 0) { + return false; + } + for (int i = 0; i < (low | (high << 8)); i++) { + if (getInputByte() < 0) { + return false; + } + } + } + for (const uint8_t flag : { static_cast(0x08), static_cast(0x10) }) { + if (flags & flag) { + int value; + do { + value = getInputByte(); + } while (value > 0); + if (value < 0) { + return false; + } + } + } + if (flags & 0x02) { + getInputByte(); + getInputByte(); + } + return true; +} + +bool InflateReader::refill() { + if (finished || error) { + return false; + } + if (streamFormat == Format::Raw) { + if (inputPosition >= inputSize) { + inputSize = 0; + inputPosition = 0; + if (!fillInput()) { + finished = true; + return false; + } + } + pending = inputBuffer + inputPosition; + pendingEnd = inputBuffer + inputSize; + inputPosition = inputSize; + return true; + } + + while (true) { + if (inputPosition >= inputSize && !inputEnded) { + fillInput(); + } + size_t inBytes = inputSize - inputPosition; + size_t outBytes = WINDOW_SIZE - windowOffset; + mz_uint32 flags = inputEnded ? 0 : TINFL_FLAG_HAS_MORE_INPUT; + if (streamFormat == Format::Zlib) { + flags |= TINFL_FLAG_PARSE_ZLIB_HEADER; + } + const tinfl_status status = tinfl_decompress( + asDecompressor(decompressor), + inputBuffer + inputPosition, + &inBytes, + window, + window + windowOffset, + &outBytes, + flags + ); + inputPosition += inBytes; + if (outBytes > 0) { + pending = window + windowOffset; + pendingEnd = pending + outBytes; + windowOffset = (windowOffset + outBytes) & (WINDOW_SIZE - 1); + if (status == TINFL_STATUS_DONE) { + finished = true; + } + return true; + } + if (status == TINFL_STATUS_DONE) { + finished = true; + return false; + } + if (status < TINFL_STATUS_DONE) { + error = true; + return false; + } + if (status == TINFL_STATUS_NEEDS_MORE_INPUT && inputEnded) { + error = true; + return false; + } + } +} + +size_t InflateReader::read(uint8_t* buffer, size_t size) { + size_t done = 0; + while (done < size) { + if (pending == pendingEnd && !refill()) { + break; + } + size_t count = static_cast(pendingEnd - pending); + if (count > size - done) { + count = size - done; + } + memcpy(buffer + done, pending, count); + pending += count; + done += count; + } + position += done; + return done; +} + +bool InflateReader::skip(size_t count) { + while (count > 0) { + if (pending == pendingEnd && !refill()) { + return false; + } + size_t available = static_cast(pendingEnd - pending); + if (available > count) { + available = count; + } + pending += available; + position += available; + count -= available; + } + return true; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/mvt/InflateReader.h b/Apps/Maps/main/Source/mvt/InflateReader.h new file mode 100644 index 0000000..4306262 --- /dev/null +++ b/Apps/Maps/main/Source/mvt/InflateReader.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ByteInput.h" + +#include +#include + +namespace maps { + +/** + * Reads a gzip, zlib or uncompressed stream byte by byte, inflating with tinfl into a 32 KB ring window. + * The format is detected from the first two bytes. Memory is allocated once in init(). + */ +class InflateReader { +public: + enum class Format : uint8_t { + Raw, + Gzip, + Zlib + }; + + InflateReader() = default; + ~InflateReader(); + InflateReader(const InflateReader&) = delete; + InflateReader& operator=(const InflateReader&) = delete; + + bool init(); + /** Starts reading input from its beginning. */ + bool begin(ByteInput* input); + + /** Returns the next byte, or -1 at the end of the stream or on error. */ + inline int get() { + if (pending == pendingEnd && !refill()) { + return -1; + } + position++; + return *pending++; + } + + size_t read(uint8_t* buffer, size_t size); + bool skip(size_t count); + + /** Bytes of output consumed since begin(). */ + size_t tell() const { return position; } + bool failed() const { return error; } + Format format() const { return streamFormat; } + +private: + bool refill(); + bool fillInput(); + bool skipGzipHeader(); + int getInputByte(); + + ByteInput* input = nullptr; + void* decompressor = nullptr; + uint8_t* window = nullptr; + uint8_t* inputBuffer = nullptr; + size_t inputPosition = 0; + size_t inputSize = 0; + bool inputEnded = false; + size_t windowOffset = 0; + const uint8_t* pending = nullptr; + const uint8_t* pendingEnd = nullptr; + size_t position = 0; + Format streamFormat = Format::Raw; + bool finished = false; + bool error = false; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/mvt/MvtDecoder.cpp b/Apps/Maps/main/Source/mvt/MvtDecoder.cpp new file mode 100644 index 0000000..4ab73a3 --- /dev/null +++ b/Apps/Maps/main/Source/mvt/MvtDecoder.cpp @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "MvtDecoder.h" + +#include "../platform/Platform.h" + +#include + +namespace maps { + +namespace { + +constexpr uint32_t WIRE_VARINT = 0; +constexpr uint32_t WIRE_64BIT = 1; +constexpr uint32_t WIRE_LENGTH = 2; +constexpr uint32_t WIRE_32BIT = 5; + +constexpr uint16_t INTEGER_FLAG = 0x8000; + +constexpr uint32_t COMMAND_MOVE_TO = 1; +constexpr uint32_t COMMAND_LINE_TO = 2; +constexpr uint32_t COMMAND_CLOSE_PATH = 7; + +constexpr int32_t zigzag(uint32_t value) { + return static_cast((value >> 1) ^ (~(value & 1) + 1)); +} + +uint16_t integerCode(int64_t value) { + if (value < 0) { + return 0; + } + return static_cast(INTEGER_FLAG | (value > 0x7FFF ? 0x7FFF : value)); +} + +} // namespace + +MvtDecoder::~MvtDecoder() { + largeFree(valuePool); + largeFree(chunk); +} + +bool MvtDecoder::init() { + if (valuePool == nullptr) { + valuePool = static_cast(largeAlloc(VALUE_POOL_SIZE * sizeof(uint16_t))); + } + if (chunk == nullptr) { + chunk = static_cast(largeAlloc(CHUNK_POINTS * sizeof(TilePoint))); + } + return reader.init() && valuePool != nullptr && chunk != nullptr; +} + +bool MvtDecoder::readVarint(uint64_t& value) { + value = 0; + for (int shift = 0; shift < 64; shift += 7) { + const int byte = reader.get(); + if (byte < 0) { + return false; + } + value |= static_cast(byte & 0x7F) << shift; + if ((byte & 0x80) == 0) { + return true; + } + } + return false; +} + +bool MvtDecoder::skipField(uint32_t wireType) { + uint64_t value; + switch (wireType) { + case WIRE_VARINT: + return readVarint(value); + case WIRE_64BIT: + return reader.skip(8); + case WIRE_LENGTH: + return readVarint(value) && reader.skip(static_cast(value)); + case WIRE_32BIT: + return reader.skip(4); + default: + return false; + } +} + +uint16_t MvtDecoder::valueCode(const LayerTable& table, uint32_t index) const { + if (index >= table.valueCount) { + return 0; + } + return valuePool[table.valueOffset + index]; +} + +bool MvtDecoder::readLayerName(size_t layerEnd, Layer& layer) { + layer = Layer::Unknown; + while (reader.tell() < layerEnd) { + uint64_t key; + if (!readVarint(key)) { + return false; + } + if ((key >> 3) == 1 && (key & 7) == WIRE_LENGTH) { + uint64_t length; + if (!readVarint(length)) { + return false; + } + char name[24]; + if (length >= sizeof(name)) { + return reader.skip(static_cast(length)); + } + if (reader.read(reinterpret_cast(name), static_cast(length)) != length) { + return false; + } + layer = layerFromName(name, static_cast(length)); + return true; + } + if (!skipField(static_cast(key & 7))) { + return false; + } + } + return true; +} + +bool MvtDecoder::scanValue(size_t valueEnd, uint16_t& code) { + code = 0; + while (reader.tell() < valueEnd) { + uint64_t key; + if (!readVarint(key)) { + return false; + } + const uint32_t field = static_cast(key >> 3); + const uint32_t wire = static_cast(key & 7); + uint64_t number; + if (field == 1 && wire == WIRE_LENGTH) { + uint64_t length; + if (!readVarint(length)) { + return false; + } + char text[32]; + if (length < sizeof(text)) { + if (reader.read(reinterpret_cast(text), static_cast(length)) != length) { + return false; + } + code = static_cast(valueFromString(text, static_cast(length))); + } else if (!reader.skip(static_cast(length))) { + return false; + } + } else if ((field == 4 || field == 5 || field == 7) && wire == WIRE_VARINT) { + if (!readVarint(number)) { + return false; + } + code = integerCode(static_cast(number)); + } else if (field == 6 && wire == WIRE_VARINT) { + if (!readVarint(number)) { + return false; + } + code = integerCode(zigzag(static_cast(number))); + } else if (!skipField(wire)) { + return false; + } + } + return true; +} + +bool MvtDecoder::scanLayer(size_t layerEnd, LayerTable& table, Layer layer) { + (void)layer; + uint32_t keyIndex = 0; + table.valueOffset = valuePoolUsed; + table.valueCount = 0; + while (reader.tell() < layerEnd) { + uint64_t key; + if (!readVarint(key)) { + return false; + } + const uint32_t field = static_cast(key >> 3); + const uint32_t wire = static_cast(key & 7); + if (field == 3 && wire == WIRE_LENGTH) { + uint64_t length; + if (!readVarint(length)) { + return false; + } + char name[16]; + if (length < sizeof(name)) { + if (reader.read(reinterpret_cast(name), static_cast(length)) != length) { + return false; + } + name[length] = '\0'; + const auto index = static_cast(keyIndex); + if (strcmp(name, "class") == 0) { + table.classKey = index; + } else if (strcmp(name, "subclass") == 0) { + table.subclassKey = index; + } else if (strcmp(name, "brunnel") == 0) { + table.brunnelKey = index; + } else if (strcmp(name, "admin_level") == 0) { + table.adminLevelKey = index; + } else if (strcmp(name, "maritime") == 0) { + table.maritimeKey = index; + } else if (strcmp(name, "ramp") == 0) { + table.rampKey = index; + } + } else if (!reader.skip(static_cast(length))) { + return false; + } + keyIndex++; + } else if (field == 4 && wire == WIRE_LENGTH) { + uint64_t length; + if (!readVarint(length)) { + return false; + } + uint16_t code; + if (!scanValue(reader.tell() + static_cast(length), code)) { + return false; + } + if (valuePoolUsed < VALUE_POOL_SIZE) { + valuePool[valuePoolUsed++] = code; + table.valueCount++; + } + } else if (field == 5 && wire == WIRE_VARINT) { + uint64_t extent; + if (!readVarint(extent)) { + return false; + } + table.extent = static_cast(extent); + } else if (!skipField(wire)) { + return false; + } + } + return true; +} + +bool MvtDecoder::scan(ByteInput* source) { + input = source; + valuePoolUsed = 0; + totalSize = 0; + for (auto& table : tables) { + table = { -1, -1, -1, -1, -1, -1, -1, 0, 0, 4096 }; + } + if (!reader.begin(input)) { + return false; + } + int8_t layerIndex = 0; + while (true) { + uint64_t key; + if (!readVarint(key)) { + break; + } + if ((key >> 3) == 3 && (key & 7) == WIRE_LENGTH) { + uint64_t length; + if (!readVarint(length)) { + return false; + } + const size_t layerEnd = reader.tell() + static_cast(length); + Layer layer; + if (!readLayerName(layerEnd, layer)) { + return false; + } + if (layer != Layer::Unknown) { + LayerTable& table = tables[static_cast(layer)]; + table.fileIndex = layerIndex; + if (!scanLayer(layerEnd, table, layer)) { + return false; + } + } + if (reader.tell() < layerEnd && !reader.skip(layerEnd - reader.tell())) { + return false; + } + layerIndex++; + } else if (!skipField(static_cast(key & 7))) { + return false; + } + } + totalSize = reader.tell(); + return !reader.failed(); +} + +void MvtDecoder::applyTag(const LayerTable& table, uint32_t key, uint32_t value, FeatureInfo& info) const { + const auto keyIndex = static_cast(key); + const uint16_t code = valueCode(table, value); + const bool isInteger = (code & INTEGER_FLAG) != 0; + if (keyIndex == table.classKey && !isInteger) { + info.featureClass = static_cast(code); + } else if (keyIndex == table.subclassKey && !isInteger) { + info.subclass = static_cast(code); + } else if (keyIndex == table.brunnelKey && !isInteger) { + info.brunnel = static_cast(code); + } else if (keyIndex == table.adminLevelKey && isInteger) { + info.adminLevel = static_cast(code & 0x7F); + } else if (keyIndex == table.maritimeKey && isInteger) { + info.maritime = (code & 0x7FFF) != 0; + } else if (keyIndex == table.rampKey && isInteger) { + info.ramp = (code & 0x7FFF) != 0; + } +} + +bool MvtDecoder::decodeGeometry(size_t geometryEnd, GeometrySink& sink) { + int32_t x = 0; + int32_t y = 0; + int count = 0; + bool ringOpen = false; + while (reader.tell() < geometryEnd) { + uint64_t commandInteger; + if (!readVarint(commandInteger)) { + return false; + } + const uint32_t command = static_cast(commandInteger & 7); + const uint32_t repeat = static_cast(commandInteger >> 3); + if (command == COMMAND_MOVE_TO || command == COMMAND_LINE_TO) { + for (uint32_t i = 0; i < repeat; i++) { + uint64_t dx; + uint64_t dy; + if (!readVarint(dx) || !readVarint(dy)) { + return false; + } + x += zigzag(static_cast(dx)); + y += zigzag(static_cast(dy)); + if (command == COMMAND_MOVE_TO) { + if (ringOpen) { + sink.points(chunk, count); + sink.endRing(false); + } + sink.beginRing(); + ringOpen = true; + count = 0; + } else if (count == CHUNK_POINTS) { + sink.points(chunk, count); + chunk[0] = chunk[count - 1]; + count = 1; + } + chunk[count++] = { x, y }; + } + } else if (command == COMMAND_CLOSE_PATH) { + if (ringOpen) { + sink.points(chunk, count); + sink.endRing(true); + ringOpen = false; + count = 0; + } + } else { + return false; + } + } + if (ringOpen) { + sink.points(chunk, count); + sink.endRing(false); + } + return true; +} + +bool MvtDecoder::decodeFeature(size_t featureEnd, Layer layer, GeometrySink& sink) { + const LayerTable& table = tables[static_cast(layer)]; + FeatureInfo info; + info.layer = layer; + while (reader.tell() < featureEnd) { + uint64_t key; + if (!readVarint(key)) { + return false; + } + const uint32_t field = static_cast(key >> 3); + const uint32_t wire = static_cast(key & 7); + if (field == 2 && wire == WIRE_LENGTH) { + uint64_t length; + if (!readVarint(length)) { + return false; + } + const size_t tagsEnd = reader.tell() + static_cast(length); + while (reader.tell() < tagsEnd) { + uint64_t tagKey; + uint64_t tagValue; + if (!readVarint(tagKey) || !readVarint(tagValue)) { + return false; + } + applyTag(table, static_cast(tagKey), static_cast(tagValue), info); + } + } else if (field == 3 && wire == WIRE_VARINT) { + uint64_t type; + if (!readVarint(type)) { + return false; + } + info.geometryType = static_cast(type); + } else if (field == 4 && wire == WIRE_LENGTH) { + uint64_t length; + if (!readVarint(length)) { + return false; + } + const size_t geometryEnd = reader.tell() + static_cast(length); + if (sink.beginFeature(info)) { + if (!decodeGeometry(geometryEnd, sink)) { + return false; + } + sink.endFeature(); + } else if (!reader.skip(static_cast(length))) { + return false; + } + } else if (!skipField(wire)) { + return false; + } + } + return true; +} + +bool MvtDecoder::inflate(ByteInput* source, uint8_t* out, size_t size) { + if (!reader.begin(source)) { + return false; + } + if (reader.read(out, size) != size || reader.failed()) { + return false; + } + // Nothing may follow + return reader.get() < 0 && !reader.failed(); +} + +bool MvtDecoder::sweep(const Layer* layers, int count, GeometrySink& sink) { + int remaining = 0; + bool wanted[static_cast(Layer::Count)] = {}; + for (int i = 0; i < count; i++) { + const int index = static_cast(layers[i]); + if (tables[index].fileIndex >= 0 && !wanted[index]) { + wanted[index] = true; + remaining++; + } + } + if (remaining == 0) { + return true; + } + if (!reader.begin(input)) { + return false; + } + while (remaining > 0) { + uint64_t key; + if (!readVarint(key)) { + break; + } + if ((key >> 3) != 3 || (key & 7) != WIRE_LENGTH) { + if (!skipField(static_cast(key & 7))) { + return false; + } + continue; + } + uint64_t length; + if (!readVarint(length)) { + return false; + } + const size_t layerEnd = reader.tell() + static_cast(length); + Layer layer; + if (!readLayerName(layerEnd, layer)) { + return false; + } + if (layer != Layer::Unknown && wanted[static_cast(layer)]) { + while (reader.tell() < layerEnd) { + uint64_t layerKey; + if (!readVarint(layerKey)) { + return false; + } + if ((layerKey >> 3) == 2 && (layerKey & 7) == WIRE_LENGTH) { + if (sink.shouldStop()) { + return false; + } + uint64_t featureLength; + if (!readVarint(featureLength)) { + return false; + } + if (!decodeFeature(reader.tell() + static_cast(featureLength), layer, sink)) { + return false; + } + } else if (!skipField(static_cast(layerKey & 7))) { + return false; + } + } + remaining--; + } else if (reader.tell() < layerEnd && !reader.skip(layerEnd - reader.tell())) { + return false; + } + } + return !reader.failed(); +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/mvt/MvtDecoder.h b/Apps/Maps/main/Source/mvt/MvtDecoder.h new file mode 100644 index 0000000..902457e --- /dev/null +++ b/Apps/Maps/main/Source/mvt/MvtDecoder.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ByteInput.h" +#include "InflateReader.h" +#include "Schema.h" + +#include + +namespace maps { + +struct TilePoint { + int32_t x; + int32_t y; +}; + +/** + * Receives decoded features. Each ring or line arrives in chunks of points between beginRing() and endRing(), + * so no complete geometry is ever buffered. + */ +class GeometrySink { +public: + virtual ~GeometrySink() = default; + /** Returns false to skip the geometry of this feature. */ + virtual bool beginFeature(const FeatureInfo& info) = 0; + virtual void beginRing() = 0; + virtual void points(const TilePoint* points, int count) = 0; + virtual void endRing(bool closed) = 0; + virtual void endFeature() = 0; + /** Checked before each feature: true ends the sweep early, which then fails. */ + virtual bool shouldStop() { return false; } +}; + +/** + * Streaming Mapbox Vector Tile (2.1) decoder. scan() reads the key and value tables of the wanted layers, + * which come after the features in each layer, then each sweep() decodes features of some layers in file order. + * Every pass inflates the tile again from the start instead of keeping the decompressed tile in memory. + */ +class MvtDecoder { +public: + static constexpr int CHUNK_POINTS = 256; + static constexpr int VALUE_POOL_SIZE = 8192; + + MvtDecoder() = default; + ~MvtDecoder(); + MvtDecoder(const MvtDecoder&) = delete; + MvtDecoder& operator=(const MvtDecoder&) = delete; + + bool init(); + bool scan(ByteInput* input); + /** Decodes the features of the given layers, in the order they appear in the tile. */ + bool sweep(const Layer* layers, int count, GeometrySink& sink); + + /** Position of a layer in the tile, -1 when the tile does not have it. */ + int fileIndex(Layer layer) const { return tables[static_cast(layer)].fileIndex; } + int extent(Layer layer) const { return tables[static_cast(layer)].extent; } + size_t decompressedSize() const { return totalSize; } + /** Inflates a whole stream into out with the decoder's decompressor. Returns false unless exactly size bytes came out. */ + bool inflate(ByteInput* source, uint8_t* out, size_t size); + InflateReader::Format format() const { return reader.format(); } + +private: + struct LayerTable { + int8_t fileIndex; + int16_t classKey; + int16_t subclassKey; + int16_t brunnelKey; + int16_t adminLevelKey; + int16_t maritimeKey; + int16_t rampKey; + uint32_t valueOffset; + uint32_t valueCount; + int32_t extent; + }; + + bool readVarint(uint64_t& value); + bool readLayerName(size_t layerEnd, Layer& layer); + bool scanLayer(size_t layerEnd, LayerTable& table, Layer layer); + bool scanValue(size_t valueEnd, uint16_t& code); + bool decodeFeature(size_t featureEnd, Layer layer, GeometrySink& sink); + void applyTag(const LayerTable& table, uint32_t key, uint32_t value, FeatureInfo& info) const; + bool decodeGeometry(size_t geometryEnd, GeometrySink& sink); + bool skipField(uint32_t wireType); + uint16_t valueCode(const LayerTable& table, uint32_t index) const; + + InflateReader reader; + ByteInput* input = nullptr; + LayerTable tables[static_cast(Layer::Count)] {}; + uint16_t* valuePool = nullptr; + uint32_t valuePoolUsed = 0; + TilePoint* chunk = nullptr; + size_t totalSize = 0; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/mvt/Schema.cpp b/Apps/Maps/main/Source/mvt/Schema.cpp new file mode 100644 index 0000000..1635665 --- /dev/null +++ b/Apps/Maps/main/Source/mvt/Schema.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Schema.h" + +#include + +namespace maps { + +namespace { + +const char* const VALUE_NAMES[] = { + "", +#define MAPS_VALUE_NAME(name) #name, + MAPS_SCHEMA_VALUES(MAPS_VALUE_NAME) +#undef MAPS_VALUE_NAME +}; + +struct LayerName { + const char* name; + Layer layer; +}; + +constexpr LayerName LAYER_NAMES[] = { + { "aeroway", Layer::Aeroway }, + { "boundary", Layer::Boundary }, + { "building", Layer::Building }, + { "landcover", Layer::Landcover }, + { "landuse", Layer::Landuse }, + { "park", Layer::Park }, + { "transportation", Layer::Transportation }, + { "water", Layer::Water }, + { "waterway", Layer::Waterway }, +}; + +bool equals(const char* text, size_t length, const char* name) { + return strlen(name) == length && memcmp(text, name, length) == 0; +} + +} // namespace + +Layer layerFromName(const char* name, size_t length) { + for (const auto& entry : LAYER_NAMES) { + if (equals(name, length, entry.name)) { + return entry.layer; + } + } + return Layer::Unknown; +} + +Value valueFromString(const char* text, size_t length) { + for (uint16_t i = 1; i < static_cast(Value::Count); i++) { + if (equals(text, length, VALUE_NAMES[i])) { + return static_cast(i); + } + } + return Value::None; +} + +const char* valueName(Value value) { + const auto index = static_cast(value); + return index < static_cast(Value::Count) ? VALUE_NAMES[index] : ""; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/mvt/Schema.h b/Apps/Maps/main/Source/mvt/Schema.h new file mode 100644 index 0000000..149fdcb --- /dev/null +++ b/Apps/Maps/main/Source/mvt/Schema.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +namespace maps { + +/** The OpenMapTiles layers that are drawn. Every other layer is skipped. */ +enum class Layer : uint8_t { + Unknown, + Aeroway, + Boundary, + Building, + Landcover, + Landuse, + Park, + Transportation, + Water, + Waterway, + Count +}; + +#define MAPS_SCHEMA_VALUES(X) \ + X(ocean) X(lake) X(river) X(pond) X(dock) X(swimming_pool) X(stream) X(canal) X(drain) X(ditch) \ + X(farmland) X(ice) X(wood) X(rock) X(grass) X(wetland) X(sand) \ + X(residential) X(suburb) X(quarter) X(neighbourhood) X(commercial) X(industrial) X(retail) X(railway) \ + X(cemetery) X(military) X(hospital) X(school) X(university) X(college) X(kindergarten) X(stadium) \ + X(pitch) X(playground) X(track) X(theme_park) X(zoo) X(garages) X(dam) X(park) X(garden) \ + X(national_park) X(nature_reserve) X(protected_area) \ + X(motorway) X(trunk) X(primary) X(secondary) X(tertiary) X(minor) X(service) X(path) X(raceway) \ + X(busway) X(bus_guideway) X(ferry) X(rail) X(transit) X(cable_car) X(gondola) X(pier) X(bridge) \ + X(tunnel) X(ford) X(aerodrome) X(heliport) X(runway) X(helipad) X(taxiway) X(apron) X(gate) + +/** String values the style needs. Value::None means a string that is not in this list. */ +enum class Value : uint16_t { + None, +#define MAPS_VALUE_ENUM(name) name, + MAPS_SCHEMA_VALUES(MAPS_VALUE_ENUM) +#undef MAPS_VALUE_ENUM + Count +}; + +/** Feature attributes the style needs, resolved from the tags of one feature. */ +struct FeatureInfo { + Layer layer = Layer::Unknown; + uint8_t geometryType = 0; + Value featureClass = Value::None; + Value subclass = Value::None; + Value brunnel = Value::None; + int8_t adminLevel = -1; + bool maritime = false; + bool ramp = false; +}; + +enum GeometryType : uint8_t { + GEOMETRY_UNKNOWN = 0, + GEOMETRY_POINT = 1, + GEOMETRY_LINE = 2, + GEOMETRY_POLYGON = 3 +}; + +Layer layerFromName(const char* name, size_t length); +Value valueFromString(const char* text, size_t length); +const char* valueName(Value value); + +} // namespace maps diff --git a/Apps/Maps/main/Source/platform/Platform.h b/Apps/Maps/main/Source/platform/Platform.h new file mode 100644 index 0000000..c132cd4 --- /dev/null +++ b/Apps/Maps/main/Source/platform/Platform.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +namespace maps { + +/** Allocates a buffer, preferring external RAM where the device has it. */ +void* largeAlloc(size_t size); +void largeFree(void* pointer); +/** Allocates a small buffer that is used very often, preferring internal RAM. Free it with largeFree(). */ +void* fastAlloc(size_t size); + +uint32_t nowMs(); +uint64_t nowUs(); + +void logInfo(const char* format, ...); +void logError(const char* format, ...); + +bool fileExists(const char* path); +/** The size of a regular file. Returns false when there is none. */ +bool fileSize(const char* path, uint32_t& size); + +bool removeFile(const char* path); +/** Removes an empty folder. */ +bool removeFolder(const char* path); + +/** Calls back for each entry of a folder until the callback returns false. */ +using DirectoryCallback = bool (*)(const char* name, bool isDirectory, void* context); +bool listDirectory(const char* path, DirectoryCallback callback, void* context); + +/** Logs free internal RAM and external RAM with a label. */ +void logMemory(const char* label); + +} // namespace maps diff --git a/Apps/Maps/main/Source/platform/Position.h b/Apps/Maps/main/Source/platform/Position.h new file mode 100644 index 0000000..52ab145 --- /dev/null +++ b/Apps/Maps/main/Source/platform/Position.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +namespace maps { + +/** One position fix. Values the provider doesn't know are NAN (floats) or 0. */ +struct PositionFix { + double latitude; + double longitude; + /** Radius of 68 % confidence in metres. */ + float accuracy; + /** Direction of travel in degrees clockwise from north. */ + float course; + /** Metres per second. */ + float speed; + /** Milliseconds since the Unix epoch. */ + int64_t timeMs; +}; + +enum class PositionRead : uint8_t { + Fix, + /** Location services are on, but no fix was reported yet. */ + NoFix, + /** Location services are off in the system settings. */ + Off +}; + +/** + * Where the position comes from. On the device this is Tactility's location module and nothing else, + * on the PC a fake that tests feed with fixes. The app only reads, it never changes location settings. + */ +class PositionSource { +public: + virtual ~PositionSource() = default; + virtual PositionRead read(PositionFix& fix) = 0; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/CacheIndex.cpp b/Apps/Maps/main/Source/render/CacheIndex.cpp new file mode 100644 index 0000000..47af6e7 --- /dev/null +++ b/Apps/Maps/main/Source/render/CacheIndex.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "CacheIndex.h" + +#include "../platform/Platform.h" +#include "PreparedTile.h" + +#include + +namespace maps { + +namespace { + +constexpr char MAGIC[4] = { 'T', 'M', 'C', 'I' }; +constexpr uint32_t FORMAT_VERSION = 1; +constexpr size_t ENTRY_BYTES = 17; + +uint32_t readU32(const uint8_t* bytes) { + return static_cast(bytes[0]) | (static_cast(bytes[1]) << 8) | (static_cast(bytes[2]) << 16) | + (static_cast(bytes[3]) << 24); +} + +} // namespace + +CacheIndex::~CacheIndex() { + largeFree(entries); +} + +bool CacheIndex::init(int maxEntries, uint64_t capBytes) { + largeFree(entries); + entries = static_cast(largeAlloc(sizeof(Entry) * static_cast(maxEntries))); + capacity = entries != nullptr ? maxEntries : 0; + cap = capBytes; + clear(); + return entries != nullptr; +} + +void CacheIndex::clear() { + count = 0; + total = 0; + clock = 0; + evicting = false; + complete = false; + dirty = false; +} + +int CacheIndex::find(const TileKey& tile) const { + for (int i = 0; i < count; i++) { + if (entries[i].tile == tile) { + return i; + } + } + return -1; +} + +bool CacheIndex::contains(const TileKey& tile) const { + return find(tile) >= 0; +} + +void CacheIndex::put(const TileKey& tile, uint32_t size) { + const int index = find(tile); + if (index >= 0) { + total = total - entries[index].size + size; + entries[index].size = size; + entries[index].lastUse = ++clock; + } else if (count < capacity) { + entries[count++] = { tile, size, ++clock }; + total += size; + } else { + // The index is full: the tile is on the card but unknown, the next evictions make room + return; + } + dirty = true; +} + +void CacheIndex::touch(const TileKey& tile) { + const int index = find(tile); + if (index >= 0) { + entries[index].lastUse = ++clock; + dirty = true; + } +} + +void CacheIndex::erase(const TileKey& tile) { + const int index = find(tile); + if (index < 0) { + return; + } + total -= entries[index].size; + entries[index] = entries[--count]; + dirty = true; +} + +bool CacheIndex::nextEviction(TileKey& tile) { + const uint64_t lowWater = cap * LOW_WATER_PERCENT / 100; + const int lowEntries = capacity * LOW_WATER_PERCENT / 100; + if (isOverCap()) { + evicting = true; + } + if (evicting && total <= lowWater && count <= lowEntries) { + evicting = false; + } + if (!evicting || count == 0) { + return false; + } + int oldest = 0; + for (int i = 1; i < count; i++) { + if (entries[i].lastUse < entries[oldest].lastUse) { + oldest = i; + } + } + tile = entries[oldest].tile; + return true; +} + +void CacheIndex::save(ByteWriter& out) const { + out.clear(); + out.putBytes(MAGIC, 4); + out.putU32(FORMAT_VERSION); + out.putU32(static_cast(count)); + out.putU32(clock); + for (int i = 0; i < count; i++) { + const Entry& entry = entries[i]; + out.put(static_cast(entry.tile.z)); + out.putU32(static_cast(entry.tile.x)); + out.putU32(static_cast(entry.tile.y)); + out.putU32(entry.size); + out.putU32(entry.lastUse); + } +} + +bool CacheIndex::load(const uint8_t* data, size_t size) { + clear(); + if (data == nullptr || size < 16 || memcmp(data, MAGIC, 4) != 0 || readU32(data + 4) != FORMAT_VERSION) { + return false; + } + const uint32_t stored = readU32(data + 8); + if (size != 16 + static_cast(stored) * ENTRY_BYTES) { + return false; + } + clock = readU32(data + 12); + const uint8_t* cursor = data + 16; + for (uint32_t i = 0; i < stored && count < capacity; i++, cursor += ENTRY_BYTES) { + const TileKey tile { cursor[0], static_cast(readU32(cursor + 1)), static_cast(readU32(cursor + 5)) }; + const uint32_t fileSize = readU32(cursor + 9); + const uint32_t lastUse = readU32(cursor + 13); + if (!isValidTile(tile)) { + continue; + } + entries[count++] = { tile, fileSize, lastUse }; + total += fileSize; + clock = lastUse > clock ? lastUse : clock; + } + complete = true; + return true; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/CacheIndex.h b/Apps/Maps/main/Source/render/CacheIndex.h new file mode 100644 index 0000000..8ac4758 --- /dev/null +++ b/Apps/Maps/main/Source/render/CacheIndex.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../tiles/TileMath.h" + +#include +#include + +namespace maps { + +class ByteWriter; + +/** + * Sizes and last use of the rendered tiles in the SD card cache of one style, so the cache stays under a size + * cap by deleting the least recently used tiles first. Kept in cache//index.bin. + */ +class CacheIndex { +public: + static constexpr uint64_t DEFAULT_CAP_BYTES = 200ull * 1000 * 1000; + /** Eviction stops at this share of the cap, so it does not run after every write. */ + static constexpr int LOW_WATER_PERCENT = 90; + + CacheIndex() = default; + ~CacheIndex(); + CacheIndex(const CacheIndex&) = delete; + CacheIndex& operator=(const CacheIndex&) = delete; + + bool init(int maxEntries, uint64_t capBytes = DEFAULT_CAP_BYTES); + void clear(); + + /** Adds or updates a tile and marks it as just used. */ + void put(const TileKey& tile, uint32_t size); + void touch(const TileKey& tile); + void erase(const TileKey& tile); + bool contains(const TileKey& tile) const; + + uint64_t totalBytes() const { return total; } + int size() const { return count; } + bool isFull() const { return count >= capacity; } + bool isOverCap() const { return total > cap || isFull(); } + + /** + * The least recently used tile while the cache is above the low water mark, or while it is over the cap or + * the entry limit. Returns false when nothing needs to go. + */ + bool nextEviction(TileKey& tile); + + void save(ByteWriter& out) const; + bool load(const uint8_t* data, size_t size); + + /** Set once the index matches the files on the card. */ + bool isComplete() const { return complete; } + void setComplete(bool value) { complete = value; } + bool isDirty() const { return dirty; } + void setClean() { dirty = false; } + +private: + struct Entry { + TileKey tile; + uint32_t size; + uint32_t lastUse; + }; + + int find(const TileKey& tile) const; + + Entry* entries = nullptr; + int count = 0; + int capacity = 0; + uint64_t total = 0; + uint64_t cap = DEFAULT_CAP_BYTES; + uint32_t clock = 0; + bool evicting = false; + bool complete = false; + bool dirty = false; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/CacheWalker.cpp b/Apps/Maps/main/Source/render/CacheWalker.cpp new file mode 100644 index 0000000..e124666 --- /dev/null +++ b/Apps/Maps/main/Source/render/CacheWalker.cpp @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "CacheWalker.h" + +#include "../platform/Platform.h" + +#include +#include +#include + +namespace maps { + +namespace { + +bool isNumber(const char* text) { + if (*text == '\0') { + return false; + } + for (; *text != '\0'; text++) { + if (*text < '0' || *text > '9') { + return false; + } + } + return true; +} + +struct TopList { + char (*names)[16]; + int count; + int capacity; +}; + +bool addTop(const char* name, bool isDirectory, void* context) { + auto* list = static_cast(context); + if (isDirectory && strlen(name) < 16 && list->count < list->capacity) { + // FAT may report short names in upper case + char* out = list->names[list->count++]; + for (size_t i = 0; i <= strlen(name); i++) { + out[i] = (name[i] >= 'A' && name[i] <= 'Z') ? static_cast(name[i] - 'A' + 'a') : name[i]; + } + } + return true; +} + +struct ColumnList { + int* columns; + int count; + int capacity; +}; + +bool addColumn(const char* name, bool isDirectory, void* context) { + auto* list = static_cast(context); + if (isDirectory && isNumber(name) && list->count < list->capacity) { + list->columns[list->count++] = atoi(name); + } + return true; +} + +} // namespace + +CacheWalker::~CacheWalker() { + largeFree(columns); +} + +void CacheWalker::begin(const char* cacheRoot, const char* styleId, bool keepCurrent) { + snprintf(root, sizeof(root), "%s", cacheRoot); + snprintf(style, sizeof(style), "%s", styleId); + keep = keepCurrent; + phase = Phase::ListTop; + topCount = 0; + topIndex = 0; + zoom = -1; + columnCount = -1; + column = 0; +} + +bool CacheWalker::removeInCache(const char* path, bool folder) { + // Everything outside the cache folder, maps/ above all, is never deleted here + if (!isInsideCache(path, root)) { + logError("Refusing to delete %s outside the cache", path); + return false; + } + return folder ? removeFolder(path) : removeFile(path); +} + +bool CacheWalker::listColumns(const char* folder) { + if (columns == nullptr) { + columns = static_cast(largeAlloc(MAX_COLUMNS * sizeof(int))); + if (columns == nullptr) { + return false; + } + } + ColumnList list { columns, 0, MAX_COLUMNS }; + listDirectory(folder, addColumn, &list); + columnCount = list.count; + column = 0; + return true; +} + +void CacheWalker::indexColumn(CacheIndex& index, const IndexLock* lock, int z, int x) { + char folder[PATH_MAX_LENGTH]; + snprintf(folder, sizeof(folder), "%s/cache/%s/%d/%d", root, style, z, x); + struct Visit { + CacheIndex* index; + const IndexLock* lock; + const char* folder; + int z; + int x; + } visit { &index, lock, folder, z, x }; + listDirectory(folder, [](const char* name, bool isDirectory, void* context) { + auto* v = static_cast(context); + if (isDirectory || !isNumber(name)) { + return true; + } + char path[PATH_MAX_LENGTH]; + uint32_t size = 0; + snprintf(path, sizeof(path), "%s/%s", v->folder, name); + const TileKey tile { v->z, v->x, atoi(name) }; + if (!fileSize(path, size)) { + return true; + } + if (v->lock != nullptr) { + v->lock->lock(v->lock->context); + } + // Files already in the index keep their last use + if (!v->index->contains(tile)) { + v->index->put(tile, size); + } + if (v->lock != nullptr) { + v->lock->unlock(v->lock->context); + } + return true; + }, &visit); +} + +bool CacheWalker::deleteColumn(const char* zoomFolder, int x) { + char folder[PATH_MAX_LENGTH]; + snprintf(folder, sizeof(folder), "%s/%d", zoomFolder, x); + struct Visit { + CacheWalker* walker; + const char* folder; + int removed; + } visit { this, folder, 0 }; + listDirectory(folder, [](const char* name, bool isDirectory, void* context) { + auto* v = static_cast(context); + if (!isDirectory) { + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/%s", v->folder, name); + v->walker->removeInCache(path, false); + v->removed++; + } + // A few files per step, so the card is free again soon for the tiles on screen + return v->removed < MAX_DELETES_PER_STEP; + }, &visit); + if (visit.removed >= MAX_DELETES_PER_STEP) { + return false; + } + removeInCache(folder, true); + return true; +} + +bool CacheWalker::step(CacheIndex& index, const IndexLock* lock) { + while (phase != Phase::Done) { + if (phase == Phase::ListTop) { + char folder[PATH_MAX_LENGTH]; + snprintf(folder, sizeof(folder), "%s/cache", root); + TopList list { top, 0, MAX_TOP }; + listDirectory(folder, addTop, &list); + topCount = list.count; + topIndex = 0; + zoom = -1; + columnCount = -1; + phase = Phase::Top; + return true; + } + if (topIndex >= topCount) { + // The index of the current style matches the card now, also when it has no folder yet + index.setComplete(true); + phase = Phase::Done; + break; + } + const char* name = top[topIndex]; + const bool current = keep && strcmp(name, style) == 0; + const bool legacy = isNumber(name); + if (current && index.isComplete()) { + topIndex++; + continue; + } + // The old layout has zoom folders right under cache/, a style folder has them one level down + if (zoom < 0) { + zoom = 0; + } + if (zoom > MAX_VIEW_ZOOM) { + char path[PATH_MAX_LENGTH]; + if (current) { + index.setComplete(true); + } else { + if (!legacy) { + snprintf(path, sizeof(path), "%s/cache/%s/index.bin", root, name); + removeInCache(path, false); + } + snprintf(path, sizeof(path), "%s/cache/%s", root, name); + removeInCache(path, true); + } + topIndex++; + zoom = -1; + columnCount = -1; + continue; + } + char zoomFolder[PATH_MAX_LENGTH]; + if (legacy) { + snprintf(zoomFolder, sizeof(zoomFolder), "%s/cache/%s", root, name); + } else { + snprintf(zoomFolder, sizeof(zoomFolder), "%s/cache/%s/%d", root, name, zoom); + } + if (columnCount < 0 && !listColumns(zoomFolder)) { + return false; + } + if (column < columnCount) { + if (current) { + indexColumn(index, lock, zoom, columns[column]); + } else { + if (!deleteColumn(zoomFolder, columns[column])) { + return true; + } + } + column++; + return true; + } + if (!current && !legacy) { + removeInCache(zoomFolder, true); + } + // The old layout has a single zoom folder: moving past it ends this entry + zoom = legacy ? MAX_VIEW_ZOOM + 1 : zoom + 1; + columnCount = -1; + } + return false; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/CacheWalker.h b/Apps/Maps/main/Source/render/CacheWalker.h new file mode 100644 index 0000000..f9f54d9 --- /dev/null +++ b/Apps/Maps/main/Source/render/CacheWalker.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../tiles/TilePaths.h" +#include "CacheIndex.h" + +#include + +namespace maps { + +/** + * Tidies the SD card cache a folder at a time in idle time: it fills the index of the current style from the + * files when the index was lost, and deletes the folders of other styles and of the old cache layout + * (cache///). It only ever deletes inside /cache. + */ +class CacheWalker { +public: + /** Guards the index while the walk adds to it, it is shared with the render lanes. */ + struct IndexLock { + void (*lock)(void* context); + void (*unlock)(void* context); + void* context; + }; + + ~CacheWalker(); + + /** keepCurrent false deletes the current style's cache too, when the SD card cache is switched off. */ + void begin(const char* root, const char* style, bool keepCurrent = true); + bool isComplete() const { return phase == Phase::Done; } + + /** Looks at one folder. Returns false once the walk is complete. lock may be null. */ + bool step(CacheIndex& index, const IndexLock* lock = nullptr); + +private: + enum class Phase : uint8_t { + ListTop, + Top, + Done + }; + + static constexpr int MAX_TOP = 32; + static constexpr int MAX_COLUMNS = 4096; + /** A FAT delete holds the card, and the display's shared bus, for up to about 150 ms. */ + static constexpr int MAX_DELETES_PER_STEP = 2; + + bool listColumns(const char* folder); + void indexColumn(CacheIndex& index, const IndexLock* lock, int z, int x); + /** Deletes up to MAX_DELETES_PER_STEP files of a column. Returns true once the column is gone. */ + bool deleteColumn(const char* zoomFolder, int x); + bool removeInCache(const char* path, bool folder); + + char root[PATH_MAX_LENGTH] = ""; + char style[16] = ""; + bool keep = true; + Phase phase = Phase::Done; + char top[MAX_TOP][16] {}; + int topCount = 0; + int topIndex = 0; + /** The zoom folder being worked on, -1 before the first. */ + int zoom = -1; + int* columns = nullptr; + int columnCount = -1; + int column = 0; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/Compose.cpp b/Apps/Maps/main/Source/render/Compose.cpp new file mode 100644 index 0000000..38e8308 --- /dev/null +++ b/Apps/Maps/main/Source/render/Compose.cpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Compose.h" + +#include +#include + +namespace maps { + +int composeView(uint16_t* view, int width, int height, double centerX, double centerY, int zoom, TileLookup lookup, void* context, uint16_t placeholder) { + const auto left = static_cast(std::floor(centerX - width / 2.0)); + const auto top = static_cast(std::floor(centerY - height / 2.0)); + const int64_t worldSize = static_cast(TILE_SIZE) << zoom; + const auto firstTileX = static_cast(std::floor(static_cast(left) / TILE_SIZE)); + const auto firstTileY = static_cast(std::floor(static_cast(top) / TILE_SIZE)); + const auto lastTileX = static_cast(std::floor(static_cast(left + width - 1) / TILE_SIZE)); + const auto lastTileY = static_cast(std::floor(static_cast(top + height - 1) / TILE_SIZE)); + int missing = 0; + for (int tileY = firstTileY; tileY <= lastTileY; tileY++) { + for (int tileX = firstTileX; tileX <= lastTileX; tileX++) { + const int64_t tileLeft = static_cast(tileX) * TILE_SIZE; + const int64_t tileTop = static_cast(tileY) * TILE_SIZE; + const int x0 = static_cast(std::max(tileLeft, left) - left); + const int x1 = static_cast(std::min(tileLeft + TILE_SIZE, left + width) - left); + const int y0 = static_cast(std::max(tileTop, top) - top); + const int y1 = static_cast(std::min(tileTop + TILE_SIZE, top + height) - top); + const uint16_t* pixels = nullptr; + if (tileTop >= 0 && tileTop < worldSize) { + pixels = lookup({ zoom, wrapTileX(tileX, zoom), tileY }, context); + } + if (pixels == nullptr) { + missing++; + for (int y = y0; y < y1; y++) { + uint16_t* row = view + y * width; + for (int x = x0; x < x1; x++) { + row[x] = placeholder; + } + } + continue; + } + const int sourceX = static_cast(left + x0 - tileLeft); + for (int y = y0; y < y1; y++) { + const int sourceY = static_cast(top + y - tileTop); + memcpy(view + y * width + x0, pixels + sourceY * TILE_SIZE + sourceX, static_cast(x1 - x0) * sizeof(uint16_t)); + } + } + } + return missing; +} + +void scaleView(const uint16_t* source, uint16_t* target, int width, int height, float scale, float fromX, float fromY, float toX, float toY, + uint16_t fill) { + if (scale <= 0.0f) { + return; + } + // Source column of each target column, worked out once per frame + int columns[1024]; + const int columnCount = width < 1024 ? width : 1024; + for (int x = 0; x < columnCount; x++) { + columns[x] = static_cast(std::floor(fromX + (static_cast(x) + 0.5f - toX) / scale)); + } + for (int y = 0; y < height; y++) { + const int sourceY = static_cast(std::floor(fromY + (static_cast(y) + 0.5f - toY) / scale)); + uint16_t* out = target + y * width; + if (sourceY < 0 || sourceY >= height) { + for (int x = 0; x < width; x++) { + out[x] = fill; + } + continue; + } + const uint16_t* row = source + sourceY * width; + for (int x = 0; x < columnCount; x++) { + const int sourceX = columns[x]; + out[x] = sourceX >= 0 && sourceX < width ? row[sourceX] : fill; + } + } +} + +int snapZoom(int zoom, float scale, int maxZoom) { + if (scale <= 0.0f) { + return zoom; + } + // The nearest power of two, without log2f, which Tactility does not export to apps + int steps = 0; + while (scale >= 1.41421356f) { + scale *= 0.5f; + steps++; + } + while (scale < 0.70710678f) { + scale *= 2.0f; + steps--; + } + const int snapped = zoom + steps; + return snapped < 0 ? 0 : (snapped > maxZoom ? maxZoom : snapped); +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/Compose.h b/Apps/Maps/main/Source/render/Compose.h new file mode 100644 index 0000000..abd994c --- /dev/null +++ b/Apps/Maps/main/Source/render/Compose.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../tiles/TileMath.h" + +#include + +namespace maps { + +/** Returns the rendered 256x256 pixels of a tile, or nullptr when it is not ready. */ +using TileLookup = const uint16_t* (*)(const TileKey& tile, void* context); + +/** + * Copies the tiles that cover a view into it. centerX and centerY are world pixel coordinates at zoom. + * Returns the number of tiles that were missing and drawn as placeholder. + */ +int composeView(uint16_t* view, int width, int height, double centerX, double centerY, int zoom, TileLookup lookup, void* context, uint16_t placeholder); + +/** + * Draws source scaled by scale into target, both width x height, so the source point fromX, fromY lands on + * toX, toY. Nearest pixel, for live pinch zoom from the pixels on screen. Uncovered parts get fill. + */ +void scaleView(const uint16_t* source, uint16_t* target, int width, int height, float scale, float fromX, float fromY, float toX, float toY, + uint16_t fill); + +/** The whole zoom level nearest to zoom scaled by scale, within 0 and maxZoom. */ +int snapZoom(int zoom, float scale, int maxZoom); + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/PreparedScanner.cpp b/Apps/Maps/main/Source/render/PreparedScanner.cpp new file mode 100644 index 0000000..3b65991 --- /dev/null +++ b/Apps/Maps/main/Source/render/PreparedScanner.cpp @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "PreparedScanner.h" + +#include "../platform/Platform.h" +#include "PreparedTile.h" + +#include +#include +#include + +namespace maps { + +namespace { + +struct ColumnList { + int* columns; + int count; + int capacity; +}; + +bool isNumber(const char* text) { + if (*text == '\0') { + return false; + } + for (; *text != '\0'; text++) { + if (*text < '0' || *text > '9') { + return false; + } + } + return true; +} + +bool addColumn(const char* name, bool isDirectory, void* context) { + auto* list = static_cast(context); + if (isDirectory && isNumber(name) && list->count < list->capacity) { + list->columns[list->count++] = atoi(name); + } + return true; +} + +struct FileVisit { + const TileStore* store; + int region; + int zoom; + int x; + PreparedScanner::StaleCallback callback; + void* context; + bool stopped; + /** Originals seen in this folder so far, the first skip of them were looked at in earlier steps. */ + int seen; + int skip; + int checks; +}; + +/** Headers read per step: each is a file open on the card, which holds it for the tiles on screen. */ +constexpr int CHECKS_PER_STEP = 4; + +bool visitFile(const char* name, bool isDirectory, void* context) { + auto* visit = static_cast(context); + const size_t length = strlen(name); + // FAT may report short names in upper case + if (isDirectory || length < 5 || (strcmp(name + length - 4, ".pbf") != 0 && strcmp(name + length - 4, ".PBF") != 0)) { + return true; + } + char number[16]; + if (length - 4 >= sizeof(number)) { + return true; + } + memcpy(number, name, length - 4); + number[length - 4] = '\0'; + if (!isNumber(number)) { + return true; + } + if (visit->seen++ < visit->skip) { + return true; + } + if (visit->checks >= CHECKS_PER_STEP) { + // The rest of the folder comes in the next step + visit->seen--; + visit->stopped = true; + return false; + } + visit->checks++; + const TileKey tile { visit->zoom, visit->x, atoi(number) }; + const TileStore& store = *visit->store; + char path[PATH_MAX_LENGTH]; + uint32_t size = 0; + if (!tilePath(path, sizeof(path), store.root(), store.regionName(visit->region), tile) || !fileSize(path, size) || + !preparedPath(path, sizeof(path), store.root(), store.regionName(visit->region), tile)) { + return true; + } + if (PreparedScanner::isCurrent(path, tile, size)) { + return true; + } + if (!visit->callback(tile, visit->region, visit->context)) { + // The queue is full: this tile is looked at again next time + visit->seen--; + visit->stopped = true; + return false; + } + return true; +} + +} // namespace + +PreparedScanner::~PreparedScanner() { + largeFree(columns); +} + +void PreparedScanner::restart() { + region = 0; + zoom = 0; + column = 0; + columnCount = -1; + fileSkip = 0; + complete = false; +} + +bool PreparedScanner::isCurrent(const char* preparedPath, const TileKey& tile, uint32_t sourceSize) { + FILE* file = fopen(preparedPath, "rb"); + if (file == nullptr) { + return false; + } + uint8_t bytes[PREPARED_HEADER_SIZE]; + const bool read = fread(bytes, 1, sizeof(bytes), file) == sizeof(bytes); + fclose(file); + PreparedHeader header {}; + return read && readPreparedHeader(bytes, sizeof(bytes), header) && isPreparedCurrent(header, tile, sourceSize); +} + +bool PreparedScanner::listColumns(const TileStore& store) { + if (columns == nullptr) { + columns = static_cast(largeAlloc(MAX_COLUMNS * sizeof(int))); + if (columns == nullptr) { + return false; + } + } + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/maps/%s/%d", store.root(), store.regionName(region), zoom); + ColumnList list { columns, 0, MAX_COLUMNS }; + listDirectory(path, addColumn, &list); + columnCount = list.count; + column = 0; + return true; +} + +bool PreparedScanner::step(const TileStore& store, StaleCallback callback, void* context) { + while (!complete) { + if (region >= store.regionCount()) { + complete = true; + break; + } + if (zoom > MAX_SOURCE_ZOOM) { + region++; + zoom = 0; + columnCount = -1; + continue; + } + if (columnCount < 0 && !listColumns(store)) { + return false; + } + if (column >= columnCount) { + zoom++; + columnCount = -1; + continue; + } + char path[PATH_MAX_LENGTH]; + snprintf(path, sizeof(path), "%s/maps/%s/%d/%d", store.root(), store.regionName(region), zoom, columns[column]); + FileVisit visit { &store, region, zoom, columns[column], callback, context, false, 0, fileSkip, 0 }; + listDirectory(path, visitFile, &visit); + // A stopped folder goes on next time after the originals already looked at + if (visit.stopped) { + fileSkip = visit.seen; + } else { + column++; + fileSkip = 0; + } + return true; + } + return false; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/PreparedScanner.h b/Apps/Maps/main/Source/render/PreparedScanner.h new file mode 100644 index 0000000..569d395 --- /dev/null +++ b/Apps/Maps/main/Source/render/PreparedScanner.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../tiles/TileStore.h" + +#include + +namespace maps { + +/** + * Walks the region folders a little at a time and reports original tiles whose prepared copy is missing or was + * made by an older format version, so idle time converts the tiles already on the card. + */ +class PreparedScanner { +public: + /** Called for a tile that needs converting. Returning false stops the step, it is looked at again next time. */ + using StaleCallback = bool (*)(const TileKey& tile, int region, void* context); + + ~PreparedScanner(); + + /** Starts over from the first region, after tiles arrived or the format changed. */ + void restart(); + bool isComplete() const { return complete; } + + /** Looks at one folder of tiles. Returns false once every region was looked at. */ + bool step(const TileStore& store, StaleCallback callback, void* context); + + /** Whether the prepared copy at path belongs to this original. */ + static bool isCurrent(const char* preparedPath, const TileKey& tile, uint32_t sourceSize); + +private: + static constexpr int MAX_COLUMNS = 4096; + + bool listColumns(const TileStore& store); + + int region = 0; + int zoom = 0; + int column = 0; + int columnCount = -1; + /** Originals of the current folder already looked at. */ + int fileSkip = 0; + int* columns = nullptr; + bool complete = false; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/PreparedTile.cpp b/Apps/Maps/main/Source/render/PreparedTile.cpp new file mode 100644 index 0000000..7103dbe --- /dev/null +++ b/Apps/Maps/main/Source/render/PreparedTile.cpp @@ -0,0 +1,493 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "PreparedTile.h" + +#include "../platform/Platform.h" +#include "../tiles/TileStore.h" + +#include + +#include +#include + +namespace maps { + +namespace { + +constexpr char MAGIC[4] = { 'T', 'M', 'P', 'T' }; +constexpr size_t RING_START_CAPACITY = 1024; +/** Longer rings are stored without simplification. */ +constexpr size_t RING_MAX_POINTS = 65536; +constexpr uint8_t FLAG_MARITIME = 1 << 2; +constexpr uint8_t FLAG_RAMP = 1 << 3; +constexpr uint8_t FLAG_SUBCLASS = 1 << 4; +constexpr uint8_t FLAG_BRUNNEL = 1 << 5; +constexpr uint8_t FLAG_ADMIN = 1 << 6; + +uint16_t readU16(const uint8_t* bytes) { + return static_cast(bytes[0] | (bytes[1] << 8)); +} + +uint32_t readU32(const uint8_t* bytes) { + return static_cast(bytes[0]) | (static_cast(bytes[1]) << 8) | (static_cast(bytes[2]) << 16) | + (static_cast(bytes[3]) << 24); +} + +int32_t roundToInt(float value) { + return static_cast(std::floor(value + 0.5f)); +} + +/** The section a layer's features go to, -1 for layers that are not drawn. */ +int sectionOf(Layer layer) { + for (int i = 0; i < static_cast(Stage::Count); i++) { + const auto stage = static_cast(i); + if (stage == preparedSection(stage) && stageLayer(stage) == layer) { + return i; + } + } + return -1; +} + +int deflateCallback(const void* buffer, int length, void* user) { + auto* out = static_cast(user); + out->putBytes(buffer, static_cast(length)); + return out->failed() ? 0 : 1; +} + +} // namespace + +bool readPreparedHeader(const uint8_t* data, size_t size, PreparedHeader& header) { + if (data == nullptr || size < PREPARED_HEADER_SIZE || memcmp(data, MAGIC, 4) != 0) { + return false; + } + header.version = readU16(data + 4); + header.flags = readU16(data + 6); + header.tile.z = data[8]; + header.extent = readU16(data + 10); + header.tile.x = static_cast(readU32(data + 12)); + header.tile.y = static_cast(readU32(data + 16)); + header.sourceSize = readU32(data + 20); + header.bodySize = readU32(data + 24); + header.storedSize = readU32(data + 28); + memcpy(header.sourceVersion, data + 32, PREPARED_VERSION_LENGTH); + header.sourceVersion[PREPARED_VERSION_LENGTH] = '\0'; + return true; +} + +bool isPreparedCurrent(const PreparedHeader& header, const TileKey& tile, uint32_t sourceSize) { + return header.version == PREPARED_FORMAT_VERSION && header.tile == tile && header.sourceSize == sourceSize && + header.extent == PREPARED_EXTENT; +} + +// --------------------------------------------------------------------------------------------------------------- + +ByteWriter::~ByteWriter() { + largeFree(data); +} + +void ByteWriter::release() { + largeFree(data); + data = nullptr; + capacity = 0; + clear(); +} + +bool ByteWriter::reserve(size_t size) { + if (size <= capacity) { + return true; + } + size_t next = capacity < 4096 ? 4096 : capacity; + while (next < size) { + next *= 2; + } + auto* grown = static_cast(largeAlloc(next)); + if (grown == nullptr) { + error = true; + return false; + } + if (length > 0) { + memcpy(grown, data, length); + } + largeFree(data); + data = grown; + capacity = next; + return true; +} + +void ByteWriter::put(uint8_t value) { + if (length < capacity || reserve(length + 1)) { + data[length++] = value; + } +} + +void ByteWriter::putBytes(const void* bytes, size_t count) { + if (count > 0 && reserve(length + count)) { + memcpy(data + length, bytes, count); + length += count; + } +} + +void ByteWriter::putVarint(uint64_t value) { + while (value >= 0x80) { + put(static_cast(value | 0x80)); + value >>= 7; + } + put(static_cast(value)); +} + +void ByteWriter::putU16(uint16_t value) { + put(static_cast(value)); + put(static_cast(value >> 8)); +} + +void ByteWriter::putU32(uint32_t value) { + for (int i = 0; i < 4; i++) { + put(static_cast(value >> (8 * i))); + } +} + +void ByteWriter::setU32(size_t offset, uint32_t value) { + if (offset + 4 <= length) { + for (int i = 0; i < 4; i++) { + data[offset + i] = static_cast(value >> (8 * i)); + } + } +} + +bool deflateBytes(const uint8_t* data, size_t size, ByteWriter& out) { + auto* compressor = static_cast(largeAlloc(sizeof(tdefl_compressor))); + if (compressor == nullptr) { + return false; + } + // 128 probes per search and a zlib header, so InflateReader recognises the stream + const int flags = 128 | TDEFL_WRITE_ZLIB_HEADER; + bool ok = tdefl_init(compressor, deflateCallback, &out, flags) == TDEFL_STATUS_OKAY && + tdefl_compress_buffer(compressor, data, size, TDEFL_FINISH) == TDEFL_STATUS_DONE; + largeFree(compressor); + return ok && !out.failed(); +} + +// --------------------------------------------------------------------------------------------------------------- + +TileConverter::~TileConverter() { + largeFree(ringPoints); + largeFree(keep); + largeFree(stack); +} + +bool TileConverter::growRing(size_t points) { + if (points <= ringCapacity) { + return true; + } + if (points > RING_MAX_POINTS) { + return false; + } + size_t next = ringCapacity == 0 ? RING_START_CAPACITY : ringCapacity; + while (next < points) { + next *= 2; + } + auto* grownPoints = static_cast(largeAlloc(next * 2 * sizeof(float))); + auto* grownKeep = static_cast(largeAlloc(next)); + auto* grownStack = static_cast(largeAlloc(next * 2 * sizeof(uint32_t))); + if (grownPoints == nullptr || grownKeep == nullptr || grownStack == nullptr) { + largeFree(grownPoints); + largeFree(grownKeep); + largeFree(grownStack); + return false; + } + if (ringLength > 0) { + memcpy(grownPoints, ringPoints, ringLength * 2 * sizeof(float)); + } + largeFree(ringPoints); + largeFree(keep); + largeFree(stack); + ringPoints = grownPoints; + keep = grownKeep; + stack = grownStack; + ringCapacity = next; + return true; +} + +bool TileConverter::convert(MvtDecoder& tileDecoder, ByteInput* input, const TileKey& sourceTile, uint32_t sourceSize, + const char* sourceVersion, bool deflate, ByteWriter& out, ConvertStats* stats) { + const uint32_t start = nowMs(); + decoder = &tileDecoder; + tile = sourceTile; + counters = {}; + // A tile is drawn at its own zoom level and magnified for the levels above it while no closer tile exists + minZoom = tile.z; + maxZoom = tile.z + TileStore::MAX_OVERZOOM < MAX_VIEW_ZOOM ? tile.z + TileStore::MAX_OVERZOOM : MAX_VIEW_ZOOM; + // Units per pixel at z16, where the drop and simplification thresholds are measured + pixelsPerUnitAt16 = tile.z <= 16 ? std::ldexp(static_cast(TILE_SIZE) / PREPARED_EXTENT, 16 - tile.z) : 1.0f; + for (int i = 0; i < static_cast(Stage::Count); i++) { + sections[i].clear(); + sectionCounts[i] = 0; + } + if (!decoder->scan(input)) { + return false; + } + Layer layers[static_cast(Stage::Count)]; + int layerCount = 0; + for (int i = 0; i < static_cast(Stage::Count); i++) { + const auto stage = static_cast(i); + if (stage == preparedSection(stage)) { + layers[layerCount++] = stageLayer(stage); + } + } + // The sections are separate buffers, so one pass in file order collects all of them + if (!decoder->sweep(layers, layerCount, *this)) { + return false; + } + + ByteWriter body; + for (int i = 0; i < static_cast(Stage::Count); i++) { + if (sectionCounts[i] == 0) { + continue; + } + body.put(static_cast(i)); + body.putVarint(sectionCounts[i]); + body.putVarint(sections[i].size()); + body.putBytes(sections[i].bytes(), sections[i].size()); + } + body.put(0xFF); + if (body.failed()) { + return false; + } + + out.clear(); + out.putBytes(MAGIC, 4); + out.putU16(PREPARED_FORMAT_VERSION); + out.putU16(deflate ? PREPARED_FLAG_DEFLATED : 0); + out.put(static_cast(tile.z)); + out.put(0); + out.putU16(PREPARED_EXTENT); + out.putU32(static_cast(tile.x)); + out.putU32(static_cast(tile.y)); + out.putU32(sourceSize); + out.putU32(static_cast(body.size())); + out.putU32(0); + // Zero padded, without a terminator when the version fills the field + char version[PREPARED_VERSION_LENGTH] = {}; + if (sourceVersion != nullptr) { + const size_t length = strlen(sourceVersion); + memcpy(version, sourceVersion, length < PREPARED_VERSION_LENGTH ? length : PREPARED_VERSION_LENGTH); + } + out.putBytes(version, PREPARED_VERSION_LENGTH); + if (deflate) { + if (!deflateBytes(body.bytes(), body.size(), out)) { + return false; + } + } else { + out.putBytes(body.bytes(), body.size()); + } + const auto stored = static_cast(out.size() - PREPARED_HEADER_SIZE); + out.setU32(28, stored); + if (stats != nullptr) { + *stats = counters; + stats->totalMs = nowMs() - start; + stats->bodyBytes = static_cast(body.size()); + stats->storedBytes = stored; + } + return !out.failed(); +} + +bool TileConverter::beginFeature(const FeatureInfo& info) { + counters.featuresIn++; + section = sectionOf(info.layer); + if (section < 0) { + return false; + } + // Kept when the style draws it in one of the section's stages at one of the zoom levels the tile is used for + const Palette& palette = colorPalette(); + bool drawn = false; + for (int zoom = minZoom; zoom <= maxZoom && !drawn; zoom++) { + for (int i = 0; i < static_cast(Stage::Count) && !drawn; i++) { + const auto stage = static_cast(i); + DrawStyle style {}; + drawn = static_cast(preparedSection(stage)) == section && styleFor(info, stage, zoom, palette, style); + } + } + if (!drawn) { + return false; + } + feature = info; + rings.clear(); + ringCount = 0; + lastX = 0; + lastY = 0; + minX = 1e30f; + minY = 1e30f; + maxX = -1e30f; + maxY = -1e30f; + unitsPerSourceUnit = static_cast(PREPARED_EXTENT) / static_cast(decoder->extent(info.layer)); + return true; +} + +void TileConverter::beginRing() { + ringLength = 0; + ringOverflow = false; +} + +void TileConverter::points(const TilePoint* chunk, int count) { + counters.pointsIn += static_cast(count); + if (!growRing(ringLength + static_cast(count))) { + // Kept unsimplified in the buffer it has, the rest of a huge ring is lost + ringOverflow = true; + count = static_cast(ringCapacity - ringLength); + } + for (int i = 0; i < count; i++) { + const float x = static_cast(chunk[i].x) * unitsPerSourceUnit; + const float y = static_cast(chunk[i].y) * unitsPerSourceUnit; + // The decoder repeats the last point of a chunk at the start of the next one + if (ringLength > 0 && ringPoints[ringLength * 2 - 2] == x && ringPoints[ringLength * 2 - 1] == y) { + continue; + } + ringPoints[ringLength * 2] = x; + ringPoints[ringLength * 2 + 1] = y; + ringLength++; + } +} + +int TileConverter::simplifyRing(int count, float tolerance) { + // Douglas-Peucker with an explicit stack, keeping the end points + for (int i = 0; i < count; i++) { + keep[i] = 0; + } + keep[0] = 1; + keep[count - 1] = 1; + int depth = 0; + stack[depth++] = 0; + stack[depth++] = static_cast(count - 1); + const float toleranceSquared = tolerance * tolerance; + while (depth > 0) { + const auto last = static_cast(stack[--depth]); + const auto first = static_cast(stack[--depth]); + if (last - first < 2) { + continue; + } + const float ax = ringPoints[first * 2]; + const float ay = ringPoints[first * 2 + 1]; + const float dx = ringPoints[last * 2] - ax; + const float dy = ringPoints[last * 2 + 1] - ay; + const float lengthSquared = dx * dx + dy * dy; + float worst = -1.0f; + int worstIndex = -1; + for (int i = first + 1; i < last; i++) { + const float px = ringPoints[i * 2] - ax; + const float py = ringPoints[i * 2 + 1] - ay; + float distanceSquared; + if (lengthSquared <= 0.0f) { + distanceSquared = px * px + py * py; + } else { + const float cross = px * dy - py * dx; + distanceSquared = cross * cross / lengthSquared; + } + if (distanceSquared > worst) { + worst = distanceSquared; + worstIndex = i; + } + } + if (worst > toleranceSquared) { + keep[worstIndex] = 1; + stack[depth++] = static_cast(first); + stack[depth++] = static_cast(worstIndex); + stack[depth++] = static_cast(worstIndex); + stack[depth++] = static_cast(last); + } + } + int kept = 0; + for (int i = 0; i < count; i++) { + if (keep[i] != 0) { + ringPoints[kept * 2] = ringPoints[i * 2]; + ringPoints[kept * 2 + 1] = ringPoints[i * 2 + 1]; + kept++; + } + } + return kept; +} + +void TileConverter::endRing(bool closed) { + if (ringLength == 0) { + return; + } + // A pixel at z18 (a quarter at z16), about half a unit, so lines stay smooth when magnified up to z19 + const float tolerance = 0.25f / pixelsPerUnitAt16; + int count = static_cast(ringLength); + if (count > 2 && !ringOverflow) { + count = simplifyRing(count, tolerance); + } + // Quantised in place, dropping points that land on the previous one + auto* quantised = reinterpret_cast(ringPoints); + int kept = 0; + for (int i = 0; i < count; i++) { + const float x = ringPoints[i * 2]; + const float y = ringPoints[i * 2 + 1]; + const int32_t qx = roundToInt(x); + const int32_t qy = roundToInt(y); + if (kept > 0 && quantised[kept * 2 - 2] == qx && quantised[kept * 2 - 1] == qy) { + continue; + } + minX = std::fmin(minX, x); + minY = std::fmin(minY, y); + maxX = std::fmax(maxX, x); + maxY = std::fmax(maxY, y); + quantised[kept * 2] = qx; + quantised[kept * 2 + 1] = qy; + kept++; + } + if (closed && kept > 1 && quantised[0] == quantised[kept * 2 - 2] && quantised[1] == quantised[kept * 2 - 1]) { + kept--; + } + if (kept < (closed ? 3 : 2)) { + return; + } + rings.putVarint((static_cast(kept) << 1) | (closed ? 1 : 0)); + for (int i = 0; i < kept; i++) { + rings.putZigzag(quantised[i * 2] - lastX); + rings.putZigzag(quantised[i * 2 + 1] - lastY); + lastX = quantised[i * 2]; + lastY = quantised[i * 2 + 1]; + } + counters.pointsKept += static_cast(kept); + ringCount++; +} + +void TileConverter::endFeature() { + // Features smaller than a pixel at z16 are dropped + if (ringCount == 0 || ((maxX - minX) * pixelsPerUnitAt16 < 1.0f && (maxY - minY) * pixelsPerUnitAt16 < 1.0f)) { + return; + } + ByteWriter& out = sections[section]; + uint8_t flags = feature.geometryType & 3; + if (feature.maritime) { + flags |= FLAG_MARITIME; + } + if (feature.ramp) { + flags |= FLAG_RAMP; + } + if (feature.subclass != Value::None) { + flags |= FLAG_SUBCLASS; + } + if (feature.brunnel != Value::None) { + flags |= FLAG_BRUNNEL; + } + if (feature.adminLevel >= 0) { + flags |= FLAG_ADMIN; + } + out.put(flags); + out.putVarint(static_cast(feature.featureClass)); + if (feature.subclass != Value::None) { + out.putVarint(static_cast(feature.subclass)); + } + if (feature.brunnel != Value::None) { + out.putVarint(static_cast(feature.brunnel)); + } + if (feature.adminLevel >= 0) { + out.put(static_cast(feature.adminLevel)); + } + out.putVarint(ringCount); + out.putBytes(rings.bytes(), rings.size()); + sectionCounts[section]++; + counters.featuresKept++; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/PreparedTile.h b/Apps/Maps/main/Source/render/PreparedTile.h new file mode 100644 index 0000000..f37f7c2 --- /dev/null +++ b/Apps/Maps/main/Source/render/PreparedTile.h @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../mvt/MvtDecoder.h" +#include "../tiles/TileMath.h" +#include "Style.h" + +#include +#include + +namespace maps { + +/** + * Prepared tiles: a render-ready copy of a vector tile, stored next to the untouched original as .prt. + * It keeps only the features the style can draw at the zoom levels the tile is used for, with only the + * attributes the style reads, coordinates quantised to PREPARED_EXTENT units per tile, grouped by draw stage. + * Raising PREPARED_FORMAT_VERSION makes the app regenerate every prepared tile from the originals. Raise it + * when the style draws features it used to skip, or when the order of the Value list in Schema.h changes, since + * classes are stored as Value numbers. + * + * Layout (little endian): a 64-byte header, then the body, raw or as a zlib stream. + * header: "TMPT", u16 version, u16 flags, u8 z, u8 0, u16 extent, u32 x, u32 y, u32 source size, + * u32 body size, u32 stored body size, char source version[32] + * body: sections, each u8 stage, varint feature count, varint byte length, features; then 0xFF. + * The RoadCasing section is drawn again for the RoadFill stage. + * feature: u8 flags (bits 0-1 geometry type, 2 maritime, 3 ramp, 4 has subclass, 5 has brunnel, + * 6 has admin level), varint class, [varint subclass], [varint brunnel], [u8 admin level], + * varint ring count, rings: varint (point count << 1 | closed), zigzag varint deltas x, y. + * Deltas continue across the rings of a feature and start at 0, 0. + */ +constexpr uint16_t PREPARED_FORMAT_VERSION = 2; +// 2 px per unit at z18 from z14 data. 1024 looked stepped at z17-z19; 4096 made deflated tiles 35 % larger +// (docs/MEASUREMENTS.md, M3.2). +constexpr int PREPARED_EXTENT = 2048; +constexpr size_t PREPARED_HEADER_SIZE = 64; +constexpr size_t PREPARED_VERSION_LENGTH = 32; +constexpr uint16_t PREPARED_FLAG_DEFLATED = 1; + +struct PreparedHeader { + uint16_t version; + uint16_t flags; + TileKey tile; + int extent; + uint32_t sourceSize; + uint32_t bodySize; + uint32_t storedSize; + char sourceVersion[PREPARED_VERSION_LENGTH + 1]; +}; + +bool readPreparedHeader(const uint8_t* data, size_t size, PreparedHeader& header); + +/** Whether a prepared tile was made by this format version from this original. */ +bool isPreparedCurrent(const PreparedHeader& header, const TileKey& tile, uint32_t sourceSize); + +/** A byte buffer that grows as needed. */ +class ByteWriter { +public: + ByteWriter() = default; + ~ByteWriter(); + ByteWriter(const ByteWriter&) = delete; + ByteWriter& operator=(const ByteWriter&) = delete; + + void clear() { + length = 0; + error = false; + } + /** Clears and frees the buffer. */ + void release(); + bool reserve(size_t size); + void put(uint8_t value); + void putBytes(const void* bytes, size_t count); + void putVarint(uint64_t value); + void putZigzag(int32_t value) { putVarint((static_cast(value) << 1) ^ static_cast(value >> 31)); } + void putU16(uint16_t value); + void putU32(uint32_t value); + void setU32(size_t offset, uint32_t value); + + const uint8_t* bytes() const { return data; } + uint8_t* bytes() { return data; } + size_t size() const { return length; } + bool failed() const { return error; } + +private: + uint8_t* data = nullptr; + size_t capacity = 0; + size_t length = 0; + bool error = false; +}; + +struct ConvertStats { + uint32_t totalMs; + uint32_t featuresIn; + uint32_t featuresKept; + uint32_t pointsIn; + uint32_t pointsKept; + uint32_t bodyBytes; + uint32_t storedBytes; +}; + +/** + * Converts a vector tile into a prepared tile. It uses the decoder of a render lane, so a conversion needs no + * second decompressor. + */ +class TileConverter final : private GeometrySink { +public: + TileConverter() = default; + ~TileConverter() override; + TileConverter(const TileConverter&) = delete; + TileConverter& operator=(const TileConverter&) = delete; + + /** + * Writes the prepared tile of the vector tile in input to out. sourceSize is the size of the original file, + * sourceVersion the tile set version it came from. + */ + bool convert(MvtDecoder& decoder, ByteInput* input, const TileKey& tile, uint32_t sourceSize, const char* sourceVersion, + bool deflate, ByteWriter& out, ConvertStats* stats); + +private: + bool beginFeature(const FeatureInfo& info) override; + void beginRing() override; + void points(const TilePoint* points, int count) override; + void endRing(bool closed) override; + void endFeature() override; + + bool growRing(size_t points); + int simplifyRing(int count, float tolerance); + + MvtDecoder* decoder = nullptr; + TileKey tile {}; + int minZoom = 0; + int maxZoom = 0; + float unitsPerSourceUnit = 1.0f; + float pixelsPerUnitAt16 = 1.0f; + ByteWriter sections[static_cast(Stage::Count)]; + uint32_t sectionCounts[static_cast(Stage::Count)] {}; + ByteWriter rings; + int section = 0; + FeatureInfo feature {}; + uint32_t ringCount = 0; + int32_t lastX = 0; + int32_t lastY = 0; + float minX = 0.0f; + float minY = 0.0f; + float maxX = 0.0f; + float maxY = 0.0f; + float* ringPoints = nullptr; + uint8_t* keep = nullptr; + uint32_t* stack = nullptr; + size_t ringCapacity = 0; + size_t ringLength = 0; + bool ringOverflow = false; + ConvertStats counters {}; +}; + +/** The section a stage draws from: road fills reuse the road casing section. */ +inline Stage preparedSection(Stage stage) { + return stage == Stage::RoadFill ? Stage::RoadCasing : stage; +} + +/** Compresses data into a zlib stream. */ +bool deflateBytes(const uint8_t* data, size_t size, ByteWriter& out); + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/Raster.cpp b/Apps/Maps/main/Source/render/Raster.cpp new file mode 100644 index 0000000..88897bb --- /dev/null +++ b/Apps/Maps/main/Source/render/Raster.cpp @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Raster.h" + +#include "../platform/Platform.h" + +#include +#include +#include +#include + +namespace maps { + +namespace { + +constexpr float PI_F = 3.14159265f; +constexpr int MAX_DOT_SEGMENTS = 32; + +/** Integral of the "left of x" fraction for a row crossing that spans left..right horizontally. */ +inline float coverageIntegral(float x, float left, float right) { + if (x <= left) { + return 0.0f; + } + if (x >= right) { + return 0.5f * (right - left) + (x - right); + } + const float t = x - left; + return t * t / (2.0f * (right - left)); +} + +} // namespace + +uint16_t blendRgb565(uint16_t foreground, uint16_t background, uint32_t alpha32) { + const uint32_t fg = (foreground | (static_cast(foreground) << 16)) & 0x07E0F81FU; + const uint32_t bg = (background | (static_cast(background) << 16)) & 0x07E0F81FU; + const uint32_t result = ((((fg - bg) * alpha32) >> 5) + bg) & 0x07E0F81FU; + return static_cast(result | (result >> 16)); +} + +Raster::~Raster() { + largeFree(cells); + largeFree(rowFirst); + largeFree(rowLast); +} + +void Raster::resetRows() { + for (int row = 0; row < rasterHeight; row++) { + rowFirst[row] = static_cast(stride); + rowLast[row] = -1; + } +} + +bool Raster::init(int width, int height) { + if (cells != nullptr && width == rasterWidth && height == rasterHeight) { + return true; + } + largeFree(cells); + largeFree(rowFirst); + largeFree(rowLast); + rasterWidth = width; + rasterHeight = height; + stride = width + 2; + cells = static_cast(largeAlloc(static_cast(stride) * height * sizeof(float))); + rowFirst = static_cast(fastAlloc(static_cast(height) * sizeof(int16_t))); + rowLast = static_cast(fastAlloc(static_cast(height) * sizeof(int16_t))); + if (cells == nullptr || rowFirst == nullptr || rowLast == nullptr) { + return false; + } + memset(cells, 0, static_cast(stride) * height * sizeof(float)); + resetRows(); + minRow = rasterHeight; + maxRow = -1; + minColumn = stride; + maxColumn = 0; + return true; +} + +void Raster::markRows(int top, int bottom) { + minRow = std::min(minRow, top); + maxRow = std::max(maxRow, bottom); +} + +void Raster::addLine(float x0, float y0, float x1, float y1) { + const auto height = static_cast(rasterHeight); + if (y0 == y1 || (y0 <= 0.0f && y1 <= 0.0f) || (y0 >= height && y1 >= height)) { + return; + } + if (!std::isfinite(x0) || !std::isfinite(y0) || !std::isfinite(x1) || !std::isfinite(y1)) { + return; + } + const float slope = (x1 - x0) / (y1 - y0); + if (y0 < 0.0f) { + x0 += (0.0f - y0) * slope; + y0 = 0.0f; + } else if (y0 > height) { + x0 += (height - y0) * slope; + y0 = height; + } + if (y1 < 0.0f) { + x1 += (0.0f - y1) * slope; + y1 = 0.0f; + } else if (y1 > height) { + x1 += (height - y1) * slope; + y1 = height; + } + addClippedLine(x0, y0, x1, y1); +} + +void Raster::addClippedLine(float x0, float y0, float x1, float y1) { + const auto width = static_cast(rasterWidth); + // Splits the edge where it crosses x = 0 and x = width. Parts left of the raster become vertical edges + // at x = 0, parts right of it do not affect any visible pixel. + float splits[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; + int count = 1; + if (x0 != x1) { + for (const float edge : { 0.0f, width }) { + const float t = (edge - x0) / (x1 - x0); + if (t > 0.0f && t < 1.0f) { + splits[count++] = t; + } + } + } + splits[count] = 1.0f; + if (count == 3 && splits[1] > splits[2]) { + std::swap(splits[1], splits[2]); + } + for (int i = 0; i < count; i++) { + const float ta = splits[i]; + const float tb = splits[i + 1]; + float ax = x0 + (x1 - x0) * ta; + const float ay = y0 + (y1 - y0) * ta; + float bx = x0 + (x1 - x0) * tb; + const float by = y0 + (y1 - y0) * tb; + const float middle = 0.5f * (ax + bx); + if (middle >= width) { + // Rows left of a skipped edge keep their coverage up to the right border + const int top = std::max(0, static_cast(std::min(ay, by))); + const int bottom = std::min(rasterHeight - 1, static_cast(std::ceil(std::max(ay, by))) - 1); + for (int row = top; row <= bottom; row++) { + rowLast[row] = static_cast(stride - 1); + } + maxColumn = stride - 1; + continue; + } + if (middle <= 0.0f) { + ax = 0.0f; + bx = 0.0f; + } + accumulate(std::clamp(ax, 0.0f, width), ay, std::clamp(bx, 0.0f, width), by); + } +} + +void Raster::accumulateAliased(float x0, float y0, float x1, float y1) { + float direction = 1.0f; + if (y0 > y1) { + std::swap(x0, x1); + std::swap(y0, y1); + direction = -1.0f; + } + // The rows whose centre lies inside the edge's span + const int firstRow = std::max(0, static_cast(std::ceil(y0 - 0.5f))); + const int lastRow = std::min(rasterHeight - 1, static_cast(std::ceil(y1 - 0.5f)) - 1); + if (firstRow > lastRow) { + return; + } + markRows(firstRow, lastRow); + const float slope = (x1 - x0) / (y1 - y0); + for (int row = firstRow; row <= lastRow; row++) { + const float x = x0 + (static_cast(row) + 0.5f - y0) * slope; + // Pixels whose centre is right of the edge are inside + const int column = std::clamp(static_cast(std::floor(x + 0.5f)), 0, stride - 1); + cells[row * stride + column] += direction; + minColumn = std::min(minColumn, column); + maxColumn = std::max(maxColumn, column); + if (column < rowFirst[row]) { + rowFirst[row] = static_cast(column); + } + if (column > rowLast[row]) { + rowLast[row] = static_cast(column); + } + } +} + +void Raster::accumulate(float x0, float y0, float x1, float y1) { + if (y0 == y1) { + return; + } + if (!antialias) { + accumulateAliased(x0, y0, x1, y1); + return; + } + float direction = 1.0f; + if (y0 > y1) { + std::swap(x0, x1); + std::swap(y0, y1); + direction = -1.0f; + } + const float slope = (x1 - x0) / (y1 - y0); + const int firstRow = static_cast(y0); + const int lastRow = std::min(rasterHeight - 1, static_cast(std::ceil(y1)) - 1); + if (firstRow > lastRow) { + return; + } + markRows(firstRow, lastRow); + for (int row = firstRow; row <= lastRow; row++) { + const float top = std::max(y0, static_cast(row)); + const float bottom = std::min(y1, static_cast(row + 1)); + const float dy = bottom - top; + if (dy <= 0.0f) { + continue; + } + const float xa = x0 + (top - y0) * slope; + const float xb = x0 + (bottom - y0) * slope; + const float left = std::min(xa, xb); + const float right = std::max(xa, xb); + const float area = dy * direction; + float* line = cells + row * stride; + const int firstColumn = static_cast(left); + const int lastColumn = std::min(stride - 1, static_cast(right) + 1); + minColumn = std::min(minColumn, firstColumn); + maxColumn = std::max(maxColumn, lastColumn); + if (firstColumn < rowFirst[row]) { + rowFirst[row] = static_cast(firstColumn); + } + if (lastColumn > rowLast[row]) { + rowLast[row] = static_cast(lastColumn); + } + if (right - left < 1e-4f) { + const float fraction = 1.0f - (left - static_cast(firstColumn)); + line[firstColumn] += area * fraction; + if (firstColumn + 1 < stride) { + line[firstColumn + 1] += area * (1.0f - fraction); + } + continue; + } + float previous = 0.0f; + for (int column = firstColumn; column <= lastColumn; column++) { + const auto c = static_cast(column); + const float cover = coverageIntegral(c + 1.0f, left, right) - coverageIntegral(c, left, right); + line[column] += area * (cover - previous); + previous = cover; + } + } +} + +void Raster::addSegment(float x0, float y0, float x1, float y1, float halfWidth) { + const float dx = x1 - x0; + const float dy = y1 - y0; + const float length = std::sqrt(dx * dx + dy * dy); + if (length < 1e-4f) { + return; + } + const float extent = halfWidth + 1.0f; + const auto width = static_cast(rasterWidth); + const auto height = static_cast(rasterHeight); + if ((x0 < -extent && x1 < -extent) || (x0 > width + extent && x1 > width + extent) || + (y0 < -extent && y1 < -extent) || (y0 > height + extent && y1 > height + extent)) { + return; + } + const float nx = -dy / length * halfWidth; + const float ny = dx / length * halfWidth; + addLine(x0 + nx, y0 + ny, x1 + nx, y1 + ny); + addLine(x1 + nx, y1 + ny, x1 - nx, y1 - ny); + addLine(x1 - nx, y1 - ny, x0 - nx, y0 - ny); + addLine(x0 - nx, y0 - ny, x0 + nx, y0 + ny); +} + +void Raster::addDot(float x, float y, float radius) { + const float extent = radius + 1.0f; + if (x < -extent || y < -extent || x > static_cast(rasterWidth) + extent || y > static_cast(rasterHeight) + extent) { + return; + } + const int segments = std::clamp(static_cast(radius * 2.0f) + 4, 6, MAX_DOT_SEGMENTS); + float previousX = x + radius; + float previousY = y; + for (int i = 1; i <= segments; i++) { + // Clockwise in screen coordinates, the same winding as the segment quads + const float angle = -2.0f * PI_F * static_cast(i) / static_cast(segments); + const float nextX = x + radius * std::cos(angle); + const float nextY = y + radius * std::sin(angle); + addLine(previousX, previousY, nextX, nextY); + previousX = nextX; + previousY = nextY; + } +} + +void Raster::fill(uint16_t* pixels, int pixelStride, uint16_t color, uint8_t opacity) { + if (isEmpty()) { + return; + } + const float scale = static_cast(opacity) * (32.0f / 255.0f); + for (int row = minRow; row <= maxRow; row++) { + const int first = rowFirst[row]; + int last = rowLast[row]; + if (last < first) { + continue; + } + // A row whose edges are all left of the raster is covered up to the right border + float* line = cells + row * stride; + uint16_t* out = pixels + row * pixelStride; + float sum = 0.0f; + for (int column = first; column <= last; column++) { + sum += line[column]; + line[column] = 0.0f; + if (column >= rasterWidth) { + continue; + } + const float coverage = std::min(1.0f, std::fabs(sum)); + const auto alpha = static_cast(coverage * scale + 0.5f); + if (alpha >= 32) { + out[column] = color; + } else if (alpha > 0) { + out[column] = blendRgb565(color, out[column], alpha); + } + if (column == last && last < rasterWidth - 1 && std::fabs(sum) > 1e-3f) { + last = rasterWidth - 1; + } + } + rowFirst[row] = static_cast(stride); + rowLast[row] = -1; + } + minRow = rasterHeight; + maxRow = -1; + minColumn = stride; + maxColumn = 0; +} + +void Raster::clear() { + if (cells != nullptr && !isEmpty()) { + for (int row = minRow; row <= maxRow; row++) { + memset(cells + row * stride, 0, static_cast(stride) * sizeof(float)); + } + resetRows(); + } + minRow = rasterHeight; + maxRow = -1; + minColumn = stride; + maxColumn = 0; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/Raster.h b/Apps/Maps/main/Source/render/Raster.h new file mode 100644 index 0000000..5a23f21 --- /dev/null +++ b/Apps/Maps/main/Source/render/Raster.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +namespace maps { + +/** + * Anti-aliased rasteriser with exact area coverage. Edges add signed area deltas to an accumulation buffer, + * and fill() turns the running sum of each row into coverage, so one shape made of many polygons, quads and + * dots is blended only once. Coverage is min(1, |winding|), which gives nonzero filling. + * The buffer is allocated once in init(), shapes allocate nothing. + */ +class Raster { +public: + Raster() = default; + ~Raster(); + Raster(const Raster&) = delete; + Raster& operator=(const Raster&) = delete; + + bool init(int width, int height); + + void addLine(float x0, float y0, float x1, float y1); + + /** A stroke segment as a quad, wound like the dots so overlaps never cancel. */ + void addSegment(float x0, float y0, float x1, float y1, float halfWidth); + /** A round join or cap. */ + void addDot(float x, float y, float radius); + + /** + * Without anti-aliasing each edge adds one step per row, at the pixel centres: cheaper, for many small + * shapes where the soft edge does not show. Change it only while the buffer is empty. + */ + void setAntialias(bool enabled) { antialias = enabled; } + + /** Blends the accumulated shape into pixels with the given colour and clears the buffer. */ + void fill(uint16_t* pixels, int stride, uint16_t color, uint8_t opacity); + void clear(); + + bool isEmpty() const { return minRow > maxRow; } + int width() const { return rasterWidth; } + int height() const { return rasterHeight; } + +private: + void addClippedLine(float x0, float y0, float x1, float y1); + void accumulate(float x0, float y0, float x1, float y1); + void accumulateAliased(float x0, float y0, float x1, float y1); + void markRows(int top, int bottom); + void resetRows(); + + float* cells = nullptr; + /** First and last touched column of each row, so fill() skips the untouched part of a row. */ + int16_t* rowFirst = nullptr; + int16_t* rowLast = nullptr; + int rasterWidth = 0; + int rasterHeight = 0; + int stride = 0; + int minRow = 0; + int maxRow = -1; + int minColumn = 0; + int maxColumn = 0; + bool antialias = true; +}; + +uint16_t blendRgb565(uint16_t foreground, uint16_t background, uint32_t alpha32); + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/Style.cpp b/Apps/Maps/main/Source/render/Style.cpp new file mode 100644 index 0000000..97ac7a0 --- /dev/null +++ b/Apps/Maps/main/Source/render/Style.cpp @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Style.h" + +#include +#include + +namespace maps { + +namespace { + +constexpr uint16_t rgb(uint32_t hex) { + return static_cast(((hex >> 8) & 0xF800) | ((hex >> 5) & 0x07E0) | ((hex >> 3) & 0x001F)); +} + +constexpr Palette COLOR_PALETTE = { + .background = rgb(0xF2EFE9), + .placeholder = rgb(0xE4E1DB), + .water = rgb(0xAAD3DF), + .wood = rgb(0xADD19E), + .grass = rgb(0xCDEBB0), + .farmland = rgb(0xEEF0D5), + .wetland = rgb(0xD6E8D2), + .sand = rgb(0xF5E9C6), + .ice = rgb(0xDDECEC), + .rock = rgb(0xE2DED8), + .residential = rgb(0xE0DFDF), + .commercial = rgb(0xF2DAD9), + .industrial = rgb(0xEBDBE8), + .cemetery = rgb(0xAACBAF), + .hospital = rgb(0xFFF0F0), + .school = rgb(0xFFFFE5), + .pitch = rgb(0xAAE0CB), + .military = rgb(0xF3E2E1), + .park = rgb(0xC8FACC), + .building = rgb(0xD9D0C9), + .aerodrome = rgb(0xE9E7E2), + .runway = rgb(0xBBBBCC), + .boundary = rgb(0x8D618B), + .motorway = rgb(0xE892A2), + .motorwayCasing = rgb(0xC24E6B), + .trunk = rgb(0xF9B29C), + .trunkCasing = rgb(0xC84E2F), + .primary = rgb(0xFCD6A4), + .primaryCasing = rgb(0xA06B00), + .secondary = rgb(0xF7FABF), + .secondaryCasing = rgb(0x707D05), + .minor = rgb(0xFFFFFF), + .minorCasing = rgb(0xB8B4AE), + .path = rgb(0xC08A5A), + .rail = rgb(0x707070), + .pier = rgb(0xF2EFE9), + .text = rgb(0x202020), + .halo = rgb(0xFFFFFF), + .marker = rgb(0x1A73E8), +}; + +constexpr Palette MONOCHROME_PALETTE = { + .background = rgb(0xFFFFFF), + .placeholder = rgb(0xEEEEEE), + .water = rgb(0xA0A0A0), + .wood = rgb(0xC8C8C8), + .grass = rgb(0xE0E0E0), + .farmland = rgb(0xFFFFFF), + .wetland = rgb(0xD8D8D8), + .sand = rgb(0xF0F0F0), + .ice = rgb(0xF0F0F0), + .rock = rgb(0xE8E8E8), + .residential = rgb(0xF4F4F4), + .commercial = rgb(0xF0F0F0), + .industrial = rgb(0xECECEC), + .cemetery = rgb(0xD0D0D0), + .hospital = rgb(0xF4F4F4), + .school = rgb(0xF4F4F4), + .pitch = rgb(0xE0E0E0), + .military = rgb(0xECECEC), + .park = rgb(0xDCDCDC), + .building = rgb(0xB8B8B8), + .aerodrome = rgb(0xF0F0F0), + .runway = rgb(0x909090), + .boundary = rgb(0x404040), + .motorway = rgb(0xFFFFFF), + .motorwayCasing = rgb(0x000000), + .trunk = rgb(0xFFFFFF), + .trunkCasing = rgb(0x000000), + .primary = rgb(0xFFFFFF), + .primaryCasing = rgb(0x000000), + .secondary = rgb(0xFFFFFF), + .secondaryCasing = rgb(0x202020), + .minor = rgb(0xFFFFFF), + .minorCasing = rgb(0x404040), + .path = rgb(0x303030), + .rail = rgb(0x000000), + .pier = rgb(0xFFFFFF), + .text = rgb(0x000000), + .halo = rgb(0xFFFFFF), + .marker = rgb(0x000000), +}; + +enum class RoadKind : uint8_t { + None, + Motorway, + Trunk, + Primary, + Secondary, + Tertiary, + Minor, + Service, + Path, + Rail, + Pier +}; + +struct RoadSpec { + float widthAt14; + int8_t minZoom; + int8_t casingMinZoom; +}; + +constexpr RoadSpec ROAD_SPECS[] = { + { 0.0f, 99, 99 }, // None + { 4.5f, 0, 10 }, // Motorway + { 4.0f, 5, 11 }, // Trunk + { 3.5f, 8, 12 }, // Primary + { 3.0f, 9, 12 }, // Secondary + { 2.6f, 11, 13 }, // Tertiary + { 1.8f, 13, 14 }, // Minor + { 1.1f, 14, 15 }, // Service + { 0.8f, 14, 99 }, // Path + { 1.0f, 10, 99 }, // Rail + { 1.6f, 14, 99 }, // Pier +}; + +RoadKind roadKind(Value featureClass) { + switch (featureClass) { + case Value::motorway: + return RoadKind::Motorway; + case Value::trunk: + return RoadKind::Trunk; + case Value::primary: + return RoadKind::Primary; + case Value::secondary: + return RoadKind::Secondary; + case Value::tertiary: + return RoadKind::Tertiary; + case Value::minor: + case Value::raceway: + case Value::busway: + return RoadKind::Minor; + case Value::service: + return RoadKind::Service; + case Value::path: + case Value::track: + return RoadKind::Path; + case Value::rail: + case Value::transit: + return RoadKind::Rail; + case Value::pier: + return RoadKind::Pier; + default: + return RoadKind::None; + } +} + +/** Line widths grow slower than the map below z14 and close to the map above it. */ +float zoomScale(int zoom) { + const float steps = static_cast(zoom - 14); + return std::pow(2.0f, steps * (zoom < 14 ? 0.55f : 0.85f)); +} + +bool roadStyle(const FeatureInfo& info, Stage stage, int zoom, const Palette& palette, DrawStyle& style) { + const RoadKind kind = roadKind(info.featureClass); + if (kind == RoadKind::None) { + return false; + } + const RoadSpec& spec = ROAD_SPECS[static_cast(kind)]; + if (zoom < spec.minZoom) { + return false; + } + if (info.geometryType == GEOMETRY_POLYGON) { + if (kind != RoadKind::Pier || stage != Stage::RoadFill) { + return false; + } + style = { palette.pier, 255, 0.0f }; + return true; + } + if (info.geometryType != GEOMETRY_LINE) { + return false; + } + const bool tunnel = info.brunnel == Value::tunnel; + float width = std::fmax(0.6f, spec.widthAt14 * zoomScale(zoom)); + if (kind == RoadKind::Path) { + width = std::fmin(1.4f, width); + } + uint16_t fill; + uint16_t casing; + switch (kind) { + case RoadKind::Motorway: + fill = palette.motorway; + casing = palette.motorwayCasing; + break; + case RoadKind::Trunk: + fill = palette.trunk; + casing = palette.trunkCasing; + break; + case RoadKind::Primary: + fill = palette.primary; + casing = palette.primaryCasing; + break; + case RoadKind::Secondary: + fill = palette.secondary; + casing = palette.secondaryCasing; + break; + case RoadKind::Path: + fill = palette.path; + casing = palette.path; + break; + case RoadKind::Rail: + fill = palette.rail; + casing = palette.rail; + break; + case RoadKind::Pier: + fill = palette.pier; + casing = palette.minorCasing; + break; + default: + fill = palette.minor; + casing = palette.minorCasing; + break; + } + if (stage == Stage::RoadCasing) { + if (tunnel || zoom < spec.casingMinZoom) { + return false; + } + style = { casing, 255, width + std::fmax(1.0f, width * 0.35f) }; + return true; + } + // Roads without a casing at this zoom level use the casing colour, so white roads stay visible + const bool cased = zoom >= spec.casingMinZoom && kind != RoadKind::Path && kind != RoadKind::Rail; + uint8_t opacity = tunnel ? 110 : 255; + if (kind == RoadKind::Path) { + opacity = zoom < 15 ? 120 : 190; + } + style = { cased || kind == RoadKind::Path || kind == RoadKind::Rail ? fill : casing, opacity, width }; + return true; +} + +bool waterwayStyle(const FeatureInfo& info, int zoom, const Palette& palette, DrawStyle& style) { + if (info.geometryType != GEOMETRY_LINE) { + return false; + } + float width; + int minZoom; + switch (info.featureClass) { + case Value::river: + width = 2.2f; + minZoom = 8; + break; + case Value::canal: + width = 1.8f; + minZoom = 11; + break; + case Value::stream: + width = 0.9f; + minZoom = 13; + break; + default: + width = 0.7f; + minZoom = 14; + break; + } + if (zoom < minZoom) { + return false; + } + style = { palette.water, static_cast(info.brunnel == Value::tunnel ? 90 : 255), std::fmax(0.6f, width * zoomScale(zoom)) }; + return true; +} + +bool areaColor(const FeatureInfo& info, const Palette& palette, uint16_t& color, uint8_t& opacity) { + opacity = 255; + switch (info.layer) { + case Layer::Water: + color = palette.water; + return true; + case Layer::Park: + color = palette.park; + opacity = 140; + return true; + case Layer::Building: + color = palette.building; + return true; + case Layer::Landcover: + switch (info.featureClass) { + case Value::wood: + color = palette.wood; + return true; + case Value::grass: + color = info.subclass == Value::park || info.subclass == Value::garden ? palette.park : palette.grass; + return true; + case Value::farmland: + color = palette.farmland; + return true; + case Value::wetland: + color = palette.wetland; + return true; + case Value::sand: + color = palette.sand; + return true; + case Value::ice: + color = palette.ice; + return true; + case Value::rock: + color = palette.rock; + return true; + default: + return false; + } + case Layer::Landuse: + switch (info.featureClass) { + case Value::residential: + case Value::suburb: + case Value::quarter: + case Value::neighbourhood: + color = palette.residential; + opacity = 150; + return true; + case Value::commercial: + case Value::retail: + color = palette.commercial; + return true; + case Value::industrial: + case Value::garages: + case Value::railway: + case Value::dam: + color = palette.industrial; + return true; + case Value::cemetery: + color = palette.cemetery; + return true; + case Value::hospital: + color = palette.hospital; + return true; + case Value::school: + case Value::university: + case Value::college: + case Value::kindergarten: + color = palette.school; + return true; + case Value::pitch: + case Value::stadium: + case Value::playground: + case Value::track: + case Value::theme_park: + case Value::zoo: + color = palette.pitch; + return true; + case Value::military: + color = palette.military; + return true; + default: + return false; + } + default: + return false; + } +} + +} // namespace + +Layer stageLayer(Stage stage) { + switch (stage) { + case Stage::Landcover: + return Layer::Landcover; + case Stage::Landuse: + return Layer::Landuse; + case Stage::Park: + return Layer::Park; + case Stage::Water: + return Layer::Water; + case Stage::Waterway: + return Layer::Waterway; + case Stage::Aeroway: + return Layer::Aeroway; + case Stage::Boundary: + return Layer::Boundary; + case Stage::Building: + return Layer::Building; + case Stage::RoadCasing: + case Stage::RoadFill: + return Layer::Transportation; + default: + return Layer::Unknown; + } +} + +void styleCacheId(bool monochrome, char* out, size_t size) { + snprintf(out, size, "%c%u", monochrome ? 'm' : 'c', static_cast(STYLE_VERSION)); +} + +const Palette& colorPalette() { + return COLOR_PALETTE; +} + +const Palette& monochromePalette() { + return MONOCHROME_PALETTE; +} + +bool styleFor(const FeatureInfo& info, Stage stage, int zoom, const Palette& palette, DrawStyle& style) { + switch (stage) { + case Stage::Landcover: + case Stage::Landuse: + case Stage::Park: + case Stage::Water: + case Stage::Building: { + if (info.geometryType != GEOMETRY_POLYGON) { + return false; + } + if (stage == Stage::Building && zoom < 13) { + return false; + } + uint16_t color; + uint8_t opacity; + if (!areaColor(info, palette, color, opacity)) { + return false; + } + style = { color, opacity, 0.0f }; + return true; + } + case Stage::Waterway: + return waterwayStyle(info, zoom, palette, style); + case Stage::Aeroway: + if (info.geometryType == GEOMETRY_POLYGON) { + const bool paved = info.featureClass == Value::runway || info.featureClass == Value::taxiway || info.featureClass == Value::apron; + style = { paved ? palette.runway : palette.aerodrome, 255, 0.0f }; + return info.featureClass != Value::None; + } + if (info.geometryType == GEOMETRY_LINE && (info.featureClass == Value::runway || info.featureClass == Value::taxiway)) { + const float width = info.featureClass == Value::runway ? 6.0f : 2.0f; + style = { palette.runway, 255, std::fmax(0.8f, width * zoomScale(zoom)) }; + return zoom >= 11; + } + return false; + case Stage::Boundary: + if (info.geometryType != GEOMETRY_LINE || info.maritime || info.adminLevel < 0 || info.adminLevel > 4) { + return false; + } + style = { palette.boundary, 170, info.adminLevel <= 2 ? 1.4f : 0.9f }; + return true; + case Stage::RoadCasing: + case Stage::RoadFill: + return roadStyle(info, stage, zoom, palette, style); + default: + return false; + } +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/Style.h b/Apps/Maps/main/Source/render/Style.h new file mode 100644 index 0000000..f3bc7af --- /dev/null +++ b/Apps/Maps/main/Source/render/Style.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../mvt/Schema.h" + +#include +#include + +namespace maps { + +/** Draw stages in painting order. Each stage draws one kind of feature of one layer. */ +enum class Stage : uint8_t { + Landcover, + Landuse, + Park, + Water, + Waterway, + Aeroway, + Boundary, + Building, + RoadCasing, + RoadFill, + Count +}; + +Layer stageLayer(Stage stage); + +/** Changes whenever the style changes, so tiles rendered with an older style are rendered again. */ +constexpr uint32_t STYLE_VERSION = 3; + +/** A short, 8.3-safe name for the style and palette, the SD card cache folder of their rendered tiles. */ +void styleCacheId(bool monochrome, char* out, size_t size); + +struct Palette { + uint16_t background; + uint16_t placeholder; + uint16_t water; + uint16_t wood; + uint16_t grass; + uint16_t farmland; + uint16_t wetland; + uint16_t sand; + uint16_t ice; + uint16_t rock; + uint16_t residential; + uint16_t commercial; + uint16_t industrial; + uint16_t cemetery; + uint16_t hospital; + uint16_t school; + uint16_t pitch; + uint16_t military; + uint16_t park; + uint16_t building; + uint16_t aerodrome; + uint16_t runway; + uint16_t boundary; + uint16_t motorway; + uint16_t motorwayCasing; + uint16_t trunk; + uint16_t trunkCasing; + uint16_t primary; + uint16_t primaryCasing; + uint16_t secondary; + uint16_t secondaryCasing; + uint16_t minor; + uint16_t minorCasing; + uint16_t path; + uint16_t rail; + uint16_t pier; + uint16_t text; + uint16_t halo; + uint16_t marker; +}; + +const Palette& colorPalette(); +/** High contrast palette for monochrome and grayscale displays. */ +const Palette& monochromePalette(); + +struct DrawStyle { + uint16_t color; + uint8_t opacity; + /** Filled polygon when 0, otherwise a stroke of this width in pixels. */ + float lineWidth; +}; + +/** + * Picks how a feature is drawn in a stage. zoom is the zoom level of the rendered tile, which can be above the + * zoom level of the source data. Returns false when the feature is not drawn in this stage. + */ +bool styleFor(const FeatureInfo& info, Stage stage, int zoom, const Palette& palette, DrawStyle& style); + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/TileCache.cpp b/Apps/Maps/main/Source/render/TileCache.cpp new file mode 100644 index 0000000..ef6f489 --- /dev/null +++ b/Apps/Maps/main/Source/render/TileCache.cpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "TileCache.h" + +#include "../platform/Platform.h" + +namespace maps { + +TileCache::~TileCache() { + for (int i = 0; i < count; i++) { + largeFree(slots[i].pixels); + } + largeFree(slots); +} + +int TileCache::init(int maxSlots) { + slots = static_cast(largeAlloc(sizeof(Slot) * maxSlots)); + if (slots == nullptr) { + return 0; + } + count = 0; + for (int i = 0; i < maxSlots; i++) { + auto* pixels = static_cast(largeAlloc(TILE_BYTES)); + if (pixels == nullptr) { + break; + } + slots[count++] = { { -1, 0, 0 }, 0, false, false, false, 0, pixels }; + } + return count; +} + +const uint16_t* TileCache::find(const TileKey& tile) { + for (int i = 0; i < count; i++) { + if (slots[i].valid && slots[i].key == tile) { + slots[i].lastUse = ++useCounter; + return slots[i].pixels; + } + } + return nullptr; +} + +const uint16_t* TileCache::peek(const TileKey& tile) const { + for (int i = 0; i < count; i++) { + if (slots[i].valid && slots[i].key == tile) { + return slots[i].pixels; + } + } + return nullptr; +} + +bool TileCache::contains(const TileKey& tile) const { + for (int i = 0; i < count; i++) { + if (slots[i].valid && !slots[i].stale && slots[i].key == tile) { + return true; + } + } + return false; +} + +uint16_t* TileCache::reserve(const TileKey& tile) { + if (count == 0) { + return nullptr; + } + int victim = -1; + for (int i = 0; i < count; i++) { + if (slots[i].reserved) { + continue; + } + if (!slots[i].valid) { + victim = i; + break; + } + if (victim < 0 || slots[i].lastUse < slots[victim].lastUse) { + victim = i; + } + } + if (victim < 0) { + return nullptr; + } + slots[victim].valid = false; + slots[victim].reserved = true; + slots[victim].key = tile; + return slots[victim].pixels; +} + +void TileCache::commit(const TileKey& tile, int sourceZoom) { + int committed = -1; + for (int i = 0; i < count && committed < 0; i++) { + if (slots[i].reserved && slots[i].key == tile) { + slots[i].valid = true; + slots[i].stale = false; + slots[i].sourceZoom = static_cast(sourceZoom); + slots[i].reserved = false; + slots[i].lastUse = ++useCounter; + committed = i; + } + } + // The stale copy it replaces goes + for (int i = 0; i < count && committed >= 0; i++) { + if (i != committed && slots[i].valid && slots[i].key == tile) { + slots[i].valid = false; + } + } +} + +void TileCache::release(const TileKey& tile) { + for (int i = 0; i < count; i++) { + if (slots[i].reserved && slots[i].key == tile) { + slots[i].reserved = false; + return; + } + } +} + +void TileCache::dropMagnified() { + for (int i = 0; i < count; i++) { + const int best = slots[i].key.z < MAX_SOURCE_ZOOM ? slots[i].key.z : MAX_SOURCE_ZOOM; + if (slots[i].valid && slots[i].sourceZoom < best) { + slots[i].stale = true; + } + } +} + +void TileCache::clear() { + for (int i = 0; i < count; i++) { + if (!slots[i].reserved) { + slots[i].valid = false; + } + } +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/TileCache.h b/Apps/Maps/main/Source/render/TileCache.h new file mode 100644 index 0000000..a885975 --- /dev/null +++ b/Apps/Maps/main/Source/render/TileCache.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../tiles/TileMath.h" + +#include + +namespace maps { + +/** Rendered tiles in a fixed number of slots, the least recently used slot is reused first. */ +class TileCache { +public: + static constexpr int TILE_BYTES = TILE_SIZE * TILE_SIZE * 2; + + ~TileCache(); + + /** Allocates up to maxSlots tiles, fewer when memory runs out. Returns the number of slots. */ + int init(int maxSlots); + + /** The pixels of a cached tile, marking it as recently used, or nullptr. Stale tiles are found too. */ + const uint16_t* find(const TileKey& tile); + /** Whether a current (not stale) copy of the tile is cached, so it needs no drawing. */ + bool contains(const TileKey& tile) const; + /** The pixels of a cached tile without marking it as used. */ + const uint16_t* peek(const TileKey& tile) const; + + /** A slot for a new tile, which is not findable until commit() and is not reused until commit() or release(). */ + uint16_t* reserve(const TileKey& tile); + /** sourceZoom is the zoom level of the vector tile the pixels were drawn from. Replaces a stale copy. */ + void commit(const TileKey& tile, int sourceZoom); + void release(const TileKey& tile); + + void clear(); + /** + * Marks tiles drawn magnified from a lower zoom level as stale, after better data may have arrived. They stay + * on screen until they are drawn again, so the map does not blink while a download stores tiles. + */ + void dropMagnified(); + int slotCount() const { return count; } + +private: + struct Slot { + TileKey key; + uint32_t lastUse; + bool valid; + bool stale; + bool reserved; + int8_t sourceZoom; + uint16_t* pixels; + }; + + Slot* slots = nullptr; + int count = 0; + uint32_t useCounter = 0; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/TileCodec.cpp b/Apps/Maps/main/Source/render/TileCodec.cpp new file mode 100644 index 0000000..ceb0f5a --- /dev/null +++ b/Apps/Maps/main/Source/render/TileCodec.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "TileCodec.h" + +namespace maps { + +namespace { + +constexpr uint16_t RUN_FLAG = 0x8000; +constexpr size_t MAX_TOKEN = 0x7FFF; +constexpr size_t MIN_RUN = 3; + +} // namespace + +size_t encodeTile(const uint16_t* pixels, size_t count, uint16_t* out, size_t outCapacity) { + size_t used = 0; + size_t position = 0; + while (position < count) { + size_t run = 1; + while (position + run < count && run < MAX_TOKEN && pixels[position + run] == pixels[position]) { + run++; + } + if (run >= MIN_RUN) { + if (used + 2 > outCapacity) { + return 0; + } + out[used++] = static_cast(RUN_FLAG | run); + out[used++] = pixels[position]; + position += run; + continue; + } + // Literal pixels up to the next run of MIN_RUN equal pixels + size_t literal = 0; + while (position + literal < count && literal < MAX_TOKEN) { + const size_t next = position + literal; + if (next + MIN_RUN <= count && pixels[next] == pixels[next + 1] && pixels[next] == pixels[next + 2]) { + break; + } + literal++; + } + if (used + 1 + literal > outCapacity) { + return 0; + } + out[used++] = static_cast(literal); + for (size_t i = 0; i < literal; i++) { + out[used++] = pixels[position + i]; + } + position += literal; + } + return used; +} + +bool decodeTile(const uint16_t* data, size_t dataCount, uint16_t* pixels, size_t count) { + size_t read = 0; + size_t written = 0; + while (read < dataCount && written < count) { + const uint16_t token = data[read++]; + const size_t length = token & MAX_TOKEN; + if (length == 0 || written + length > count) { + return false; + } + if (token & RUN_FLAG) { + if (read >= dataCount) { + return false; + } + const uint16_t pixel = data[read++]; + for (size_t i = 0; i < length; i++) { + pixels[written++] = pixel; + } + } else { + if (read + length > dataCount) { + return false; + } + for (size_t i = 0; i < length; i++) { + pixels[written++] = data[read++]; + } + } + } + return written == count && read == dataCount; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/TileCodec.h b/Apps/Maps/main/Source/render/TileCodec.h new file mode 100644 index 0000000..ea0aa7d --- /dev/null +++ b/Apps/Maps/main/Source/render/TileCodec.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +namespace maps { + +/** + * Run-length coding of rendered RGB565 tiles for the SD card cache, where reading is the slow part. + * The data is a sequence of 16-bit tokens: a token with the top bit set repeats the next pixel (token & 0x7FFF) + * times, otherwise the token counts the literal pixels that follow. + */ +size_t encodeTile(const uint16_t* pixels, size_t count, uint16_t* out, size_t outCapacity); +bool decodeTile(const uint16_t* data, size_t dataCount, uint16_t* pixels, size_t count); + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/TileRenderer.cpp b/Apps/Maps/main/Source/render/TileRenderer.cpp new file mode 100644 index 0000000..f7aa930 --- /dev/null +++ b/Apps/Maps/main/Source/render/TileRenderer.cpp @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "TileRenderer.h" + +#include "../platform/Platform.h" +#include "PreparedTile.h" + +#include + +namespace maps { + +namespace { + +/** Thinner lines get no round joins or caps, they would not show. */ +constexpr float MIN_JOIN_WIDTH = 1.5f; +/** Cosine of the smallest turn that gets a round join, about 8 degrees. */ +constexpr float JOIN_COSINE = 0.99f; +/** Buildings smaller than this, in pixels both ways, are left out at native z14. */ +constexpr float MIN_BUILDING_PIXELS = 2.0f; + +} // namespace + +TileRenderer::~TileRenderer() { + largeFree(body); +} + +bool TileRenderer::init() { + return decoder.init() && raster.init(TILE_SIZE, TILE_SIZE); +} + +void TileRenderer::begin(const TileKey& source, const TileKey& target, const Palette& tilePalette, uint16_t* out) { + palette = &tilePalette; + pixels = out; + zoom = target.z; + featureCount = 0; + geometryUs = 0; + fillUs = 0; + buildingUs = 0; + for (int i = 0; i < TILE_SIZE * TILE_SIZE; i++) { + pixels[i] = palette->background; + } + raster.clear(); + cancelled = false; + dz = target.z - source.z; + offsetX = static_cast((target.x - (source.x << dz)) * TILE_SIZE); + offsetY = static_cast((target.y - (source.y << dz)) * TILE_SIZE); +} + +void TileRenderer::finishStats(RenderStats* stats, uint32_t start, uint32_t scanned, uint32_t sweeps) const { + if (stats == nullptr) { + return; + } + stats->totalMs = nowMs() - start; + stats->scanMs = scanned - start; + stats->sweeps = sweeps; + stats->features = featureCount; + stats->geometryMs = static_cast(geometryUs / 1000); + stats->fillMs = static_cast(fillUs / 1000); + stats->geometryUs = static_cast(geometryUs); + stats->fillUs = static_cast(fillUs); + stats->buildingUs = static_cast(buildingUs); +} + +bool TileRenderer::render(ByteInput* input, const TileKey& source, const TileKey& target, const Palette& tilePalette, uint16_t* out, RenderStats* stats) { + const uint32_t start = nowMs(); + preparedExtent = 0; + begin(source, target, tilePalette, out); + + if (!decoder.scan(input)) { + logError("Tile %d/%d/%d could not be read", source.z, source.x, source.y); + return false; + } + const uint32_t scanned = nowMs(); + + // Stages are drawn in order. A sweep reads the tile once and draws consecutive stages as long as their + // layers come later in the file than the layer of the previous stage of that sweep. + uint32_t sweeps = 0; + int next = 0; + constexpr int stageCount = static_cast(Stage::Count); + while (next < stageCount) { + Stage sweepStages[stageCount]; + Layer sweepLayers[stageCount]; + int count = 0; + int lastIndex = -1; + while (next < stageCount) { + const auto candidate = static_cast(next); + const Layer layer = stageLayer(candidate); + const int index = decoder.fileIndex(layer); + if (index < 0) { + next++; + continue; + } + if (index <= lastIndex) { + break; + } + sweepStages[count] = candidate; + sweepLayers[count] = layer; + count++; + lastIndex = index; + next++; + } + if (count == 0) { + break; + } + // One sweep visits each layer once, so its stages all use different layers + for (int i = 0; i < count; i++) { + layerStages[static_cast(sweepLayers[i])] = sweepStages[i]; + } + if (!decoder.sweep(sweepLayers, count, *this)) { + if (!cancelled) { + logError("Tile %d/%d/%d is damaged", source.z, source.x, source.y); + } + return false; + } + sweeps++; + } + + finishStats(stats, start, scanned, sweeps); + if (stats != nullptr) { + stats->decompressedBytes = static_cast(decoder.decompressedSize()); + } + return true; +} + +namespace { + +bool readVarint(const uint8_t*& data, const uint8_t* end, uint64_t& value) { + value = 0; + for (int shift = 0; shift < 64 && data < end; shift += 7) { + const uint8_t byte = *data++; + value |= static_cast(byte & 0x7F) << shift; + if ((byte & 0x80) == 0) { + return true; + } + } + return false; +} + +int32_t unzigzag(uint64_t value) { + return static_cast((value >> 1) ^ (~(value & 1) + 1)); +} + +} // namespace + +bool TileRenderer::renderPrepared(const uint8_t* data, size_t size, const TileKey& target, const Palette& tilePalette, uint16_t* out, RenderStats* stats) { + const uint32_t start = nowMs(); + PreparedHeader header {}; + if (!readPreparedHeader(data, size, header) || header.extent <= 0 || PREPARED_HEADER_SIZE + header.storedSize > size) { + logError("Prepared tile for %d/%d/%d is damaged", target.z, target.x, target.y); + return false; + } + if (target.z < header.tile.z || !(ancestorOf(target, header.tile.z) == header.tile)) { + return false; + } + const uint8_t* bodyStart = data + PREPARED_HEADER_SIZE; + if ((header.flags & PREPARED_FLAG_DEFLATED) != 0) { + if (header.bodySize > bodyCapacity) { + largeFree(body); + bodyCapacity = (header.bodySize + 0xFFFF) & ~static_cast(0xFFFF); + body = static_cast(largeAlloc(bodyCapacity)); + if (body == nullptr) { + bodyCapacity = 0; + return false; + } + } + MemoryInput stored(bodyStart, header.storedSize); + if (!decoder.inflate(&stored, body, header.bodySize)) { + logError("Prepared tile %d/%d/%d does not inflate", header.tile.z, header.tile.x, header.tile.y); + return false; + } + bodyStart = body; + } else if (header.bodySize != header.storedSize) { + return false; + } + const uint32_t inflated = nowMs(); + const uint8_t* bodyEnd = bodyStart + header.bodySize; + + // Where each section starts, then the stages in draw order + const uint8_t* sectionData[static_cast(Stage::Count)] = {}; + const uint8_t* sectionEnd[static_cast(Stage::Count)] = {}; + const uint8_t* cursor = bodyStart; + while (cursor < bodyEnd && *cursor != 0xFF) { + const uint8_t id = *cursor++; + uint64_t count; + uint64_t length; + if (id >= static_cast(Stage::Count) || !readVarint(cursor, bodyEnd, count) || !readVarint(cursor, bodyEnd, length) || + length > static_cast(bodyEnd - cursor)) { + logError("Prepared tile %d/%d/%d is damaged", header.tile.z, header.tile.x, header.tile.y); + return false; + } + sectionData[id] = cursor; + sectionEnd[id] = cursor + length; + cursor += length; + } + + preparedExtent = header.extent; + begin(header.tile, target, tilePalette, out); + for (int i = 0; i < static_cast(Stage::Count); i++) { + const auto drawStage = static_cast(i); + const int section = static_cast(preparedSection(drawStage)); + if (sectionData[section] != nullptr && !playSection(sectionData[section], sectionEnd[section], drawStage)) { + if (!cancelled) { + logError("Prepared tile %d/%d/%d is damaged", header.tile.z, header.tile.x, header.tile.y); + } + preparedExtent = 0; + return false; + } + } + preparedExtent = 0; + finishStats(stats, start, inflated, 1); + if (stats != nullptr) { + stats->decompressedBytes = header.bodySize; + } + return true; +} + +bool TileRenderer::playSection(const uint8_t* data, const uint8_t* end, Stage drawStage) { + const Layer layer = stageLayer(preparedSection(drawStage)); + layerStages[static_cast(layer)] = drawStage; + TilePoint chunk[64]; + while (data < end) { + if (shouldStop()) { + return false; + } + FeatureInfo info; + info.layer = layer; + const uint8_t flags = *data++; + info.geometryType = flags & 3; + info.maritime = (flags & (1 << 2)) != 0; + info.ramp = (flags & (1 << 3)) != 0; + uint64_t value; + if (!readVarint(data, end, value)) { + return false; + } + info.featureClass = static_cast(value); + if ((flags & (1 << 4)) != 0) { + if (!readVarint(data, end, value)) { + return false; + } + info.subclass = static_cast(value); + } + if ((flags & (1 << 5)) != 0) { + if (!readVarint(data, end, value)) { + return false; + } + info.brunnel = static_cast(value); + } + if ((flags & (1 << 6)) != 0) { + if (data >= end) { + return false; + } + info.adminLevel = static_cast(*data++); + } + uint64_t rings; + if (!readVarint(data, end, rings)) { + return false; + } + const bool drawn = beginFeature(info); + int32_t x = 0; + int32_t y = 0; + for (uint64_t ring = 0; ring < rings; ring++) { + uint64_t ringHeader; + if (!readVarint(data, end, ringHeader)) { + return false; + } + const uint64_t pointCount = ringHeader >> 1; + if (drawn) { + beginRing(); + } + int count = 0; + for (uint64_t i = 0; i < pointCount; i++) { + uint64_t dx; + uint64_t dy; + if (!readVarint(data, end, dx) || !readVarint(data, end, dy)) { + return false; + } + x += unzigzag(dx); + y += unzigzag(dy); + if (!drawn) { + continue; + } + chunk[count++] = { x, y }; + if (count == 64) { + points(chunk, count); + count = 0; + } + } + if (drawn) { + points(chunk, count); + endRing((ringHeader & 1) != 0); + } + } + if (drawn) { + endFeature(); + } + } + return true; +} + +bool TileRenderer::beginFeature(const FeatureInfo& info) { + stage = layerStages[static_cast(info.layer)]; + const int extent = preparedExtent > 0 ? preparedExtent : decoder.extent(info.layer); + scale = static_cast(TILE_SIZE << dz) / static_cast(extent); + if (!styleFor(info, stage, zoom, *palette, style)) { + return false; + } + featureCount++; + featureStartUs = nowUs(); + // At the tile's own z14 buildings are many and small, overzoomed views keep full detail + cheapFeature = stage == Stage::Building && dz == 0 && zoom == MAX_SOURCE_ZOOM; + raster.setAntialias(!cheapFeature); + boundsMinX = 1e30f; + boundsMinY = 1e30f; + boundsMaxX = -1e30f; + boundsMaxY = -1e30f; + return true; +} + +void TileRenderer::beginRing() { + firstPoint = true; +} + +void TileRenderer::addPoint(float x, float y) { + if (cheapFeature) { + boundsMinX = std::fmin(boundsMinX, x); + boundsMinY = std::fmin(boundsMinY, y); + boundsMaxX = std::fmax(boundsMaxX, x); + boundsMaxY = std::fmax(boundsMaxY, y); + } + if (firstPoint) { + firstPoint = false; + hasDirection = false; + startX = x; + startY = y; + if (style.lineWidth >= MIN_JOIN_WIDTH) { + raster.addDot(x, y, style.lineWidth * 0.5f); + } + } else if (style.lineWidth > 0.0f) { + const float dx = x - lastX; + const float dy = y - lastY; + const float length = std::sqrt(dx * dx + dy * dy); + if (length < 0.05f) { + return; + } + const float half = style.lineWidth * 0.5f; + raster.addSegment(lastX, lastY, x, y, half); + // A round join only shows where the line turns noticeably + const float directionX = dx / length; + const float directionY = dy / length; + if (hasDirection && style.lineWidth >= MIN_JOIN_WIDTH && directionX * lastDirectionX + directionY * lastDirectionY < JOIN_COSINE) { + raster.addDot(lastX, lastY, half); + } + hasDirection = true; + lastDirectionX = directionX; + lastDirectionY = directionY; + } else { + raster.addLine(lastX, lastY, x, y); + } + lastX = x; + lastY = y; +} + +void TileRenderer::points(const TilePoint* chunk, int count) { + for (int i = 0; i < count; i++) { + addPoint(static_cast(chunk[i].x) * scale - offsetX, static_cast(chunk[i].y) * scale - offsetY); + } +} + +void TileRenderer::endRing(bool closed) { + if (firstPoint) { + return; + } + if (style.lineWidth > 0.0f) { + if (closed) { + raster.addSegment(lastX, lastY, startX, startY, style.lineWidth * 0.5f); + } + if (style.lineWidth >= MIN_JOIN_WIDTH) { + raster.addDot(lastX, lastY, style.lineWidth * 0.5f); + } + } else { + raster.addLine(lastX, lastY, startX, startY); + } +} + +bool TileRenderer::shouldStop() { + if (cancelFlag != nullptr && cancelFlag->load()) { + cancelled = true; + } + return cancelled; +} + +void TileRenderer::endFeature() { + const uint64_t filled = nowUs(); + geometryUs += filled - featureStartUs; + if (cheapFeature && boundsMaxX - boundsMinX < MIN_BUILDING_PIXELS && boundsMaxY - boundsMinY < MIN_BUILDING_PIXELS) { + raster.clear(); + } else { + raster.fill(pixels, TILE_SIZE, style.color, style.opacity); + } + if (cheapFeature) { + raster.setAntialias(true); + } + const uint64_t done = nowUs(); + fillUs += done - filled; + if (stage == Stage::Building) { + buildingUs += done - featureStartUs; + } +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/render/TileRenderer.h b/Apps/Maps/main/Source/render/TileRenderer.h new file mode 100644 index 0000000..5b502ce --- /dev/null +++ b/Apps/Maps/main/Source/render/TileRenderer.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "../mvt/MvtDecoder.h" +#include "../tiles/TileMath.h" +#include "Raster.h" +#include "Style.h" + +#include +#include + +namespace maps { + +struct RenderStats { + uint32_t totalMs; + uint32_t scanMs; + uint32_t sweeps; + uint32_t features; + uint32_t decompressedBytes; + /** Time spent decoding the geometry of drawn features and adding their edges. */ + uint32_t geometryMs; + /** Time spent turning coverage into pixels. */ + uint32_t fillMs; + /** The same in microseconds, and the part of geometry and fill spent on buildings. */ + uint32_t geometryUs; + uint32_t fillUs; + uint32_t buildingUs; +}; + +/** + * Renders one 256x256 RGB565 tile from a vector tile file. The source tile can be an ancestor of the target + * tile, which is then drawn magnified from the part of the source that covers it (overzoom). + */ +class TileRenderer final : private GeometrySink { +public: + TileRenderer() = default; + ~TileRenderer() override; + TileRenderer(const TileRenderer&) = delete; + TileRenderer& operator=(const TileRenderer&) = delete; + + bool init(); + bool render(ByteInput* input, const TileKey& source, const TileKey& target, const Palette& palette, uint16_t* pixels, RenderStats* stats); + /** Renders from a prepared tile (see PreparedTile.h), whose header names the source tile. */ + bool renderPrepared(const uint8_t* data, size_t size, const TileKey& target, const Palette& palette, uint16_t* pixels, RenderStats* stats); + + /** The decoder, lent to a TileConverter so a lane needs only one decompressor. */ + MvtDecoder& tileDecoder() { return decoder; } + + /** A flag that stops the running render when set, which then returns false. */ + void setCancelFlag(const std::atomic* flag) { cancelFlag = flag; } + bool wasCancelled() const { return cancelled; } + +private: + void begin(const TileKey& source, const TileKey& target, const Palette& palette, uint16_t* out); + void finishStats(RenderStats* stats, uint32_t start, uint32_t scanned, uint32_t sweeps) const; + bool playSection(const uint8_t* data, const uint8_t* end, Stage drawStage); + + bool beginFeature(const FeatureInfo& info) override; + void beginRing() override; + void points(const TilePoint* points, int count) override; + void endRing(bool closed) override; + void endFeature() override; + bool shouldStop() override; + + void addPoint(float x, float y); + + MvtDecoder decoder; + Raster raster; + const Palette* palette = nullptr; + uint16_t* pixels = nullptr; + Stage stage = Stage::Landcover; + Stage layerStages[static_cast(Layer::Count)] {}; + int dz = 0; + int zoom = 0; + float scale = 1.0f; + float offsetX = 0.0f; + float offsetY = 0.0f; + DrawStyle style {}; + /** Buildings at native z14: aliased, and skipped when smaller than about 2 px. */ + bool cheapFeature = false; + float boundsMinX = 0.0f; + float boundsMinY = 0.0f; + float boundsMaxX = 0.0f; + float boundsMaxY = 0.0f; + bool firstPoint = true; + float startX = 0.0f; + float startY = 0.0f; + float lastX = 0.0f; + float lastY = 0.0f; + bool hasDirection = false; + float lastDirectionX = 0.0f; + float lastDirectionY = 0.0f; + uint32_t featureCount = 0; + uint64_t featureStartUs = 0; + uint64_t geometryUs = 0; + uint64_t fillUs = 0; + uint64_t buildingUs = 0; + /** Coordinate units per tile of prepared tiles, 0 while rendering a vector tile. */ + int preparedExtent = 0; + uint8_t* body = nullptr; + size_t bodyCapacity = 0; + const std::atomic* cancelFlag = nullptr; + bool cancelled = false; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/DownloadPlan.cpp b/Apps/Maps/main/Source/tiles/DownloadPlan.cpp new file mode 100644 index 0000000..e1d5465 --- /dev/null +++ b/Apps/Maps/main/Source/tiles/DownloadPlan.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "DownloadPlan.h" + +namespace maps { + +namespace { + +// Mean compressed bytes per tile of 290 OpenFreeMap tiles (planet 20261025) from Budapest, Vienna, London, +// Split and rural Hungary. Dense city centres are up to twice as large, the countryside much smaller. +constexpr uint32_t TYPICAL_TILE_BYTES[MAX_SOURCE_ZOOM + 1] = { + 48000, 124000, 700000, 509000, 360000, 292000, 198000, 225000, 179000, 158000, 90000, 66000, 72000, 60000, 136000 +}; + +} // namespace + +uint32_t typicalTileBytes(int z) { + if (z < 0) { + return 0; + } + return TYPICAL_TILE_BYTES[z > MAX_SOURCE_ZOOM ? MAX_SOURCE_ZOOM : z]; +} + +uint64_t estimateBytes(const BoundingBox& box, int minZoom, int maxZoom) { + uint64_t total = 0; + for (int z = minZoom; z <= maxZoom; z++) { + total += static_cast(tileRange(box, z).count()) * typicalTileBytes(z); + } + return total; +} + +DownloadPlan planDownload(const BoundingBox& box, int requestedMaxZoom) { + DownloadPlan plan { box, 0, 0, 0, 0, false }; + const int top = requestedMaxZoom < MAX_SOURCE_ZOOM ? requestedMaxZoom : MAX_SOURCE_ZOOM; + for (int maxZoom = top; maxZoom >= 0; maxZoom--) { + const int64_t tiles = tileCount(box, 0, maxZoom); + const uint64_t bytes = estimateBytes(box, 0, maxZoom); + if (tiles <= MAX_DOWNLOAD_TILES && bytes <= MAX_DOWNLOAD_BYTES) { + plan.maxZoom = maxZoom; + plan.tiles = tiles; + plan.estimatedBytes = bytes; + plan.fits = true; + return plan; + } + } + plan.tiles = tileCount(box, 0, 0); + plan.estimatedBytes = estimateBytes(box, 0, 0); + return plan; +} + +bool tileAtIndex(const DownloadPlan& plan, int64_t index, TileKey& tile) { + if (index < 0) { + return false; + } + for (int z = plan.minZoom; z <= plan.maxZoom; z++) { + const TileRange range = tileRange(plan.box, z); + const int64_t count = range.count(); + if (index < count) { + const int64_t width = range.maxX - range.minX + 1; + tile = { z, range.minX + static_cast(index % width), range.minY + static_cast(index / width) }; + return true; + } + index -= count; + } + return false; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/DownloadPlan.h b/Apps/Maps/main/Source/tiles/DownloadPlan.h new file mode 100644 index 0000000..4ba9ab9 --- /dev/null +++ b/Apps/Maps/main/Source/tiles/DownloadPlan.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "TileMath.h" + +#include + +namespace maps { + +/** OpenFreeMap's limits for one offline download. They are not to be worked around by splitting an area. */ +constexpr int64_t MAX_DOWNLOAD_TILES = 3000; +constexpr uint64_t MAX_DOWNLOAD_BYTES = 50ull * 1000 * 1000; +constexpr int MAX_PARALLEL_REQUESTS = 4; + +struct DownloadPlan { + BoundingBox box; + int minZoom; + int maxZoom; + int64_t tiles; + uint64_t estimatedBytes; + /** False when even the lowest zoom levels do not fit the limits. */ + bool fits; +}; + +/** Typical compressed size of a tile at a zoom level, measured from real OpenFreeMap tiles. */ +uint32_t typicalTileBytes(int z); + +uint64_t estimateBytes(const BoundingBox& box, int minZoom, int maxZoom); + +/** The plan with the highest max zoom, at most requestedMaxZoom, that fits the download limits. */ +DownloadPlan planDownload(const BoundingBox& box, int requestedMaxZoom); + +/** + * The tile at a position in the download order: zoom levels from low to high, rows from top to bottom, + * columns from left to right. The order never changes, so a download resumes from its done count. + */ +bool tileAtIndex(const DownloadPlan& plan, int64_t index, TileKey& tile); + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/IoQueue.h b/Apps/Maps/main/Source/tiles/IoQueue.h new file mode 100644 index 0000000..f63740a --- /dev/null +++ b/Apps/Maps/main/Source/tiles/IoQueue.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +namespace maps { + +/** What an SD card job is for, in the order the I/O task serves them. */ +enum class IoPriority : uint8_t { + /** Tiles on screen, and the UI waiting for a file. */ + Visible, + /** Tiles around the screen. */ + Prefetch, + /** Cache writes, conversions, downloads, deletes, folder walks. */ + Background, + Count +}; + +/** + * The waiting jobs of the I/O task: the highest priority first, the oldest first within a priority. + * Not thread safe, the I/O task guards it with its own lock. + */ +template +class IoQueue { +public: + bool push(const Item& item, IoPriority priority) { + if (count >= CAPACITY) { + return false; + } + entries[count++] = { item, priority, nextSequence++ }; + return true; + } + + bool pop(Item& item, IoPriority* priority = nullptr) { + int best = -1; + for (int i = 0; i < count; i++) { + if (best < 0 || entries[i].priority < entries[best].priority || + (entries[i].priority == entries[best].priority && entries[i].sequence < entries[best].sequence)) { + best = i; + } + } + if (best < 0) { + return false; + } + item = entries[best].item; + if (priority != nullptr) { + *priority = entries[best].priority; + } + entries[best] = entries[--count]; + return true; + } + + /** Whether a job more urgent than priority waits. */ + bool hasMoreUrgent(IoPriority priority) const { + for (int i = 0; i < count; i++) { + if (entries[i].priority < priority) { + return true; + } + } + return false; + } + + int size() const { return count; } + +private: + struct Entry { + Item item; + IoPriority priority; + uint32_t sequence; + }; + + Entry entries[CAPACITY] {}; + int count = 0; + uint32_t nextSequence = 0; +}; + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/RegionMeta.cpp b/Apps/Maps/main/Source/tiles/RegionMeta.cpp new file mode 100644 index 0000000..43b9293 --- /dev/null +++ b/Apps/Maps/main/Source/tiles/RegionMeta.cpp @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "RegionMeta.h" + +#include +#include +#include + +namespace maps { + +namespace { + +const char* findValue(const char* text, const char* key) { + char pattern[32]; + snprintf(pattern, sizeof(pattern), "\"%s\"", key); + const char* found = strstr(text, pattern); + if (found == nullptr) { + return nullptr; + } + found = strchr(found + strlen(pattern), ':'); + if (found == nullptr) { + return nullptr; + } + found++; + while (*found == ' ' || *found == '\t' || *found == '\n' || *found == '\r') { + found++; + } + return found; +} + +bool readString(const char* text, const char* key, char* out, size_t size) { + const char* value = findValue(text, key); + if (value == nullptr || *value != '"') { + return false; + } + value++; + size_t length = 0; + while (value[length] != '"' && value[length] != '\0') { + length++; + } + if (value[length] != '"' || length >= size) { + return false; + } + memcpy(out, value, length); + out[length] = '\0'; + return true; +} + +bool readInteger(const char* text, const char* key, int64_t& out) { + const char* value = findValue(text, key); + if (value == nullptr) { + return false; + } + const bool negative = *value == '-'; + if (negative) { + value++; + } + if (*value < '0' || *value > '9') { + return false; + } + int64_t number = 0; + while (*value >= '0' && *value <= '9') { + number = number * 10 + (*value - '0'); + value++; + } + out = negative ? -number : number; + return true; +} + +} // namespace + +bool formatRegionMeta(const RegionMeta& meta, char* out, size_t size) { + const int written = snprintf(out, size, + "{\n" + " \"name\": \"%s\",\n" + " \"bbox\": [%.6f,%.6f,%.6f,%.6f],\n" + " \"minzoom\": %d,\n" + " \"maxzoom\": %d,\n" + " \"source\": \"%s\",\n" + " \"version\": \"%s\",\n" + " \"date\": \"%s\",\n" + " \"total\": %" PRId64 ",\n" + " \"done\": %" PRId64 ",\n" + " \"bytes\": %" PRIu64 "\n" + "}\n", + meta.name, meta.box.west, meta.box.south, meta.box.east, meta.box.north, meta.minZoom, meta.maxZoom, + meta.source, meta.version, meta.date, meta.total, meta.done, meta.bytes); + return written > 0 && static_cast(written) < size; +} + +bool parseRegionMeta(const char* text, RegionMeta& meta) { + meta = {}; + const char* bbox = findValue(text, "bbox"); + if (bbox == nullptr || sscanf(bbox, "[%lf,%lf,%lf,%lf]", &meta.box.west, &meta.box.south, &meta.box.east, &meta.box.north) != 4) { + return false; + } + int64_t minZoom = 0; + int64_t maxZoom = 0; + if (!readInteger(text, "minzoom", minZoom) || !readInteger(text, "maxzoom", maxZoom) || + minZoom < 0 || maxZoom > MAX_SOURCE_ZOOM || minZoom > maxZoom) { + return false; + } + meta.minZoom = static_cast(minZoom); + meta.maxZoom = static_cast(maxZoom); + readString(text, "name", meta.name, sizeof(meta.name)); + readString(text, "source", meta.source, sizeof(meta.source)); + readString(text, "version", meta.version, sizeof(meta.version)); + readString(text, "date", meta.date, sizeof(meta.date)); + int64_t bytes = 0; + readInteger(text, "bytes", bytes); + meta.bytes = static_cast(bytes); + if (!readInteger(text, "total", meta.total)) { + meta.total = tileCount(meta.box, meta.minZoom, meta.maxZoom); + } + if (!readInteger(text, "done", meta.done)) { + meta.done = 0; + } + return true; +} + +bool writeRegionMeta(const char* path, const RegionMeta& meta) { + char text[640]; + if (!formatRegionMeta(meta, text, sizeof(text))) { + return false; + } + char temporary[PATH_MAX_LENGTH + 8]; + snprintf(temporary, sizeof(temporary), "%s.tmp", path); + FILE* file = fopen(temporary, "w"); + if (file == nullptr) { + return false; + } + const size_t length = strlen(text); + const bool ok = fwrite(text, 1, length, file) == length; + fclose(file); + remove(path); + if (!ok || rename(temporary, path) != 0) { + remove(temporary); + return false; + } + return true; +} + +bool readRegionMeta(const char* path, RegionMeta& meta) { + FILE* file = fopen(path, "r"); + if (file == nullptr) { + return false; + } + char text[1024]; + const size_t length = fread(text, 1, sizeof(text) - 1, file); + fclose(file); + text[length] = '\0'; + return parseRegionMeta(text, meta); +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/RegionMeta.h b/Apps/Maps/main/Source/tiles/RegionMeta.h new file mode 100644 index 0000000..62f149a --- /dev/null +++ b/Apps/Maps/main/Source/tiles/RegionMeta.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "DownloadPlan.h" +#include "TilePaths.h" + +#include +#include + +namespace maps { + +/** The state of a downloaded region, kept in maps//meta.json so a download resumes after a restart. */ +struct RegionMeta { + char name[REGION_NAME_MAX + 1]; + BoundingBox box; + int minZoom; + int maxZoom; + char source[160]; + char version[48]; + char date[24]; + int64_t total; + int64_t done; + uint64_t bytes; + + bool isComplete() const { return total > 0 && done >= total; } + DownloadPlan plan() const { return { box, minZoom, maxZoom, total, 0, true }; } +}; + +bool writeRegionMeta(const char* path, const RegionMeta& meta); +bool readRegionMeta(const char* path, RegionMeta& meta); +bool formatRegionMeta(const RegionMeta& meta, char* out, size_t size); +bool parseRegionMeta(const char* text, RegionMeta& meta); + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/TileJson.cpp b/Apps/Maps/main/Source/tiles/TileJson.cpp new file mode 100644 index 0000000..c6494ee --- /dev/null +++ b/Apps/Maps/main/Source/tiles/TileJson.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "TileJson.h" + +#include +#include + +namespace maps { + +bool parseTileJson(const char* text, char* urlTemplate, size_t templateSize, char* version, size_t versionSize) { + const char* tiles = strstr(text, "\"tiles\""); + if (tiles == nullptr) { + return false; + } + const char* open = strchr(tiles, '['); + if (open == nullptr) { + return false; + } + const char* start = strchr(open, '"'); + if (start == nullptr) { + return false; + } + start++; + const char* end = strchr(start, '"'); + if (end == nullptr || static_cast(end - start) >= templateSize) { + return false; + } + const size_t length = static_cast(end - start); + memcpy(urlTemplate, start, length); + urlTemplate[length] = '\0'; + // Only https URLs with all three placeholders are accepted + if (strncmp(urlTemplate, "https://", 8) != 0 || strstr(urlTemplate, "{z}") == nullptr || + strstr(urlTemplate, "{x}") == nullptr || strstr(urlTemplate, "{y}") == nullptr || + strstr(urlTemplate, "openstreetmap.org") != nullptr) { + return false; + } + version[0] = '\0'; + const char* placeholder = strstr(urlTemplate, "/{z}"); + if (placeholder != nullptr) { + const char* segment = placeholder; + while (segment > urlTemplate && segment[-1] != '/') { + segment--; + } + const size_t versionLength = static_cast(placeholder - segment); + if (versionLength < versionSize) { + memcpy(version, segment, versionLength); + version[versionLength] = '\0'; + } + } + return true; +} + +bool formatTileUrl(const char* urlTemplate, const TileKey& tile, char* out, size_t size) { + size_t used = 0; + for (const char* p = urlTemplate; *p != '\0';) { + int written = 0; + if (strncmp(p, "{z}", 3) == 0) { + written = snprintf(out + used, size - used, "%d", tile.z); + p += 3; + } else if (strncmp(p, "{x}", 3) == 0) { + written = snprintf(out + used, size - used, "%d", tile.x); + p += 3; + } else if (strncmp(p, "{y}", 3) == 0) { + written = snprintf(out + used, size - used, "%d", tile.y); + p += 3; + } else { + if (used + 1 >= size) { + return false; + } + out[used] = *p++; + written = 1; + } + if (written <= 0 || used + static_cast(written) >= size) { + return false; + } + used += static_cast(written); + } + out[used] = '\0'; + return true; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/TileJson.h b/Apps/Maps/main/Source/tiles/TileJson.h new file mode 100644 index 0000000..5f7b973 --- /dev/null +++ b/Apps/Maps/main/Source/tiles/TileJson.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "TileMath.h" + +#include + +namespace maps { + +constexpr const char* TILEJSON_URL = "https://tiles.openfreemap.org/planet"; +constexpr const char* USER_AGENT = "TactilityMaps/0.0.1 (+https://github.com/TactilityProject/TactilityApps)"; + +/** + * Reads the first tile URL template of a TileJSON document, like ".../planet/20261025_080012_pt/{z}/{x}/{y}.pbf", + * and the version, which is the path segment before "/{z}". + */ +bool parseTileJson(const char* text, char* urlTemplate, size_t templateSize, char* version, size_t versionSize); + +/** Fills {z}, {x} and {y} of a URL template. */ +bool formatTileUrl(const char* urlTemplate, const TileKey& tile, char* out, size_t size); + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/TileMath.cpp b/Apps/Maps/main/Source/tiles/TileMath.cpp new file mode 100644 index 0000000..4ee0645 --- /dev/null +++ b/Apps/Maps/main/Source/tiles/TileMath.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "TileMath.h" + +#include +#include + +namespace maps { + +namespace { + +constexpr double PI = 3.14159265358979323846; +constexpr double EARTH_CIRCUMFERENCE_M = 40075016.686; + +double worldSize(int z) { + return static_cast(TILE_SIZE) * static_cast(1u << z); +} + +} // namespace + +double lonToWorldX(double lon, int z) { + return (lon + 180.0) / 360.0 * worldSize(z); +} + +double latToWorldY(double lat, int z) { + lat = std::clamp(lat, -MAX_LATITUDE, MAX_LATITUDE); + const double rad = lat * PI / 180.0; + return (1.0 - std::log(std::tan(rad) + 1.0 / std::cos(rad)) / PI) / 2.0 * worldSize(z); +} + +double worldXToLon(double x, int z) { + return x / worldSize(z) * 360.0 - 180.0; +} + +double worldYToLat(double y, int z) { + const double n = PI - 2.0 * PI * y / worldSize(z); + return 180.0 / PI * std::atan(std::sinh(n)); +} + +TileKey tileAt(double lat, double lon, int z) { + const int last = (1 << z) - 1; + const int x = std::clamp(static_cast(std::floor(lonToWorldX(lon, z) / TILE_SIZE)), 0, last); + const int y = std::clamp(static_cast(std::floor(latToWorldY(lat, z) / TILE_SIZE)), 0, last); + return { z, x, y }; +} + +TileRange tileRange(const BoundingBox& box, int z) { + const TileKey topLeft = tileAt(box.north, box.west, z); + const TileKey bottomRight = tileAt(box.south, box.east, z); + return { + .z = z, + .minX = std::min(topLeft.x, bottomRight.x), + .minY = std::min(topLeft.y, bottomRight.y), + .maxX = std::max(topLeft.x, bottomRight.x), + .maxY = std::max(topLeft.y, bottomRight.y), + }; +} + +int64_t tileCount(const BoundingBox& box, int minZoom, int maxZoom) { + int64_t total = 0; + for (int z = minZoom; z <= maxZoom; z++) { + total += tileRange(box, z).count(); + } + return total; +} + +double metresPerPixel(double lat, int z) { + return EARTH_CIRCUMFERENCE_M * std::cos(lat * PI / 180.0) / worldSize(z); +} + +TileKey ancestorOf(const TileKey& tile, int z) { + const int shift = tile.z - z; + return { z, tile.x >> shift, tile.y >> shift }; +} + +bool isValidTile(const TileKey& tile) { + if (tile.z < 0 || tile.z > 30) { + return false; + } + const int count = 1 << tile.z; + return tile.x >= 0 && tile.x < count && tile.y >= 0 && tile.y < count; +} + +int wrapTileX(int x, int z) { + const int count = 1 << z; + x %= count; + return x < 0 ? x + count : x; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/TileMath.h b/Apps/Maps/main/Source/tiles/TileMath.h new file mode 100644 index 0000000..be912ff --- /dev/null +++ b/Apps/Maps/main/Source/tiles/TileMath.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +namespace maps { + +constexpr int TILE_SIZE = 256; +constexpr int MAX_SOURCE_ZOOM = 14; +constexpr int MAX_VIEW_ZOOM = 19; +constexpr double MAX_LATITUDE = 85.05112878; + +struct TileKey { + int z; + int x; + int y; + + bool operator==(const TileKey& other) const { return z == other.z && x == other.x && y == other.y; } +}; + +struct TileRange { + int z; + int minX; + int minY; + int maxX; + int maxY; + + int64_t count() const { return static_cast(maxX - minX + 1) * (maxY - minY + 1); } +}; + +struct BoundingBox { + double west; + double south; + double east; + double north; +}; + +/** World pixel position at a zoom level, where the whole world is TILE_SIZE * 2^z pixels wide. */ +double lonToWorldX(double lon, int z); +double latToWorldY(double lat, int z); +double worldXToLon(double x, int z); +double worldYToLat(double y, int z); + +TileKey tileAt(double lat, double lon, int z); + +/** Tiles covering a bounding box at one zoom level. The box must not cross the antimeridian. */ +TileRange tileRange(const BoundingBox& box, int z); + +int64_t tileCount(const BoundingBox& box, int minZoom, int maxZoom); + +/** Ground metres per pixel at a latitude and zoom level. */ +double metresPerPixel(double lat, int z); + +/** The tile at zoom z that contains the given tile, z must not exceed tile.z. */ +TileKey ancestorOf(const TileKey& tile, int z); + +bool isValidTile(const TileKey& tile); + +/** The tile at x, wrapped around the antimeridian. */ +int wrapTileX(int x, int z); + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/TilePaths.cpp b/Apps/Maps/main/Source/tiles/TilePaths.cpp new file mode 100644 index 0000000..2e9a39b --- /dev/null +++ b/Apps/Maps/main/Source/tiles/TilePaths.cpp @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "TilePaths.h" + +#include +#include + +namespace maps { + +namespace { + +bool fits(int written, size_t size) { + return written > 0 && static_cast(written) < size; +} + +} // namespace + +bool tilePath(char* out, size_t size, const char* root, const char* region, const TileKey& tile) { + return fits(snprintf(out, size, "%s/maps/%s/%d/%d/%d.pbf", root, region, tile.z, tile.x, tile.y), size); +} + +bool preparedPath(char* out, size_t size, const char* root, const char* region, const TileKey& tile) { + return fits(snprintf(out, size, "%s/maps/%s/%d/%d/%d.prt", root, region, tile.z, tile.x, tile.y), size); +} + +bool preparedTempPath(char* out, size_t size, const char* root, const char* region, const TileKey& tile) { + return fits(snprintf(out, size, "%s/maps/%s/%d/%d/%d.ptm", root, region, tile.z, tile.x, tile.y), size); +} + +bool tileFolderPath(char* out, size_t size, const char* root, const char* region, const TileKey& tile) { + return fits(snprintf(out, size, "%s/maps/%s/%d/%d", root, region, tile.z, tile.x), size); +} + +bool cachePath(char* out, size_t size, const char* root, const char* style, const TileKey& tile) { + return fits(snprintf(out, size, "%s/cache/%s/%d/%d/%d", root, style, tile.z, tile.x, tile.y), size); +} + +bool cacheFolderPath(char* out, size_t size, const char* root, const char* style, const TileKey& tile) { + return fits(snprintf(out, size, "%s/cache/%s/%d/%d", root, style, tile.z, tile.x), size); +} + +bool isInsideCache(const char* path, const char* root) { + const size_t rootLength = strlen(root); + if (strncmp(path, root, rootLength) != 0 || strncmp(path + rootLength, "/cache/", 7) != 0 || path[rootLength + 7] == '\0') { + return false; + } + // No way back out of the cache folder + return strstr(path, "/..") == nullptr && strstr(path, "\\") == nullptr; +} + +bool isValidRegionName(const char* name) { + const size_t length = strlen(name); + if (length == 0 || length > REGION_NAME_MAX) { + return false; + } + for (size_t i = 0; i < length; i++) { + const char c = name[i]; + const bool valid = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; + if (!valid) { + return false; + } + } + return true; +} + +} // namespace maps diff --git a/Apps/Maps/main/Source/tiles/TilePaths.h b/Apps/Maps/main/Source/tiles/TilePaths.h new file mode 100644 index 0000000..2525155 --- /dev/null +++ b/Apps/Maps/main/Source/tiles/TilePaths.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "TileMath.h" + +#include + +namespace maps { + +constexpr size_t PATH_MAX_LENGTH = 128; +constexpr size_t REGION_NAME_MAX = 8; + +/** Writes "/maps////.pbf". Returns false when the path does not fit. */ +bool tilePath(char* out, size_t size, const char* root, const char* region, const TileKey& tile); + +/** Writes "/maps////.prt", the prepared copy next to the original. */ +bool preparedPath(char* out, size_t size, const char* root, const char* region, const TileKey& tile); +/** Writes "/maps////.ptm", where a prepared tile is written before it is renamed. */ +bool preparedTempPath(char* out, size_t size, const char* root, const char* region, const TileKey& tile); + +/** Writes "/maps///", the folder of a tile column. */ +bool tileFolderPath(char* out, size_t size, const char* root, const char* region, const TileKey& tile); + +/** Writes "/cache/