非英语语言环境中的损坏文件(编码问题?)

Currupted file in non-english locale (encoding problem?)

在我的 MSI Windows 安装程序中,我有一个自定义 VBScript 操作,它从 'Binary' table 中提取一些文件到文件系统。这是我正在使用的代码:

灵感来自https://www.itninja.com/question/how-to-call-an-exe-which-is-stored-in-a-binary-table-through-a-vbscript-custom-action-in-the-msi

Function ExtractFromBinary(ByVal binaryName, ByVal binaryOutputFile)

 Dim oFSO : Set oFSO = CreateObject("Scripting.FileSystemObject")

 Const msiReadStreamInteger = 0
 Const msiReadStreamBytes = 1
 Const msiReadStreamAnsi = 2 
 Const msiReadStreamDirect = 3

 Dim binaryView : Set binaryView = Session.Database.OpenView("SELECT Data FROM Binary WHERE Name = '" & binaryName & "'") 
 binaryView.Execute

 Dim binaryRecord : Set binaryRecord = binaryView.Fetch 
 Dim binaryData : binaryData = binaryRecord.ReadStream(1, binaryRecord.DataSize(1), msiReadStreamAnsi) 
 Set binaryRecord = Nothing

 Dim binaryStream : Set binaryStream = oFSO.CreateTextFile(binaryOutputFile, True, False) 
 binaryStream.Write binaryData
 binaryStream.Close
 Set binaryStream = Nothing 

End Function

这已经在生产中使用了 2-3 年,没有任何问题。但是,现在我们遇到了一个日语 Windows 安装案例,其中提取的二进制文件已损坏:

如您所见,问题通常出现在“?”之后脚本插入 'E' 或覆盖以下字符的位置。

ReadStream方法和CreateTextFile方法都有一个影响编码的参数。上面显示的组合似乎是唯一适用于我的英语 Windows 10.

我需要对上面的代码进行哪些更改才能使其在日语系统上也能正常工作?

Japanese Code Page: From this blog entry: "Binary Files and the File System Object Do Not Mix": "In the Japanese code page, just-plain-chr(E0) is not even a legal character, so Chr will turn it into a zero... Do not use the FSO to read/write binary files, you're just asking for a world of hurt as soon as someone in DBCS-land runs your code."


备选方案? .NET 怎么样?我意识到您正在执行自定义操作时为时已晚,我将示例制作为独立的 .NET 控制台应用程序。 WiX 框架具有创建 DTF 自定义操作的机制。 Found this on github.com.

Rehashing?: Can we ask what you are actually doing? Why do you need to extract files this way? There could be other approaches that are more reliable if you explain the scenario?


DTF / .NET:虽然我不是部署使用的 .NET 忠实粉丝(依赖层太多),我认为为此使用 .NET/DTF 会做得更好。 What is DTF?

示例 DTF C# 应用程序:下面是一个简单的 C# 示例应用程序,展示了一种从二进制 table(还有其他几种方法,我不是.NET专家)。

  1. 创建新的 C# 控制台应用程序 (.NET Framework)。
  2. 粘贴以下代码并调整参数。
  3. 添加对 Microsoft.Deployment.WindowsInstaller.dll(DTF 框架)的引用。
using Microsoft.Deployment.WindowsInstaller;

namespace MSIExtractBinaryTableEntry
{
    class Program
    {
        static void Main(string[] args)
        {
            // ADJUST 1: Name of Binary Table Entry
            var binarytableentry = "ImageBmp"; 

            // ADJUST 2: Source MSI path
            var msifullpath = @"C:\MySetup.msi";

            // ADJUST 3: Output target path for binary stream
            var binaryfileoutputpath = @"C:\Output.XXX";

            using (var db = new Database(msifullpath, DatabaseOpenMode.ReadOnly))
            {
                using (var binaryView = db.OpenView("SELECT Name, Data FROM Binary WHERE Name='" + binarytableentry + "'"))
                {
                    binaryView.Execute();
                    binaryView.Fetch().GetStream(2, binaryfileoutputpath); // force overwrites output path
                }
            }
        }
    }
}

Alternative:这是一个将整个二进制文件 Table 导出到名为 "Output" 的文件夹的调整在用户的桌面上。

创建测试项目的过程与上述相同。只需指定一个参数:输入 MSI 的完整路径。

using System;
using System.IO;
using Microsoft.Deployment.WindowsInstaller;

namespace MSIExtractBinaryTableEntry
{
    class Program
    {
        static void Main(string[] args)
        {
            // ADJUST 1: Specify MSI file path
            var msifullpath = @"C:\MySetup.msi";

            var outputpath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), @"Output\");
            Directory.CreateDirectory(outputpath);

            using (var db = new Database(msifullpath, DatabaseOpenMode.ReadOnly))
            {
                using (var binaryView = db.OpenView("SELECT Name, Data FROM Binary"))
                {
                    binaryView.Execute();

                    foreach (var rec in binaryView)
                    {
                        rec.GetStream("Data", outputpath + rec.GetString("Name"));
                    }
                }
            }
        }
    }
}

@Robert-Hegner 我会提出这个作为答案,即使它受制于您的测试(我无法测试我在哪里)!

我添加了一个 updated approach here(您需要向下滚动到第二个示例)

它使用 msiReadStreamDirect(而不是 msiReadStreamAnsi)提取字节对的字符串,将它们转换为二进制文件并使用 ADODB.Stream(不是 FSO)创建输出文件。

Dim oFSO : Set oFSO = CreateObject("Scripting.FileSystemObject")

Dim tempFolder : tempFolder = oFSO.GetSpecialFolder(2) 
Dim outputFile : outputFile = tempFolder & "\notepad.exe"

extractFromBinary "notepad", outputFile

Function MultiByteToBinary(MultiByte)
  'obtained from http://www.motobit.com
  'MultiByteToBinary converts multibyte string To real binary data (VT_UI1 | VT_ARRAY)
  'Using recordset
  Dim RS, LMultiByte, Binary
  Const adLongVarBinary = 205
  Set RS = CreateObject("ADODB.Recordset")
  LMultiByte = LenB(MultiByte)
  If LMultiByte>0 Then
    RS.Fields.Append "mBinary", adLongVarBinary, LMultiByte
    RS.Open
    RS.AddNew
      RS("mBinary").AppendChunk MultiByte & ChrB(0)
    RS.Update
    Binary = RS("mBinary").GetChunk(LMultiByte)
  End If
  Set RS = Nothing
  MultiByteToBinary = Binary
End Function

Function SaveBinaryData(FileName, ByteArray)
  Const adTypeBinary = 1
  Const adSaveCreateOverWrite = 2

  'Create Stream object
  Dim BinaryStream
  Set BinaryStream = CreateObject("ADODB.Stream")

  'Specify stream type - we want To save binary data.
  BinaryStream.Type = adTypeBinary

  'Open the stream And write binary data To the object
  BinaryStream.Open
  BinaryStream.Write ByteArray

  'Save binary data To disk
  BinaryStream.SaveToFile FileName, adSaveCreateOverWrite

  Set BinaryStream = Nothing
End Function

Function extractFromBinary(ByVal binaryName, ByVal binaryOutputFile)

    Const msiReadStreamInteger = 0 
    Const msiReadStreamBytes = 1 
    Const msiReadStreamAnsi = 2  
    Const msiReadStreamDirect = 3

    Dim binaryView : Set binaryView = Session.Database.OpenView("SELECT * FROM Binary WHERE Name = '" & binaryName & "'")  
    binaryView.Execute

    Dim binaryRecord : Set binaryRecord = binaryView.Fetch  
    Dim binaryData : binaryData = binaryRecord.ReadStream(2, binaryRecord.DataSize(2), msiReadStreamDirect)  
    Set binaryRecord = Nothing  

    'convert to string of byte pairs to binary
    binaryData = MultiByteToBinary(binaryData)

    'save binary data
    SaveBinaryData binaryOutputFile, binaryData

End Function

Set oFSO = Nothing

这是我最后得到的结果。

根据 Stein Åsmul I rewrote the custom action using C# (.NET / DTF). Initially I was hesitant to writing custom actions in C# as it introduces additional prerequisites to the installer. But it turns out that if the custom action targets .NET Framework 2.0, it should be supported on most machines without the need to manually install the framework (see here 的建议)。

所以这是我的代码:

public static class TemporaryFilesExtractor
{

    [CustomAction]
    public static ActionResult ExtractTemporaryFiles(Session session)
    {
        ExtractFromBinary(session, "binaryname1", "<filePath1>");
        ExtractFromBinary(session, "binaryname2", "<filePath2>");
        return ActionResult.Success;
    }

    private static void ExtractFromBinary(Session session, string binaryName, string binaryOutputFile)
    {
        session.Log($"Extracting {binaryName} to {binaryOutputFile}");
        byte[] buffer = new byte[4096];

        using (var view = session.Database.OpenView("SELECT Data FROM Binary WHERE Name = '{0}'", binaryName))
        {
            view.Execute();
            using (var record = view.Fetch())
            using (var dbStream = record.GetStream(1))
            using (var fileStream = File.OpenWrite(binaryOutputFile))
            {
                int count;
                while ((count = dbStream.Read(buffer, 0, buffer.Length)) != 0)
                    fileStream.Write(buffer, 0, count);
            }
        }
    }

}