From 9e85cabdea0a59aac6e47b7437f7c674f07e5703 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Thu, 12 Dec 2024 20:33:45 +0000 Subject: [PATCH] fix: `minItems` for relevant geometry types Among other uses, we use this module to generate random geometries (see `@mapeo/mock-data`). Without `minItems` annotations, we would generate invalid geometries. This fixes that. This also fixes a related bug: I used `minLength`, not `minItems`, for `LineString` and `MultiLineString`. --- json/geometry.json | 19 +++-- src/index.ts | 79 +++++++++++++++---- .../bad-proto/linestring-no-coordinates.json | 5 ++ .../multilinestring-no-coordinates.json | 5 ++ .../bad-proto/multipoint-no-coordinates.json | 5 ++ .../multipolygon-no-coordinates.json | 5 ++ ...dinates.json => point-no-coordinates.json} | 0 .../bad-proto/polygon-no-coordinates.json | 5 ++ .../too-big-length-multilinestring.json | 2 +- ...-short-default-length-multilinestring.json | 5 ++ .../too-short-line-multilinestring.json | 2 +- .../bad-proto/too-short-linestring.json | 2 +- 12 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 test/fixture/bad-proto/linestring-no-coordinates.json create mode 100644 test/fixture/bad-proto/multilinestring-no-coordinates.json create mode 100644 test/fixture/bad-proto/multipoint-no-coordinates.json create mode 100644 test/fixture/bad-proto/multipolygon-no-coordinates.json rename test/fixture/bad-proto/{no-coordinates.json => point-no-coordinates.json} (100%) create mode 100644 test/fixture/bad-proto/polygon-no-coordinates.json create mode 100644 test/fixture/bad-proto/too-short-default-length-multilinestring.json diff --git a/json/geometry.json b/json/geometry.json index 818ab97..3da46ef 100644 --- a/json/geometry.json +++ b/json/geometry.json @@ -64,7 +64,7 @@ "items": { "$ref": "#/definitions/position" }, - "minLength": 2 + "minItems": 2 } }, "additionalProperties": false @@ -85,8 +85,9 @@ "items": { "$ref": "#/definitions/position" }, - "minLength": 2 - } + "minItems": 2 + }, + "minItems": 1 } }, "additionalProperties": false @@ -104,7 +105,8 @@ "type": "array", "items": { "$ref": "#/definitions/linearRing" - } + }, + "minItems": 1 } }, "additionalProperties": false @@ -122,7 +124,8 @@ "type": "array", "items": { "$ref": "#/definitions/position" - } + }, + "minItems": 1 } }, "additionalProperties": false @@ -142,8 +145,10 @@ "type": "array", "items": { "$ref": "#/definitions/linearRing" - } - } + }, + "minItems": 1 + }, + "minItems": 1 } }, "additionalProperties": false diff --git a/src/index.ts b/src/index.ts index 9a965cd..805d127 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,12 +83,18 @@ function encodePoint({ coordinates }: Point): GeometryProto { function decodeLineString({ coordinates: rawCoords, }: GeometryProto): LineString { + const coordinates = readPositionArray(rawCoords, { + start: 0, + length: rawCoords.length / 2, + }) + assertLengthIsAtLeast( + coordinates, + 2, + 'LineString must have at least 2 positions' + ) return { type: 'LineString', - coordinates: readPositionArray(rawCoords, { - start: 0, - length: rawCoords.length / 2, - }), + coordinates, } } @@ -104,21 +110,39 @@ function decodeMultiLineString({ coordinates: rawCoords, lengths, }: GeometryProto): MultiLineString { - let coordinates: Position[][] + let coordinates if (lengths.length === 0) { - coordinates = [ - readPositionArray(rawCoords, { start: 0, length: rawCoords.length / 2 }), - ] + const line = readPositionArray(rawCoords, { + start: 0, + length: rawCoords.length / 2, + }) + assertLengthIsAtLeast( + line, + 2, + 'MultiLineString line must have at least 2 positions' + ) + coordinates = [line] } else { let start = 0 coordinates = lengths.map((length) => { const line = readPositionArray(rawCoords, { start, length }) + assertLengthIsAtLeast( + line, + 2, + 'MultiLineString line must have at least 2 positions' + ) start += length * 2 return line }) } + assertLengthIsAtLeast( + coordinates, + 1, + 'MultiLineString must have at least 1 line' + ) + return { type: 'MultiLineString', coordinates, @@ -140,12 +164,14 @@ function encodeMultiLineString({ function decodeMultiPoint({ coordinates: rawCoords, }: GeometryProto): MultiPoint { + const coordinates = readPositionArray(rawCoords, { + start: 0, + length: rawCoords.length / 2, + }) + assertLengthIsAtLeast(coordinates, 1, 'MultiPoint must have at least 1 point') return { type: 'MultiPoint', - coordinates: readPositionArray(rawCoords, { - start: 0, - length: rawCoords.length / 2, - }), + coordinates, } } @@ -174,6 +200,7 @@ function decodePolygon({ rings[i] = ring start += length * 2 } + assertLengthIsAtLeast(rings, 1, 'Polygon must have at least 1 ring') return { type: 'Polygon', coordinates: rings, @@ -195,7 +222,7 @@ function decodeMultiPolygon({ lengths, }: GeometryProto): MultiPolygon { lengths = lengths.length === 0 ? [1, 1, rawCoords.length / 2] : lengths - const polygons = new Array(lengths[0]) + const polygons = new Array>(lengths[0]) let start = 0 for (let i = 0, j = 1; i < lengths[0]; i++) { const ringsLength = lengths[j + i] @@ -211,8 +238,14 @@ function decodeMultiPolygon({ start += length * 2 } j += ringsLength + assertLengthIsAtLeast(rings, 1, 'Polygon must have at least 1 ring') polygons[i] = rings } + assertLengthIsAtLeast( + polygons, + 1, + 'MultiPolygon must have at least 1 polygon' + ) return { type: 'MultiPolygon', coordinates: polygons, @@ -260,9 +293,7 @@ const validateLatitude = rangeValidator(90) const validateLongitude = rangeValidator(180) function validateLinearRing(ring: Position[]): asserts ring is LinearRing { - if (ring.length < 4) { - throw new Error('Invalid number of coordinates in linear ring') - } + assertLengthIsAtLeast(ring, 4, 'Invalid number of coordinates in linear ring') } function readPositionArray( @@ -287,6 +318,22 @@ function readPositionArray( return positions } +type BuildArrayMinLength< + T, + N extends number, + Current extends T[], +> = Current['length'] extends N + ? [...Current, ...T[]] + : BuildArrayMinLength + +function assertLengthIsAtLeast( + arr: T[], + length: N, + message: string +): asserts arr is BuildArrayMinLength { + if (arr.length < length) throw new Error(message) +} + class ExhaustivenessError extends Error { constructor( value: never, diff --git a/test/fixture/bad-proto/linestring-no-coordinates.json b/test/fixture/bad-proto/linestring-no-coordinates.json new file mode 100644 index 0000000..b5579d1 --- /dev/null +++ b/test/fixture/bad-proto/linestring-no-coordinates.json @@ -0,0 +1,5 @@ +{ + "type": "TYPE_LINE_STRING", + "coordinates": [], + "lengths": [] +} diff --git a/test/fixture/bad-proto/multilinestring-no-coordinates.json b/test/fixture/bad-proto/multilinestring-no-coordinates.json new file mode 100644 index 0000000..3a006d5 --- /dev/null +++ b/test/fixture/bad-proto/multilinestring-no-coordinates.json @@ -0,0 +1,5 @@ +{ + "type": "TYPE_MULTI_LINE_STRING", + "coordinates": [], + "lengths": [] +} diff --git a/test/fixture/bad-proto/multipoint-no-coordinates.json b/test/fixture/bad-proto/multipoint-no-coordinates.json new file mode 100644 index 0000000..650104d --- /dev/null +++ b/test/fixture/bad-proto/multipoint-no-coordinates.json @@ -0,0 +1,5 @@ +{ + "type": "TYPE_MULTI_POINT", + "coordinates": [], + "lengths": [] +} diff --git a/test/fixture/bad-proto/multipolygon-no-coordinates.json b/test/fixture/bad-proto/multipolygon-no-coordinates.json new file mode 100644 index 0000000..7eba17f --- /dev/null +++ b/test/fixture/bad-proto/multipolygon-no-coordinates.json @@ -0,0 +1,5 @@ +{ + "type": "TYPE_MULTI_POLYGON", + "coordinates": [], + "lengths": [] +} diff --git a/test/fixture/bad-proto/no-coordinates.json b/test/fixture/bad-proto/point-no-coordinates.json similarity index 100% rename from test/fixture/bad-proto/no-coordinates.json rename to test/fixture/bad-proto/point-no-coordinates.json diff --git a/test/fixture/bad-proto/polygon-no-coordinates.json b/test/fixture/bad-proto/polygon-no-coordinates.json new file mode 100644 index 0000000..6754830 --- /dev/null +++ b/test/fixture/bad-proto/polygon-no-coordinates.json @@ -0,0 +1,5 @@ +{ + "type": "TYPE_POLYGON", + "coordinates": [], + "lengths": [] +} diff --git a/test/fixture/bad-proto/too-big-length-multilinestring.json b/test/fixture/bad-proto/too-big-length-multilinestring.json index f7f67ea..d2b88af 100644 --- a/test/fixture/bad-proto/too-big-length-multilinestring.json +++ b/test/fixture/bad-proto/too-big-length-multilinestring.json @@ -1,5 +1,5 @@ { - "type": "MultiLineString", + "type": "TYPE_MULTI_LINE_STRING", "lengths": [2, 1], "coordinates": [12, 34] } diff --git a/test/fixture/bad-proto/too-short-default-length-multilinestring.json b/test/fixture/bad-proto/too-short-default-length-multilinestring.json new file mode 100644 index 0000000..779b792 --- /dev/null +++ b/test/fixture/bad-proto/too-short-default-length-multilinestring.json @@ -0,0 +1,5 @@ +{ + "type": "TYPE_MULTI_LINE_STRING", + "lengths": [], + "coordinates": [1, 2] +} diff --git a/test/fixture/bad-proto/too-short-line-multilinestring.json b/test/fixture/bad-proto/too-short-line-multilinestring.json index 0fd0fe9..617e192 100644 --- a/test/fixture/bad-proto/too-short-line-multilinestring.json +++ b/test/fixture/bad-proto/too-short-line-multilinestring.json @@ -1,5 +1,5 @@ { - "type": "MultiLineString", + "type": "TYPE_MULTI_LINE_STRING", "lengths": [2, 1], "coordinates": [170.0, 45.0, 180.0, 45.0, -180.0, 45.0] } diff --git a/test/fixture/bad-proto/too-short-linestring.json b/test/fixture/bad-proto/too-short-linestring.json index 3b0251d..839aea5 100644 --- a/test/fixture/bad-proto/too-short-linestring.json +++ b/test/fixture/bad-proto/too-short-linestring.json @@ -1,5 +1,5 @@ { - "type": "LineString", + "type": "TYPE_LINE_STRING", "lengths": [], "coordinates": [-170, 10] }