瀏覽代碼

fix(server): handle invalid coordinates (#2648)

Michel Heusschen 2 年之前
父節點
當前提交
1b301984dd

+ 7 - 6
server/apps/microservices/src/processors/metadata-extraction.processor.ts

@@ -22,6 +22,7 @@ import fs from 'node:fs';
 import sharp from 'sharp';
 import { Repository } from 'typeorm/repository/Repository';
 import { promisify } from 'util';
+import { parseLatitude, parseLongitude } from '../utils/coordinates';
 
 const ffprobe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
 
@@ -174,8 +175,8 @@ export class MetadataExtractionProcessor {
     // files MAY return an array of numbers instead.
     const iso = getExifProperty('ISO');
     newExif.iso = Array.isArray(iso) ? iso[0] : iso || null;
-    newExif.latitude = getExifProperty('GPSLatitude');
-    newExif.longitude = getExifProperty('GPSLongitude');
+    newExif.latitude = parseLatitude(getExifProperty('GPSLatitude'));
+    newExif.longitude = parseLongitude(getExifProperty('GPSLongitude'));
     newExif.livePhotoCID = getExifProperty('MediaGroupUUID');
 
     if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
@@ -274,8 +275,8 @@ export class MetadataExtractionProcessor {
       const match = location.match(locationRegex);
 
       if (match?.length === 3) {
-        newExif.latitude = parseFloat(match[1]);
-        newExif.longitude = parseFloat(match[2]);
+        newExif.latitude = parseLatitude(match[1]);
+        newExif.longitude = parseLongitude(match[2]);
       }
     } else if (videoTags && videoTags['com.apple.quicktime.location.ISO6709']) {
       const location = videoTags['com.apple.quicktime.location.ISO6709'] as string;
@@ -283,8 +284,8 @@ export class MetadataExtractionProcessor {
       const match = location.match(locationRegex);
 
       if (match?.length === 4) {
-        newExif.latitude = parseFloat(match[1]);
-        newExif.longitude = parseFloat(match[2]);
+        newExif.latitude = parseLatitude(match[1]);
+        newExif.longitude = parseLongitude(match[2]);
       }
     }
 

+ 46 - 0
server/apps/microservices/src/utils/coordinates.spec.ts

@@ -0,0 +1,46 @@
+import { describe, it, expect } from '@jest/globals';
+import { parseLatitude, parseLongitude } from './coordinates';
+
+describe('parsing latitude from string input', () => {
+  it('returns null for invalid inputs', () => {
+    expect(parseLatitude('')).toBeNull();
+    expect(parseLatitude('NaN')).toBeNull();
+    expect(parseLatitude('Infinity')).toBeNull();
+    expect(parseLatitude('-Infinity')).toBeNull();
+    expect(parseLatitude('90.001')).toBeNull();
+    expect(parseLatitude('-90.000001')).toBeNull();
+    expect(parseLatitude('1000')).toBeNull();
+    expect(parseLatitude('-1000')).toBeNull();
+  });
+
+  it('returns the numeric coordinate for valid inputs', () => {
+    expect(parseLatitude('90')).toBeCloseTo(90);
+    expect(parseLatitude('-90')).toBeCloseTo(-90);
+    expect(parseLatitude('89.999999')).toBeCloseTo(89.999999);
+    expect(parseLatitude('-89.9')).toBeCloseTo(-89.9);
+    expect(parseLatitude('0')).toBeCloseTo(0);
+    expect(parseLatitude('-0.0')).toBeCloseTo(-0.0);
+  });
+});
+
+describe('parsing longitude from string input', () => {
+  it('returns null for invalid inputs', () => {
+    expect(parseLongitude('')).toBeNull();
+    expect(parseLongitude('NaN')).toBeNull();
+    expect(parseLongitude('Infinity')).toBeNull();
+    expect(parseLongitude('-Infinity')).toBeNull();
+    expect(parseLongitude('180.001')).toBeNull();
+    expect(parseLongitude('-180.000001')).toBeNull();
+    expect(parseLongitude('1000')).toBeNull();
+    expect(parseLongitude('-1000')).toBeNull();
+  });
+
+  it('returns the numeric coordinate for valid inputs', () => {
+    expect(parseLongitude('180')).toBeCloseTo(180);
+    expect(parseLongitude('-180')).toBeCloseTo(-180);
+    expect(parseLongitude('179.999999')).toBeCloseTo(179.999999);
+    expect(parseLongitude('-179.9')).toBeCloseTo(-179.9);
+    expect(parseLongitude('0')).toBeCloseTo(0);
+    expect(parseLongitude('-0.0')).toBeCloseTo(-0.0);
+  });
+});

+ 17 - 0
server/apps/microservices/src/utils/coordinates.ts

@@ -0,0 +1,17 @@
+export function parseLatitude(input: string): number | null {
+  const latitude = Number.parseFloat(input);
+
+  if (latitude < -90 || latitude > 90 || Number.isNaN(latitude)) {
+    return null;
+  }
+  return latitude;
+}
+
+export function parseLongitude(input: string): number | null {
+  const longitude = Number.parseFloat(input);
+
+  if (longitude < -180 || longitude > 180 || Number.isNaN(longitude)) {
+    return null;
+  }
+  return longitude;
+}

+ 6 - 3
server/libs/infra/src/repositories/asset.repository.ts

@@ -11,7 +11,7 @@ import {
 } from '@app/domain';
 import { Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
-import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
+import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Raw, Repository } from 'typeorm';
 import { AssetEntity, AssetType } from '../entities';
 import OptionalBetween from '../utils/optional-between.util';
 import { paginate } from '../utils/pagination.util';
@@ -214,6 +214,9 @@ export class AssetRepository implements IAssetRepository {
 
   async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
     const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
+    const coordinateFilter = Raw(
+      (column) => `${column} IS NOT NULL AND ${column} NOT IN ('NaN', 'Infinity', '-Infinity')`,
+    );
 
     const assets = await this.repository.find({
       select: {
@@ -228,8 +231,8 @@ export class AssetRepository implements IAssetRepository {
         isVisible: true,
         isArchived: false,
         exifInfo: {
-          latitude: Not(IsNull()),
-          longitude: Not(IsNull()),
+          latitude: coordinateFilter,
+          longitude: coordinateFilter,
         },
         isFavorite,
         fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore),