Initial flame_lua_runtime package
This commit is contained in:
117
lib/runtime/packages/package_verifier.dart
Normal file
117
lib/runtime/packages/package_verifier.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'game_package.dart';
|
||||
import 'game_package_manifest.dart';
|
||||
|
||||
class PackageVerifier {
|
||||
const PackageVerifier({required this.runtimeApiVersion});
|
||||
|
||||
final int runtimeApiVersion;
|
||||
|
||||
Future<void> verify(GamePackage package) async {
|
||||
_verifyManifest(package);
|
||||
await _verifyEntry(package);
|
||||
await _verifyDeclaredModules(package);
|
||||
await _verifyDeclaredResources(package);
|
||||
}
|
||||
|
||||
void _verifyManifest(GamePackage package) {
|
||||
final manifest = package.manifest;
|
||||
if (manifest.runtimeApiVersion > runtimeApiVersion) {
|
||||
throw FormatException(
|
||||
'Package runtimeApiVersion ${manifest.runtimeApiVersion} is newer than runtime $runtimeApiVersion',
|
||||
);
|
||||
}
|
||||
if (manifest.gameId.isEmpty ||
|
||||
manifest.version.isEmpty ||
|
||||
manifest.entry.isEmpty) {
|
||||
throw const FormatException('Package manifest is incomplete');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyEntry(GamePackage package) async {
|
||||
final script = await package.readText(package.manifest.entry);
|
||||
if (!script.contains('function init')) {
|
||||
throw const FormatException('Lua package must define function init(ctx)');
|
||||
}
|
||||
if (!script.contains('function on_event')) {
|
||||
throw const FormatException(
|
||||
'Lua package must define function on_event(event)',
|
||||
);
|
||||
}
|
||||
if (!script.contains('function smoke_test')) {
|
||||
throw const FormatException(
|
||||
'Lua package must define function smoke_test(ctx)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyDeclaredModules(GamePackage package) async {
|
||||
for (final entry in package.manifest.modules.entries) {
|
||||
final name = entry.key;
|
||||
final path = entry.value;
|
||||
if (!_isSafeModuleName(name)) {
|
||||
throw FormatException('Unsafe Lua module name: $name');
|
||||
}
|
||||
if (!_isSafeModulePath(path)) {
|
||||
throw FormatException(
|
||||
'Lua module path must be scripts/*.lua or runtime:*.lua: $path',
|
||||
);
|
||||
}
|
||||
await package.readText(path);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isSafeModuleName(String value) {
|
||||
return RegExp(r'^[A-Za-z0-9_.-]+$').hasMatch(value) &&
|
||||
!value.contains('..') &&
|
||||
!value.startsWith('.') &&
|
||||
!value.endsWith('.');
|
||||
}
|
||||
|
||||
bool _isSafeModulePath(String path) {
|
||||
if (path.startsWith(GamePackage.runtimeLuaPrefix)) {
|
||||
final name = path.substring(GamePackage.runtimeLuaPrefix.length);
|
||||
return name.isNotEmpty &&
|
||||
name.endsWith('.lua') &&
|
||||
!name.contains('/') &&
|
||||
!name.contains('..');
|
||||
}
|
||||
return path.startsWith('scripts/') &&
|
||||
path.endsWith('.lua') &&
|
||||
!path.contains('..');
|
||||
}
|
||||
|
||||
Future<void> _verifyDeclaredResources(GamePackage package) async {
|
||||
for (final resource in package.manifest.resources.values) {
|
||||
final paths = _resourcePaths(resource);
|
||||
for (final path in paths) {
|
||||
if (path.contains('..')) {
|
||||
throw const FormatException('Resource path must not contain ..');
|
||||
}
|
||||
if (package.isAsset) {
|
||||
await package.readBytes(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
final root = p.normalize(package.rootPath);
|
||||
final target = p.normalize(p.join(root, path));
|
||||
if (!p.isWithin(root, target) && target != root) {
|
||||
throw const FormatException('Resource path escapes package root');
|
||||
}
|
||||
if (!File(target).existsSync()) {
|
||||
throw FormatException('Missing declared resource: $path');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<String> _resourcePaths(GameResource resource) {
|
||||
if (resource.type == GameResourceType.spine) {
|
||||
return [resource.atlas!, resource.skeleton!];
|
||||
}
|
||||
return [resource.path];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user