Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,19 @@ uses-sdk:minSdkVersion 23 cannot be smaller than version 24 declared in library
```

Raise your app's `minSdk` to `24`. Overriding the check with `tools:overrideLibrary` is not supported and may crash on API 23 devices.

---

## 6. Data Parsers Declare `IOException` for Java Callers

Starting with `v7.0.0`, `KmzParser.parse`, `KmlParser.parse`, `KmlParser.parseAsKml`, `GpxParser.parse` and `GeoJsonParser.parse` are annotated with `@Throws(IOException::class)`. These methods always propagated `IOException` at runtime (stream failures and the KMZ entry count and size limits), but the Java signatures did not declare it, so Java callers could not write `catch (IOException e)`.

Kotlin callers are unaffected, and so is binary compatibility. Java callers that recompile must now catch or declare `IOException`:

```java
try {
Kml kml = new KmzParser().parse(inputStream);
} catch (IOException e) {
// Handle the read failure or rejected KMZ
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,22 @@ import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.io.IOException
import java.io.InputStream

public class GeoJsonParser @JvmOverloads public constructor(
private val maxInputSize: Long = DEFAULT_MAX_INPUT_SIZE,
private val maxStructuralDepth: Int = DEFAULT_MAX_STRUCTURAL_DEPTH,
) {
/**
* Parses a GeoJSON document from an InputStream.
*
* @param inputStream The InputStream of the GeoJSON document, read as UTF-8.
* @return The parsed GeoJSON object, or `null` if the top-level `type` is not recognized.
* @throws IOException If the stream cannot be read.
* @throws IllegalArgumentException If the document exceeds the size or nesting limits.
*/
@Throws(IOException::class)
public fun parse(inputStream: InputStream): GeoJsonObject? {
// Bound the untrusted input *before* materialising it. [Json.parseToJsonElement] reads the
// whole document into an in-memory tree, so without these guards a hostile document can
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package com.google.maps.android.data.parser.gpx

import kotlinx.serialization.decodeFromString
import nl.adaptivity.xmlutil.serialization.XML
import java.io.IOException
import java.io.InputStream
import java.nio.charset.StandardCharsets

Expand All @@ -35,6 +36,14 @@ public class GpxParser public constructor() {
isCollectingNSAttributes = true
}

/**
* Parses a GPX document from an InputStream.
*
* @param inputStream The InputStream of the GPX document, read as UTF-8.
* @return The parsed GPX object.
* @throws IOException If the stream cannot be read.
*/
@Throws(IOException::class)
public fun parse(inputStream: InputStream): Gpx {
val xmlContent = inputStream.bufferedReader(StandardCharsets.UTF_8).use { it.readText() }
// Normalize GPX 1.0 namespace to 1.1 to allow parsing with the same model
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import nl.adaptivity.xmlutil.XmlException
import nl.adaptivity.xmlutil.XmlReader
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.xmlStreaming
import java.io.IOException
import java.io.InputStream
import java.nio.charset.StandardCharsets

Expand All @@ -33,13 +34,29 @@ public class KmlParser public constructor() {
isCollectingNSAttributes = true
}

/**
* Parses a KML document from an InputStream.
*
* @param inputStream The InputStream of the KML document, read as UTF-8.
* @return The parsed KML object.
* @throws IOException If the stream cannot be read.
*/
@Throws(IOException::class)
public fun parseAsKml(inputStream: InputStream): Kml {
val xmlContent = inputStream.bufferedReader(StandardCharsets.UTF_8).use { it.readText() }
return DepthLimitingReader(xmlStreaming.newReader(xmlContent)).use { reader ->
xml.decodeFromReader(Kml.serializer(), reader)
}
}

/**
* Parses a KML document from an InputStream. Equivalent to [parseAsKml].
*
* @param inputStream The InputStream of the KML document, read as UTF-8.
* @return The parsed KML object.
* @throws IOException If the stream cannot be read.
*/
@Throws(IOException::class)
public fun parse(inputStream: InputStream): Kml = parseAsKml(inputStream)

public companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ public class KmzParser @JvmOverloads public constructor(
*
* @param inputStream The InputStream of the KMZ file.
* @return The parsed KML object, including extracted images.
* @throws IOException If the stream cannot be read, or the KMZ exceeds the entry count or
* uncompressed size limits.
* @throws IllegalArgumentException If the KMZ does not contain a KML file.
*/
@Throws(IOException::class)
public fun parse(inputStream: InputStream): Kml {
val images = mutableMapOf<String, Bitmap>()
var kml: Kml? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.data.parser;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import com.google.maps.android.data.parser.geojson.GeoJsonParser;
import com.google.maps.android.data.parser.gpx.GpxParser;
import com.google.maps.android.data.parser.kml.KmlParser;
import com.google.maps.android.data.parser.kml.KmzParser;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Test;

/**
* Verifies from Java that the parsers declare {@link IOException}. Each {@code catch (IOException)}
* below only compiles if the called method declares it, so this test also guards the Java
* signature.
*/
public class ParserCheckedExceptionTest {
private static final String MESSAGE = "stream failure";

private static InputStream failingStream() {
return new InputStream() {
@Override
public int read() throws IOException {
throw new IOException(MESSAGE);
}
};
}

@Test
public void kmzParserDeclaresIOException() {
try {
new KmzParser().parse(failingStream());
fail("Expected IOException");
} catch (IOException e) {
assertEquals(MESSAGE, e.getMessage());
}
}

@Test
public void kmlParserDeclaresIOException() {
try {
new KmlParser().parse(failingStream());
fail("Expected IOException");
} catch (IOException e) {
assertEquals(MESSAGE, e.getMessage());
}
}

@Test
public void kmlParserParseAsKmlDeclaresIOException() {
try {
new KmlParser().parseAsKml(failingStream());
fail("Expected IOException");
} catch (IOException e) {
assertEquals(MESSAGE, e.getMessage());
}
}

@Test
public void gpxParserDeclaresIOException() {
try {
new GpxParser().parse(failingStream());
fail("Expected IOException");
} catch (IOException e) {
assertEquals(MESSAGE, e.getMessage());
}
}

@Test
public void geoJsonParserDeclaresIOException() {
try {
new GeoJsonParser().parse(failingStream());
fail("Expected IOException");
} catch (IOException e) {
assertEquals(MESSAGE, e.getMessage());
}
}
}
Loading