Skip to content

feat(url): open remote data and styles with ?data= and ?style= - #1795

Merged
giswqs merged 6 commits into
mainfrom
feat/data-url-deep-link
Aug 9, 2026
Merged

feat(url): open remote data and styles with ?data= and ?style=#1795
giswqs merged 6 commits into
mainfrom
feat/data-url-deep-link

Conversation

@giswqs

@giswqs giswqs commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary

  • Add a ?data= URL parameter that opens hosted GeoJSON, a REST endpoint returning a GeoJSON FeatureCollection, a COG, or a ZIP/REST response holding several GeoJSON files (ZIP detected from headers or the PKZIP signature, so an extensionless API endpoint works). Imported layers are framed by their combined extent and the welcome wizard is suppressed for the deep link.
  • Add a companion ?style= parameter applying Mapbox/MapLibre vector style JSON, or raster style JSON for a COG (mode, bands, rescale, colormap, reversed, nodata, opacity, gamma, stretch, index preset). For a ZIP, each style layer's source binds to a GeoJSON filename stem; all matches are validated before any layer is added, so a typo cannot leave a partial import.
  • Add Layer actions -> Styles -> Export GeoLibre URL style, writing a compact .geolibre.style.json that carries symbology without feature data and whose render-layer source is the original filename stem. The existing style import accepts the same file and applies it to the selected layer.
  • Document the parameters and the export/import workflow in the embedding, layers, and styling guides.

Test plan

  • npm run test:frontend (5544 pass, 0 fail), including new tests/data-url.test.ts and tests/query-param-style.test.ts
  • pre-commit run --files <changed> clean, including the npm build hook and eslint
  • Open ?data=<geojson>&style=<style> in the web build and confirm the layer loads styled and framed
  • Open ?data=<zip>&style=<multi-layer style> and confirm each GeoJSON member picks up its filename-matched style
  • Open ?data=<cog>&style=<raster style> and confirm the raster style applies
  • Export a GeoLibre URL style from a vector layer, re-import it, and confirm the symbology round-trips

Summary by CodeRabbit

  • New Features

    • Open hosted GeoJSON, GeoParquet, PMTiles, COG, REST GeoJSON, and ZIP data directly from URL links.
    • Apply optional vector and raster styles with automatic extent fitting and clear loading errors.
    • Export and import compact GeoLibre URL styles alongside Mapbox GL, SLD, and QML formats.
    • Added localized GeoLibre style export labels across supported languages.
  • Documentation

    • Added guidance for remote data links, embedding, styling, and layer style workflows.
  • Bug Fixes

    • Direct data links now correctly bypass onboarding.

Deep links could only open saved .geolibre.json projects, so sharing a
hosted GeoJSON, COG, or ZIP of GeoJSON files meant publishing a project
file first. The new ?data= parameter loads that data directly, ?style=
applies vector or raster symbology beside it, and a matching compact
style export makes the file easy to produce from an existing layer.
Copilot AI lite review requested due to automatic review settings August 9, 2026 14:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The desktop app now loads remote geospatial data from URL parameters. It validates and applies styles, fits loaded layers, reports errors, and suppresses onboarding. The layer panel supports GeoLibre URL style export and import with localization and documentation.

Changes

Remote data deep-link loading

Layer / File(s) Summary
Remote data parsing and validation
apps/geolibre-desktop/src/lib/data-url.ts, tests/data-url.test.ts
Remote utilities support GeoJSON, COG, PMTiles, GeoParquet, REST, and ZIP inputs. They validate raster styles, filter vector styles, and enforce download limits.
Desktop deep-link loading
apps/geolibre-desktop/src/App.tsx, apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts, apps/geolibre-desktop/src/components/layout/DesktopShell.tsx, apps/geolibre-desktop/src/lib/onboarding-suppression.ts, packages/plugins/src/index.ts, tests/onboarding-suppression.test.ts
The app loads URL data, adds layers, fits their bounds, reports errors, and suppresses onboarding for data links.

GeoLibre style interchange

Layer / File(s) Summary
GeoLibre style export and import
packages/map/src/query-param-style.ts, packages/map/src/index.ts, apps/geolibre-desktop/src/components/panels/LayerPanel.tsx, tests/query-param-style.test.ts, apps/geolibre-desktop/src/i18n/locales/*
The map package builds source-matched GeoLibre URL styles. The layer panel exports and imports them. Tests and translations cover the format.
URL workflow documentation
docs/features.md, docs/index.md, docs/user-guide/embedding.md, docs/user-guide/layers.md, docs/user-guide/styling.md
The documentation describes remote data parameters, hosted styles, GeoLibre style interchange, raster styling, and embedding requirements.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BrowserURL
  participant App
  participant useDataUrlLoader
  participant DesktopShell
  participant MapAPI
  BrowserURL->>App: provide data and style parameters
  App->>useDataUrlLoader: pass map API
  useDataUrlLoader->>MapAPI: add remote layers and styles
  useDataUrlLoader-->>DesktopShell: return load state and layer IDs
  DesktopShell->>MapAPI: fit loaded layer bounds
Loading

Possibly related PRs

Poem

A rabbit sends data through URLs bright,
GeoJSON hops into view at night.
Styles match sources, bounds settle true,
New labels bloom in every hue.
The map opens wide beneath the moon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: opening remote data and styles through the new URL parameters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/data-url-deep-link

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://6acf3410.geolibre-preview.pages.dev
Demo app https://6acf3410.geolibre-preview.pages.dev/demo/
Commit e5063c7

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1795/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1795/demo/
Commit e5063c7

Note

GitHub Pages built this preview successfully, but its serving edge returned HTTP 403 when checked. The links may still be propagating.

Comment thread apps/geolibre-desktop/src/App.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx Outdated
Comment thread apps/geolibre-desktop/src/lib/data-url.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/layout/DesktopShell.tsx`:
- Around line 2768-2775: Adjust the error-banner rendering in DesktopShell so
simultaneous projectUrlLoadState and dataUrlLoadState errors remain readable
instead of sharing the same absolute position. Stack the two banners vertically
or render both messages within a single positioned container, preserving each
existing error message and styling.

In `@apps/geolibre-desktop/src/components/panels/LayerPanel.tsx`:
- Around line 1693-1700: Localize the new GeoLibre style picker text: in
apps/geolibre-desktop/src/components/panels/LayerPanel.tsx lines 1693-1700, add
an English file-type translation key and pass t(...) to filters.name and
browserTypes.description; at lines 1774-1774, use the translated picker-filter
label. In apps/geolibre-desktop/src/i18n/locales/en.json line 4868, retain the
source label and update layers.importStyle across locale catalogs to include
GeoLibre URL styles.

In `@apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts`:
- Around line 31-104: Guard the data URL import in the useDataUrlLoader effect
with a ref so the startup import runs at most once per session, regardless of
mapAppAPI identity changes. Mark the import as started before launching the
fetch, and adjust cleanup so completed imports are not aborted by unrelated
re-renders while still cancelling in-flight work on unmount.

In `@apps/geolibre-desktop/src/lib/data-url.ts`:
- Around line 119-134: Update the ZIP decoding flow around unzipSync to use
fflate’s asynchronous unzip API so large archives do not block the UI,
preserving the existing entry filtering and cumulative MAX_ZIP_GEOJSON_BYTES
checks. Define and throw a dedicated ZipTooLargeError for size-limit violations,
then catch it with instanceof instead of inspecting error.message; continue
wrapping other archive failures as the invalid ZIP error.

In `@docs/user-guide/embedding.md`:
- Around line 84-85: Update the `welcome` row in the onboarding documentation
table to state that the wizard is suppressed by deep links containing either
`url=` or `data=`, matching `shouldSuppressOnboarding` behavior. Leave the other
onboarding descriptions unchanged.

In `@packages/map/src/query-param-style.ts`:
- Around line 14-24: Update geoLibreStyleSourceName to strip URL fragments from
remote URLs before extracting the pathname and filename, while preserving
fragments that identify ZIP entries in the export.zip#path/file.geojson
convention. Ensure https://example.com/data.geojson#view resolves to data and
existing ZIP entry handling remains unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0e5e5288-aa34-462e-9519-5af39f214856

📥 Commits

Reviewing files that changed from the base of the PR and between 07cc360 and d3bc3cc.

📒 Files selected for processing (34)
  • apps/geolibre-desktop/src/App.tsx
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/components/panels/LayerPanel.tsx
  • apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fa.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/th.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/data-url.ts
  • apps/geolibre-desktop/src/lib/onboarding-suppression.ts
  • docs/features.md
  • docs/index.md
  • docs/user-guide/embedding.md
  • docs/user-guide/layers.md
  • docs/user-guide/styling.md
  • packages/map/src/index.ts
  • packages/map/src/query-param-style.ts
  • tests/data-url.test.ts
  • tests/onboarding-suppression.test.ts
  • tests/query-param-style.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
Comment thread apps/geolibre-desktop/src/components/panels/LayerPanel.tsx
Comment thread apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts
Comment thread apps/geolibre-desktop/src/lib/data-url.ts Outdated
Comment thread docs/user-guide/embedding.md Outdated
Comment thread packages/map/src/query-param-style.ts
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

All three inline comments posted. Final summary below.

Code review

Bugs

  • apps/geolibre-desktop/src/App.tsx:56onMapReady is bound straight to setMapAppAPI, but MapCanvas fires onControllerReady on every basemap swap, not just once. Since createAppAPI() returns a new object each call, mapAppAPI's identity changes on every basemap switch, which re-triggers useDataUrlLoader's effect (useDataUrlLoader.ts:104, dependent on [mapAppAPI, params]) — re-fetching the remote data and duplicating the imported layer(s) each time the user changes the basemap after a ?data= deep link has loaded. High confidence.

Quality

  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx:614-640addRasterToMap already zooms to a new raster by default (zoomTo ?? true), and useDataUrlLoader doesn't override it, so a ?data=<cog> link triggers two back-to-back camera fits (the raster's own zoom, then this effect's fitLayer ~150ms later). Medium confidence. Also flagged at low confidence: the fixed 150ms timeout is a guess at layer-sync duration and could race on a slow/large ZIP import.
  • apps/geolibre-desktop/src/lib/data-url.ts:88 — the initial response is buffered in full via arrayBuffer() before any size check; MAX_ZIP_GEOJSON_BYTES only bounds decompressed ZIP entries, not the raw/initial payload or a plain GeoJSON response. A large response from a shared/untrusted ?data= endpoint could hang or crash the tab. Low confidence — consistent with the lack of a similar guard on the existing ?url= project loader, but this path specifically targets arbitrary third-party endpoints.

Security

  • No injection, unsafe eval, or leaked-secret issues found. httpUrl() correctly restricts data/style to http:/https:, preventing file:///javascript: deep links.

CLAUDE.md

  • No violations found: no touched third-party internal-constant mirrors (MAX_VECTOR_PMTILES_ZOOM, MAX_VECTOR_BYTES, etc.), i18n strings added to en.json plus all locale catalogs, and no direct MapLibre mutation outside the store/sync pattern.

Cloud-native vector formats were the obvious gap in the data deep link:
both are already loadable in the app and both stream over range requests,
so a hosted GeoParquet or vector PMTiles archive can now be shared as a
link and styled with ?style= like any other vector source.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/user-guide/layers.md (1)

33-36: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document CORS requirements for both URLs.

The browser fetches the data and style URLs separately. Each remote response must allow the GeoLibre origin. State that both URLs require CORS access, especially when they use different hosts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/user-guide/layers.md` around lines 33 - 36, Update the GeoLibre URL
style usage guidance to state that both the data and style URLs must be served
with CORS access for the GeoLibre origin. Clarify that this requirement applies
independently when the URLs are hosted on different domains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts`:
- Around line 79-83: Update the PMTiles raster/vector-style validation in the
addedLayers loop to remove the already-created control-managed layer from the
store before throwing the error, ensuring the failed deep-link import leaves no
raster layer behind. Use the existing layer-removal mechanism and preserve the
current error behavior after cleanup.
- Around line 59-82: Localize the new user-facing errors in the data-loading
flow around the PMTiles/GeoParquet layer additions by replacing the hardcoded
messages passed into DataUrlLoadState.error with react-i18next t() calls. Add
translation keys for each new message, including unsupported vector styles,
failed layer addition, missing created layers, and raster PMTiles style
incompatibility, while preserving the existing error conditions and dynamic
values.

---

Outside diff comments:
In `@docs/user-guide/layers.md`:
- Around line 33-36: Update the GeoLibre URL style usage guidance to state that
both the data and style URLs must be served with CORS access for the GeoLibre
origin. Clarify that this requirement applies independently when the URLs are
hosted on different domains.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9bf2e3fc-bf7a-4467-81c1-0ed9459c598a

📥 Commits

Reviewing files that changed from the base of the PR and between d3bc3cc and b025a2a.

📒 Files selected for processing (9)
  • apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts
  • apps/geolibre-desktop/src/lib/data-url.ts
  • docs/features.md
  • docs/index.md
  • docs/user-guide/embedding.md
  • docs/user-guide/layers.md
  • docs/user-guide/styling.md
  • packages/plugins/src/index.ts
  • tests/data-url.test.ts

Comment thread apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts
Comment thread apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts
Comment thread apps/geolibre-desktop/src/lib/data-url.ts
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

All three inline comments posted. Now finalizing the summary.

Code review

Bugs

  • ?data=/?style= layers are silently re-imported (duplicated) every time the map controller "ready" callback refires — which happens not only on initial load but on every basemap switch (MapCanvas.tsx's style.load handler calls onControllerReadyRef.current?.() again). useDataUrlLoader's effect depends on mapAppAPI, and createAppAPI returns a fresh, unmemoized object on every call, so each basemap change re-triggers the whole fetch-and-add pipeline and duplicates every previously imported layer. (apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts:141) — High confidence
  • In the PMTiles/vector branch, addPMTilesLayerFromUrl mutates the store before the check that rejects a vector style on a raster PMTiles archive runs, so a mismatched style leaves an orphaned, untracked raster layer on the map while the UI only shows an error — contradicting the "validate before adding" guarantee the PR implements for the ZIP GeoJSON path. (apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts:63-90) — Medium-high confidence

Security

  • No new issues beyond noting that ?data=/?style= fetch arbitrary user-supplied URLs client-side (same pattern as the existing ?url= project loader), now extended to binary/ZIP content; acceptable given the existing precedent, but worth confirming CORS/byte-range behavior is consistent in the Tauri desktop build. — Low confidence, informational only

Performance

  • The non-ZIP data= response path has no size cap before buffering (response.arrayBuffer()) and JSON-parsing, unlike the 250 MB per-entry/cumulative cap enforced for ZIP members — a very large REST/GeoJSON response could hang or crash the tab. (apps/geolibre-desktop/src/lib/data-url.ts:119) — Low-medium confidence
  • unzipSync runs synchronously on the main thread; decompressing near the 250 MB cap will noticeably block the UI. Not flagged inline since it's a pre-existing pattern risk rather than a clear regression.

Quality

  • No significant issues found. The ZIP per-file style validation logic, mapboxStyleForDataLayer source-matching, and buildGeoLibreQueryStyle/geoLibreStyleSourceName round-trip are well-structured and covered by the new tests.

CLAUDE.md

  • No violations found — i18n strings added consistently across all locale files with en.json as source, docs updated, new tests added under tests/.

- Capture the map app API once in App.tsx: onMapReady re-fires on every
  basemap swap with a fresh object, which re-ran the one-shot ?data=
  import and duplicated its layers.
- Frame only the GeoJSON layers a data link added. The COG, PMTiles, and
  GeoParquet loaders already move the camera, so the shell effect fit a
  second time; it also no longer guesses at layer-sync timing with a
  fixed 150ms delay, since the store bounds are available immediately.
- Offset the data-URL error banner when the project-URL banner is also
  showing, so a link carrying both url= and data= does not hide one
  message behind the other.
- Unzip archive members off the main thread and signal the size ceiling
  with a typed ZipTooLargeError instead of sniffing the message text.
- Refuse a response whose advertised Content-Length exceeds the download
  ceiling, before buffering an arbitrary third-party body.
- Read an ordinary URL hash as a hash in geoLibreStyleSourceName; only a
  fragment naming a .geojson/.json member identifies a ZIP entry.
- Add GeoLibre URL to layers.importStyle in the 17 non-English catalogs,
  which still listed only Mapbox GL / SLD / QML.
- Note in the embedding docs that a data= deep link also suppresses the
  welcome wizard.
Comment thread apps/geolibre-desktop/src/lib/data-url.ts
Comment thread apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts
Comment thread apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts Outdated
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • apps/geolibre-desktop/src/lib/data-url.ts (lines 149–178): for a non-ZIP ?data= response with no Content-Length header, the body is fully buffered via response.arrayBuffer() and JSON.parsed with no size cap — only the ZIP branch enforces a post-hoc byte ceiling (MAX_ZIP_GEOJSON_BYTES). A chunked/streaming endpoint can bypass the documented MAX_DOWNLOAD_BYTES protection and OOM/freeze the tab. Medium-high confidence.
  • apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts (lines 66–92): the PMTiles/GeoParquet branch identifies "its" newly added layer(s) by diffing store layer ids before/after an await. Since ?url= and ?data= can be combined and both loaders run concurrently, a concurrent loadProject(...) call (which replaces the entire layers array) mid-await would make unrelated project layers look "added," causing misapplied styles or a spurious raster/vector-style error. Medium confidence — plausible given the code explicitly anticipates combined params, not reproduced at runtime.
  • Same file (lines 83–92), lower confidence: if a single PMTiles/vector add ever yields multiple GeoLibreLayers, the loop can call setLayerStyle on earlier layers before hitting the raster/vector-style mismatch throw on a later one — a partial mutation-before-validation, unlike the ZIP branch which validates everything before adding.

Security

  • Same as the first bug above — framed as a resource-exhaustion/DoS vector since ?data= intentionally accepts arbitrary third-party URLs.

Performance

  • No new issues found beyond the byte-cap gap noted above (ZIP path already offloads inflate and caps memory sensibly).

Quality

  • No significant nits found; the new code is well-commented and the duplicate filename-stemming logic (styleSourceName in data-url.ts vs. geoLibreStyleSourceName in query-param-style.ts) serves distinct purposes (case-insensitive dispatch vs. canonical export naming), so not flagged as a real duplication problem.

CLAUDE.md

  • No violations found: i18n strings added across all locale catalogs, no direct MapLibre mutation (goes through the store), no relevant mirrored-constant or CSP host list needed updating for this change.

Posted 3 inline comments covering the findings above.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/i18n/locales/fa.json (1)

4854-4854: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the established Persian label for “style.”

fa.json already uses شیوهٔ نمایش for surrounding style menu/export/import labels, including importStyle, exportMapboxStyle, and exportStyleError. Keep exportGeoLibreStyle consistent by replacing سبک with شیوهٔ نمایش.

Proposed wording alignment
-    "exportGeoLibreStyle": "خروجی سبک URL ژئولیبر",
+    "exportGeoLibreStyle": "خروجی شیوهٔ نمایش URL ژئولیبر",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/i18n/locales/fa.json` at line 4854, Update the
exportGeoLibreStyle translation in fa.json to use the established Persian label
شیوهٔ نمایش instead of سبک, matching the terminology used by importStyle,
exportMapboxStyle, and exportStyleError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/map/src/query-param-style.ts`:
- Around line 19-21: Decode the selected ZIP fragment or source pathname before
deriving the filename stem, so encoded separators such as %2F are treated as
path delimiters before basename splitting. Update the logic around fragment/raw
selection and add a regression test covering archive.zip#folder%2Fparks.geojson,
ensuring the derived stem is parks and style matching remains successful.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/i18n/locales/fa.json`:
- Line 4854: Update the exportGeoLibreStyle translation in fa.json to use the
established Persian label شیوهٔ نمایش instead of سبک, matching the terminology
used by importStyle, exportMapboxStyle, and exportStyleError.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b6351bf1-3d15-4ab4-997f-420a9402393a

📥 Commits

Reviewing files that changed from the base of the PR and between b025a2a and 14b60ab.

📒 Files selected for processing (25)
  • apps/geolibre-desktop/src/App.tsx
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fa.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/th.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/data-url.ts
  • docs/user-guide/embedding.md
  • packages/map/src/query-param-style.ts
  • tests/data-url.test.ts
  • tests/query-param-style.test.ts

Comment thread packages/map/src/query-param-style.ts Outdated
- Roll the PMTiles import back before reporting a raster/vector-style
  mismatch, and decide it over all added layers before styling any of
  them, so a failed deep link leaves no layer behind and applies no
  partial symbology.
- Prefer the added layers that carry the data URL over a plain id diff,
  so a concurrent ?url= project load cannot make project layers look
  like the ones this import just created.
- Enforce the download ceiling while the body streams, not only when the
  server advertises Content-Length, so a chunked response is cut off at
  the limit instead of after it has been buffered whole.
- Decode a percent-encoded ZIP member fragment before taking the
  filename stem, which otherwise yielded "folder/parks".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts (1)

90-102: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not fall back to layers that are not proven to belong to this import.

If ?url= replaces the layer array while the native loader is awaited, every project layer is in synced. If layerPointsAt does not recognize the native loader source value, Line 91 selects those project layers. The raster check can then remove project layers at Line 102. Otherwise, Line 109 applies the remote style to project layers.

Remove this fallback. Make addPMTilesLayerFromUrl and addVectorLayerFromUrl return the created layer IDs, or require them to persist the exact remote.url before selecting or rolling back layers.

#!/bin/bash
set -euo pipefail

# Locate native loader definitions and inspect their layer creation and return contracts.
rg -n -C 8 --type ts '\b(addPMTilesLayerFromUrl|addVectorLayerFromUrl)\b' .

# Inspect source-path assignment and project replacement behavior.
rg -n -C 6 --type ts 'sourcePath|source:\s*\{[^}]*url|projectGeneration|setState\(\{.*layers|layers:' \
  apps packages
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts` around lines 90 - 102,
The import flow around pointingAtData must never use all synced project layers
as a fallback. Update addPMTilesLayerFromUrl and addVectorLayerFromUrl to return
the IDs of layers they create, or ensure they persist the exact remote.url and
select only those proven layers for styling and raster rollback; preserve
failure handling when no import layers are created.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts`:
- Around line 90-102: The import flow around pointingAtData must never use all
synced project layers as a fallback. Update addPMTilesLayerFromUrl and
addVectorLayerFromUrl to return the IDs of layers they create, or ensure they
persist the exact remote.url and select only those proven layers for styling and
raster rollback; preserve failure handling when no import layers are created.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8e3a1a59-7a5a-42b1-b0ce-f3652b7f0a33

📥 Commits

Reviewing files that changed from the base of the PR and between 14b60ab and 27c1cbe.

📒 Files selected for processing (5)
  • apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts
  • apps/geolibre-desktop/src/lib/data-url.ts
  • packages/map/src/query-param-style.ts
  • tests/data-url.test.ts
  • tests/query-param-style.test.ts

Comment thread apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/data-url.ts
Comment thread apps/geolibre-desktop/src/lib/data-url.ts
Comment thread apps/geolibre-desktop/src/lib/data-url.ts
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • useDataUrlLoader.ts:73-91 — When a link combines url= (project) and data=, loadProject performs a wholesale replace of state.layers. If that replace happens while the PMTiles/vector data-URL add is still in flight, the fallback addedLayers = pointingAtData.length ? pointingAtData : synced can misidentify the entire freshly-loaded project's layer set as "the added layer," leading to the deep-link style being applied to (or the layers being deleted from) the user's project instead of the intended data layer. Medium-high confidence.

Security

  • data-url.ts:238-248fetchRemoteStyle fetches the style= URL with response.text() directly, skipping the readCappedBytes streaming-size guard used by fetchRemoteData. A chunked/unbounded response from the (attacker-controllable) style URL bypasses the documented MAX_DOWNLOAD_BYTES ceiling. Medium confidence.
  • data-url.ts:149-174 — The ZIP-bomb guard in unzipGeoJsonEntries trusts each entry's declared originalSize from the archive metadata rather than actual inflated bytes; a crafted archive that lies about this field could bypass the size cap, depending on whether fflate's async unzip independently enforces it. Low-medium confidence.

Quality

  • data-url.ts:53-58remoteName calls decodeURIComponent unguarded; a malformed percent-sequence in the URL path (e.g. a literal %) throws URIError, which is still caught upstream but produces an unfriendly raw error message instead of this file's usual descriptive errors. Low confidence/severity; suggested fix included inline.

Performance / CLAUDE.md

  • No issues found. The size-capping design, i18n catalog updates (all locale files kept in sync), doc updates, and validate-before-mutate ordering for ZIP style imports all look solid and consistent with the stated project conventions.

Other things checked and found sound: the buildMapboxStyle/buildGeoLibreQueryStyle single-source assumption, mapboxStyleForDataLayer's case-insensitive filename matching, the abort/cleanup handling in useDataUrlLoader, the fitLayerIds effect timing in DesktopShell.tsx, and the streaming-cap tests for fetchRemoteData.

- Identify the layers a PMTiles/GeoParquet add created only by the data
  URL they record, with no fallback to the raw id diff. A concurrent
  project load that replaced the layer array could otherwise hand this
  branch the project's own layers to restyle or remove.
- Read the ?style= body through the same capped reader as ?data=, so an
  unbounded chunked style response is cut off at the ceiling too.
- Re-check the inflated ZIP total against the ceiling, instead of
  trusting the size each entry declares in the archive metadata.
- Decode a data URL filename defensively, so a literal percent sign in
  the path no longer surfaces as a raw "URI malformed".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/lib/data-url.ts`:
- Around line 178-185: Update unzipGeoJsonEntries to use fflate’s streaming
Unzip API instead of materializing all entries before validation. Track
cumulative decompressed bytes in each ondata chunk, terminate the stream and
reject with ZipTooLargeError as soon as MAX_ZIP_GEOJSON_BYTES is exceeded, while
preserving successful collection and resolution of selected GeoJSON entries.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e6f6a4b1-896e-4827-8542-8f826c5a0e40

📥 Commits

Reviewing files that changed from the base of the PR and between 27c1cbe and 07052fe.

📒 Files selected for processing (3)
  • apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts
  • apps/geolibre-desktop/src/lib/data-url.ts
  • tests/data-url.test.ts

Comment thread apps/geolibre-desktop/src/lib/data-url.ts Outdated
Comment on lines +241 to +249
const layers = Object.entries(entries).map(([name, contents]) => ({
data: parseFeatureCollection(strFromU8(contents), name),
name: name
.split("/")
.pop()!
.replace(/\.(?:geojson|json)$/i, ""),
sourcePath: `${url}#${name}`,
}));
if (!layers.length) throw new Error("The ZIP archive does not contain any GeoJSON files.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ZIP entry filter (unzipGeoJsonEntries) accepts both .geojson and .json files, but parseFeatureCollection here throws hard if any matched entry isn't a FeatureCollection. Real-world REST-export ZIPs commonly bundle a metadata.json/manifest.json alongside the actual GeoJSON payloads; one such file in the archive currently aborts the entire multi-layer import (... is not a GeoJSON FeatureCollection.) instead of importing the valid members and skipping/warning about the rest. Worth confirming this is the intended behavior, since it can turn an otherwise-successful multi-file import into a hard failure because of one unrelated .json file in the archive.

Confidence: medium.

Comment on lines +48 to +51
void Promise.all([
fetchRemoteData(params.dataUrl, { signal: controller.signal }),
params.styleUrl ? fetchRemoteStyle(params.styleUrl, { signal: controller.signal }) : null,
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise.all shares controller.signal between the data and style fetches, but nothing aborts the still-in-flight fetch when its sibling rejects first (e.g. a fast 404 on ?style= while ?data= is still streaming a large file, or vice versa). The overall load is already reported as failed via the .catch below, but the "losing" fetch keeps consuming bandwidth/CPU up to MAX_DOWNLOAD_BYTES (250 MB) for no benefit. Calling controller.abort() when either promise rejects would stop the wasted work.

Confidence: low — cosmetic/perf only, no functional impact since the result is discarded either way.

Comment on lines +142 to +147
setState({
error: null,
fitLayerIds,
message: `Loaded ${count} layer${count === 1 ? "" : "s"} from URL`,
status: "loaded",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state.message is populated with real user-facing copy ("Loading data from URL...", "Loaded N layer(s) from URL") but DesktopShell.tsx only ever reads dataUrlLoadState.error.message isn't rendered anywhere. This mirrors the pre-existing ProjectUrlLoadState.message, which appears to have the same gap, but since this PR adds a brand-new consumer it's worth double-checking that a loading/success indicator was intentionally dropped and not just forgotten — otherwise a ?data= deep link that takes a while (large COG/GeoParquet, slow REST endpoint) gives the user no feedback at all until it either silently finishes or shows an error banner.

Confidence: low-medium (may be intentional/deferred, and mirrors existing pattern).

Comment on lines +65 to +114
} else if (remote.kind === "pmtiles" || remote.kind === "vector") {
// Validate the style before invoking a native loader. Those controls
// assign their own ids, so collect the newly synchronized store
// layers after the awaited add completes.
const styleResult = rawStyle === null ? null : parseMapboxStyle(rawStyle);
if (styleResult && styleResult.matchedLayerCount === 0) {
throw new Error("The remote style has no supported vector style layers.");
}
const previousIds = new Set(store.layers.map((layer) => layer.id));
const added =
remote.kind === "pmtiles"
? await addPMTilesLayerFromUrl(mapAppAPI, remote.url)
: await addVectorLayerFromUrl(mapAppAPI, remote.url, {
name: remote.name,
fitBounds: true,
});
if (!added) throw new Error(`Could not add ${remote.name} to the map.`);
// These loaders assign their own ids, so the added layers have to be
// recovered from the store. Identify them by the data URL they record
// and not by an id diff alone: a concurrent `?url=` project load
// replaces the whole layer array, which would make every project
// layer look new here and hand this branch someone else's layers to
// restyle or remove. No match is treated as "could not identify",
// never as "take whatever is new".
const addedLayers = useAppStore
.getState()
.layers.filter(
(layer) => !previousIds.has(layer.id) && layerPointsAt(layer, remote.url),
);
if (!addedLayers.length) {
throw new Error(
`The ${remote.kind === "pmtiles" ? "PMTiles" : "GeoParquet"} loader did not create a layer for ${remote.url}.`,
);
}
// Check every added layer before styling any of them: the archive's
// tile type is only known once the control has read it, so a raster
// archive paired with a vector style has to be rolled back rather
// than left behind by a deep link that reports failure.
if (styleResult && addedLayers.some((layer) => layer.metadata.tileType === "raster")) {
for (const layer of addedLayers) store.removeLayer(layer.id);
throw new Error(
"MapLibre vector styles cannot be applied to a raster PMTiles archive.",
);
}
if (styleResult) {
for (const layer of addedLayers) {
store.setLayerStyle(layer.id, applyMapboxStyleImport(layer.style, styleResult));
}
}
count = addedLayers.length;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the most intricate part of the new loader — id-diffing against a concurrent ?url= project load, matching added layers by URL, rolling back a raster archive styled with a vector style, applying styles per added layer — yet it's exercised only indirectly (no test imports useDataUrlLoader itself). tests/data-url.test.ts and tests/query-param-style.test.ts cover the pure helpers (fetchRemoteData, mapboxStyleForDataLayer, parseRasterUrlStyle, buildGeoLibreQueryStyle) but not this orchestration logic. Given the concurrency edge case explicitly called out in the comments above (a project load racing this hook), a test double for the store/app API exercising this branch would catch regressions here that the current suite can't.

Confidence: medium (test-coverage gap, not a functional bug).

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

All inline comments are posted. Here's the summary.

Code review

Bugs

  • A ZIP passed to ?data= treats every .json member (not just .geojson) as a FeatureCollection; a non-GeoJSON .json file bundled alongside valid GeoJSON (e.g. a metadata.json from a REST export) throws and aborts the whole multi-layer import instead of importing the valid members. apps/geolibre-desktop/src/lib/data-url.ts:241-249. Confidence: medium.

Performance

  • Promise.all([fetchRemoteData, fetchRemoteStyle]) shares one AbortController, but neither fetch is aborted when its sibling rejects first — a fast style-fetch failure lets a large in-flight data download keep running to completion (up to the 250 MB ceiling) for a result that's already discarded. apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts:48-51. Confidence: low.

Quality

  • DataUrlLoadState.message carries real loading/success copy ("Loading data from URL...", "Loaded N layer(s) from URL") but DesktopShell.tsx only renders .error, so users get no feedback while a ?data= link is loading or once it succeeds — only silence or a failure banner. Mirrors an apparent pre-existing gap in the project URL loader, but worth confirming it's intentional here too. apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts:142-147. Confidence: low-medium.
  • The PMTiles/GeoParquet branch of useDataUrlLoader — id-diffing against a concurrent project load, URL-based layer matching, raster/vector style rollback — is the most complex new logic in the PR but has no direct test coverage; only the pure helper functions in lib/data-url.ts and query-param-style.ts are tested. apps/geolibre-desktop/src/hooks/useDataUrlLoader.ts:65-114. Confidence: medium.

Security

  • ?data=/?style= fetch and auto-apply arbitrary attacker-controlled content without user confirmation when a crafted link is opened, but this mirrors the already-established ?url= project-deep-link precedent in this codebase, so it's not a new class of risk — not raised as a separate finding.

CLAUDE.md

  • No violations found: i18n additions are present across all locale files (not just en.json), no changes touch the documented mirror constants (MAX_VECTOR_PMTILES_ZOOM, MAX_VECTOR_BYTES, MAP_PANEL_SELECTOR, propertySpecFor, DISTANCE_SEGMENTS), and the store-driven data flow (addGeoJsonLayer / setLayerStyle via the Zustand store) is followed rather than mutating MapLibre directly.

Enforce the ZIP import ceiling while the entries inflate, using fflate's
streaming Unzip and terminating the readers once the running total passes
the limit. Checking after unzip() had materialized every entry ran after
the allocation it was meant to prevent, which a ZIP that understates its
declared entry sizes could exploit. The declared size is still the cheap
first pass for an honestly-large archive.
@giswqs
giswqs merged commit 1793770 into main Aug 9, 2026
26 checks passed
@giswqs
giswqs deleted the feat/data-url-deep-link branch August 9, 2026 15:43
Comment on lines +118 to +128
const imports = remote.layers.map((layer) => {
if (rawStyle === null) return { layer, styleResult: null };
const styleResult = parseMapboxStyle(mapboxStyleForDataLayer(rawStyle, layer.name));
if (styleResult.matchedLayerCount === 0) {
throw new Error(
`The remote style has no supported layers for "${layer.name}.geojson". ` +
`Set each style layer's source to the matching filename stem (for example, "${layer.name}").`,
);
}
return { layer, styleResult };
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapboxStyleForDataLayer is applied here for every GeoJSON load, not only ZIP archives — remote.layers has exactly one entry for a plain single-file ?data= link too, and it goes through the same source filter.

That filter (data-url.ts mapboxStyleForDataLayer) keeps a style layer only when it has no source, or its source (stem-normalized) equals the data filename stem. A style produced by the general "Export as Mapbox GL style" (or any hand-authored/third-party Mapbox style — the spec requires source on every non-background layer) sets source to something derived from the layer name/id (buildMapboxStyle's ${idBase}-source), which will essentially never equal the raw filename stem. So pairing a single-file ?data= link with such a style throws "no supported layers" here, even though the docs and PR description describe ?style= as generically applying "Mapbox/MapLibre style JSON," and only call out source-stem binding "for a ZIP."

In practice only styles from the new Export GeoLibre URL style action (which deliberately renames its source to the stem) or a source-less style work for the single-file case. Worth either scoping this filter to the multi-layer (ZIP) case only, or calling out the source-binding requirement in the docs for single-file data=+style= too.

Confidence: medium — traced through buildMapboxStyle's sourceKey and mapboxStyleForDataLayer's matching logic, but haven't run it end-to-end in a browser.

Comment on lines +185 to +200
if (!settled && pushed && pending === 0) {
settled = true;
resolve(entries);
}
};

const unzipper = new Unzip((file) => {
if (file.name.endsWith("/") || !/\.(?:geojson|json)$/i.test(file.name)) return;
const declared = file.originalSize ?? 0;
if (declared > MAX_ZIP_GEOJSON_BYTES || total + declared > MAX_ZIP_GEOJSON_BYTES) {
fail(new ZipTooLargeError());
return;
}
const chunks: Uint8Array[] = [];
let size = 0;
pending += 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cumulative-size guard total + declared > MAX_ZIP_GEOJSON_BYTES doesn't actually reserve capacity for entries that were already accepted but haven't started producing bytes yet: total is only incremented inside ondata as real bytes stream in (line ~207), not when an entry passes this precheck. Since fflate's Unzip callback fires for every local-file-header it discovers within a single push() call — before any of those entries' ondata has fired — several individually-under-the-cap entries whose combined declared size exceeds MAX_ZIP_GEOJSON_BYTES can all pass this check (each sees total === 0) and all get file.start()'d concurrently.

The actual cap does still get enforced once real bytes start arriving (shared total across all entries, checked in ondata), so this isn't an unbounded memory blowup, but the doc comment above (unzipGeoJsonEntries) claims the declared size is consulted "as the cheap way to reject an honestly-large archive before inflating anything" — that guarantee doesn't hold once more than one qualifying entry is present, since acceptance isn't reserved into total until decompression has already begun.

A fix would be to increment total by declared immediately upon acceptance here (and correct for the discrepancy once the real size is known), so concurrently-registered entries can't all slip past the cumulative check.

Confidence: medium — based on reading fflate's streaming Unzip API usage here; I haven't run this against a crafted multi-entry ZIP to confirm the concurrent-registration behavior in practice.

}

function extension(url: string): string {
return new URL(url).pathname.split(".").pop()?.toLowerCase() ?? "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this splits the whole pathname on . rather than just the last path segment, so a URL with a dot in an earlier segment but none in the final one (e.g. /v1.2/export) yields a bogus "extension" ("2/export") instead of "". It's harmless today — the bogus value never matches the known-extension list, so it just falls through to the generic fetch+content-sniff path — but it's fragile if more extensions are added later or the fallback behavior changes.

Suggested change
return new URL(url).pathname.split(".").pop()?.toLowerCase() ?? "";
function extension(url: string): string {
const basename = new URL(url).pathname.split("/").pop() ?? "";
const dot = basename.lastIndexOf(".");
return dot >= 0 ? basename.slice(dot + 1).toLowerCase() : "";
}

Confidence: low — cosmetic/defensive, current behavior isn't user-visibly wrong.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • useDataUrlLoader.ts (single-file ?style= validation, medium confidence): mapboxStyleForDataLayer is applied uniformly to single-file and ZIP ?data= loads, filtering style layers by matching source to the filename stem. A style from the general "Export as Mapbox GL style" action (or any third-party/hand-authored Mapbox style, which per spec sets source on every non-background layer) uses a source key derived from the layer id/name, not the raw filename stem, so pairing it with a single-file ?data= link will almost always throw "no supported layers" — even though the docs/PR body describe ?style= as generically applying "Mapbox/MapLibre style JSON" and only call out source-stem binding for ZIPs. Only the new "Export GeoLibre URL style" output (or a source-less style) reliably works for the single-file case.

Performance

  • data-url.ts unzipGeoJsonEntries (medium confidence): the cumulative-size precheck (total + declared > MAX_ZIP_GEOJSON_BYTES) compares against total, which is only incremented as real bytes stream in via ondata, not when an entry is accepted. Because fflate's Unzip callback can fire for every entry in a single push() call before any ondata fires, multiple entries whose declared sizes are each individually under the cap but combined exceed it can all pass the precheck and start decompressing concurrently. The real cap still gets enforced once actual bytes arrive (bounded, not unbounded), but the "reject an honestly-large archive before inflating anything" guarantee in the doc comment doesn't fully hold for multi-entry archives.

Quality

  • data-url.ts extension() (low confidence, cosmetic): splits the whole pathname on . instead of just the last path segment, so a dot in an earlier segment (e.g. /v1.2/export) produces a bogus non-matching "extension" string. Currently harmless (falls through to the generic fetch+sniff path), but fragile. Suggested a small fix inline.
  • geoLibreStyleSourceName's fallback (no sourcePath) uses the raw, unsanitized layer.name as the exported style's source id/key — fine functionally (matching is case/stem-normalized), but produces an unusual embedded source id for layers not loaded from a URL. Very low severity, not flagged inline.

Security

  • No new issues beyond the pre-existing trust model already established by the ?url= project loader: ?data=/?style= fetch arbitrary attacker-suppliable URLs, and the CSP (connect-src ... https: ...) already permits broad HTTPS fetches, consistent with existing behavior. Size ceilings (250MB download, 250MB ZIP GeoJSON) and streaming enforcement are reasonable defenses against naive zip-bomb/large-response abuse. Low confidence, informational only.

CLAUDE.md

  • New UI strings use t() and are translated across all locale files with en.json as source of truth; new icon uses logical me-2 spacing (RTL-safe). No CSP/mirrored-constant updates were needed since this feature doesn't add a new baked-in host. No adherence issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants