using System.IO.Compression;
using System.Text;
using System.Text.Json;
namespace XFEWorkflow.Abstractions;
/// <summary>Portable definitions only. Inspection never loads code or starts triggers.</summary>
public static class WorkflowPackage
{
public const long MaximumBytes = 32L * 1024 * 1024;
public static WorkflowDefinition Inspect(string path)
{
if (new FileInfo(path).Length is <= 0 or > MaximumBytes) throw new InvalidDataException("工作流文件大小无效(最多 32 MiB)");
using var file = File.OpenRead(path);
WorkflowDefinition? workflow;
if (Path.GetExtension(path).Equals(".json", StringComparison.OrdinalIgnoreCase)) workflow = JsonSerializer.Deserialize<WorkflowDefinition>(file, WorkflowJson.Options);
else
{
using var archive = new ZipArchive(file, ZipArchiveMode.Read);
if (archive.Entries.Count != 1 || archive.Entries[0].FullName != "workflow.json" || archive.Entries[0].Length > MaximumBytes)
throw new InvalidDataException("工作流包必须只包含 workflow.json,且解压后不超过 32 MiB");
using var definition = archive.Entries[0].Open();
workflow = JsonSerializer.Deserialize<WorkflowDefinition>(definition, WorkflowJson.Options);
}
Validate(workflow); return workflow!;
}
public static void Validate(WorkflowDefinition? workflow)
{
if (workflow is null || workflow.SchemaVersion != 1 || !Enum.IsDefined(workflow.Kind)) throw new InvalidDataException("工作流格式不兼容");
if (workflow.Id is null || workflow.Id.Length is < 1 or > 100 || workflow.Id.Any(c => !char.IsAsciiLetterOrDigit(c) && c is not '-' and not '_')) throw new InvalidDataException("工作流 ID 无效");
if (string.IsNullOrWhiteSpace(workflow.Name) || workflow.Name.Length > 150 || workflow.Description is null || workflow.Description.Length > 10000)
throw new InvalidDataException("工作流名称或描述无效");
if (workflow.PluginVersions is null || workflow.Inputs is null || workflow.Outputs is null || workflow.InputDefaults is null || workflow.Variables is null || workflow.Code is null || workflow.Triggers is null)
throw new InvalidDataException("工作流字段缺失");
void Ports(List<PortDefinition> ports)
{
HashSet<string> names = [];
foreach (var port in ports)
if (port is null || string.IsNullOrWhiteSpace(port.Name) || port.Name.Length > 200 || !names.Add(port.Name) || !Enum.IsDefined(port.Type)
|| port.Label is null || port.Description is null || port.TypeId is null || port.Aliases is null || port.Choices is null)
throw new InvalidDataException("工作流输入或输出端口无效");
}
Ports(workflow.Inputs); Ports(workflow.Outputs);
foreach (var (id, version) in workflow.PluginVersions)
if (!PluginPackage.IsIdentifier(id) || !SemanticVersion.TryParse(version, out _)) throw new InvalidDataException("工作流插件版本锁定无效");
HashSet<string> ids = []; var count = 0;
void Graph(GraphDefinition? graph, int depth)
{
if (depth > 32 || graph?.Nodes is null || graph.Edges is null) throw new InvalidDataException("工作流子图缺失或嵌套过深");
HashSet<string> local = [];
foreach (var node in graph.Nodes)
{
if (node is null || string.IsNullOrWhiteSpace(node.Id) || node.Id.Length > 200 || !ids.Add(node.Id) || ++count > 10000
|| string.IsNullOrWhiteSpace(node.CapabilityId) || node.CapabilityId.Length > 200 || node.Parameters is null || node.Children is null || node.Name is null || node.Code is null)
throw new InvalidDataException("工作流节点无效、重复或超过 10000 个");
local.Add(node.Id); foreach (var child in node.Children.Values) Graph(child, depth + 1);
}
foreach (var edge in graph.Edges)
if (edge is null || !local.Contains(edge.FromNode) || !local.Contains(edge.ToNode) || string.IsNullOrWhiteSpace(edge.FromPort) || string.IsNullOrWhiteSpace(edge.ToPort) || !Enum.IsDefined(edge.Kind))
throw new InvalidDataException("工作流连线无效");
}
Graph(workflow.Graph, 0);
}
public static void Create(WorkflowDefinition workflow, string path)
{
Validate(workflow); var copy = WorkflowJson.Clone(workflow); copy.Enabled = false;
var bytes = Encoding.UTF8.GetBytes(WorkflowJson.SerializeDisplay(copy));
if (bytes.LongLength > MaximumBytes) throw new InvalidDataException("工作流定义超过 32 MiB");
using var file = new FileStream(path, FileMode.CreateNew, FileAccess.Write);
using var archive = new ZipArchive(file, ZipArchiveMode.Create);
using var entry = archive.CreateEntry("workflow.json", CompressionLevel.Optimal).Open(); entry.Write(bytes);
}
}
using System.IO.Compression;
using System.Text;
using System.Text.Json;
namespace XFEWorkflow.Abstractions;
/// <summary>Portable definitions only. Inspection never loads code or starts triggers.</summary>
public static class WorkflowPackage
{
public const long MaximumBytes = 32L * 1024 * 1024;
public static WorkflowDefinition Inspect(string path)
{
if (new FileInfo(path).Length is <= 0 or > MaximumBytes) throw new InvalidDataException("工作流文件大小无效(最多 32 MiB)");
using var file = File.OpenRead(path);
WorkflowDefinition? workflow;
if (Path.GetExtension(path).Equals(".json", StringComparison.OrdinalIgnoreCase)) workflow = JsonSerializer.Deserialize<WorkflowDefinition>(file, WorkflowJson.Options);
else
{
using var archive = new ZipArchive(file, ZipArchiveMode.Read);
if (archive.Entries.Count != 1 || archive.Entries[0].FullName != "workflow.json" || archive.Entries[0].Length > MaximumBytes)
throw new InvalidDataException("工作流包必须只包含 workflow.json,且解压后不超过 32 MiB");
using var definition = archive.Entries[0].Open();
workflow = JsonSerializer.Deserialize<WorkflowDefinition>(definition, WorkflowJson.Options);
}
Validate(workflow); return workflow!;
}
public static void Validate(WorkflowDefinition? workflow)
{
if (workflow is null || workflow.SchemaVersion != 1 || !Enum.IsDefined(workflow.Kind)) throw new InvalidDataException("工作流格式不兼容");
if (workflow.Id is null || workflow.Id.Length is < 1 or > 100 || workflow.Id.Any(c => !char.IsAsciiLetterOrDigit(c) && c is not '-' and not '_')) throw new InvalidDataException("工作流 ID 无效");
if (string.IsNullOrWhiteSpace(workflow.Name) || workflow.Name.Length > 150 || workflow.Description is null || workflow.Description.Length > 10000)
throw new InvalidDataException("工作流名称或描述无效");
if (workflow.PluginVersions is null || workflow.Inputs is null || workflow.Outputs is null || workflow.InputDefaults is null || workflow.Variables is null || workflow.Code is null || workflow.Triggers is null)
throw new InvalidDataException("工作流字段缺失");
void Ports(List<PortDefinition> ports)
{
HashSet<string> names = [];
foreach (var port in ports)
if (port is null || string.IsNullOrWhiteSpace(port.Name) || port.Name.Length > 200 || !names.Add(port.Name) || !Enum.IsDefined(port.Type)
|| port.Label is null || port.Description is null || port.TypeId is null || port.Aliases is null || port.Choices is null)
throw new InvalidDataException("工作流输入或输出端口无效");
}
Ports(workflow.Inputs); Ports(workflow.Outputs);
foreach (var (id, version) in workflow.PluginVersions)
if (!PluginPackage.IsIdentifier(id) || !SemanticVersion.TryParse(version, out _)) throw new InvalidDataException("工作流插件版本锁定无效");
HashSet<string> ids = []; var count = 0;
void Graph(GraphDefinition? graph, int depth)
{
if (depth > 32 || graph?.Nodes is null || graph.Edges is null) throw new InvalidDataException("工作流子图缺失或嵌套过深");
HashSet<string> local = [];
foreach (var node in graph.Nodes)
{
if (node is null || string.IsNullOrWhiteSpace(node.Id) || node.Id.Length > 200 || !ids.Add(node.Id) || ++count > 10000
|| string.IsNullOrWhiteSpace(node.CapabilityId) || node.CapabilityId.Length > 200 || node.Parameters is null || node.Children is null || node.Name is null || node.Code is null)
throw new InvalidDataException("工作流节点无效、重复或超过 10000 个");
local.Add(node.Id); foreach (var child in node.Children.Values) Graph(child, depth + 1);
}
foreach (var edge in graph.Edges)
if (edge is null || !local.Contains(edge.FromNode) || !local.Contains(edge.ToNode) || string.IsNullOrWhiteSpace(edge.FromPort) || string.IsNullOrWhiteSpace(edge.ToPort) || !Enum.IsDefined(edge.Kind))
throw new InvalidDataException("工作流连线无效");
}
Graph(workflow.Graph, 0);
}
public static void Create(WorkflowDefinition workflow, string path)
{
Validate(workflow); var copy = WorkflowJson.Clone(workflow); copy.Enabled = false;
var bytes = Encoding.UTF8.GetBytes(WorkflowJson.SerializeDisplay(copy));
if (bytes.LongLength > MaximumBytes) throw new InvalidDataException("工作流定义超过 32 MiB");
using var file = new FileStream(path, FileMode.CreateNew, FileAccess.Write);
using var archive = new ZipArchive(file, ZipArchiveMode.Create);
using var entry = archive.CreateEntry("workflow.json", CompressionLevel.Optimal).Open(); entry.Write(bytes);
}
}