Add readFile function

This commit is contained in:
acalcutt 2025-01-05 01:18:01 -05:00
parent 3fedd5bb77
commit 1f693003ed
2 changed files with 164 additions and 152 deletions

View file

@ -7,7 +7,7 @@ import clone from 'clone';
import express from 'express'; import express from 'express';
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec'; import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
import { fixUrl, allowedOptions } from './utils.js'; import { fixUrl, allowedOptions, readFile } from './utils.js';
const httpTester = /^https?:\/\//i; const httpTester = /^https?:\/\//i;
const allowedSpriteFormats = allowedOptions(['png', 'json']); const allowedSpriteFormats = allowedOptions(['png', 'json']);
@ -101,94 +101,98 @@ export const serve_style = {
* @param {string} req.params.format - Format of the sprite file, 'png' or 'json'. * @param {string} req.params.format - Format of the sprite file, 'png' or 'json'.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
app.get(`/:id/sprite{/:spriteID}{@:scale}{.:format}`, (req, res, next) => { app.get(
const { spriteID = 'default', id, format, scale } = req.params; `/:id/sprite{/:spriteID}{@:scale}{.:format}`,
const sanitizedId = String(id).replace(/\n|\r/g, ''); async (req, res, next) => {
const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : ''; const { spriteID = 'default', id, format, scale } = req.params;
const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, ''); const sanitizedId = String(id).replace(/\n|\r/g, '');
const sanitizedFormat = format const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : '';
? '.' + String(format).replace(/\n|\r/g, '') const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, '');
: ''; const sanitizedFormat = format
if (verbose) { ? '.' + String(format).replace(/\n|\r/g, '')
console.log( : '';
`Handling sprite request for: /styles/%s/sprite/%s%s%s`, if (verbose) {
sanitizedId, console.log(
sanitizedSpriteID, `Handling sprite request for: /styles/%s/sprite/%s%s%s`,
sanitizedScale,
sanitizedFormat,
);
}
const item = repo[id];
const validatedFormat = allowedSpriteFormats(format);
if (!item || !validatedFormat) {
if (verbose)
console.error(
`Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
sanitizedId, sanitizedId,
sanitizedSpriteID, sanitizedSpriteID,
sanitizedScale, sanitizedScale,
sanitizedFormat, sanitizedFormat,
); );
return res.sendStatus(404);
}
const sprite = item.spritePaths.find((sprite) => sprite.id === spriteID);
const spriteScale = allowedSpriteScales(scale);
if (!sprite || spriteScale === null) {
if (verbose)
console.error(
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
sanitizedId,
sanitizedSpriteID,
sanitizedScale,
sanitizedFormat,
);
return res.status(400).send('Bad Sprite ID or Scale');
}
const modifiedSince = req.get('if-modified-since');
const cc = req.get('cache-control');
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
if (
new Date(item.lastModified).getTime() ===
new Date(modifiedSince).getTime()
) {
return res.sendStatus(304);
} }
} const item = repo[id];
const validatedFormat = allowedSpriteFormats(format);
const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, ''); if (!item || !validatedFormat) {
const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
if (verbose) console.log(`Loading sprite from: %s`, filename);
// eslint-disable-next-line security/detect-non-literal-fs-filename
fs.readFile(filename, (err, data) => {
if (err) {
if (verbose) if (verbose)
console.error(
`Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
sanitizedId,
sanitizedSpriteID,
sanitizedScale,
sanitizedFormat,
);
return res.sendStatus(404);
}
const sprite = item.spritePaths.find(
(sprite) => sprite.id === spriteID,
);
const spriteScale = allowedSpriteScales(scale);
if (!sprite || spriteScale === null) {
if (verbose)
console.error(
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
sanitizedId,
sanitizedSpriteID,
sanitizedScale,
sanitizedFormat,
);
return res.status(400).send('Bad Sprite ID or Scale');
}
const modifiedSince = req.get('if-modified-since');
const cc = req.get('cache-control');
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
if (
new Date(item.lastModified).getTime() ===
new Date(modifiedSince).getTime()
) {
return res.sendStatus(304);
}
}
const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, '');
const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
if (verbose) console.log(`Loading sprite from: %s`, filename);
try {
const data = await readFile(filename);
if (validatedFormat === 'json') {
res.header('Content-type', 'application/json');
} else if (validatedFormat === 'png') {
res.header('Content-type', 'image/png');
}
if (verbose)
console.log(
`Responding with sprite data for /styles/%s/sprite/%s%s%s`,
sanitizedId,
sanitizedSpriteID,
sanitizedScale,
sanitizedFormat,
);
res.set({ 'Last-Modified': item.lastModified });
return res.send(data);
} catch (err) {
if (verbose) {
console.error( console.error(
'Sprite load error: %s, Error: %s', 'Sprite load error: %s, Error: %s',
filename, filename,
String(err), String(err),
); );
}
return res.sendStatus(404); return res.sendStatus(404);
} }
},
if (validatedFormat === 'json') { );
res.header('Content-type', 'application/json');
} else if (validatedFormat === 'png') {
res.header('Content-type', 'image/png');
}
if (verbose)
console.log(
`Responding with sprite data for /styles/%s/sprite/%s%s%s`,
sanitizedId,
sanitizedSpriteID,
sanitizedScale,
sanitizedFormat,
);
res.set({ 'Last-Modified': item.lastModified });
return res.send(data);
});
});
return app; return app;
}, },

View file

@ -185,6 +185,24 @@ export function fixTileJSONCenter(tileJSON) {
} }
} }
/**
* Reads a file and returns a Promise with the file data.
* @param {string} filename - Path to the file to read.
* @returns {Promise<Buffer>} - A Promise that resolves with the file data as a Buffer or rejects with an error.
*/
export function readFile(filename) {
return new Promise((resolve, reject) => {
// eslint-disable-next-line security/detect-non-literal-fs-filename
fs.readFile(filename, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
/** /**
* Retrieves font data for a given font and range. * Retrieves font data for a given font and range.
* @param {object} allowedFonts - An object of allowed fonts. * @param {object} allowedFonts - An object of allowed fonts.
@ -194,85 +212,75 @@ export function fixTileJSONCenter(tileJSON) {
* @param {object} [fallbacks] - Optional fallback font list. * @param {object} [fallbacks] - Optional fallback font list.
* @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error. * @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error.
*/ */
function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) { async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
return new Promise((resolve, reject) => { if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
if (!allowedFonts || (allowedFonts[name] && fallbacks)) { const fontMatch = name?.match(/^[\w\s-]+$/);
const fontMatch = name?.match(/^[\w\s-]+$/); const sanitizedName = fontMatch?.[0] || 'invalid';
const sanitizedName = fontMatch?.[0] || 'invalid'; if (!name || typeof name !== 'string' || name.trim() === '' || !fontMatch) {
if ( console.error(
!name || 'ERROR: Invalid font name: %s',
typeof name !== 'string' || sanitizedName.replace(/\n|\r/g, ''),
name.trim() === '' ||
!fontMatch
) {
console.error(
'ERROR: Invalid font name: %s',
sanitizedName.replace(/\n|\r/g, ''),
);
return reject('Invalid font name');
}
const rangeMatch = range?.match(/^[\d-]+$/);
const sanitizedRange = rangeMatch?.[0] || 'invalid';
if (!/^\d+-\d+$/.test(range)) {
console.error(
'ERROR: Invalid range: %s',
sanitizedRange.replace(/\n|\r/g, ''),
);
return reject('Invalid range');
}
const filename = path.join(
fontPath,
sanitizedName,
`${sanitizedRange}.pbf`,
); );
if (!fallbacks) { throw new Error('Invalid font name');
fallbacks = clone(allowedFonts || {});
}
delete fallbacks[name];
// eslint-disable-next-line security/detect-non-literal-fs-filename
fs.readFile(filename, (err, data) => {
if (err) {
console.error(
'ERROR: Font not found: %s, Error: %s',
filename.replace(/\n|\r/g, ''),
String(err),
);
if (fallbacks && Object.keys(fallbacks).length) {
let fallbackName;
let fontStyle = name.split(' ').pop();
if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) {
fontStyle = 'Regular';
}
fallbackName = `Noto Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = `Open Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = Object.keys(fallbacks)[0];
}
}
console.error(
`ERROR: Trying to use %s as a fallback for: %s`,
fallbackName,
sanitizedName,
);
delete fallbacks[fallbackName];
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
resolve,
reject,
);
} else {
reject('Font load error');
}
} else {
resolve(data);
}
});
} else {
reject('Font not allowed');
} }
});
const rangeMatch = range?.match(/^[\d-]+$/);
const sanitizedRange = rangeMatch?.[0] || 'invalid';
if (!/^\d+-\d+$/.test(range)) {
console.error(
'ERROR: Invalid range: %s',
sanitizedRange.replace(/\n|\r/g, ''),
);
throw new Error('Invalid range');
}
const filename = path.join(
fontPath,
sanitizedName,
`${sanitizedRange}.pbf`,
);
if (!fallbacks) {
fallbacks = clone(allowedFonts || {});
}
delete fallbacks[name];
try {
const data = await readFile(filename);
return data;
} catch (err) {
console.error(
'ERROR: Font not found: %s, Error: %s',
filename.replace(/\n|\r/g, ''),
String(err),
);
if (fallbacks && Object.keys(fallbacks).length) {
let fallbackName;
let fontStyle = name.split(' ').pop();
if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) {
fontStyle = 'Regular';
}
fallbackName = `Noto Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = `Open Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = Object.keys(fallbacks)[0];
}
}
console.error(
`ERROR: Trying to use %s as a fallback for: %s`,
fallbackName,
sanitizedName,
);
delete fallbacks[fallbackName];
return getFontPbf(null, fontPath, fallbackName, range, fallbacks);
} else {
throw new Error('Font load error');
}
}
} else {
throw new Error('Font not allowed');
}
} }
/** /**
* Combines multiple font pbf buffers into one. * Combines multiple font pbf buffers into one.