From f7352c18ddb56d7b76104637c5a81070a6582bf0 Mon Sep 17 00:00:00 2001 From: VaDiM Date: Fri, 19 Jun 2026 21:04:55 +0300 Subject: [PATCH] Fix some bugs with invalid names during export --- AssetStudioCLI/Exporter.cs | 68 +++++++++++++++---- AssetStudioCLI/ParallelExporter.cs | 14 +--- AssetStudioCLI/Studio.cs | 14 ++-- AssetStudioGUI/Exporter.cs | 64 +++++++++++++---- ...{ParallelExport.cs => ParallelExporter.cs} | 14 +--- AssetStudioGUI/Studio.cs | 36 ++++++---- 6 files changed, 143 insertions(+), 67 deletions(-) rename AssetStudioGUI/{ParallelExport.cs => ParallelExporter.cs} (95%) diff --git a/AssetStudioCLI/Exporter.cs b/AssetStudioCLI/Exporter.cs index fd9ba23..baab5b2 100644 --- a/AssetStudioCLI/Exporter.cs +++ b/AssetStudioCLI/Exporter.cs @@ -11,10 +11,16 @@ namespace AssetStudioCLI internal static class Exporter { private static readonly HashSet ExportPathHashSet = new HashSet(System.StringComparer.OrdinalIgnoreCase); + private static readonly HashSet InvalidChars = new HashSet(Path.GetInvalidFileNameChars()); + + static Exporter() + { + InvalidChars.UnionWith(new[] { '"', '<', '>', '|', ':', '*', '?', '\\', '/' }); + } private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath, string mode = "Export") { - var fileName = FixFileName(item.Text); + var fileName = item.Text; var filenameFormat = CLIOptions.o_filenameFormat.Value; var canOverwrite = CLIOptions.f_overwriteExisting.Value; switch (filenameFormat) @@ -26,7 +32,7 @@ namespace AssetStudioCLI fileName = item.m_PathID.ToString(); break; } - fullPath = Path.Combine(dir, fileName + extension); + fullPath = SafeFilePath(dir, $"{fileName}{extension}"); if (ExportPathHashSet.Add(fullPath)) { if (CanWrite(fullPath, dir, canOverwrite)) @@ -36,7 +42,7 @@ namespace AssetStudioCLI } else if (filenameFormat == FilenameFormat.AssetName) { - fullPath = Path.Combine(dir, fileName + item.UniqueID + extension); + fullPath = SafeFilePath(dir, $"{fileName}{item.UniqueID}{extension}"); if (CanWrite(fullPath, dir, canOverwrite)) { return true; @@ -268,10 +274,10 @@ namespace AssetStudioCLI public static bool ExportAnimator(AssetItem item, string exportPath, List animationList = null) { - var exportFullPath = Path.Combine(exportPath, "FBX_Animator", item.Text, item.Text + ".fbx"); + var exportFullPath = SafeFilePath(exportPath, "FBX_Animator", item.Text, item.Text + ".fbx"); if (File.Exists(exportFullPath)) { - exportFullPath = Path.Combine(exportPath, item.Text + item.UniqueID, item.Text + ".fbx"); + exportFullPath = SafeFilePath(exportPath, item.Text + item.UniqueID, item.Text + ".fbx"); } var m_Animator = (Animator)item.Asset; var convert = animationList != null @@ -302,10 +308,10 @@ namespace AssetStudioCLI { var jsonDoc = mat.ToJsonDoc(); var str = System.Text.Json.JsonSerializer.Serialize(jsonDoc, Object.JsonOptions); - var exportFullPath = Path.Combine(exportDir, $"{mat.m_Name}.json"); + var exportFullPath = SafeFilePath(exportDir, $"{mat.m_Name}.json"); if (File.Exists(exportFullPath)) { - exportFullPath = Path.Combine(exportDir, $"{mat.m_Name} #{mat.GetHashCode():x}.json"); + exportFullPath = SafeFilePath(exportDir, $"{mat.m_Name} #{mat.GetHashCode():x}.json"); } File.WriteAllText(exportFullPath, str); } @@ -436,20 +442,54 @@ namespace AssetStudioCLI var convert = animationList != null ? new ModelConverter(gameObject, CLIOptions.o_imageFormat.Value, animationList.Select(x => (AnimationClip)x.Asset).ToList()) : new ModelConverter(gameObject, CLIOptions.o_imageFormat.Value); - var modelName = FixFileName(gameObject.m_Name); - var exportFullPath = Path.Combine(exportPath, "FBX_GameObjects", modelName, modelName + ".fbx"); + var modelName = gameObject.m_Name; + var exportFullPath = SafeFilePath(exportPath, "FBX_GameObjects", modelName, modelName + ".fbx"); if (File.Exists(exportFullPath)) { - exportFullPath = Path.Combine(exportPath, $"{modelName}_{gameObject.GetHashCode():X}", modelName + ".fbx"); + exportFullPath = SafeFilePath(exportPath, $"{modelName}_{gameObject.GetHashCode():X}", modelName + ".fbx"); } ExportFbx(convert, exportFullPath); } - public static string FixFileName(string str) + public static string SafeFileName(string str) { - return str.Length >= 260 - ? Path.GetRandomFileName() - : Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_')); + if (str.Length >= 255) + { + var newName = Path.GetRandomFileName(); + Logger.Warning("The asset file name is too long.\n" + + $"\"{str}\"\n" + + "Was replaced with:\n" + + $"\"{newName}\""); + return newName; + } + + return InvalidChars.Aggregate(str, (current, c) => current.Replace(c, '_')).Trim(); + } + + public static string SafeFilePath(string path, params string[] names) + { + var result = path; + foreach (var name in names) + { + result = Path.Combine(result, SafeFileName(name)); + } + + if (result.Length >= 4096) + { + var newPath = Path.Combine(Directory.GetCurrentDirectory(), SafeFileName(names.Last())); + Logger.Warning("The export path is too long.\n" + + $"\"{result}\"\n" + + "Was reduced to:\n" + + $"\"{newPath}\""); + return newPath; + } + + return Path.GetInvalidPathChars().Aggregate(result, (current, c) => current.Replace(c, '_')); + } + + public static string SafePathCombine(string pathA, string pathB) + { + return SafeFilePath(pathA, pathB.Split('\\', '/')); } public static void ClearHash() diff --git a/AssetStudioCLI/ParallelExporter.cs b/AssetStudioCLI/ParallelExporter.cs index 855bf35..9e9d0d9 100644 --- a/AssetStudioCLI/ParallelExporter.cs +++ b/AssetStudioCLI/ParallelExporter.cs @@ -3,7 +3,6 @@ using AssetStudioCLI.Options; using System; using System.Collections.Concurrent; using System.IO; -using System.Linq; using System.Text; namespace AssetStudioCLI @@ -192,7 +191,7 @@ namespace AssetStudioCLI private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath) { - var fileName = FixFileName(item.Text); + var fileName = item.Text; var filenameFormat = CLIOptions.o_filenameFormat.Value; var canOverwrite = CLIOptions.f_overwriteExisting.Value; switch (filenameFormat) @@ -208,7 +207,7 @@ namespace AssetStudioCLI break; } extension = extension.ToLower(); - fullPath = Path.Combine(dir, $"{fileName}{extension}"); + fullPath = Exporter.SafeFilePath(dir, $"{fileName}{extension}"); if (ExportPathDict.TryAdd(fullPath, true)) { if (CanWrite(fullPath, dir, canOverwrite)) @@ -218,7 +217,7 @@ namespace AssetStudioCLI } else if (filenameFormat == FilenameFormat.AssetName || (filenameFormat == FilenameFormat.AssetName_Hash && item.Hash == 0)) { - fullPath = Path.Combine(dir, $"{fileName}{item.UniqueID}{extension}"); + fullPath = Exporter.SafeFilePath(dir, $"{fileName}{item.UniqueID}{extension}"); if (CanWrite(fullPath, dir, canOverwrite)) { return true; @@ -252,13 +251,6 @@ namespace AssetStudioCLI } } - private static string FixFileName(string str) - { - return str.Length >= 260 - ? Path.GetRandomFileName() - : Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_')); - } - public static void ClearHash() { ExportPathDict.Clear(); diff --git a/AssetStudioCLI/Studio.cs b/AssetStudioCLI/Studio.cs index 2b28f4d..86bb697 100644 --- a/AssetStudioCLI/Studio.cs +++ b/AssetStudioCLI/Studio.cs @@ -679,7 +679,7 @@ namespace AssetStudioCLI exportPath = Path.Combine(savePath, containerPath); if (groupOption == AssetGroupOption.ContainerPathFull) { - exportPath = Path.Combine(exportPath, Path.GetFileNameWithoutExtension(asset.Container)); + exportPath = Exporter.SafeFilePath(exportPath, Path.GetFileNameWithoutExtension(asset.Container)); } } else @@ -700,7 +700,7 @@ namespace AssetStudioCLI case AssetGroupOption.SceneHierarchy: if (asset.Node != null) { - exportPath = Path.Combine(savePath, asset.Node.FullPath); + exportPath = Exporter.SafePathCombine(savePath, asset.Node.FullPath); } else { @@ -866,7 +866,7 @@ namespace AssetStudioCLI var exportedCount = 0; var k = 0; - Logger.Info($"Searching for objects to export.."); + Logger.Info("Searching for objects to export.."); Progress.Reset(); var count = gameObjectTree.Sum(x => x.nodes.Count); foreach (var node in gameObjectTree) @@ -913,14 +913,14 @@ namespace AssetStudioCLI foreach (var gameObjectNode in exportableObjects) { var gameObject = gameObjectNode.gameObject; - var filename = Exporter.FixFileName(gameObject.m_Name); - var targetPath = $"{savePath}{filename}{Path.DirectorySeparatorChar}"; + var filename = gameObject.m_Name; + var targetPath = $"{Exporter.SafeFilePath(savePath, filename)}{Path.DirectorySeparatorChar}"; //重名文件处理 for (int i = 1; ; i++) { if (Directory.Exists(targetPath)) { - targetPath = $"{savePath}{filename} ({i}){Path.DirectorySeparatorChar}"; + targetPath = $"{Exporter.SafeFilePath(savePath, filename)} ({i}){Path.DirectorySeparatorChar}"; } else { @@ -1229,7 +1229,7 @@ namespace AssetStudioCLI break; } - var destPath = Path.Combine(baseDestPath, modelPath); + var destPath = Exporter.SafePathCombine(baseDestPath, modelPath); cubismExtractor.ExtractCubismModel(destPath, motionMode, forceBezier, parallelTaskCount); modelCounter++; } diff --git a/AssetStudioGUI/Exporter.cs b/AssetStudioGUI/Exporter.cs index 1c97ff8..23371df 100644 --- a/AssetStudioGUI/Exporter.cs +++ b/AssetStudioGUI/Exporter.cs @@ -10,10 +10,16 @@ namespace AssetStudioGUI internal static class Exporter { private static readonly HashSet ExportPathHashSet = new HashSet(System.StringComparer.OrdinalIgnoreCase); + private static readonly HashSet InvalidChars = new HashSet(Path.GetInvalidFileNameChars()); + + static Exporter() + { + InvalidChars.UnionWith(new[] { '"', '<', '>', '|', ':', '*', '?', '\\', '/' }); + } private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath, string mode = "Export") { - var fileName = FixFileName(item.Text); + var fileName = item.Text; var filenameFormatIndex = Properties.Settings.Default.filenameFormat; var canOverwrite = Properties.Settings.Default.overwriteExistingFiles; switch (filenameFormatIndex) @@ -25,7 +31,7 @@ namespace AssetStudioGUI fileName = item.m_PathID.ToString(); break; } - fullPath = Path.Combine(dir, fileName + extension); + fullPath = SafeFilePath(dir, $"{fileName}{extension}"); if (ExportPathHashSet.Add(fullPath)) { if (CanWrite(fullPath, dir, canOverwrite)) @@ -35,7 +41,7 @@ namespace AssetStudioGUI } else if (filenameFormatIndex == 0) //assetName { - fullPath = Path.Combine(dir, fileName + item.UniqueID + extension); + fullPath = SafeFilePath(dir, $"{fileName}{item.UniqueID}{extension}"); if (CanWrite(fullPath, dir, canOverwrite)) { return true; @@ -234,10 +240,10 @@ namespace AssetStudioGUI public static bool ExportAnimator(AssetItem item, string exportPath, List animationList = null) { - var exportFullPath = Path.Combine(exportPath, item.Text, item.Text + ".fbx"); + var exportFullPath = SafeFilePath(exportPath, item.Text, item.Text + ".fbx"); if (File.Exists(exportFullPath)) { - exportFullPath = Path.Combine(exportPath, item.Text + item.UniqueID, item.Text + ".fbx"); + exportFullPath = SafeFilePath(exportPath, item.Text + item.UniqueID, item.Text + ".fbx"); } if (!Studio.FbxSettings.ExportAnimations) animationList = new List(); @@ -261,10 +267,10 @@ namespace AssetStudioGUI { var jsonDoc = mat.ToJsonDoc(); var str = System.Text.Json.JsonSerializer.Serialize(jsonDoc, Object.JsonOptions); - var exportFullPath = Path.Combine(exportDir, $"{mat.m_Name}.json"); + var exportFullPath = SafeFilePath(exportDir, $"{mat.m_Name}.json"); if (File.Exists(exportFullPath)) { - exportFullPath = Path.Combine(exportDir, $"{mat.m_Name} #{mat.GetHashCode():x}.json"); + exportFullPath = SafeFilePath(exportDir, $"{mat.m_Name} #{mat.GetHashCode():x}.json"); } File.WriteAllText(exportFullPath, str); } @@ -398,7 +404,7 @@ namespace AssetStudioGUI var convert = animationList != null ? new ModelConverter(gameObject, Properties.Settings.Default.convertType, animationList.Select(x => (AnimationClip)x.Asset).ToList()) : new ModelConverter(gameObject, Properties.Settings.Default.convertType); - exportPath = exportPath + FixFileName(gameObject.m_Name) + ".fbx"; + exportPath = SafeFilePath(exportPath, $"{gameObject.m_Name}.fbx"); ExportFbx(convert, exportPath); } @@ -413,11 +419,45 @@ namespace AssetStudioGUI ExportFbx(convert, exportPath); } - public static string FixFileName(string str) + public static string SafeFileName(string str) { - return str.Length >= 260 - ? Path.GetRandomFileName() - : Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_')); + if (str.Length >= 255) + { + var newName = Path.GetRandomFileName(); + Logger.Warning("The asset file name is too long.\n" + + $"\"{str}\"\n" + + "Was replaced with:\n" + + $"\"{newName}\""); + return newName; + } + + return InvalidChars.Aggregate(str, (current, c) => current.Replace(c, '_')).Trim(); + } + + public static string SafeFilePath(string path, params string[] names) + { + var result = path; + foreach (var name in names) + { + result = Path.Combine(result, SafeFileName(name)); + } + + if (result.Length >= 4096) + { + var newPath = Path.Combine(Directory.GetCurrentDirectory(), SafeFileName(names.Last())); + Logger.Warning("The export path is too long.\n" + + $"\"{result}\"\n" + + "Was reduced to:\n" + + $"\"{newPath}\""); + return newPath; + } + + return Path.GetInvalidPathChars().Aggregate(result, (current, c) => current.Replace(c, '_')); + } + + public static string SafePathCombine(string pathA, string pathB) + { + return SafeFilePath(pathA, pathB.Split('\\', '/')); } public static void ClearHash() diff --git a/AssetStudioGUI/ParallelExport.cs b/AssetStudioGUI/ParallelExporter.cs similarity index 95% rename from AssetStudioGUI/ParallelExport.cs rename to AssetStudioGUI/ParallelExporter.cs index 1899975..ceb6f53 100644 --- a/AssetStudioGUI/ParallelExport.cs +++ b/AssetStudioGUI/ParallelExporter.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.IO; -using System.Linq; using System.Text; namespace AssetStudioGUI @@ -191,7 +190,7 @@ namespace AssetStudioGUI private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath) { - var fileName = FixFileName(item.Text); + var fileName = item.Text; var filenameFormatIndex = Properties.Settings.Default.filenameFormat; var canOverwrite = Properties.Settings.Default.overwriteExistingFiles; switch (filenameFormatIndex) @@ -207,7 +206,7 @@ namespace AssetStudioGUI break; } extension = extension.ToLower(); - fullPath = Path.Combine(dir, $"{fileName}{extension}"); + fullPath = Exporter.SafeFilePath(dir, $"{fileName}{extension}"); if (ExportPathDict.TryAdd(fullPath, true)) { if (CanWrite(fullPath, dir, canOverwrite)) @@ -217,7 +216,7 @@ namespace AssetStudioGUI } else if (filenameFormatIndex == 0 || (filenameFormatIndex == 3 && item.Hash == 0)) //assetName, asset name@hash { - fullPath = Path.Combine(dir, $"{fileName}{item.UniqueID}{extension}"); + fullPath = Exporter.SafeFilePath(dir, $"{fileName}{item.UniqueID}{extension}"); if (CanWrite(fullPath, dir, canOverwrite)) { return true; @@ -251,13 +250,6 @@ namespace AssetStudioGUI } } - private static string FixFileName(string str) - { - return str.Length >= 260 - ? Path.GetRandomFileName() - : Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_')); - } - public static void ClearHash() { ExportPathDict.Clear(); diff --git a/AssetStudioGUI/Studio.cs b/AssetStudioGUI/Studio.cs index 0a789b8..6403026 100644 --- a/AssetStudioGUI/Studio.cs +++ b/AssetStudioGUI/Studio.cs @@ -552,7 +552,7 @@ namespace AssetStudioGUI exportPath = Path.Combine(savePath, Path.GetDirectoryName(asset.Container)); if (groupOption == AssetGroupOption.ContainerPathFull) { - exportPath = Path.Combine(exportPath, Path.GetFileNameWithoutExtension(asset.Container)); + exportPath = Exporter.SafeFilePath(exportPath, Path.GetFileNameWithoutExtension(asset.Container)); } } else @@ -573,7 +573,7 @@ namespace AssetStudioGUI case AssetGroupOption.SceneHierarchy: if (asset.TreeNode != null) { - exportPath = Path.Combine(savePath, asset.TreeNode.FullPath); + exportPath = Exporter.SafePathCombine(savePath, asset.TreeNode.FullPath); } else { @@ -793,15 +793,15 @@ namespace AssetStudioGUI continue; } //处理非法文件名 - var filename = Exporter.FixFileName(j.Text); + var fileName = j.Text; //每个文件存放在单独的文件夹 - var targetPath = $"{savePath}{filename}{Path.DirectorySeparatorChar}"; + var targetPath = $"{Exporter.SafeFilePath(savePath, fileName)}{Path.DirectorySeparatorChar}"; //重名文件处理 for (int i = 1;; i++) { if (Directory.Exists(targetPath)) { - targetPath = $"{savePath}{filename} ({i}){Path.DirectorySeparatorChar}"; + targetPath = $"{Exporter.SafeFilePath(savePath, fileName)} ({i}){Path.DirectorySeparatorChar}"; } else { @@ -810,7 +810,7 @@ namespace AssetStudioGUI } Directory.CreateDirectory(targetPath); //导出FBX - Logger.Info($"Exporting {filename}.fbx"); + Logger.Info($"Exporting {fileName}.fbx"); try { Exporter.ExportGameObject(j.gameObject, targetPath); @@ -821,7 +821,7 @@ namespace AssetStudioGUI } Progress.Report(++k, count); - Logger.Info($"Finished exporting {filename}.fbx"); + Logger.Info($"Finished exporting {fileName}.fbx"); } } if (Properties.Settings.Default.openAfterExport) @@ -878,16 +878,28 @@ namespace AssetStudioGUI Progress.Reset(); foreach (var gameObject in gameObjects) { - Logger.Info($"Exporting {gameObject.m_Name}"); + var fileName = gameObject.m_Name; + Logger.Info($"Exporting {fileName}"); try { - var modelExportPath = Path.Combine(exportPath, gameObject.m_Name) + Path.DirectorySeparatorChar; + var modelExportPath = $"{Exporter.SafeFilePath(exportPath, fileName)}{Path.DirectorySeparatorChar}"; + for (var j = 1;; j++) + { + if (Directory.Exists(modelExportPath)) + { + modelExportPath = $"{Exporter.SafeFilePath(exportPath, fileName)} ({i}){Path.DirectorySeparatorChar}"; + } + else + { + break; + } + } Exporter.ExportGameObject(gameObject, modelExportPath, animationList); - Logger.Info($"Finished exporting {gameObject.m_Name}"); + Logger.Info($"Finished exporting {fileName}"); } catch (Exception ex) { - Logger.Error($"Export GameObject:{gameObject.m_Name} error", ex); + Logger.Error($"Export GameObject:{fileName} error", ex); Logger.Info("Error in export"); } @@ -1189,7 +1201,7 @@ namespace AssetStudioGUI : container; break; } - var destPath = Path.Combine(baseDestPath, modelPath); + var destPath = Exporter.SafePathCombine(baseDestPath, modelPath); cubismExtractor.ExtractCubismModel(destPath, motionMode, forceBezier, GetParallelExportCount()); modelCounter++; }