From f9628c822eb8c3af92d727c3acb9cd8fed3b1ceb Mon Sep 17 00:00:00 2001 From: leehack Date: Wed, 11 Nov 2026 04:02:24 -0500 Subject: [PATCH] Refuse unparseable and uppercase-scheme model URLs in the chat-app-litert-json-schema e2e (#1861) --- test/unit/tooling/run_local_e2e_test.dart | 16 ++++++++++ tool/testing/run_local_e2e.dart | 37 ++++++++++++----------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/test/unit/tooling/run_local_e2e_test.dart b/test/unit/tooling/run_local_e2e_test.dart index f0d603ec2..01b014c77 100644 --- a/test/unit/tooling/run_local_e2e_test.dart +++ b/test/unit/tooling/run_local_e2e_test.dart @@ -1708,6 +1708,9 @@ Future main(List args) async { 'https://user:token@example.com/m.litertlm', 'https://example.com/m.litertlm?X-Amz-Signature=abc', 'https://example.com/m.litertlm#token', + 'https://[bad/token?x=1', + 'https://h:port/m?token=S', + 'HTTPS://user:token@example.com/m.litertlm', ]) ['--model-path', secret, '--backend', 'cpu'], ]) { @@ -1731,6 +1734,19 @@ Future main(List args) async { '--dart-define=LITERT_LM_BACKEND=gpu', ), ); + final upper = await dryRun([ + '--model-path', + 'HTTPS://example.com/Qwen3-0.6B.litertlm', + '--backend', + 'cpu', + ]); + expect( + upper.stdout, + contains( + '--dart-define=LITERT_LM_MODEL_URL=' + 'HTTPS://example.com/Qwen3-0.6B.litertlm', + ), + ); final url = await dryRun([ '--model-path', 'http://10.0.2.2:8765/gemma-4-E2B-it.litertlm', diff --git a/tool/testing/run_local_e2e.dart b/tool/testing/run_local_e2e.dart index ad1116099..43104e421 100644 --- a/tool/testing/run_local_e2e.dart +++ b/tool/testing/run_local_e2e.dart @@ -2247,8 +2247,7 @@ List buildLocalE2eScenarios({String? projectRoot}) { requiresDevice: true, stepsBuilder: (context) { final model = context.modelPath!; - final remote = - model.startsWith('http://') || model.startsWith('https://'); + final remote = _isRemoteModel(model); return [ LocalE2eCommandStep( workingDirectory: context.chatAppDir, @@ -2993,21 +2992,22 @@ Future runLocalE2e( stderr: '--model-path is required for ${scenario.name}.\n', ); } - // The URL is printed and compiled into the app, so it must carry no secret. - final modelUrl = scenario.name == 'chat-app-litert-json-schema' - ? Uri.tryParse(parsed.modelPath!) - : null; - if (modelUrl != null && - modelUrl.hasScheme && - (modelUrl.userInfo.isNotEmpty || - modelUrl.hasQuery || - modelUrl.hasFragment)) { - return LocalE2eResult( - 64, - stderr: - '${scenario.name} accepts a model URL without credentials, query or ' - 'fragment; serve the bundle from a plain URL.\n', - ); + // The URL is printed and compiled into the app, so it must carry no secret; + // a URL that does not parse cannot be checked, so it is refused too. + if (scenario.name == 'chat-app-litert-json-schema' && + _isRemoteModel(parsed.modelPath!)) { + final modelUrl = Uri.tryParse(parsed.modelPath!); + if (modelUrl == null || + modelUrl.userInfo.isNotEmpty || + modelUrl.hasQuery || + modelUrl.hasFragment) { + return LocalE2eResult( + 64, + stderr: + '${scenario.name} accepts a model URL without credentials, query or ' + 'fragment; serve the bundle from a plain URL.\n', + ); + } } if ((scenario.name == 'litert-lm-json-schema' || scenario.name == 'chat-app-litert-json-schema') && @@ -4021,3 +4021,6 @@ Future main(List args) async { await stderr.flush(); exit(result.exitCode); } + +bool _isRemoteModel(String model) => + RegExp(r'^https?://', caseSensitive: false).hasMatch(model);