Files
IOT_SIM/runtime/Sim.Runner/ApiPolicy.cs
T
xuyueandClaude Opus 5.5 670abe5921
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
项目 6 数据存储:仿冒 SqlClient(T-SQL 转写成 SQLite)、数据库页面、DataGridView、DateTimePicker
- 每个学生一套数据库,长期保存(data/databases/<用户编号>/库名.db);学生程序运行时挂进沙箱可读写
- Sim.Data:SqlConnection / SqlCommand / SqlDataReader / SqlDataAdapter / SqlCommandBuilder / SqlParameter;
  连接字符串只按库名打开学生自己的库;ScriptDom 解析 T-SQL 后按白名单转写成 SQLite 语句,
  STRICT 表 + 元数据表还原 SQL Server 的列类型,报错编号和中文信息与 SQL Server 一致;
  INSERT 不写列名时跳过自增列,日期统一格式(书上 DateTime.ToString() 拼接的日期能正确比较)
- 常驻 SQL 服务(Sim.Runner sql-server):数据库页面的查询、对象资源管理器,评分前按实验 database.sql 建库
- 数据库页面(代替 SSMS):对象资源管理器、查询窗口、编辑前 200 行、表设计器、恢复实验初始数据
- DataGridView(绑定 DataView/DataTable/DataSet,单元格修改写回 DataTable)、DateTimePicker;设计器工具箱加入
- 评分:每个检查项一份新建的实验库;新增点 / 修改表格单元格、选日期、表格里有 / 没有某行的检查
- 示例:项目 6 任务 2、3;时间格式改成和中文 Windows 一致(9:05:03);异常堆栈不暴露服务器路径
- 测试:数据库端到端、接口(每人只看到自己的库、恢复实验数据)、评分出错情况、T-SQL 生成

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:18:56 +08:00

110 lines
5.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Sim.Runner;
/// <summary>
/// 编译时检查学生代码用到的 API:网络、启动外部程序、反射、调用本机代码等课程用不到、只会带来风险的功能,
/// 直接给出中文报错。真正的隔离靠运行时沙箱,这里是第二道防线,也让学生看到明确的原因。
/// </summary>
internal static class ApiPolicy
{
#pragma warning disable RS2008 // 不是分析器,不需要发布跟踪
private static readonly DiagnosticDescriptor NotAllowed = new(
"SIM001", "平台不允许使用的功能", "平台不允许使用 {0}({1})", "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true);
#pragma warning restore RS2008
/// <summary>整个命名空间(含子命名空间)都不允许</summary>
private static readonly (string Namespace, string Reason)[] DeniedNamespaces =
{
("System.Net", "网络访问"),
("System.Reflection", "反射"),
("System.Runtime.Loader", "加载程序集"),
("System.IO.Pipes", "进程间通信"),
("System.IO.MemoryMappedFiles", "进程间通信"),
};
/// <summary>单个类型不允许(所在命名空间的其他类型可以用)</summary>
private static readonly Dictionary<string, string> DeniedTypes = new()
{
["System.Diagnostics.Process"] = "启动其他程序",
["System.Diagnostics.ProcessStartInfo"] = "启动其他程序",
["System.AppDomain"] = "加载程序集",
["System.Runtime.InteropServices.Marshal"] = "调用本机代码",
["System.Runtime.InteropServices.DllImportAttribute"] = "调用本机代码",
["System.Runtime.InteropServices.LibraryImportAttribute"] = "调用本机代码",
["System.Runtime.InteropServices.NativeLibrary"] = "调用本机代码",
["System.Runtime.InteropServices.NativeMemory"] = "调用本机代码",
};
/// <summary>
/// 不允许的类型里单独放行的成员:教材用 AppDomain.CurrentDomain.BaseDirectory 拼出保存照片的路径;
/// 类型的名字(GetType().Name、DataColumn.DataType.Name)只是读名字,没有风险
/// </summary>
private static readonly HashSet<string> AllowedMembers = new()
{
"System.AppDomain.CurrentDomain",
"System.AppDomain.BaseDirectory",
"System.Reflection.MemberInfo.Name",
"System.Type.FullName",
};
public static IEnumerable<Diagnostic> Check(CSharpCompilation compilation)
{
foreach (var tree in compilation.SyntaxTrees)
{
var model = compilation.GetSemanticModel(tree);
foreach (var name in tree.GetRoot().DescendantNodes().OfType<SimpleNameSyntax>())
{
// A.B.C 只检查最右边的名字,避免同一处报好几次
if (name.Parent is QualifiedNameSyntax q && q.Left == name) continue;
// var 推断出的类型在初始化表达式里已经检查过
if (name is IdentifierNameSyntax { IsVar: true }) continue;
if (name.Parent is MemberAccessExpressionSyntax m && m.Expression == name && model.GetSymbolInfo(name).Symbol is INamespaceOrTypeSymbol) continue;
var symbol = model.GetSymbolInfo(name).Symbol;
if (symbol == null) continue;
if (Reason(symbol) is { } reason)
{
yield return Diagnostic.Create(NotAllowed, name.GetLocation(), Display(symbol), reason);
}
}
}
}
private static string? Reason(ISymbol symbol)
{
if (symbol is IDynamicTypeSymbol) return "dynamic 动态调用";
// using 指令里的命名空间不算:VS 生成的 AssemblyInfo.cs 会 using System.Runtime.InteropServices
if (symbol is INamespaceSymbol) return null;
if (symbol is IPropertySymbol && AllowedMembers.Contains($"{symbol.ContainingType.ToDisplayString()}.{symbol.Name}")) return null;
var type = symbol as INamedTypeSymbol ?? symbol.ContainingType;
// 方法的返回值、参数类型由调用处检查;这里看符号本身属于哪个类型
for (var t = type; t != null; t = t.ContainingType)
{
var fullName = t.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "");
if (DeniedTypes.TryGetValue(fullName, out var reason)) return reason;
}
if (type == null) return null;
// AssemblyInfo.cs 里的 [assembly: AssemblyTitle(...)] 等特性可以用
if (type.Name.EndsWith("Attribute") && type.ContainingNamespace.ToDisplayString() == "System.Reflection") return null;
var ns = type.ContainingNamespace.ToDisplayString();
foreach (var (denied, reason) in DeniedNamespaces)
{
if (ns == denied || ns.StartsWith(denied + ".")) return reason;
}
return null;
}
private static string Display(ISymbol symbol) => symbol switch
{
IDynamicTypeSymbol => "dynamic",
INamedTypeSymbol type => type.ToDisplayString(),
_ => $"{symbol.ContainingType.ToDisplayString()}.{symbol.Name}",
};
}