@mapbox/mapbox-gl-style-spec 14.24.1 → 14.25.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.
Files changed (57) hide show
  1. package/bin/gl-style-composite.js +9 -4
  2. package/bin/gl-style-format.js +10 -4
  3. package/bin/gl-style-migrate.js +9 -4
  4. package/bin/gl-style-validate.js +16 -15
  5. package/deref.ts +8 -6
  6. package/dist/index.cjs +7743 -9443
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.ts +9 -11
  9. package/dist/index.es.js +7743 -9443
  10. package/dist/index.es.js.map +1 -1
  11. package/expression/compound_expression.ts +1 -1
  12. package/expression/definitions/assertion.ts +1 -1
  13. package/expression/definitions/at_interpolated.ts +2 -1
  14. package/expression/definitions/case.ts +1 -1
  15. package/expression/definitions/coalesce.ts +1 -1
  16. package/expression/definitions/coercion.ts +1 -1
  17. package/expression/definitions/distance.ts +1 -9
  18. package/expression/definitions/interpolate.ts +3 -3
  19. package/expression/definitions/literal.ts +1 -1
  20. package/expression/definitions/match.ts +1 -1
  21. package/expression/definitions/number_format.ts +8 -6
  22. package/expression/definitions/within.ts +1 -8
  23. package/expression/evaluation_context.ts +1 -10
  24. package/expression/index.ts +1 -1
  25. package/expression/values.ts +3 -4
  26. package/feature_filter/index.ts +1 -1
  27. package/function/convert.ts +1 -1
  28. package/group_by_layout.ts +8 -2
  29. package/package.json +4 -6
  30. package/read_style.ts +4 -4
  31. package/reference/v8.json +20 -7
  32. package/rollup.config.js +7 -4
  33. package/test.js +1 -1
  34. package/types/config_options.ts +7 -7
  35. package/types/lut.ts +5 -5
  36. package/types/tile_id.ts +3 -3
  37. package/types.ts +3 -1
  38. package/union-to-intersection.ts +2 -2
  39. package/util/assert.ts +3 -0
  40. package/util/color.ts +33 -18
  41. package/util/deep_equal.ts +6 -4
  42. package/util/geometry_util.ts +1 -1
  43. package/util/lerp.ts +3 -1
  44. package/util/mercator.ts +16 -0
  45. package/util/unbundle_jsonlint.ts +3 -2
  46. package/validate/validate.ts +2 -0
  47. package/validate/validate_appearance.ts +1 -1
  48. package/validate/validate_color.ts +5 -2
  49. package/validate/validate_expression.ts +16 -8
  50. package/validate/validate_function.ts +3 -2
  51. package/validate/validate_iconset.ts +1 -1
  52. package/validate/validate_import.ts +7 -0
  53. package/validate/validate_layer.ts +4 -1
  54. package/validate/validate_object.ts +20 -7
  55. package/validate/validate_option.ts +37 -0
  56. package/validate/validate_terrain.ts +1 -1
  57. package/validate_style.ts +3 -3
@@ -14,8 +14,9 @@ export function deepUnbundle(value: unknown): unknown {
14
14
  const unbundledValue: {
15
15
  [key: string]: unknown;
16
16
  } = {};
17
- for (const key in value) {
18
- unbundledValue[key] = deepUnbundle(value[key]);
17
+ const valueRec = value as Record<string, unknown>;
18
+ for (const key in valueRec) {
19
+ unbundledValue[key] = deepUnbundle(valueRec[key]);
19
20
  }
20
21
  return unbundledValue;
21
22
  }
@@ -2,6 +2,7 @@ import {unbundle, deepUnbundle} from '../util/unbundle_jsonlint';
2
2
  import {isExpression} from '../expression/index';
3
3
  import {isFunction} from '../function/index';
4
4
  import validateImport from './validate_import';
5
+ import validateOption from './validate_option';
5
6
  import validateFunction from './validate_function';
6
7
  import validateExpression from './validate_expression';
7
8
  import validateObject from './validate_object';
@@ -54,6 +55,7 @@ const VALIDATORS: Record<string, (...args: unknown[]) => ValidationError[]> = {
54
55
  'projection': validateProjection,
55
56
  'import': validateImport,
56
57
  'iconset': validateIconset,
58
+ 'option': validateOption,
57
59
  };
58
60
 
59
61
  export type ValidatorOptions = {
@@ -90,7 +90,7 @@ function validateCondition(options: AppearanceValidatorOptions): Array<Validatio
90
90
  errors.push(...validateExpression({
91
91
  key: options.key,
92
92
  value: condition,
93
- valueSpec: (latest['appearance'] as Record<string, unknown>)['condition'] as StylePropertySpecification,
93
+ valueSpec: (latest['appearance'] as Record<string, unknown>)['condition'],
94
94
  expressionContext: 'appearance'
95
95
  }));
96
96
 
@@ -1,6 +1,7 @@
1
1
  import ValidationError from '../error/validation_error';
2
2
  import {getType, isString} from '../util/get_type';
3
- import {parseCSSColor} from 'csscolorparser';
3
+ import {unbundle} from '../util/unbundle_jsonlint';
4
+ import Color from '../util/color';
4
5
 
5
6
  type ColorValidatorOptions = {
6
7
  key: string;
@@ -12,7 +13,9 @@ export default function validateColor({key, value}: ColorValidatorOptions): Vali
12
13
  return [new ValidationError(key, value, `color expected, ${getType(value)} found`)];
13
14
  }
14
15
 
15
- if (parseCSSColor(value) === null) {
16
+ // `value` may be a jsonlint-lines-primitives `String` wrapper; unbundle to a
17
+ // primitive so `Color.parse`'s `typeof input === 'string'` check passes.
18
+ if (Color.parse(unbundle(value) as string) === undefined) {
16
19
  return [new ValidationError(key, value, `color expected, "${value}" found`)];
17
20
  }
18
21
 
@@ -50,7 +50,7 @@ export default function validateExpression(options: ExpressionValidatorOptions):
50
50
 
51
51
  if (options.expressionContext === 'appearance') {
52
52
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
53
- return checkDisallowedParameters(expressionObj, options);
53
+ return checkDisallowedAppearancesParameters(expressionObj, options);
54
54
  }
55
55
 
56
56
  if (options.expressionContext && options.expressionContext.indexOf('cluster') === 0) {
@@ -103,32 +103,40 @@ export function disallowedFilterParameters(e: Expression, options: any): Validat
103
103
  return errors;
104
104
  }
105
105
 
106
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
- function checkDisallowedParameters(e: Expression, options: any): ValidationError[] {
108
- const allowedParameters = new Set<string>();
106
+ const FEATURE_OPERATORS = new Set(['get', 'has', 'properties', 'geometry-type', 'id']);
107
+ const APPEARANCES_DISALLOWED_PARAMETERS = new Set([
108
+ 'zoom', 'pitch', 'distance-from-center',
109
+ 'feature', 'feature-state',
110
+ 'measure-light',
111
+ 'heatmap-density', 'line-progress',
112
+ 'raster-value', 'raster-particle-speed', 'sky-radial-progress',
113
+ ]);
109
114
 
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ function checkDisallowedAppearancesParameters(e: Expression, options: any): ValidationError[] {
110
117
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
111
118
  if (options.valueSpec && options.valueSpec.expression) {
112
119
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
113
120
  for (const param of options.valueSpec.expression.parameters) {
114
121
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
115
- allowedParameters.add(param);
122
+ APPEARANCES_DISALLOWED_PARAMETERS.delete(param);
116
123
  }
117
124
  }
118
125
 
119
- if (allowedParameters.size === 0) {
126
+ if (APPEARANCES_DISALLOWED_PARAMETERS.size === 0) {
120
127
  return [];
121
128
  }
122
129
  const errors: ValidationError[] = [];
123
130
 
124
131
  if (e instanceof CompoundExpression) {
125
- if (!allowedParameters.has(e.name)) {
132
+ const param = FEATURE_OPERATORS.has(e.name) ? 'feature' : e.name;
133
+ if (APPEARANCES_DISALLOWED_PARAMETERS.has(param)) {
126
134
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
127
135
  return [new ValidationError(options.key, options.value, `["${e.name}"] is not an allowed parameter`)];
128
136
  }
129
137
  }
130
138
  e.eachChild((arg) => {
131
- errors.push(...checkDisallowedParameters(arg, options));
139
+ errors.push(...checkDisallowedAppearancesParameters(arg, options));
132
140
  });
133
141
 
134
142
  return errors;
@@ -99,7 +99,7 @@ export default function validateFunction(options: FunctionValidatorOptions): Val
99
99
  errors = errors.concat(validateArray({
100
100
  key: options.key,
101
101
  value,
102
- valueSpec: options.valueSpec as Extract<StylePropertySpecification, {type: 'array'}>,
102
+ valueSpec: options.valueSpec,
103
103
  style: options.style,
104
104
  styleSpec: options.styleSpec,
105
105
  arrayElementValidator: validateFunctionStop
@@ -207,7 +207,8 @@ export default function validateFunction(options: FunctionValidatorOptions): Val
207
207
  }
208
208
 
209
209
  if (functionType === 'categorical' && type === 'number' && (typeof value !== 'number' || !isFinite(value) || Math.floor(value) !== value)) {
210
- return [new ValidationError(options.key, reportValue, `integer expected, found ${String(value as number)}`)];
210
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- value narrowed to number|string|boolean by the check above
211
+ return [new ValidationError(options.key, reportValue, `integer expected, found ${String(value)}`)];
211
212
  }
212
213
 
213
214
  if (functionType !== 'categorical' && type === 'number' && typeof value === 'number' && typeof previousStopDomainValue === 'number' && previousStopDomainValue !== undefined && value < previousStopDomainValue) {
@@ -45,7 +45,7 @@ export default function validateIconset(options: IconsetValidatorOptions): Valid
45
45
  }));
46
46
 
47
47
  if (isSourceIconset(type, iconset)) {
48
- const source = style.sources && style.sources[iconset.source];
48
+ const source = style.sources && Object.hasOwn(style.sources, iconset.source) ? style.sources[iconset.source] : undefined;
49
49
  const sourceType = source && unbundle(source.type) as string;
50
50
  if (!source) {
51
51
  errors.push(new ValidationError(key, iconset.source, `source "${iconset.source}" not found`));
@@ -42,6 +42,13 @@ export default function validateImport(options: ImportValidatorOptions): Validat
42
42
  errors.push(new ValidationError(key, importSpec, `import id can't be an empty string`));
43
43
  }
44
44
 
45
+ // Reject reserved prototype-pollution key — the import id is used as a scope
46
+ // string that becomes a dictionary key in several runtime caches.
47
+ if (unbundle(importSpec.id) === '__proto__') {
48
+ const key = `${options.key}.id`;
49
+ errors.push(new ValidationError(key, importSpec, `import id can't be "__proto__"`));
50
+ }
51
+
45
52
  if (data) {
46
53
  const key = `${options.key}.data`;
47
54
  errors = errors.concat(validateStyle(data, styleSpec, {key}));
@@ -77,7 +77,10 @@ export default function validateLayer(options: LayerValidatorOptions): Validatio
77
77
  } else if (!isString(layer.source)) {
78
78
  errors.push(new ValidationError(`${key}.source`, layer.source, '"source" must be a string'));
79
79
  } else {
80
- const source = style.sources && style.sources[layer.source];
80
+ // Object.hasOwn: a bare lookup like `style.sources[layer.source]` would
81
+ // find inherited keys from Object.prototype (e.g. "constructor", "toString")
82
+ // and treat them as valid sources.
83
+ const source = style.sources && Object.hasOwn(style.sources, layer.source) ? style.sources[layer.source] : undefined;
81
84
  const sourceType = source && unbundle(source.type);
82
85
  if (!source) {
83
86
  errors.push(new ValidationError(key, layer.source, `source "${layer.source}" not found`));
@@ -41,18 +41,30 @@ export default function validateObject(options: ObjectValidatorOptions): Validat
41
41
 
42
42
  let errors: ValidationError[] = [];
43
43
  for (const objectKey in object) {
44
+ // Skip inherited keys: a hostile style JSON parsed with JSON.parse can
45
+ // have an own "__proto__" key, but downstream consumers may have mutated
46
+ // the prototype of `object` upstream. We never want to validate
47
+ // inherited properties as if they were user-supplied.
48
+ if (!Object.hasOwn(object, objectKey)) continue;
49
+
44
50
  const elementSpecKey = objectKey.split('.')[0]; // treat 'paint.*' as 'paint'
51
+ // Object.hasOwn: a bare lookup like `elementSpecs[objectKey]` would
52
+ // find inherited keys from Object.prototype (e.g. "__proto__",
53
+ // "constructor", "toString") when the user-supplied key matches one,
54
+ // returning a non-spec value that then fails as a validator.
55
+ const hasSpec = Object.hasOwn(elementSpecs, elementSpecKey);
56
+ const hasWildcardSpec = Object.hasOwn(elementSpecs, '*');
45
57
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
46
- const elementSpec = elementSpecs[elementSpecKey] || elementSpecs['*'];
58
+ const elementSpec = hasSpec ? elementSpecs[elementSpecKey] : (hasWildcardSpec ? elementSpecs['*'] : undefined);
47
59
 
48
60
  let validateElement: ((options: ObjectElementValidatorOptions, object?: unknown) => ValidationError[]) | undefined;
49
- if (elementValidators[elementSpecKey]) {
61
+ if (Object.hasOwn(elementValidators, elementSpecKey)) {
50
62
  validateElement = elementValidators[elementSpecKey];
51
- } else if (elementSpecs[elementSpecKey]) {
63
+ } else if (hasSpec) {
52
64
  validateElement = validateSpec;
53
- } else if (elementValidators['*']) {
65
+ } else if (Object.hasOwn(elementValidators, '*')) {
54
66
  validateElement = elementValidators['*'];
55
- } else if (elementSpecs['*']) {
67
+ } else if (hasWildcardSpec) {
56
68
  validateElement = validateSpec;
57
69
  }
58
70
 
@@ -73,13 +85,14 @@ export default function validateObject(options: ObjectValidatorOptions): Validat
73
85
  }
74
86
 
75
87
  for (const elementSpecKey in elementSpecs) {
88
+ if (!Object.hasOwn(elementSpecs, elementSpecKey)) continue;
76
89
  // Don't check `required` when there's a custom validator for that property.
77
- if (elementValidators[elementSpecKey]) {
90
+ if (Object.hasOwn(elementValidators, elementSpecKey)) {
78
91
  continue;
79
92
  }
80
93
 
81
94
  const elementSpec = elementSpecs[elementSpecKey] as {required?: boolean; default?: unknown};
82
- if (elementSpec.required && elementSpec['default'] === undefined && object[elementSpecKey] === undefined) {
95
+ if (elementSpec.required && elementSpec['default'] === undefined && (!Object.hasOwn(object, elementSpecKey) || object[elementSpecKey] === undefined)) {
83
96
  errors.push(new ValidationError(key, object, `missing required property "${elementSpecKey}"`));
84
97
  }
85
98
  }
@@ -0,0 +1,37 @@
1
+ import validateSpec from './validate';
2
+ import validateObject from './validate_object';
3
+ import {unbundle} from '../util/unbundle_jsonlint';
4
+ import {isObject} from '../util/get_type';
5
+
6
+ import type ValidationError from '../error/validation_error';
7
+ import type {ValidatorOptions} from './validate';
8
+
9
+ // Validator for a single config option (one entry in `schema`). Wraps the
10
+ // generic object validator to inherit the option's declared `type` when
11
+ // validating its `default`, so expressions in the default are checked against
12
+ // the option's expected type instead of `*` (any).
13
+ export default function validateOption(options: ValidatorOptions): ValidationError[] {
14
+ const styleSpec = options.styleSpec;
15
+ const optionValue = options.value as {type?: unknown; array?: unknown} | null | undefined;
16
+ // Don't propagate the declared type for array options: `getExpectedType`
17
+ // doesn't model the schema's `array: true` flag, so forcing a scalar type
18
+ // here would reject an array-of-T default that previously validated fine.
19
+ const isArrayOption = isObject(optionValue) && unbundle(optionValue.array) === true;
20
+ const declaredType = !isArrayOption && isObject(optionValue) ? unbundle(optionValue.type) : undefined;
21
+
22
+ return validateObject(Object.assign({}, options, {
23
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
24
+ valueSpec: styleSpec.option,
25
+ objectElementValidators: declaredType ? {
26
+ // Only propagate the declared type when the default is an
27
+ // expression (array-form). Primitive defaults keep the previous
28
+ // permissive validation, mirroring the runtime parser's narrowing
29
+ // in style.ts/parser.cpp.
30
+ default: (elementOptions: ValidatorOptions): ValidationError[] => (Array.isArray(elementOptions.value) ?
31
+ validateSpec(Object.assign({}, elementOptions, {
32
+ valueSpec: Object.assign({}, elementOptions.valueSpec, {type: declaredType}),
33
+ })) :
34
+ validateSpec(elementOptions)),
35
+ } : undefined,
36
+ }));
37
+ }
@@ -74,7 +74,7 @@ export default function validateTerrain(options: TerrainValidatorOptions): Valid
74
74
  } else if (!isString(terrain.source)) {
75
75
  errors.push(new ValidationError(`${key}.source`, terrain.source, `source must be a string`));
76
76
  } else {
77
- const source = style.sources && style.sources[terrain.source];
77
+ const source = style.sources && Object.hasOwn(style.sources, terrain.source) ? style.sources[terrain.source] : undefined;
78
78
  const sourceType = source && unbundle(source.type) as string;
79
79
  if (!source) {
80
80
  errors.push(new ValidationError(`${key}.source`, terrain.source, `source "${terrain.source}" not found`));
package/validate_style.ts CHANGED
@@ -11,8 +11,8 @@ import type {StyleSpecification} from './types';
11
11
  *
12
12
  * @private
13
13
  * @alias validate
14
- * @param {Object|String|Buffer} style The style to be validated. If a `String`
15
- * or `Buffer` is provided, the returned errors will contain line numbers.
14
+ * @param {Object|String|Uint8Array} style The style to be validated. If a `String`
15
+ * or `Uint8Array` is provided, the returned errors will contain line numbers.
16
16
  * @param {Object} [styleSpec] The style specification to validate against.
17
17
  * If omitted, the spec version is inferred from the stylesheet.
18
18
  * @returns {Array<ValidationError|ParsingError>}
@@ -22,7 +22,7 @@ import type {StyleSpecification} from './types';
22
22
  * var errors = validate(style);
23
23
  */
24
24
 
25
- export default function validateStyle(style: StyleSpecification | string | Buffer, styleSpec: StyleReference = v8): ValidationErrors {
25
+ export default function validateStyle(style: StyleSpecification | string | Uint8Array, styleSpec: StyleReference = v8): ValidationErrors {
26
26
  let s = style;
27
27
 
28
28
  try {