diff --git a/SabreTools.Serialization/Deserializers/CFB.cs b/SabreTools.Serialization/Deserializers/CFB.cs
index 42c9dd4e..ce5ba0a5 100644
--- a/SabreTools.Serialization/Deserializers/CFB.cs
+++ b/SabreTools.Serialization/Deserializers/CFB.cs
@@ -244,7 +244,7 @@ namespace SabreTools.Serialization.Deserializers
var obj = new DirectoryEntry();
byte[] name = data.ReadBytes(64);
- obj.Name = Encoding.Unicode.GetString(name);
+ obj.Name = Encoding.Unicode.GetString(name).DecodeStreamName();
obj.NameLength = data.ReadUInt16LittleEndian();
obj.ObjectType = (ObjectType)data.ReadByteValue();
obj.ColorFlag = (ColorFlag)data.ReadByteValue();
diff --git a/SabreTools.Serialization/Extensions.CFB.cs b/SabreTools.Serialization/Extensions.CFB.cs
new file mode 100644
index 00000000..dca66b71
--- /dev/null
+++ b/SabreTools.Serialization/Extensions.CFB.cs
@@ -0,0 +1,99 @@
+using System.Text;
+
+namespace SabreTools.Serialization
+{
+ public static partial class Extensions
+ {
+ ///
+ /// Decode a MIME-encoded stream name stored as a byte array
+ ///
+ /// Byte array to decode
+ /// Decoded string on success, null otherwise
+ /// Adapted from LibMSI
+ public static string? DecodeStreamName(this byte[]? input)
+ {
+ // Ignore invalid inputs
+ if (input == null)
+ return null;
+ else if (input.Length == 0)
+ return string.Empty;
+
+ int count = 0;
+ int p = 0; // inputBytes[0]
+
+ byte[] output = new byte[input.Length + 1];
+ int q = 0; // output[0]
+ while (p < input.Length && input[p] != 0)
+ {
+ byte ch = input[p];
+ if ((ch == 0xe3 && input[p + 1] >= 0xa0) || (ch == 0xe4 && input[p + 1] < 0xa0))
+ {
+ // UTF-8 encoding of 0x3800..0x47ff.
+ output[q++] = (byte)Mime2Utf(input[p + 2] & 0x7f);
+ output[q++] = (byte)Mime2Utf(input[p + 1] ^ 0xa0);
+ p += 3;
+ count += 2;
+ continue;
+ }
+
+ if (ch == 0xe4 && input[p + 1] == 0xa0)
+ {
+ // UTF-8 encoding of 0x4800..0x483f.
+ output[q++] = (byte)Mime2Utf(input[p + 2] & 0x7f);
+ p += 3;
+ count++;
+ continue;
+ }
+
+ output[q++] = input[p++];
+ if (ch >= 0xc1)
+ output[q++] = input[p++];
+ if (ch >= 0xe0)
+ output[q++] = input[p++];
+ if (ch >= 0xf0)
+ output[q++] = input[p++];
+
+ count++;
+ }
+
+ output[q] = 0;
+ return Encoding.ASCII.GetString(output);
+ }
+
+ ///
+ /// Decode a MIME-encoded stream name stored as a string
+ ///
+ /// String to decode
+ /// Decoded string on success, null otherwise
+ /// Adapted from LibMSI
+ public static string? DecodeStreamName(this string? input)
+ {
+ // Ignore invalid inputs
+ if (input == null)
+ return null;
+ else if (input.Length == 0)
+ return string.Empty;
+
+ byte[] inputBytes = Encoding.UTF8.GetBytes(input);
+ return inputBytes.DecodeStreamName();
+ }
+
+ ///
+ /// Decode a single MIME-encoded byte
+ ///
+ /// Byte to decode
+ /// Decoded byte on success, `_` otherwise
+ /// Adapted from LibMSI
+ private static int Mime2Utf(int b)
+ {
+ return b switch
+ {
+ < 10 => b + '0',
+ < 10 + 26 => b - 10 + 'A',
+ < 10 + 26 + 26 => b - 10 - 26 + 'a',
+ 10 + 26 + 26 => '.',
+ _ => '_'
+ };
+ }
+ }
+}
\ No newline at end of file