Editor/Inference/TorchCheckpoint.Script.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;

namespace HumanoidMocapX.Inference;

public sealed partial class TorchCheckpoint
{
    readonly Dictionary<string,IReadOnlyList<string>> stringLists=new(StringComparer.Ordinal);

    /// <summary>Named string lists from a script archive's data, such as joint names.
    /// Keys use dot-separated object fields; ordinary checkpoints leave this empty.</summary>
    public IReadOnlyDictionary<string,IReadOnlyList<string>> StringLists=>stringLists;

    /// <summary>Reads only stored tensors and string lists from a TorchScript archive.
    /// rootType must match its qualified root class, e.g. "__torch__.MHRDemo".
    /// Code entries and methods are never loaded or executed. Rejects executable
    /// pickle calls, invalid tensor storage, cycles and excessive object nesting.
    /// The caller owns the returned reader and must dispose it.</summary>
    public static TorchCheckpoint OpenScriptData(string path,string rootType)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(rootType);
        return new TorchCheckpoint(ZipFile.OpenRead(path),rootType);
    }

    TorchCheckpoint(ZipArchive source,string rootType)
    {
        archive=source;
        try
        {
            if(ReadMetadata(archive,out prefix,true) is not ScriptObject root||root.Type.Module+"."+root.Type.Name!=rootType)
                throw new InvalidDataException("TorchScript data root does not match "+rootType+".");
            var fields=0;Flatten(root,"",new HashSet<ScriptObject>(),0,ref fields);
            if(tensors.Count==0)throw new InvalidDataException("No tensor weights in script data.");
        }
        catch{archive.Dispose();throw;}
    }

    void Flatten(ScriptObject instance,string path,HashSet<ScriptObject> ancestors,int depth,ref int fields)
    {
        if(depth>=64||!ancestors.Add(instance))throw new InvalidDataException("TorchScript data contains a cycle or exceeds 64 nested objects.");
        if(instance.Fields is null)throw new InvalidDataException("TorchScript object has no stored fields: "+path);
        foreach(var pair in instance.Fields)
        {
            if(++fields>100_000)throw new InvalidDataException("TorchScript data exceeds the field limit.");
            if(pair.Key is not string member||member.Length==0||member.Length>1024||member.Any(c=>c!='_'&&!char.IsAsciiLetterOrDigit(c)))
                throw new InvalidDataException("Unexpected TorchScript field name.");
            var name=path.Length==0?member:path+"."+member;
            if(name.Length>4096)throw new InvalidDataException("TorchScript field path exceeds the size limit.");
            if(pair.Value is ScriptObject child)Flatten(child,name,ancestors,depth+1,ref fields);
            else if(pair.Value is TensorRef tensor)
            {
                var info=new TensorInfo(name,tensor.Storage.Key,tensor.Storage.Dtype,tensor.Storage.Count,tensor.Offset,tensor.Shape,tensor.Stride);
                Validate(info);tensors.Add(name,info);
            }
            else if(pair.Value is List<object?> list&&list.All(x=>x is string))
                stringLists.Add(name,Array.AsReadOnly(list.Cast<string>().ToArray()));
        }
        ancestors.Remove(instance);
    }
}