Skip to content

Commit 7a7ea10

Browse files
committed
Localize unreadable raster candidates
1 parent d9d5efb commit 7a7ea10

3 files changed

Lines changed: 127 additions & 91 deletions

File tree

src/PlateauResoniteLink/Application/Importing/TerrainTextureGeoReferencedRasterMetadataReader.cs

Lines changed: 86 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.IO;
33
using System.Linq;
4+
using System.Security;
45
using System.Text.RegularExpressions;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -25,8 +26,15 @@ internal static class TerrainTextureGeoReferencedRasterMetadataReader
2526
CancellationToken cancellationToken)
2627
{
2728
ArgumentNullException.ThrowIfNull(source);
28-
await using Stream stream = await source.OpenReadAsync(cancellationToken);
29-
return await TryReadMetadataAsync(stream, cancellationToken);
29+
try
30+
{
31+
await using Stream stream = await source.OpenReadAsync(cancellationToken);
32+
return await TryReadMetadataAsync(stream, cancellationToken);
33+
}
34+
catch (Exception exception) when (IsCandidateRasterReadFailure(exception))
35+
{
36+
return null;
37+
}
3038
}
3139

3240
public static async Task<GeoReferencedRasterMetadata?> TryReadMetadataAsync(
@@ -38,42 +46,45 @@ internal static class TerrainTextureGeoReferencedRasterMetadataReader
3846
return null;
3947
}
4048

41-
ImageInfo imageInfo;
4249
try
4350
{
44-
imageInfo = await Image.IdentifyAsync(sourcePath, cancellationToken)
45-
?? throw new InvalidOperationException($"Failed to identify raster '{sourcePath}'.");
51+
ImageInfo? identifiedImage = await Image.IdentifyAsync(sourcePath, cancellationToken);
52+
if (identifiedImage is null)
53+
{
54+
return null;
55+
}
56+
57+
ImageInfo imageInfo = identifiedImage;
58+
ExifProfile? exifProfile = imageInfo.Metadata.ExifProfile;
59+
GeoTiffTagSnapshot? tiffTags = await GeoTiffTagReader.TryReadAsync(sourcePath, cancellationToken);
60+
61+
double[]? tiePoints = TryGetDoubleArray(exifProfile, ExifTag.ModelTiePoint)
62+
?? tiffTags?.ModelTiePoint;
63+
double[]? pixelScale = TryGetDoubleArray(exifProfile, ExifTag.PixelScale)
64+
?? tiffTags?.PixelScale;
65+
double[]? modelTransform = TryGetDoubleArray(exifProfile, ExifTag.ModelTransform)
66+
?? tiffTags?.ModelTransform;
67+
ushort[]? geoKeyDirectory = TryGetUnsignedShortArray(exifProfile, "GeoKeyDirectoryTag")
68+
?? tiffTags?.GeoKeyDirectory;
69+
double[]? geoDoubleParams = TryGetNamedDoubleArray(exifProfile, "GeoDoubleParamsTag")
70+
?? tiffTags?.GeoDoubleParams;
71+
string? geoAsciiParams = TryGetNamedString(exifProfile, "GeoAsciiParamsTag")
72+
?? tiffTags?.GeoAsciiParams;
73+
74+
return TryCreateMetadata(
75+
imageInfo.Width,
76+
imageInfo.Height,
77+
tiePoints,
78+
pixelScale,
79+
modelTransform,
80+
geoKeyDirectory,
81+
geoDoubleParams,
82+
geoAsciiParams);
4683
}
47-
catch (UnknownImageFormatException)
84+
catch (Exception exception) when (IsCandidateRasterReadFailure(exception))
4885
{
4986
return null;
5087
}
51-
52-
ExifProfile? exifProfile = imageInfo.Metadata.ExifProfile;
53-
GeoTiffTagSnapshot? tiffTags = await GeoTiffTagReader.TryReadAsync(sourcePath, cancellationToken);
54-
55-
double[]? tiePoints = TryGetDoubleArray(exifProfile, ExifTag.ModelTiePoint)
56-
?? tiffTags?.ModelTiePoint;
57-
double[]? pixelScale = TryGetDoubleArray(exifProfile, ExifTag.PixelScale)
58-
?? tiffTags?.PixelScale;
59-
double[]? modelTransform = TryGetDoubleArray(exifProfile, ExifTag.ModelTransform)
60-
?? tiffTags?.ModelTransform;
61-
ushort[]? geoKeyDirectory = TryGetUnsignedShortArray(exifProfile, "GeoKeyDirectoryTag")
62-
?? tiffTags?.GeoKeyDirectory;
63-
double[]? geoDoubleParams = TryGetNamedDoubleArray(exifProfile, "GeoDoubleParamsTag")
64-
?? tiffTags?.GeoDoubleParams;
65-
string? geoAsciiParams = TryGetNamedString(exifProfile, "GeoAsciiParamsTag")
66-
?? tiffTags?.GeoAsciiParams;
67-
68-
return TryCreateMetadata(
69-
imageInfo.Width,
70-
imageInfo.Height,
71-
tiePoints,
72-
pixelScale,
73-
modelTransform,
74-
geoKeyDirectory,
75-
geoDoubleParams,
76-
geoAsciiParams);
7788
}
7889

7990
public static async Task<GeoReferencedRasterMetadata?> TryReadMetadataAsync(
@@ -90,42 +101,55 @@ internal static class TerrainTextureGeoReferencedRasterMetadataReader
90101
try
91102
{
92103
stream.Seek(0, SeekOrigin.Begin);
93-
imageInfo = await Image.IdentifyAsync(stream, cancellationToken)
94-
?? throw new InvalidOperationException("Failed to identify raster stream.");
104+
ImageInfo? identifiedImage = await Image.IdentifyAsync(stream, cancellationToken);
105+
if (identifiedImage is null)
106+
{
107+
return null;
108+
}
109+
110+
imageInfo = identifiedImage;
111+
ExifProfile? exifProfile = imageInfo.Metadata.ExifProfile;
112+
stream.Seek(0, SeekOrigin.Begin);
113+
GeoTiffTagSnapshot? tiffTags = await GeoTiffTagReader.TryReadAsync(stream, cancellationToken);
114+
115+
double[]? tiePoints = TryGetDoubleArray(exifProfile, ExifTag.ModelTiePoint)
116+
?? tiffTags?.ModelTiePoint;
117+
double[]? pixelScale = TryGetDoubleArray(exifProfile, ExifTag.PixelScale)
118+
?? tiffTags?.PixelScale;
119+
double[]? modelTransform = TryGetDoubleArray(exifProfile, ExifTag.ModelTransform)
120+
?? tiffTags?.ModelTransform;
121+
ushort[]? geoKeyDirectory = TryGetUnsignedShortArray(exifProfile, "GeoKeyDirectoryTag")
122+
?? tiffTags?.GeoKeyDirectory;
123+
double[]? geoDoubleParams = TryGetNamedDoubleArray(exifProfile, "GeoDoubleParamsTag")
124+
?? tiffTags?.GeoDoubleParams;
125+
string? geoAsciiParams = TryGetNamedString(exifProfile, "GeoAsciiParamsTag")
126+
?? tiffTags?.GeoAsciiParams;
127+
128+
return TryCreateMetadata(
129+
imageInfo.Width,
130+
imageInfo.Height,
131+
tiePoints,
132+
pixelScale,
133+
modelTransform,
134+
geoKeyDirectory,
135+
geoDoubleParams,
136+
geoAsciiParams);
95137
}
96-
catch (UnknownImageFormatException)
138+
catch (Exception exception) when (IsCandidateRasterReadFailure(exception))
97139
{
98140
return null;
99141
}
100-
101-
ExifProfile? exifProfile = imageInfo.Metadata.ExifProfile;
102-
stream.Seek(0, SeekOrigin.Begin);
103-
GeoTiffTagSnapshot? tiffTags = await GeoTiffTagReader.TryReadAsync(stream, cancellationToken);
104-
105-
double[]? tiePoints = TryGetDoubleArray(exifProfile, ExifTag.ModelTiePoint)
106-
?? tiffTags?.ModelTiePoint;
107-
double[]? pixelScale = TryGetDoubleArray(exifProfile, ExifTag.PixelScale)
108-
?? tiffTags?.PixelScale;
109-
double[]? modelTransform = TryGetDoubleArray(exifProfile, ExifTag.ModelTransform)
110-
?? tiffTags?.ModelTransform;
111-
ushort[]? geoKeyDirectory = TryGetUnsignedShortArray(exifProfile, "GeoKeyDirectoryTag")
112-
?? tiffTags?.GeoKeyDirectory;
113-
double[]? geoDoubleParams = TryGetNamedDoubleArray(exifProfile, "GeoDoubleParamsTag")
114-
?? tiffTags?.GeoDoubleParams;
115-
string? geoAsciiParams = TryGetNamedString(exifProfile, "GeoAsciiParamsTag")
116-
?? tiffTags?.GeoAsciiParams;
117-
118-
return TryCreateMetadata(
119-
imageInfo.Width,
120-
imageInfo.Height,
121-
tiePoints,
122-
pixelScale,
123-
modelTransform,
124-
geoKeyDirectory,
125-
geoDoubleParams,
126-
geoAsciiParams);
127142
}
128143

144+
private static bool IsCandidateRasterReadFailure(Exception exception) =>
145+
exception is UnknownImageFormatException
146+
or IOException
147+
or UnauthorizedAccessException
148+
or SecurityException
149+
or InvalidDataException
150+
or OverflowException
151+
or ArgumentOutOfRangeException;
152+
129153
internal static GeoReferencedRasterMetadata? TryCreateMetadata(
130154
int pixelWidth,
131155
int pixelHeight,

tests/PlateauResoniteLink.Tests/Profiles/DemTerrainGeoReferencedRasterCatalogTests.cs

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ public async Task TryResolveRasterSourceAsyncKeepsSuccessfulCachedTaskAfterCance
183183
}
184184

185185
[Fact]
186-
public async Task TryResolveRasterSourceAsyncEvictsFaultedBackgroundTaskAfterCanceledWaiter()
186+
public async Task TryResolveRasterSourceAsyncTreatsFaultedCandidateMaterializationAsUnavailableAfterCanceledWaiter()
187187
{
188188
using TemporaryDirectory datasetRoot = new();
189189
FaultingGateableDatasetContentSource datasetSource = CreateFaultingGateableDatasetSource(datasetRoot.Path);
@@ -203,35 +203,15 @@ public async Task TryResolveRasterSourceAsyncEvictsFaultedBackgroundTaskAfterCan
203203
datasetSource.ReleaseOpenRead.TrySetResult();
204204
await datasetSource.BackgroundCompletion.Task.WaitAsync(CancellationToken.None);
205205

206-
await AssertEventuallyRetriesFaultedBackgroundTaskAsync(catalog, datasetSource, bounds);
207-
Assert.Equal(2, datasetSource.EnsureLocalFileCallCount);
208-
Assert.Equal(0, datasetSource.OpenReadCallCount);
209-
}
210-
211-
private static async Task AssertEventuallyRetriesFaultedBackgroundTaskAsync(
212-
DemTerrainGeoReferencedRasterCatalog catalog,
213-
FaultingGateableDatasetContentSource datasetSource,
214-
GeographicRectangle bounds)
215-
{
216-
for (int attempt = 0; attempt < 50; attempt++)
217-
{
218-
await Assert.ThrowsAnyAsync<IOException>(async () => await catalog.TryResolveRasterSourceAsync(
219-
new DemTerrainRasterCacheKey("tokyo23ku", catalog.CacheScope, ThirdRegionalMeshCode.Parse("53394525"), bounds),
220-
ThirdRegionalMeshCode.Parse("53394525"),
221-
bounds,
222-
CancellationToken.None));
223-
224-
if (datasetSource.EnsureLocalFileCallCount == 2)
225-
{
226-
return;
227-
}
228-
229-
await Task.Delay(20);
230-
}
206+
TerrainTextureGeoReferencedRasterSource? retryResult = await catalog.TryResolveRasterSourceAsync(
207+
new DemTerrainRasterCacheKey("tokyo23ku", catalog.CacheScope, ThirdRegionalMeshCode.Parse("53394525"), bounds),
208+
ThirdRegionalMeshCode.Parse("53394525"),
209+
bounds,
210+
CancellationToken.None);
231211

232-
Assert.Fail(
233-
$"Faulted background task was not evicted after 50 retry attempts. "
234-
+ $"Observed EnsureLocalFileCallCount={datasetSource.EnsureLocalFileCallCount}.");
212+
Assert.Null(retryResult);
213+
Assert.Equal(1, datasetSource.EnsureLocalFileCallCount);
214+
Assert.Equal(0, datasetSource.OpenReadCallCount);
235215
}
236216

237217
private static RecordingDatasetContentSource CreateDatasetSource(string datasetRoot)

tests/PlateauResoniteLink.Tests/Targets/TerrainTextureGeoReferencedRasterSupportTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,25 @@ public void TryCreateMetadataReturnsNullWhenCoordinateSystemIsMissing()
6363
Assert.Null(metadata);
6464
}
6565

66+
[Fact]
67+
public async Task TryReadMetadataAsyncReturnsNullWhenRasterContentSourceCannotOpen()
68+
{
69+
GeoReferencedRasterMetadata? metadata = await TerrainTextureGeoReferencedRasterMetadataReader.TryReadMetadataAsync(
70+
new ThrowingRasterContentSource(new IOException("candidate raster unavailable")),
71+
CancellationToken.None);
72+
73+
Assert.Null(metadata);
74+
}
75+
76+
[Fact]
77+
public async Task TryReadMetadataAsyncPropagatesCancellationFromRasterContentSource()
78+
{
79+
await Assert.ThrowsAsync<OperationCanceledException>(
80+
async () => await TerrainTextureGeoReferencedRasterMetadataReader.TryReadMetadataAsync(
81+
new ThrowingRasterContentSource(new OperationCanceledException("cancelled")),
82+
CancellationToken.None));
83+
}
84+
6685
[Fact]
6786
public void TryCreateMetadataResolvesWebMercatorBoundsFromRealPlateauGeoTiffTags()
6887
{
@@ -563,6 +582,19 @@ await Assert.ThrowsAsync<InvalidOperationException>(
563582
Assert.Equal(0, handler.RequestCount);
564583
}
565584

585+
private sealed class ThrowingRasterContentSource(Exception exception) : ITerrainTextureRasterContentSource
586+
{
587+
public string IdentityKey => "throwing-raster";
588+
589+
public string Description => "throwing-raster";
590+
591+
public ValueTask<Stream> OpenReadAsync(CancellationToken cancellationToken)
592+
{
593+
cancellationToken.ThrowIfCancellationRequested();
594+
throw exception;
595+
}
596+
}
597+
566598
private sealed class NeverCalledMapTileHandler : HttpMessageHandler
567599
{
568600
public int RequestCount { get; private set; }

0 commit comments

Comments
 (0)