Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion AssetStudioCLI/Options/CLIOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,14 @@ internal static class CLIOptions
public static Option<List<string>> o_filterByPathID;
public static Option<List<string>> o_filterByText;
public static Option<bool> f_filterWithRegex;
public static Option<bool> f_filterExcludeMode;
//advanced
public static Option<CompressionType> o_bundleBlockInfoCompression;
public static Option<CompressionType> o_bundleBlockCompression;
public static Option<int> o_maxParallelExportTasks;
public static Option<ExportListType> o_exportAssetList;
public static Option<string> o_assemblyPath;
public static Option<string> o_stripPathPrefix;
public static Option<UnityVersion> o_unityVersion;
public static Option<bool> f_decompressToDisk;
public static Option<bool> f_notRestoreExtensionName;
Expand Down Expand Up @@ -456,7 +458,17 @@ private static void InitOptions()
optionDefaultValue: false,
optionName: "--filter-with-regex",
optionDescription: "(Flag) If specified, the filter options will handle the specified text\n" +
"as a regular expression (doesn't apply to --filter-by-pathid)",
"as a regular expression (doesn't apply to --filter-by-pathid)\n",
optionExample: "",
optionHelpGroup: HelpGroups.Filter,
isFlag: true
);
f_filterExcludeMode = new GroupedOption<bool>
(
optionDefaultValue: false,
optionName: "--filter-exclude-mode",
optionDescription: "(Flag) If specified, the filter options will work as an exclusion\n" +
"(i.e. assets that match the filter conditions will be excluded)",
optionExample: "",
optionHelpGroup: HelpGroups.Filter,
isFlag: true
Expand Down Expand Up @@ -523,6 +535,14 @@ private static void InitOptions()
optionExample: "",
optionHelpGroup: HelpGroups.Advanced
);
o_stripPathPrefix = new GroupedOption<string>
(
optionDefaultValue: "",
optionName: "--strip-path-prefix <path>",
optionDescription: "Specify a path prefix to be stripped from exported asset paths\n",
optionExample: "Example: \"--strip-path-prefix assets/models/char/\"\n",
optionHelpGroup: HelpGroups.Advanced
);
o_unityVersion = new GroupedOption<UnityVersion>
(
optionDefaultValue: null,
Expand Down Expand Up @@ -739,6 +759,10 @@ public static void ParseArgs(string[] args)
f_filterWithRegex.Value = true;
flagIndexes.Add(i);
break;
case "--filter-exclude-mode":
f_filterExcludeMode.Value = true;
flagIndexes.Add(i);
break;
case "--decompress-to-disk":
f_decompressToDisk.Value = true;
flagIndexes.Add(i);
Expand Down Expand Up @@ -1230,6 +1254,13 @@ public static void ParseArgs(string[] args)
return;
}
break;
case "--strip-path-prefix":
o_stripPathPrefix.Value = Path.Combine(Path.GetDirectoryName(value), Path.GetFileName(value));
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Path.Combine(Path.GetDirectoryName(value), Path.GetFileName(value)) to normalize the path is problematic. This approach may fail for edge cases: (1) If value is a root path like "C:", GetDirectoryName returns null; (2) If value has a trailing separator, it will be removed; (3) If value is just a filename with no directory, GetDirectoryName returns empty string and Combine will just return the filename. Consider using Path.GetFullPath(value) for proper path normalization, or simply use the value as-is and let the validation in Studio.cs handle the format checking.

Suggested change
o_stripPathPrefix.Value = Path.Combine(Path.GetDirectoryName(value), Path.GetFileName(value));
o_stripPathPrefix.Value = Path.GetFullPath(value);

Copilot uses AI. Check for mistakes.
if (!o_stripPathPrefix.Value.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
o_stripPathPrefix.Value += Path.DirectorySeparatorChar;
}
break;
case "--unity-version":
try
{
Expand Down Expand Up @@ -1457,12 +1488,14 @@ public static void ShowCurrentOptions()
}
sb.AppendLine(ShowCurrentFilter());
sb.AppendLine($"# Filter With Regex: {f_filterWithRegex}");
sb.AppendLine($"# Filter Exclusion Mode: {f_filterExcludeMode}");
sb.AppendLine($"# Assembly Path: \"{o_assemblyPath}\"");
break;
case WorkMode.Live2D:
sb.AppendLine($"# [{o_workMode} Options]");
sb.AppendLine($"# Filter by Text: \"{string.Join("\", \"", o_filterByText.Value)}\"");
sb.AppendLine($"# Filter With Regex: {f_filterWithRegex}");
sb.AppendLine($"# Filter Exclusion Mode: {f_filterExcludeMode}");
sb.AppendLine($"# Model Group Option: {o_l2dGroupOption}");
sb.AppendFormat("# Search Model-related Assets by: {0}\n", f_l2dAssetSearchByFilename.Value ? "FileName" : "Container");
sb.AppendLine($"# Motion Export Method: {o_l2dMotionMode}");
Expand All @@ -1476,6 +1509,7 @@ public static void ShowCurrentOptions()
? ShowCurrentFilter()
: $"# Filter by Name(s): \"{string.Join("\", \"", o_filterByName.Value)}\"");
sb.AppendLine($"# Filter With Regex: {f_filterWithRegex}");
sb.AppendLine($"# Filter Exclusion Mode: {f_filterExcludeMode}");
sb.AppendLine($"# Export Image Format: {o_imageFormat}");
sb.AppendLine($"# FBX Scale Factor: {o_fbxScaleFactor}");
sb.AppendLine($"# FBX Bone Size: {o_fbxBoneSize}");
Expand Down
6 changes: 6 additions & 0 deletions AssetStudioCLI/ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ Filter Options:
--filter-with-regex (Flag) If specified, the filter options will handle the specified text
as a regular expression (doesn't apply to --filter-by-pathid)

--filter-exclude-mode (Flag) If specified, the filter options will work as an exclusion
(i.e. assets that match the filter conditions will be excluded)

Advanced Options:
--blockinfo-comp <value> Specify the compression type of bundle's blockInfo data
<Value: auto(default) | zstd | oodle | lz4 | lzma>
Expand Down Expand Up @@ -183,6 +186,9 @@ Advanced Options:

--assembly-folder <path> Specify the path to the assembly folder

--strip-path-prefix <path> Specify a path prefix to be stripped from exported asset paths
Example: "--strip-path-prefix assets/models/char/"

--unity-version <text> Specify Unity version
Example: "--unity-version 2017.4.39f1"

Expand Down
41 changes: 39 additions & 2 deletions AssetStudioCLI/Studio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,14 @@ private static void FilterAssets()
);
break;
}

if (CLIOptions.f_filterExcludeMode.Value)
{
var excludeCount = assetsCount - filteredAssets.Count;
Logger.Info($"Excluding {excludeCount} asset(s) that match the filter.");
filteredAssets = parsedAssetsList.Except(filteredAssets).ToList();
}

parsedAssetsList.Clear();
parsedAssetsList = filteredAssets;
}
Expand All @@ -651,6 +659,31 @@ public static void ExportAssets()
var parallelExportCount = CLIOptions.o_maxParallelExportTasks.Value;
var toExportAssetDict = new ConcurrentDictionary<AssetItem, string>();
var toParallelExportAssetDict = new ConcurrentDictionary<AssetItem, string>();

if (CLIOptions.o_stripPathPrefix.Value != null)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null check pattern here is inconsistent with the rest of the codebase. The default value for o_stripPathPrefix is an empty string (line 540 in CLIOptions.cs), so this will never be null unless explicitly set. Throughout the codebase, string options are checked with equality to empty string (e.g., o_assemblyPath.Value == "" at line 1280). Consider using !string.IsNullOrEmpty(CLIOptions.o_stripPathPrefix.Value) for consistency, or change the default value to null if that's the intended pattern.

Copilot uses AI. Check for mistakes.
{
foreach (var asset in parsedAssetsList)
{
var containerPath = asset.Container;
if (string.IsNullOrEmpty(containerPath))
{
continue;
}
if (!Path.Combine(Path.GetDirectoryName(containerPath), Path.GetFileName(containerPath)).StartsWith(CLIOptions.o_stripPathPrefix.Value))
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic using Path.Combine(Path.GetDirectoryName(containerPath), Path.GetFileName(containerPath)) is redundant and potentially problematic. This operation reconstructs the original path but may not handle edge cases correctly (e.g., paths with trailing slashes, root paths, or paths that are just filenames without directories). Consider using containerPath directly for the StartsWith check, as it's simpler and more reliable.

Suggested change
if (!Path.Combine(Path.GetDirectoryName(containerPath), Path.GetFileName(containerPath)).StartsWith(CLIOptions.o_stripPathPrefix.Value))
if (!containerPath.StartsWith(CLIOptions.o_stripPathPrefix.Value))

Copilot uses AI. Check for mistakes.
{
Logger.Warning($"Asset container path \"{asset.Container}\" does not start with the specified path prefix \"{CLIOptions.o_stripPathPrefix.Value}\"");
Logger.Warning("strip path prefix option will be ignored.");
CLIOptions.o_stripPathPrefix.Value = null;
break;
}
}
}

if (CLIOptions.o_stripPathPrefix.Value != null)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same null check pattern issue as line 663. This should use !string.IsNullOrEmpty() for consistency with the rest of the codebase, or the default value should be changed to null.

Copilot uses AI. Check for mistakes.
{
Logger.Info($"Asset container path prefix \"{CLIOptions.o_stripPathPrefix.Value}\" will be stripped off.");
}

Parallel.ForEach(parsedAssetsList, asset =>
{
string exportPath;
Expand All @@ -663,7 +696,12 @@ public static void ExportAssets()
case AssetGroupOption.ContainerPathFull:
if (!string.IsNullOrEmpty(asset.Container))
{
exportPath = Path.Combine(savePath, Path.GetDirectoryName(asset.Container));
var containerPath = Path.GetDirectoryName(asset.Container);
if (CLIOptions.o_stripPathPrefix.Value != null)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same null check pattern issue as lines 663 and 682. This should use !string.IsNullOrEmpty() for consistency with the rest of the codebase.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Substring operation could throw an ArgumentOutOfRangeException if containerPath is shorter than the prefix length. While the validation loop above attempts to prevent this, it uses a different logic (Path.Combine with GetDirectoryName/GetFileName) that may not guarantee the same result. Add a length check here: if (containerPath.Length >= CLIOptions.o_stripPathPrefix.Value.Length) before calling Substring, or ensure the validation logic matches exactly.

Suggested change
if (CLIOptions.o_stripPathPrefix.Value != null)
if (CLIOptions.o_stripPathPrefix.Value != null &&
!string.IsNullOrEmpty(containerPath) &&
containerPath.Length >= CLIOptions.o_stripPathPrefix.Value.Length)

Copilot uses AI. Check for mistakes.
{
containerPath = containerPath.Substring(CLIOptions.o_stripPathPrefix.Value.Length);
}
exportPath = Path.Combine(savePath, containerPath);
if (groupOption == AssetGroupOption.ContainerPathFull)
{
exportPath = Path.Combine(exportPath, Path.GetFileNameWithoutExtension(asset.Container));
Expand Down Expand Up @@ -732,7 +770,6 @@ public static void ExportAssets()
toExportAssetDict.TryAdd(asset, exportPath);
}
});

foreach (var toExportAsset in toExportAssetDict)
{
var asset = toExportAsset.Key;
Expand Down