Skip to content

Commit f872cd4

Browse files
authored
fix: Parse SDK internal database file parse.db is accessible for app user on iOS and may be accidentally deleted (#826)
1 parent a19ea87 commit f872cd4

File tree

4 files changed

+309
-6
lines changed

4 files changed

+309
-6
lines changed

packages/flutter/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## [3.1.4](https://github.com/parse-community/Parse-SDK-Flutter/compare/flutter-3.1.3...flutter-3.1.4) (2023-03-01)
2+
3+
### Bug Fixes
4+
5+
* Parse SDK internal database file `parse.db` is accessible for app user on iOS and may be accidentally deleted ([#826](https://github.com/parse-community/Parse-SDK-Flutter/pull/826))
6+
17
## [3.1.3](https://github.com/parse-community/Parse-SDK-Flutter/compare/flutter-3.1.2...flutter-3.1.3) (2022-07-09)
28

39
### Bug Fixes
@@ -6,7 +12,6 @@
612
* dependency `package_info_plus` does not work in web ([#714](https://github.com/parse-community/Parse-SDK-Flutter/issues/714))
713
* missing plugin exception, no implementation found for method `getAll` ([#712](https://github.com/parse-community/Parse-SDK-Flutter/issues/712))
814

9-
1015
## [3.1.2](https://github.com/parse-community/Parse-SDK-Flutter/compare/flutter-3.1.1...flutter-3.1.2) (2022-05-30)
1116

1217
### Refactors
Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,67 @@
1-
import 'package:path_provider/path_provider.dart';
1+
import 'dart:io';
2+
3+
import 'package:flutter/foundation.dart';
4+
import 'package:path_provider/path_provider.dart' as path_provider;
5+
import 'package:path/path.dart' as path;
26

37
class CoreStoreDirectory {
48
Future<String> getDatabaseDirectory() async {
5-
return (await getApplicationDocumentsDirectory()).path;
9+
if (defaultTargetPlatform == TargetPlatform.iOS) {
10+
await _migrateDBFileToLibraryDirectory();
11+
12+
return (await path_provider.getLibraryDirectory()).path;
13+
}
14+
15+
return (await path_provider.getApplicationDocumentsDirectory()).path;
16+
}
17+
18+
/// Migrate SDK internal database file on iOS, see:
19+
/// https://github.com/parse-community/Parse-SDK-Flutter/issues/791
20+
/// TODO: Remove this migration algorithm in the future.
21+
Future<void> _migrateDBFileToLibraryDirectory() async {
22+
final dbFile = await _getDBFileIfExistsInAppDocDir();
23+
24+
if (dbFile != null) {
25+
await _moveDatabaseFileToLibraryDirectory(dbFile);
26+
}
27+
}
28+
29+
Future<File?> _getDBFileIfExistsInAppDocDir() async {
30+
final appDocDirPath =
31+
(await path_provider.getApplicationDocumentsDirectory()).path;
32+
33+
final databaseFilePath = path.join(
34+
appDocDirPath,
35+
'parse',
36+
'parse.db',
37+
);
38+
39+
final dbFile = File(databaseFilePath);
40+
41+
if (await dbFile.exists()) {
42+
return dbFile;
43+
}
44+
45+
return null;
46+
}
47+
48+
Future<void> _moveDatabaseFileToLibraryDirectory(
49+
File databaseFileToMove,
50+
) async {
51+
final libraryDirectoryPath =
52+
(await path_provider.getLibraryDirectory()).path;
53+
54+
final libraryDirectoryDatabaseFilePath = path.join(
55+
libraryDirectoryPath,
56+
'parse',
57+
'parse.db',
58+
);
59+
60+
await File(libraryDirectoryDatabaseFilePath).create(recursive: true);
61+
await databaseFileToMove.rename(libraryDirectoryDatabaseFilePath);
662
}
763

8-
Future<String?> getTempDirectory() async {
9-
return (await getTemporaryDirectory()).path;
64+
Future<String> getTempDirectory() async {
65+
return (await path_provider.getTemporaryDirectory()).path;
1066
}
1167
}

packages/flutter/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: parse_server_sdk_flutter
22
description: Flutter plugin for Parse Server, (https://parseplatform.org), (https://back4app.com)
3-
version: 3.1.3
3+
version: 3.1.4
44
homepage: https://github.com/parse-community/Parse-SDK-Flutter
55

66
environment:
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
@TestOn('dart-vm')
2+
@Timeout.factor(2)
3+
4+
import 'dart:io';
5+
import 'dart:math';
6+
7+
import 'package:flutter/foundation.dart';
8+
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
9+
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
10+
import 'package:path_provider/path_provider.dart' as path_provider;
11+
import 'package:path/path.dart' as path;
12+
import 'package:flutter_test/flutter_test.dart';
13+
import 'package:parse_server_sdk_flutter/src/storage/core_store_directory_io.dart';
14+
15+
void main() {
16+
TestWidgetsFlutterBinding.ensureInitialized();
17+
group('Core store directory IO', () {
18+
late CoreStoreDirectory coreStoreDirectory;
19+
setUp(() {
20+
PathProviderPlatform.instance = FakePathProviderPlatform();
21+
coreStoreDirectory = CoreStoreDirectory();
22+
});
23+
24+
test('getTemporaryDirectory', () async {
25+
final result = await path_provider.getTemporaryDirectory();
26+
expect(result.path, kTemporaryPath);
27+
});
28+
29+
test('getLibraryDirectory', () async {
30+
final result = await path_provider.getLibraryDirectory();
31+
expect(result.path, libraryPath);
32+
});
33+
34+
test('getApplicationDocumentsDirectory', () async {
35+
final result = await path_provider.getApplicationDocumentsDirectory();
36+
expect(result.path, applicationDocumentsPath);
37+
});
38+
39+
test('defaultTargetPlatform should equals iOS', () async {
40+
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
41+
42+
final platform = defaultTargetPlatform;
43+
expect(platform, equals(TargetPlatform.iOS));
44+
});
45+
46+
test('getTempDirectory() should return kTemporaryPath', () async {
47+
final path = await coreStoreDirectory.getTempDirectory();
48+
expect(path, kTemporaryPath);
49+
});
50+
51+
group('getDatabaseDirectory()', () {
52+
setUp(() {
53+
deleteApplicationDocumentDir();
54+
deleteLibraryDir();
55+
});
56+
57+
tearDown(() {
58+
deleteApplicationDocumentDir();
59+
deleteLibraryDir();
60+
});
61+
62+
test(
63+
'on ios, should copy the db file if exists from the old dir path '
64+
'(applicationDocumentDirectory) to the new dir path (LibraryDirectory)'
65+
' and the old db file should be deleted from the old dir path '
66+
'then return the new dir path (LibraryDirectory)', () async {
67+
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
68+
69+
final oldDBFile = create1MBParseDBFileInAppDocDir();
70+
final oldDBFileSize = oldDBFile.lengthSync();
71+
final dbDirectory = await coreStoreDirectory.getDatabaseDirectory();
72+
expect(
73+
oldDBFile.existsSync(),
74+
isFalse,
75+
reason: 'the old db file should be deleted from app doc dir',
76+
);
77+
expect(
78+
dbDirectory,
79+
equals(libraryPath),
80+
reason:
81+
'dbDirectory should be the new db dir path for iOS (LibraryDir)',
82+
);
83+
84+
final newDBFilePath = path.join(
85+
dbDirectory,
86+
'parse',
87+
'parse.db',
88+
);
89+
final newDBFile = File(newDBFilePath);
90+
expect(newDBFile.existsSync(), isTrue);
91+
expect(
92+
newDBFile.lengthSync(),
93+
equals(oldDBFileSize),
94+
reason: 'the old and the new coped db file should be the same size',
95+
);
96+
});
97+
98+
test(
99+
'on ios, if there is no db file in the old dir (applicationDocumentDirectory)'
100+
' and there is db file in the new dir (LibraryDirectory) '
101+
'the (copy) migration should not work and so the getDatabaseDirectory()'
102+
'should return the new db dir path (LibraryDirectory)', () async {
103+
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
104+
105+
final dbFileInNewPath = create1MBParseDBFileInLibraryPath();
106+
final dbFileSizeBefore = dbFileInNewPath.lengthSync();
107+
final dbFileLastModifiedBefore = dbFileInNewPath.lastModifiedSync();
108+
final dbDirectory = await coreStoreDirectory.getDatabaseDirectory();
109+
expect(dbFileInNewPath.existsSync(), isTrue);
110+
111+
final dbFileSizeAfter = dbFileInNewPath.lengthSync();
112+
final dbFileLastModifiedAfter = dbFileInNewPath.lastModifiedSync();
113+
expect(
114+
dbFileSizeBefore,
115+
equals(dbFileSizeAfter),
116+
reason: 'the db file should be the same',
117+
);
118+
expect(
119+
dbFileLastModifiedBefore.compareTo(dbFileLastModifiedAfter),
120+
equals(0), // 0 if this DateTime [isAtSameMomentAs] [other]
121+
reason: 'last modified date should not change',
122+
);
123+
expect(
124+
dbDirectory,
125+
equals(libraryPath),
126+
reason:
127+
'dbDirectory should be the new db dir path for iOS (LibraryDir)',
128+
);
129+
});
130+
131+
test(
132+
'on any platform other than iOS, the copy migration algorithm should '
133+
'not run and the db file should and will remain in '
134+
'(applicationDocumentDirectory) and getDatabaseDirectory() should '
135+
'return (applicationDocumentDirectory) as db directory', () async {
136+
final targetPlatforms = TargetPlatform.values.toSet();
137+
targetPlatforms.remove(TargetPlatform.iOS);
138+
139+
final dbFile = create1MBParseDBFileInAppDocDir();
140+
final dbFileSizeBefore = dbFile.lengthSync();
141+
final dbFileLastModifiedBefore = dbFile.lastModifiedSync();
142+
143+
for (final platform in targetPlatforms) {
144+
debugDefaultTargetPlatformOverride = platform;
145+
146+
final dbDirectory = await coreStoreDirectory.getDatabaseDirectory();
147+
expect(dbFile.existsSync(), isTrue);
148+
149+
final dbFileSizeAfter = dbFile.lengthSync();
150+
final dbFileLastModifiedSyncAfter = dbFile.lastModifiedSync();
151+
expect(
152+
dbFileSizeBefore,
153+
equals(dbFileSizeAfter),
154+
reason: 'the db file should be the same',
155+
);
156+
expect(
157+
dbFileLastModifiedBefore.compareTo(dbFileLastModifiedSyncAfter),
158+
equals(0), // 0 if this DateTime [isAtSameMomentAs] [other]
159+
reason: 'last modified date should not change',
160+
);
161+
expect(
162+
dbDirectory,
163+
equals(applicationDocumentsPath),
164+
reason:
165+
'dbDirectory should point to application Documents Directory',
166+
);
167+
}
168+
});
169+
});
170+
});
171+
}
172+
173+
File create1MBParseDBFileInAppDocDir() {
174+
final databaseFilePath = path.join(
175+
applicationDocumentsPath,
176+
'parse',
177+
'parse.db',
178+
);
179+
180+
return generate1MBFile(databaseFilePath);
181+
}
182+
183+
File create1MBParseDBFileInLibraryPath() {
184+
final databaseFilePath = path.join(
185+
libraryPath,
186+
'parse',
187+
'parse.db',
188+
);
189+
190+
return generate1MBFile(databaseFilePath);
191+
}
192+
193+
File generate1MBFile(String path) {
194+
final dbFile = File(path);
195+
dbFile.createSync(recursive: true, exclusive: false);
196+
197+
const fileSize = 1024 * 1024; // 1 MB
198+
final random = Random();
199+
final data = List.generate(fileSize, (_) => random.nextInt(256));
200+
201+
dbFile.writeAsBytesSync(data, flush: true, mode: FileMode.write);
202+
return dbFile;
203+
}
204+
205+
void deleteApplicationDocumentDir() {
206+
deleteDirectory(applicationDocumentsPath);
207+
}
208+
209+
void deleteLibraryDir() {
210+
deleteDirectory(libraryPath);
211+
}
212+
213+
void deleteDirectory(String path) {
214+
final dir = Directory(path);
215+
if (dir.existsSync()) {
216+
dir.deleteSync(recursive: true);
217+
}
218+
}
219+
220+
const String kTemporaryPath = "temporaryPath";
221+
final String libraryPath = path.join(path.current, 'library');
222+
final String applicationDocumentsPath =
223+
path.join(path.current, 'applicationDocument');
224+
225+
class FakePathProviderPlatform extends Fake
226+
with MockPlatformInterfaceMixin
227+
implements PathProviderPlatform {
228+
@override
229+
Future<String?> getTemporaryPath() async {
230+
return kTemporaryPath;
231+
}
232+
233+
@override
234+
Future<String?> getLibraryPath() async {
235+
return libraryPath;
236+
}
237+
238+
@override
239+
Future<String?> getApplicationDocumentsPath() async {
240+
return applicationDocumentsPath;
241+
}
242+
}

0 commit comments

Comments
 (0)