tileserver-gl-light 5.4.1-pre.0 → 5.5.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  getTileUrls,
22
22
  getPublicUrl,
23
23
  isValidHttpUrl,
24
+ isValidRemoteUrl,
24
25
  } from './utils.js';
25
26
 
26
27
  import { fileURLToPath } from 'url';
@@ -59,7 +60,8 @@ async function start(opts) {
59
60
  app.use(
60
61
  morgan(logFormat, {
61
62
  stream: opts.logFile
62
- ? fs.createWriteStream(opts.logFile, { flags: 'a' })
63
+ ? // eslint-disable-next-line security/detect-non-literal-fs-filename -- logFile is from CLI/config, admin-controlled
64
+ fs.createWriteStream(opts.logFile, { flags: 'a' })
63
65
  : process.stdout,
64
66
  skip: (req, res) =>
65
67
  opts.silent && (res.statusCode === 200 || res.statusCode === 304),
@@ -72,6 +74,7 @@ async function start(opts) {
72
74
  if (opts.configPath) {
73
75
  configPath = path.resolve(opts.configPath);
74
76
  try {
77
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- configPath is from CLI argument, expected behavior
75
78
  config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
76
79
  } catch (e) {
77
80
  console.log('ERROR: Config file not found or invalid!');
@@ -106,8 +109,11 @@ async function start(opts) {
106
109
  const startupPromises = [];
107
110
 
108
111
  for (const type of Object.keys(paths)) {
112
+ // eslint-disable-next-line security/detect-object-injection -- type is from Object.keys of paths config
113
+ // eslint-disable-next-line security/detect-non-literal-fs-filename, security/detect-object-injection -- paths[type] constructed from validated config paths
109
114
  if (!fs.existsSync(paths[type])) {
110
115
  console.error(
116
+ // eslint-disable-next-line security/detect-object-injection -- type is from Object.keys of paths config
111
117
  `The specified path for "${type}" does not exist (${paths[type]}).`,
112
118
  );
113
119
  process.exit(1);
@@ -122,6 +128,7 @@ async function start(opts) {
122
128
  */
123
129
  async function getFiles(directory) {
124
130
  // Fetch all entries of the directory and attach type information
131
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- directory is constructed from validated config paths
125
132
  const dirEntries = await fs.promises.readdir(directory, {
126
133
  withFileTypes: true,
127
134
  });
@@ -150,10 +157,13 @@ async function start(opts) {
150
157
 
151
158
  if (options.dataDecorator) {
152
159
  try {
160
+ // eslint-disable-next-line security/detect-non-literal-require -- dataDecorator path is from config file, admin-controlled
153
161
  options.dataDecoratorFunc = require(
154
162
  path.resolve(paths.root, options.dataDecorator),
155
163
  );
156
- } catch (e) {}
164
+ } catch (e) {
165
+ // Silently fail if dataDecorator cannot be loaded
166
+ }
157
167
  }
158
168
 
159
169
  const data = clone(config.data || {});
@@ -178,13 +188,14 @@ async function start(opts) {
178
188
  * @param {object} item - The style configuration object.
179
189
  * @param {boolean} allowMoreData - Whether to allow adding more data sources.
180
190
  * @param {boolean} reportFonts - Whether to report fonts.
181
- * @returns {Promise<void>}
191
+ * @returns {Promise<boolean>} - Returns true if successful, false otherwise.
182
192
  */
183
193
  async function addStyle(id, item, allowMoreData, reportFonts) {
184
194
  let success = true;
185
195
 
186
196
  let styleJSON;
187
197
  try {
198
+ // Style files should only be HTTP/HTTPS, not S3
188
199
  if (isValidHttpUrl(item.style)) {
189
200
  const res = await fetch(item.style);
190
201
  if (!res.ok) {
@@ -193,6 +204,7 @@ async function start(opts) {
193
204
  styleJSON = await res.json();
194
205
  } else {
195
206
  const styleFile = path.resolve(options.paths.styles, item.style);
207
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- styleFile constructed from config base path and style name
196
208
  const styleFileData = await fs.promises.readFile(styleFile);
197
209
  styleJSON = JSON.parse(styleFileData);
198
210
  }
@@ -212,11 +224,14 @@ async function start(opts) {
212
224
  (styleSourceId, protocol) => {
213
225
  let dataItemId;
214
226
  for (const id of Object.keys(data)) {
227
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
215
228
  if (id === styleSourceId) {
216
229
  // Style id was found in data ids, return that id
217
230
  dataItemId = id;
218
231
  } else {
232
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
219
233
  const fileType = Object.keys(data[id])[0];
234
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config, fileType is from Object.keys
220
235
  if (data[id][fileType] === styleSourceId) {
221
236
  // Style id was found in data filename, return the id that filename belong to
222
237
  dataItemId = id;
@@ -236,12 +251,15 @@ async function start(opts) {
236
251
  let id =
237
252
  styleSourceId.substr(0, styleSourceId.lastIndexOf('.')) ||
238
253
  styleSourceId;
239
- if (isValidHttpUrl(styleSourceId)) {
254
+ // PMTiles can be remote URLs (HTTP or S3), generate unique ID for remote sources
255
+ if (isValidRemoteUrl(styleSourceId)) {
240
256
  id =
241
257
  fnv1a(styleSourceId) + '_' + id.replace(/^.*\/(.*)$/, '$1');
242
258
  }
259
+ // eslint-disable-next-line security/detect-object-injection -- id is being checked for existence before modification
243
260
  while (data[id]) id += '_'; //if the data source id already exists, add a "_" untill it doesn't
244
261
  //Add the new data source to the data array.
262
+ // eslint-disable-next-line security/detect-object-injection -- id is constructed above to be unique
245
263
  data[id] = {
246
264
  [protocol]: styleSourceId,
247
265
  };
@@ -252,6 +270,7 @@ async function start(opts) {
252
270
  },
253
271
  (font) => {
254
272
  if (reportFonts) {
273
+ // eslint-disable-next-line security/detect-object-injection -- font is font name from style
255
274
  serving.fonts[font] = true;
256
275
  }
257
276
  },
@@ -271,8 +290,12 @@ async function start(opts) {
271
290
  let resolvedFileType;
272
291
  let resolvedInputFile;
273
292
  let resolvedSparse = false;
293
+ let resolvedS3Profile;
294
+ let resolvedRequestPayer;
295
+ let resolvedS3Region;
274
296
 
275
297
  for (const id of Object.keys(data)) {
298
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
276
299
  const sourceData = data[id];
277
300
  let currentFileType;
278
301
  let currentInputFileValue;
@@ -295,13 +318,28 @@ async function start(opts) {
295
318
  resolvedFileType = currentFileType;
296
319
  resolvedInputFile = currentInputFileValue;
297
320
 
298
- // Get sparse flag specifically from this matching source
299
- // Default to false if 'sparse' key doesn't exist or is falsy in a boolean context
321
+ // Get sparse if present
300
322
  if (sourceData.hasOwnProperty('sparse')) {
301
- resolvedSparse = !!sourceData.sparse; // Ensure boolean
323
+ resolvedSparse = !!sourceData.sparse;
302
324
  } else {
303
- resolvedSparse = false; // Explicitly set default if not present on item
325
+ resolvedSparse = false;
326
+ }
327
+
328
+ // Get s3Profile if present
329
+ if (sourceData.hasOwnProperty('s3Profile')) {
330
+ resolvedS3Profile = sourceData.s3Profile;
331
+ }
332
+
333
+ // Get requestPayer if present
334
+ if (sourceData.hasOwnProperty('requestPayer')) {
335
+ resolvedRequestPayer = !!sourceData.requestPayer;
336
+ }
337
+
338
+ // Get s3Region if present
339
+ if (sourceData.hasOwnProperty('s3Region')) {
340
+ resolvedS3Region = sourceData.s3Region;
304
341
  }
342
+
305
343
  break; // Found our match, exit the outer loop
306
344
  }
307
345
  }
@@ -316,17 +354,23 @@ async function start(opts) {
316
354
  inputFile: undefined,
317
355
  fileType: undefined,
318
356
  sparse: false,
357
+ s3Profile: undefined,
358
+ requestPayer: false,
359
+ s3Region: undefined,
319
360
  };
320
361
  }
321
362
 
322
- if (!isValidHttpUrl(resolvedInputFile)) {
363
+ // PMTiles supports remote URLs (HTTP and S3), skip path resolution for those
364
+ if (!isValidRemoteUrl(resolvedInputFile)) {
323
365
  // Ensure options.paths and options.paths[resolvedFileType] exist before trying to use them
324
366
  if (
325
367
  options &&
326
368
  options.paths &&
369
+ // eslint-disable-next-line security/detect-object-injection -- resolvedFileType is either 'pmtiles' or 'mbtiles'
327
370
  options.paths[resolvedFileType]
328
371
  ) {
329
372
  resolvedInputFile = path.resolve(
373
+ // eslint-disable-next-line security/detect-object-injection -- resolvedFileType is either 'pmtiles' or 'mbtiles'
330
374
  options.paths[resolvedFileType],
331
375
  resolvedInputFile,
332
376
  );
@@ -341,6 +385,9 @@ async function start(opts) {
341
385
  inputFile: resolvedInputFile,
342
386
  fileType: resolvedFileType,
343
387
  sparse: resolvedSparse,
388
+ s3Profile: resolvedS3Profile,
389
+ requestPayer: resolvedRequestPayer,
390
+ s3Region: resolvedS3Region,
344
391
  };
345
392
  },
346
393
  ),
@@ -353,6 +400,7 @@ async function start(opts) {
353
400
  }
354
401
 
355
402
  for (const id of Object.keys(config.styles || {})) {
403
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of config.styles
356
404
  const item = config.styles[id];
357
405
  if (!item.style || item.style.length === 0) {
358
406
  console.log(`Missing "style" property for ${id}`);
@@ -366,7 +414,9 @@ async function start(opts) {
366
414
  }),
367
415
  );
368
416
  for (const id of Object.keys(data)) {
417
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
369
418
  const item = data[id];
419
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
370
420
  const fileType = Object.keys(data[id])[0];
371
421
  if (!fileType || !(fileType === 'pmtiles' || fileType === 'mbtiles')) {
372
422
  console.log(
@@ -377,6 +427,7 @@ async function start(opts) {
377
427
  startupPromises.push(serve_data.add(options, serving.data, item, id, opts));
378
428
  }
379
429
  if (options.serveAllStyles) {
430
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- options.paths.styles is from validated config
380
431
  fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
381
432
  if (err) {
382
433
  return;
@@ -419,6 +470,7 @@ async function start(opts) {
419
470
  * Handles requests for a list of available styles.
420
471
  * @param {object} req - Express request object.
421
472
  * @param {object} res - Express response object.
473
+ * @param {object} next - Express next middleware function.
422
474
  * @param {string} [req.query.key] - Optional API key.
423
475
  * @returns {void}
424
476
  */
@@ -428,6 +480,7 @@ async function start(opts) {
428
480
  ? `?key=${encodeURIComponent(req.query.key)}`
429
481
  : '';
430
482
  for (const id of Object.keys(serving.styles)) {
483
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
431
484
  const styleJSON = serving.styles[id].styleJSON;
432
485
  result.push({
433
486
  version: styleJSON.version,
@@ -451,7 +504,9 @@ async function start(opts) {
451
504
  * @returns {Array} - An array of TileJSON objects.
452
505
  */
453
506
  function addTileJSONs(arr, req, type, tileSize) {
507
+ // eslint-disable-next-line security/detect-object-injection -- type is 'rendered' or 'data', validated by caller
454
508
  for (const id of Object.keys(serving[type])) {
509
+ // eslint-disable-next-line security/detect-object-injection -- type is 'rendered' or 'data', id is from Object.keys
455
510
  const info = clone(serving[type][id].tileJSON);
456
511
  let path = '';
457
512
  if (type === 'rendered') {
@@ -479,6 +534,7 @@ async function start(opts) {
479
534
  * Handles requests for a rendered tilejson endpoint.
480
535
  * @param {object} req - Express request object.
481
536
  * @param {object} res - Express response object.
537
+ * @param {object} next - Express next middleware function.
482
538
  * @param {string} req.params.tileSize - Optional tile size parameter.
483
539
  * @returns {void}
484
540
  */
@@ -501,6 +557,7 @@ async function start(opts) {
501
557
  * Handles requests for a combined rendered and data tilejson endpoint.
502
558
  * @param {object} req - Express request object.
503
559
  * @param {object} res - Express response object.
560
+ * @param {object} next - Express next middleware function.
504
561
  * @param {string} req.params.tileSize - Optional tile size parameter.
505
562
  * @returns {void}
506
563
  */
@@ -526,8 +583,8 @@ async function start(opts) {
526
583
  * Serves a Handlebars template.
527
584
  * @param {string} urlPath - The URL path to serve the template at
528
585
  * @param {string} template - The name of the template file
529
- * @param {Function} dataGetter - A function to get data to be passed to the template.
530
- * @returns {void}
586
+ * @param {(req: object) => object|null} dataGetter - A function to get data to be passed to the template.
587
+ * @returns {void}
531
588
  */
532
589
  function serveTemplate(urlPath, template, dataGetter) {
533
590
  let templateFile = `${templates}/${template}.tmpl`;
@@ -542,6 +599,7 @@ async function start(opts) {
542
599
  }
543
600
  }
544
601
  try {
602
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- templateFile is from internal templates or config-specified path
545
603
  const content = fs.readFileSync(templateFile, 'utf-8');
546
604
  const compiled = handlebars.compile(content.toString());
547
605
  app.get(urlPath, (req, res, next) => {
@@ -581,15 +639,17 @@ async function start(opts) {
581
639
  /**
582
640
  * Handles requests for the index page, providing a list of available styles and data.
583
641
  * @param {object} req - Express request object.
584
- * @param {object} res - Express response object.
585
- * @returns {void}
642
+ * @returns {object|null} Template data object or null
586
643
  */
587
644
  serveTemplate('/', 'index', (req) => {
588
645
  let styles = {};
589
646
  for (const id of Object.keys(serving.styles || {})) {
590
647
  let style = {
648
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
591
649
  ...serving.styles[id],
650
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
592
651
  serving_data: serving.styles[id],
652
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
593
653
  serving_rendered: serving.rendered[id],
594
654
  };
595
655
 
@@ -618,12 +678,15 @@ async function start(opts) {
618
678
  )[0];
619
679
  }
620
680
 
681
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
621
682
  styles[id] = style;
622
683
  }
623
684
  let datas = {};
624
685
  for (const id of Object.keys(serving.data || {})) {
686
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.data
625
687
  let data = Object.assign({}, serving.data[id]);
626
688
 
689
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.data
627
690
  const { tileJSON } = serving.data[id];
628
691
  const { center } = tileJSON;
629
692
 
@@ -682,6 +745,7 @@ async function start(opts) {
682
745
  }
683
746
  data.formatted_filesize = `${size.toFixed(2)} ${suffix}`;
684
747
  }
748
+ // eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.data
685
749
  datas[id] = data;
686
750
  }
687
751
  return {
@@ -693,12 +757,11 @@ async function start(opts) {
693
757
  /**
694
758
  * Handles requests for a map viewer template for a specific style.
695
759
  * @param {object} req - Express request object.
696
- * @param {object} res - Express response object.
697
- * @param {string} req.params.id - ID of the style.
698
- * @returns {void}
760
+ * @returns {object|null} Template data object or null
699
761
  */
700
762
  serveTemplate('/styles/:id/', 'viewer', (req) => {
701
763
  const { id } = req.params;
764
+ // eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
702
765
  const style = clone(((serving.styles || {})[id] || {}).styleJSON);
703
766
 
704
767
  if (!style) {
@@ -707,8 +770,11 @@ async function start(opts) {
707
770
  return {
708
771
  ...style,
709
772
  id,
773
+ // eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
710
774
  name: (serving.styles[id] || serving.rendered[id]).name,
775
+ // eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
711
776
  serving_data: serving.styles[id],
777
+ // eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
712
778
  serving_rendered: serving.rendered[id],
713
779
  };
714
780
  });
@@ -716,12 +782,11 @@ async function start(opts) {
716
782
  /**
717
783
  * Handles requests for a Web Map Tile Service (WMTS) XML template.
718
784
  * @param {object} req - Express request object.
719
- * @param {object} res - Express response object.
720
- * @param {string} req.params.id - ID of the style.
721
- * @returns {void}
785
+ * @returns {object|null} Template data object or null
722
786
  */
723
787
  serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
724
788
  const { id } = req.params;
789
+ // eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
725
790
  const wmts = clone((serving.styles || {})[id]);
726
791
 
727
792
  if (!wmts) {
@@ -746,6 +811,7 @@ async function start(opts) {
746
811
  return {
747
812
  ...wmts,
748
813
  id,
814
+ // eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
749
815
  name: (serving.styles[id] || serving.rendered[id]).name,
750
816
  baseUrl,
751
817
  };
@@ -754,13 +820,11 @@ async function start(opts) {
754
820
  /**
755
821
  * Handles requests for a data view template for a specific data source.
756
822
  * @param {object} req - Express request object.
757
- * @param {object} res - Express response object.
758
- * @param {string} req.params.id - ID of the data source.
759
- * @param {string} [req.params.view] - Optional view type.
760
- * @returns {void}
823
+ * @returns {object|null} Template data object or null
761
824
  */
762
825
  serveTemplate('/data{/:view}/:id/', 'data', (req) => {
763
826
  const { id, view } = req.params;
827
+ // eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
764
828
  const data = serving.data[id];
765
829
 
766
830
  if (!data) {
@@ -806,14 +870,40 @@ async function start(opts) {
806
870
  process.env.PORT || opts.port,
807
871
  process.env.BIND || opts.bind,
808
872
  function () {
809
- let address = this.address().address;
873
+ const addressInfo = this.address();
874
+
875
+ if (!addressInfo) {
876
+ console.error('Failed to bind to port');
877
+ return;
878
+ }
879
+
880
+ let address = addressInfo.address;
810
881
  if (address.indexOf('::') === 0) {
811
882
  address = `[${address}]`; // literal IPv6 address
812
883
  }
813
- console.log(`Listening at http://${address}:${this.address().port}/`);
884
+ console.log(`Listening at http://${address}:${addressInfo.port}/`);
814
885
  },
815
886
  );
816
887
 
888
+ // Handle server errors
889
+ server.on('error', (err) => {
890
+ const port = process.env.PORT || opts.port;
891
+ if (err.code === 'EADDRINUSE') {
892
+ console.error(`ERROR: Port ${port} is already in use.`);
893
+ console.error(`Please choose a different port with -p or --port option.`);
894
+ process.exit(1);
895
+ } else if (err.code === 'EACCES') {
896
+ console.error(`ERROR: Permission denied to bind to port ${port}.`);
897
+ console.error(
898
+ `Try using a port number above 1024 or run with appropriate permissions.`,
899
+ );
900
+ process.exit(1);
901
+ } else {
902
+ console.error('Server error:', err.message);
903
+ process.exit(1);
904
+ }
905
+ });
906
+
817
907
  // add server.shutdown() to gracefully stop serving
818
908
  enableShutdown(server);
819
909
 
package/src/utils.js CHANGED
@@ -9,18 +9,23 @@ import { existsP } from './promises.js';
9
9
  import { getPMtilesTile } from './pmtiles_adapter.js';
10
10
 
11
11
  export const allowedSpriteFormats = allowedOptions(['png', 'json']);
12
-
13
12
  export const allowedTileSizes = allowedOptions(['256', '512']);
13
+ export const httpTester = /^https?:\/\//i;
14
+ export const s3Tester = /^s3:\/\//i; // Plain AWS S3 format
15
+ export const s3HttpTester = /^s3\+https?:\/\//i; // S3-compatible with custom endpoint
16
+ export const pmtilesTester = /^pmtiles:\/\//i;
17
+ export const mbtilesTester = /^mbtiles:\/\//i;
14
18
 
15
19
  /**
16
20
  * Restrict user input to an allowed set of options.
17
21
  * @param {string[]} opts - An array of allowed option strings.
18
22
  * @param {object} [config] - Optional configuration object.
19
23
  * @param {string} [config.defaultValue] - The default value to return if input doesn't match.
20
- * @returns {function(string): string} - A function that takes a value and returns it if valid or a default.
24
+ * @returns {(value: string) => string} - A function that takes a value and returns it if valid or a default.
21
25
  */
22
26
  export function allowedOptions(opts, { defaultValue } = {}) {
23
27
  const values = Object.fromEntries(opts.map((key) => [key, key]));
28
+ // eslint-disable-next-line security/detect-object-injection -- value is checked against allowed opts keys
24
29
  return (value) => values[value] || defaultValue;
25
30
  }
26
31
 
@@ -35,7 +40,7 @@ export function allowedScales(scale, maxScale = 9) {
35
40
  return 1;
36
41
  }
37
42
 
38
- // eslint-disable-next-line security/detect-non-literal-regexp
43
+ // eslint-disable-next-line security/detect-non-literal-regexp -- maxScale is a number parameter, not user input
39
44
  const regex = new RegExp(`^[2-${maxScale}]x$`);
40
45
  if (!regex.test(scale)) {
41
46
  return null;
@@ -156,6 +161,7 @@ export function getTileUrls(
156
161
  const hostParts = urlObject.host.split('.');
157
162
  const relativeSubdomainsUsable =
158
163
  hostParts.length > 1 &&
164
+ // eslint-disable-next-line security/detect-unsafe-regex -- Simple IPv4 validation, no nested quantifiers
159
165
  !/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(urlObject.host);
160
166
  const newDomains = [];
161
167
  for (const domain of domains) {
@@ -184,7 +190,9 @@ export function getTileUrls(
184
190
  }
185
191
  const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
186
192
 
193
+ // eslint-disable-next-line security/detect-object-injection -- format is validated format string from tileJSON
187
194
  if (aliases && aliases[format]) {
195
+ // eslint-disable-next-line security/detect-object-injection -- format is validated format string from tileJSON
188
196
  format = aliases[format];
189
197
  }
190
198
 
@@ -245,7 +253,7 @@ export function fixTileJSONCenter(tileJSON) {
245
253
  export function readFile(filename) {
246
254
  return new Promise((resolve, reject) => {
247
255
  const sanitizedFilename = path.normalize(filename); // Normalize path, remove ..
248
- // eslint-disable-next-line security/detect-non-literal-fs-filename
256
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- filename is normalized and validated by caller
249
257
  fs.readFile(String(sanitizedFilename), (err, data) => {
250
258
  if (err) {
251
259
  reject(err);
@@ -266,6 +274,7 @@ export function readFile(filename) {
266
274
  * @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error.
267
275
  */
268
276
  async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
277
+ // eslint-disable-next-line security/detect-object-injection -- name is validated font name from sanitizedName check
269
278
  if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
270
279
  const fontMatch = name?.match(/^[\p{L}\p{N} \-\.~!*'()@&=+,#$\[\]]+$/u);
271
280
  const sanitizedName = fontMatch?.[0] || 'invalid';
@@ -295,6 +304,7 @@ async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
295
304
  if (!fallbacks) {
296
305
  fallbacks = clone(allowedFonts || {});
297
306
  }
307
+ // eslint-disable-next-line security/detect-object-injection -- name is validated font name
298
308
  delete fallbacks[name];
299
309
 
300
310
  try {
@@ -314,8 +324,10 @@ async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
314
324
  fontStyle = 'Regular';
315
325
  }
316
326
  fallbackName = `Noto Sans ${fontStyle}`;
327
+ // eslint-disable-next-line security/detect-object-injection -- fallbackName is constructed from validated font style
317
328
  if (!fallbacks[fallbackName]) {
318
329
  fallbackName = `Open Sans ${fontStyle}`;
330
+ // eslint-disable-next-line security/detect-object-injection -- fallbackName is constructed from validated font style
319
331
  if (!fallbacks[fallbackName]) {
320
332
  fallbackName = Object.keys(fallbacks)[0];
321
333
  }
@@ -325,6 +337,7 @@ async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
325
337
  fallbackName,
326
338
  sanitizedName,
327
339
  );
340
+ // eslint-disable-next-line security/detect-object-injection -- fallbackName is constructed from validated font style
328
341
  delete fallbacks[fallbackName];
329
342
  return getFontPbf(null, fontPath, fallbackName, range, fallbacks);
330
343
  } else {
@@ -377,13 +390,16 @@ export async function getFontsPbf(
377
390
  export async function listFonts(fontPath) {
378
391
  const existingFonts = {};
379
392
 
393
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- fontPath is from validated config
380
394
  const files = await fsPromises.readdir(fontPath);
381
395
  for (const file of files) {
396
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- file is from readdir of validated fontPath
382
397
  const stats = await fsPromises.stat(path.join(fontPath, file));
383
398
  if (
384
399
  stats.isDirectory() &&
385
400
  (await existsP(path.join(fontPath, file, '0-255.pbf')))
386
401
  ) {
402
+ // eslint-disable-next-line security/detect-object-injection -- file is from readdir, used as font name key
387
403
  existingFonts[path.basename(file)] = true;
388
404
  }
389
405
  }
@@ -392,20 +408,72 @@ export async function listFonts(fontPath) {
392
408
  }
393
409
 
394
410
  /**
395
- * Checks if a string is a valid HTTP or HTTPS URL.
396
- * @param {string} string - The string to validate.
397
- * @returns {boolean} True if the string is a valid HTTP/HTTPS URL, false otherwise.
411
+ * Checks if a string is a valid HTTP/HTTPS URL.
412
+ * @param {string} string - The string to check.
413
+ * @returns {boolean} - True if the string is a valid HTTP/HTTPS URL.
398
414
  */
399
415
  export function isValidHttpUrl(string) {
400
- let url;
416
+ try {
417
+ return httpTester.test(string);
418
+ } catch (e) {
419
+ return false;
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Checks if a string is a valid S3 URL.
425
+ * @param {string} string - The string to check.
426
+ * @returns {boolean} - True if the string is a valid S3 URL.
427
+ */
428
+ export function isS3Url(string) {
429
+ try {
430
+ return s3Tester.test(string) || s3HttpTester.test(string);
431
+ } catch (e) {
432
+ return false;
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Checks if a string is a valid remote URL (HTTP, HTTPS, or S3).
438
+ * @param {string} string - The string to check.
439
+ * @returns {boolean} - True if the string is a valid remote URL.
440
+ */
441
+ export function isValidRemoteUrl(string) {
442
+ try {
443
+ return (
444
+ httpTester.test(string) ||
445
+ s3Tester.test(string) ||
446
+ s3HttpTester.test(string)
447
+ );
448
+ } catch (e) {
449
+ return false;
450
+ }
451
+ }
401
452
 
453
+ /**
454
+ * Checks if a string uses the pmtiles:// protocol.
455
+ * @param {string} string - The string to check.
456
+ * @returns {boolean} - True if the string uses pmtiles:// protocol.
457
+ */
458
+ export function isPMTilesProtocol(string) {
402
459
  try {
403
- url = new URL(string);
404
- } catch (_) {
460
+ return pmtilesTester.test(string);
461
+ } catch (e) {
405
462
  return false;
406
463
  }
464
+ }
407
465
 
408
- return url.protocol === 'http:' || url.protocol === 'https:';
466
+ /**
467
+ * Checks if a string uses the mbtiles:// protocol.
468
+ * @param {string} string - The string to check.
469
+ * @returns {boolean} - True if the string uses mbtiles:// protocol.
470
+ */
471
+ export function isMBTilesProtocol(string) {
472
+ try {
473
+ return mbtilesTester.test(string);
474
+ } catch (e) {
475
+ return false;
476
+ }
409
477
  }
410
478
 
411
479
  /**