Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate input for CrudEntry.fromRow explicitly #239

Merged
merged 7 commits into from
Feb 5, 2025
Merged
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
6 changes: 6 additions & 0 deletions packages/powersync_core/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@

include: package:lints/recommended.yaml

analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true

# Uncomment the following section to specify additional rules.

# linter:
Expand Down
2 changes: 1 addition & 1 deletion packages/powersync_core/example/getting_started.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Future<String> getDatabasePath() async {
return join(dir, dbFilename);
}

openDatabase() async {
Future<void> openDatabase() async {
// Setup the database.
final psFactory = PowerSyncDartOpenFactory(path: await getDatabasePath());
db = PowerSyncDatabase.withFactory(psFactory, schema: schema);
Expand Down
61 changes: 37 additions & 24 deletions packages/powersync_core/lib/src/bucket_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class BucketStorage {
_init();
}

_init() {}
void _init() {}

// Use only for read statements
Future<ResultSet> select(String query,
Expand All @@ -36,7 +36,8 @@ class BucketStorage {
'SELECT name as bucket, cast(last_op as TEXT) as op_id FROM ps_buckets WHERE pending_delete = 0 AND name != \'\$local\'');
return [
for (var row in rows)
BucketState(bucket: row['bucket'], opId: row['op_id'])
BucketState(
bucket: row['bucket'] as String, opId: row['op_id'] as String)
];
}

Expand Down Expand Up @@ -157,14 +158,16 @@ class BucketStorage {
Checkpoint checkpoint) async {
final rs = await select("SELECT powersync_validate_checkpoint(?) as result",
[jsonEncode(checkpoint)]);
final result = jsonDecode(rs[0]['result']);
if (result['valid']) {
final result =
jsonDecode(rs[0]['result'] as String) as Map<String, dynamic>;
if (result['valid'] as bool) {
return SyncLocalDatabaseResult(ready: true);
} else {
return SyncLocalDatabaseResult(
checkpointValid: false,
ready: false,
checkpointFailures: result['failed_buckets'].cast<String>());
checkpointFailures:
(result['failed_buckets'] as List).cast<String>());
}
}

Expand Down Expand Up @@ -232,7 +235,7 @@ class BucketStorage {
// Nothing to update
return false;
}
int seqBefore = rs.first['seq'];
int seqBefore = rs.first['seq'] as int;
var opId = await checkpointCallback();

return await writeTransaction((tx) async {
Expand All @@ -244,7 +247,7 @@ class BucketStorage {
.execute('SELECT seq FROM sqlite_sequence WHERE name = \'ps_crud\'');
assert(rs.isNotEmpty);

int seqAfter = rs.first['seq'];
int seqAfter = rs.first['seq'] as int;
if (seqAfter != seqBefore) {
// New crud data may have been uploaded since we got the checkpoint. Abort.
return false;
Expand Down Expand Up @@ -362,12 +365,13 @@ class SyncBucketData {
this.nextAfter});

SyncBucketData.fromJson(Map<String, dynamic> json)
: bucket = json['bucket'],
hasMore = json['has_more'] ?? false,
after = json['after'],
nextAfter = json['next_after'],
data =
(json['data'] as List).map((e) => OplogEntry.fromJson(e)).toList();
: bucket = json['bucket'] as String,
hasMore = json['has_more'] as bool? ?? false,
after = json['after'] as String?,
nextAfter = json['next_after'] as String?,
data = (json['data'] as List)
.map((e) => OplogEntry.fromJson(e as Map<String, dynamic>))
.toList();

Map<String, dynamic> toJson() {
return {
Expand Down Expand Up @@ -407,16 +411,25 @@ class OplogEntry {
required this.checksum});

OplogEntry.fromJson(Map<String, dynamic> json)
: opId = json['op_id'],
op = OpType.fromJson(json['op']),
rowType = json['object_type'],
rowId = json['object_id'],
checksum = json['checksum'],
data = json['data'] is String ? json['data'] : jsonEncode(json['data']),
subkey = json['subkey'] is String ? json['subkey'] : null;
: opId = json['op_id'] as String,
op = OpType.fromJson(json['op'] as String),
rowType = json['object_type'] as String?,
rowId = json['object_id'] as String?,
checksum = json['checksum'] as int,
data = switch (json['data']) {
String data => data,
var other => jsonEncode(other),
},
subkey = switch (json['subkey']) {
String subkey => subkey,
_ => null,
};

Map<String, dynamic>? get parsedData {
return data == null ? null : jsonDecode(data!);
return switch (data) {
final data? => jsonDecode(data) as Map<String, dynamic>,
null => null,
};
}

/// Key to uniquely represent a source entry in a bucket.
Expand Down Expand Up @@ -463,16 +476,16 @@ class SyncLocalDatabaseResult {

@override
int get hashCode {
return Object.hash(
ready, checkpointValid, const ListEquality().hash(checkpointFailures));
return Object.hash(ready, checkpointValid,
const ListEquality<String?>().hash(checkpointFailures));
}

@override
bool operator ==(Object other) {
return other is SyncLocalDatabaseResult &&
other.ready == ready &&
other.checkpointValid == checkpointValid &&
const ListEquality()
const ListEquality<String?>()
.equals(other.checkpointFailures, checkpointFailures);
}
}
Expand Down
35 changes: 19 additions & 16 deletions packages/powersync_core/lib/src/connector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ class PowerSyncCredentials {
}

factory PowerSyncCredentials.fromJson(Map<String, dynamic> parsed) {
String token = parsed['token'];
String token = parsed['token'] as String;
DateTime? expiresAt = getExpiryDate(token);

return PowerSyncCredentials(
endpoint: parsed['endpoint'],
token: parsed['token'],
userId: parsed['user_id'],
endpoint: parsed['endpoint'] as String,
token: token,
userId: parsed['user_id'] as String?,
expiresAt: expiresAt);
}

Expand All @@ -110,9 +110,9 @@ class PowerSyncCredentials {
// dart:convert doesn't like missing padding
final rawData = base64Url.decode(base64.normalize(parts[1]));
final text = Utf8Decoder().convert(rawData);
Map<String, dynamic> payload = jsonDecode(text);
if (payload.containsKey('exp') && payload['exp'] is int) {
return DateTime.fromMillisecondsSinceEpoch(payload['exp'] * 1000);
final payload = jsonDecode(text) as Map<String, dynamic>;
if (payload['exp'] case int exp) {
return DateTime.fromMillisecondsSinceEpoch(exp * 1000);
}
}
return null;
Expand All @@ -131,7 +131,7 @@ class PowerSyncCredentials {
return Uri.parse(endpoint).resolve(path);
}

_validateEndpoint() {
void _validateEndpoint() {
final parsed = Uri.parse(endpoint);
if ((!parsed.isScheme('http') && !parsed.isScheme('https')) ||
parsed.host.isEmpty) {
Expand Down Expand Up @@ -162,14 +162,14 @@ class DevCredentials {

factory DevCredentials.fromJson(Map<String, dynamic> parsed) {
return DevCredentials(
endpoint: parsed['endpoint'],
token: parsed['token'],
userId: parsed['user_id']);
endpoint: parsed['endpoint'] as String,
token: parsed['token'] as String?,
userId: parsed['user_id'] as String?);
}

factory DevCredentials.fromString(String credentials) {
var parsed = jsonDecode(credentials);
return DevCredentials.fromJson(parsed);
return DevCredentials.fromJson(parsed as Map<String, dynamic>);
}

static DevCredentials? fromOptionalString(String? credentials) {
Expand Down Expand Up @@ -255,10 +255,12 @@ class DevConnector extends PowerSyncBackendConnector {

if (res.statusCode == 200) {
var parsed = jsonDecode(res.body);
var data = parsed['data'] as Map<String, dynamic>;

storeDevCredentials(DevCredentials(
endpoint: endpoint,
token: parsed['data']['token'],
userId: parsed['data']['user_id']));
token: data['token'] as String?,
userId: data['user_id'] as String?));
} else {
throw http.ClientException(res.reasonPhrase ?? 'Request failed', uri);
}
Expand All @@ -281,7 +283,8 @@ class DevConnector extends PowerSyncBackendConnector {
throw http.ClientException(res.reasonPhrase ?? 'Request failed', uri);
}

return PowerSyncCredentials.fromJson(jsonDecode(res.body)['data']);
return PowerSyncCredentials.fromJson(
jsonDecode(res.body)['data'] as Map<String, dynamic>);
}

/// Upload changes using the PowerSync dev API.
Expand Down Expand Up @@ -319,7 +322,7 @@ class DevConnector extends PowerSyncBackendConnector {
final body = jsonDecode(response.body);
// writeCheckpoint is optional, but reduces latency between writing,
// and reading back the same change.
final String? writeCheckpoint = body['data']['write_checkpoint'];
final writeCheckpoint = body['data']['write_checkpoint'] as String?;
await batch.complete(writeCheckpoint: writeCheckpoint);
}
}
16 changes: 11 additions & 5 deletions packages/powersync_core/lib/src/crud.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,15 @@ class CrudEntry {
this.opData);

factory CrudEntry.fromRow(sqlite.Row row) {
final data = jsonDecode(row['data']);
return CrudEntry(row['id'], UpdateType.fromJsonChecked(data['op'])!,
data['type'], data['id'], row['tx_id'], data['data']);
final data = jsonDecode(row['data'] as String);
return CrudEntry(
row['id'] as int,
UpdateType.fromJsonChecked(data['op'] as String)!,
data['type'] as String,
data['id'] as String,
row['tx_id'] as int,
data['data'] as Map<String, Object?>?,
);
}

/// Converts the change to JSON format, as required by the dev crud API.
Expand Down Expand Up @@ -111,13 +117,13 @@ class CrudEntry {
other.op == op &&
other.table == table &&
other.id == id &&
const MapEquality().equals(other.opData, opData));
const MapEquality<String, dynamic>().equals(other.opData, opData));
}

@override
int get hashCode {
return Object.hash(transactionId, clientId, op.toJson(), table, id,
const MapEquality().hash(opData));
const MapEquality<String, dynamic>().hash(opData));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,10 @@ class PowerSyncDatabaseImpl
await isInitialized;
final dbRef = database.isolateConnectionFactory();
ReceivePort rPort = ReceivePort();
StreamSubscription? crudUpdateSubscription;
StreamSubscription<UpdateNotification>? crudUpdateSubscription;
rPort.listen((data) async {
if (data is List) {
String action = data[0];
String action = data[0] as String;
if (action == "getCredentials") {
await (data[1] as PortCompleter).handle(() async {
final token = await connector.getCredentialsCached();
Expand All @@ -159,7 +159,7 @@ class PowerSyncDatabaseImpl
await connector.prefetchCredentials();
});
} else if (action == 'init') {
SendPort port = data[1];
SendPort port = data[1] as SendPort;
var crudStream =
database.onChange(['ps_crud'], throttle: crudThrottleTime);
crudUpdateSubscription = crudStream.listen((event) {
Expand All @@ -173,15 +173,15 @@ class PowerSyncDatabaseImpl
await connector.uploadData(this);
});
} else if (action == 'status') {
final SyncStatus status = data[1];
final SyncStatus status = data[1] as SyncStatus;
setStatus(status);
} else if (action == 'close') {
// Clear status apart from lastSyncedAt
setStatus(SyncStatus(lastSyncedAt: currentStatus.lastSyncedAt));
rPort.close();
crudUpdateSubscription?.cancel();
} else if (action == 'log') {
LogRecord record = data[1];
LogRecord record = data[1] as LogRecord;
logger.log(
record.level, record.message, record.error, record.stackTrace);
}
Expand Down Expand Up @@ -290,7 +290,7 @@ Future<void> _powerSyncDatabaseIsolate(

rPort.listen((message) async {
if (message is List) {
String action = message[0];
String action = message[0] as String;
if (action == 'update') {
crudUpdateController.add('update');
} else if (action == 'close') {
Expand Down
12 changes: 6 additions & 6 deletions packages/powersync_core/lib/src/database/powersync_db_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
try {
final row =
await database.get('SELECT powersync_rs_version() as version');
version = row['version'];
version = row['version'] as String;
} catch (e) {
throw SqliteException(
1, 'The powersync extension is not loaded correctly. Details: $e');
Expand Down Expand Up @@ -291,7 +291,7 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
/// A connection factory that can be passed to different isolates.
///
/// Use this to access the database in background isolates.
isolateConnectionFactory() {
IsolateConnectionFactory<CommonDatabase> isolateConnectionFactory() {
return database.isolateConnectionFactory();
}

Expand All @@ -309,10 +309,10 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
final row = await getOptional(
'SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ps_crud');
return UploadQueueStats(
count: row?['count'] ?? 0, size: row?['size'] ?? 0);
count: row?['count'] as int? ?? 0, size: row?['size'] as int? ?? 0);
} else {
final row = await getOptional('SELECT count(*) as count FROM ps_crud');
return UploadQueueStats(count: row?['count'] ?? 0);
return UploadQueueStats(count: row?['count'] as int? ?? 0);
}
}

Expand All @@ -331,7 +331,7 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
/// This method does include transaction ids in the result, but does not group
/// data by transaction. One batch may contain data from multiple transactions,
/// and a single transaction may be split over multiple batches.
Future<CrudBatch?> getCrudBatch({limit = 100}) async {
Future<CrudBatch?> getCrudBatch({int limit = 100}) async {
final rows = await getAll(
'SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT ?',
[limit + 1]);
Expand Down Expand Up @@ -384,7 +384,7 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
if (first == null) {
return null;
}
final int? txId = first['tx_id'];
final txId = first['tx_id'] as int?;
List<CrudEntry> all;
if (txId == null) {
all = [CrudEntry.fromRow(first)];
Expand Down
4 changes: 2 additions & 2 deletions packages/powersync_core/lib/src/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ String? _stringOrFirst(Object? details) {
return null;
} else if (details is String) {
return details;
} else if (details is List && details[0] is String) {
return details[0];
} else if (details case [final String first, ...]) {
return first;
} else {
return null;
}
Expand Down
Loading