Skip to content

Commit 56dd677

Browse files
tomasciccolaTomás CiccolaEvanHahn
authored
fix: check for icon file referenced by preset.icon (#782)
* check for icon file referenced by preset.icon * add access-point to completeConfig * add test for invalid icon name * remove unnecessary icons, fix presets.json * add icons to complete config * metter check for `'icon' in field` * compute `iconFilenames` once, repurpuse icon name regex * iconFilenames should be `Set<string>`, not `Set<string | undefined>` (#790) * Fix missing icon name, warn instead of throwing (#791) --------- Co-authored-by: Tomás Ciccola <[email protected]> Co-authored-by: Evan Hahn <[email protected]>
1 parent b017e61 commit 56dd677

File tree

14 files changed

+157
-20
lines changed

14 files changed

+157
-20
lines changed

src/config-import.js

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import yauzl from 'yauzl-promise'
22
import { validate, valueSchemas } from '@mapeo/schema'
33
import { json, buffer } from 'node:stream/consumers'
4-
import { assert } from './utils.js'
4+
import { assert, isDefined } from './utils.js'
55
import path from 'node:path'
66
import { parse as parseBCP47 } from 'bcp-47'
77
import { SUPPORTED_CONFIG_VERSION } from './constants.js'
88

99
// Throw error if a zipfile contains more than 10,000 entries
1010
const MAX_ENTRIES = 10_000
1111
const MAX_ICON_SIZE = 10_000_000
12+
const ICON_NAME_REGEX = /([a-zA-Z0-9-]+)-([a-zA-Z]+)@(\d+)x\.[a-zA-Z]+$/
1213

1314
/**
1415
* @typedef {yauzl.Entry} Entry
@@ -50,6 +51,7 @@ export async function readConfig(configPath) {
5051
const presetsFile = await findPresetsFile(entries)
5152
const translationsFile = await findTranslationsFile(entries)
5253
const metadataFile = await findMetadataFile(entries)
54+
const iconEntries = getIconEntries(entries)
5355
assert(
5456
isValidConfigFile(metadataFile),
5557
`invalid or missing config file version ${metadataFile.fileVersion}. We support version ${SUPPORTED_CONFIG_VERSION}}`
@@ -75,13 +77,6 @@ export async function readConfig(configPath) {
7577
/** @type {IconData | undefined} */
7678
let icon
7779

78-
// we sort the icons by filename so we can group variants together
79-
const iconEntries = entries
80-
.filter((entry) => entry.filename.match(/^icons\/([^/]+)$/))
81-
.sort((icon, nextIcon) =>
82-
icon.filename.localeCompare(nextIcon.filename)
83-
)
84-
8580
for (const entry of iconEntries) {
8681
if (entry.uncompressedSize > MAX_ICON_SIZE) {
8782
warnings.push(
@@ -177,6 +172,18 @@ export async function readConfig(configPath) {
177172
return sort - nextSort
178173
})
179174

175+
const iconFilenames = new Set(
176+
iconEntries
177+
.map((icon) => {
178+
const matches = path.basename(icon.filename).match(ICON_NAME_REGEX)
179+
if (matches) {
180+
const [_, name] = matches
181+
return name
182+
}
183+
})
184+
.filter(isDefined)
185+
)
186+
180187
// 5. for each preset get the corresponding fieldId and iconId, add them to the db
181188
for (const { preset, name } of sortedPresets) {
182189
/** @type {Record<string, unknown>} */
@@ -193,6 +200,20 @@ export async function readConfig(configPath) {
193200
presetValue[key] = preset[key]
194201
}
195202
}
203+
204+
if (!('icon' in preset) || typeof preset.icon !== 'string') {
205+
warnings.push(new Error(`Preset ${preset.name} doesn't have an icon`))
206+
return
207+
}
208+
if (!iconFilenames.has(preset.icon)) {
209+
warnings.push(
210+
new Error(
211+
`preset references icon with name ${preset.icon} but file doesn't exist`
212+
)
213+
)
214+
return
215+
}
216+
196217
if (!validate('preset', presetValue)) {
197218
warnings.push(new Error(`Invalid preset ${preset.name}`))
198219
continue
@@ -472,16 +493,23 @@ function translateMessageObject(warnings) {
472493
}
473494
}
474495

496+
/**
497+
* @param {ReadonlyArray<Entry>} entries
498+
*/
499+
function getIconEntries(entries) {
500+
return entries
501+
.filter((entry) => entry.filename.match(/^icons\/([^/]+)$/))
502+
.sort((icon, nextIcon) => icon.filename.localeCompare(nextIcon.filename))
503+
}
504+
475505
/**
476506
* @param {string} filename
477507
* @param {Buffer} buf
478508
* @returns {{ name: string, variant: IconData['variants'][Number] }}}
479509
*/
480510
function parseIcon(filename, buf) {
481511
const parsedFilename = path.parse(filename)
482-
const matches = parsedFilename.base.match(
483-
/([a-zA-Z0-9-]+)-([a-zA-Z]+)@(\d+)x\.[a-zA-Z]+$/
484-
)
512+
const matches = parsedFilename.base.match(ICON_NAME_REGEX)
485513
if (!matches) {
486514
throw new Error(`Unexpected icon filename ${filename}`)
487515
}

src/utils.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ export function setHas(set) {
109109
return (value) => set.has(/** @type {*} */ (value))
110110
}
111111

112+
/**
113+
* @template T
114+
* @param {undefined | T} value
115+
* @returns {value is T}
116+
*/
117+
export function isDefined(value) {
118+
return value !== undefined
119+
}
120+
112121
/**
113122
* When reading from SQLite, any optional properties are set to `null`. This
114123
* converts `null` back to `undefined` to match the input types (e.g. the types

test-e2e/translation-api.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import test from 'node:test'
22
import assert from 'node:assert/strict'
3+
import { isDefined } from '../src/utils.js'
34
import { createManagers, ManagerCustodian } from './utils.js'
45
import { defaultConfigPath } from '../tests/helpers/default-config.js'
56
import {
@@ -387,12 +388,3 @@ test('translation api - re-loading from disk', async (t) => {
387388

388389
assert(hasExpectedNumberOfTranslations)
389390
})
390-
391-
/**
392-
* @template T
393-
* @param {undefined | T} value
394-
* @returns {value is T}
395-
*/
396-
function isDefined(value) {
397-
return value !== undefined
398-
}

tests/config-import.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,27 @@ test('config import - presets', async () => {
223223
'the second error is because the preset is null'
224224
)
225225

226+
config = await readConfig('./tests/fixtures/config/noIconNameOnPreset.zip')
227+
arrayFrom(config.presets())
228+
assert.equal(
229+
config.warnings.length,
230+
1,
231+
'we got an error when loading the preset'
232+
)
233+
assert(
234+
/Punto de entrada doesn't have an icon/.test(config.warnings[0].message),
235+
"there's a warning because the preset has no icon field"
236+
)
237+
238+
config = await readConfig(
239+
'./tests/fixtures/config/invalidIconNameOnPreset.zip'
240+
)
241+
arrayFrom(config.presets())
242+
assert(
243+
/preset references icon with name/.test(config.warnings[0].message),
244+
"there's a warning because the preset references a missing icon"
245+
)
246+
226247
config = await readConfig('./tests/fixtures/config/validPreset.zip')
227248
for (const preset of config.presets()) {
228249
assert.equal(preset.value.schemaName, 'preset', `schemaName is 'preset'`)
1.29 KB
Loading
1.29 KB
Loading
948 Bytes
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "test-config",
3+
"buildDate": "2024-07-29T17:37:23.382Z",
4+
"fileVersion": "1.0"
5+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"fields":{
3+
"nombre-monitor": {
4+
"tagKey": "nombre-monitor",
5+
"type": "text",
6+
"label": "Nombre del Monitor o Mapeador",
7+
"universal": true,
8+
"placeholder": "¿Quién está haciendo esta observación?"
9+
}
10+
},
11+
"presets":{
12+
"access-point":{
13+
"icon": "access-pont",
14+
"fields": [
15+
"nombre-monitor"
16+
],
17+
"geometry": [
18+
"point",
19+
"area",
20+
"line"
21+
],
22+
"sort": 1,
23+
"tags": {
24+
"type": "threat",
25+
"threat": "access-point"
26+
},
27+
"name": "Punto de entrada"
28+
},
29+
"plant":{
30+
"icon": "plant",
31+
"fields": [
32+
"nombre-monitor"
33+
],
34+
"sort": 3,
35+
"geometry": [
36+
"point",
37+
"area"
38+
],
39+
"tags": {
40+
"type": "natural",
41+
"natural": "plant"
42+
},
43+
"terms": [],
44+
"name": "Planta"
45+
}
46+
}
47+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "test-config",
3+
"buildDate": "2024-08-28T21:20:00.069Z",
4+
"fileVersion": "1.0"
5+
}

0 commit comments

Comments
 (0)