using System;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace LibMMD.Reader
{
///
/// Extracts MMD model archives and locates PMD or PMX model files.
///
public static class MmdModelArchive
{
private static readonly int[] CandidateCodePages = { 65001, 932, 936, 950 };
///
/// Recreates , extracts the ZIP archive into it,
/// and returns the first PMX or PMD file found below that directory.
///
///
/// Entries that escape the destination directory and macOS AppleDouble files are ignored.
/// A null return value means that extraction succeeded but the archive contained no model.
///
/// A path argument is empty.
/// The archive or destination cannot be read or written.
/// The input is not a valid ZIP archive.
public static string ExtractAndFindModel(string zipPath, string destinationDirectory)
{
if (string.IsNullOrWhiteSpace(zipPath))
throw new ArgumentException("A ZIP archive path is required.", nameof(zipPath));
if (string.IsNullOrWhiteSpace(destinationDirectory))
throw new ArgumentException("A destination directory is required.", nameof(destinationDirectory));
var destinationRoot = Path.GetFullPath(destinationDirectory);
if (Directory.Exists(destinationRoot)) Directory.Delete(destinationRoot, true);
Directory.CreateDirectory(destinationRoot);
var rootPrefix = destinationRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
var entryEncoding = DetectEntryEncoding(zipPath);
using (var stream = File.OpenRead(zipPath))
using (var archive = new ZipArchive(stream, ZipArchiveMode.Read, false, entryEncoding))
{
foreach (var entry in archive.Entries)
{
var normalizedName = entry.FullName.Replace('/', Path.DirectorySeparatorChar);
var trimmedName = normalizedName.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var fileName = Path.GetFileName(trimmedName);
if (fileName.StartsWith("._", StringComparison.Ordinal)) continue;
var entryPath = Path.GetFullPath(Path.Combine(destinationRoot, normalizedName));
if (!entryPath.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase)) continue;
if (string.IsNullOrEmpty(entry.Name))
{
Directory.CreateDirectory(entryPath);
continue;
}
var entryDirectory = Path.GetDirectoryName(entryPath);
if (!string.IsNullOrEmpty(entryDirectory)) Directory.CreateDirectory(entryDirectory);
entry.ExtractToFile(entryPath, true);
}
}
return FindModelFile(destinationRoot);
}
/// Returns the first PMX or PMD file found recursively, or null when none exists.
public static string FindModelFile(string directory)
{
if (string.IsNullOrWhiteSpace(directory))
throw new ArgumentException("A directory path is required.", nameof(directory));
foreach (var file in Directory.EnumerateFiles(directory))
{
if (Path.GetFileName(file).StartsWith("._", StringComparison.Ordinal)) continue;
var extension = Path.GetExtension(file);
if (extension.Equals(".pmx", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".pmd", StringComparison.OrdinalIgnoreCase))
return file;
}
foreach (var childDirectory in Directory.EnumerateDirectories(directory))
{
var modelPath = FindModelFile(childDirectory);
if (modelPath != null) return modelPath;
}
return null;
}
private static Encoding DetectEntryEncoding(string zipPath)
{
var bestEncoding = Encoding.UTF8;
var bestScore = int.MaxValue;
foreach (var codePage in CandidateCodePages)
{
Encoding encoding;
try
{
encoding = Encoding.GetEncoding(codePage);
}
catch (ArgumentException)
{
continue;
}
var score = ScoreEncoding(zipPath, encoding);
if (score >= bestScore) continue;
bestScore = score;
bestEncoding = encoding;
if (score == 0) break;
}
return bestEncoding;
}
private static int ScoreEncoding(string zipPath, Encoding encoding)
{
try
{
using (var stream = File.OpenRead(zipPath))
using (var archive = new ZipArchive(stream, ZipArchiveMode.Read, false, encoding))
{
var replacements = 0;
foreach (var entry in archive.Entries)
foreach (var character in entry.FullName)
{
if (character == '\uFFFD' || character == '?') replacements++;
}
return replacements;
}
}
catch (InvalidDataException)
{
return int.MaxValue;
}
catch (DecoderFallbackException)
{
return int.MaxValue;
}
}
}
}