如何将MIME编码的base64字符串恢复为可写字节数组?

时间:2018-03-19 18:15:01

标签: c# encoding base64

我在数据库中有许多文件(PDF,doc,docx,jpg等),这些文件是使用以下方法添加的:

  

附件以MIME编码(base64编码)发送到数据库   字符串。然后该组件转换此MIME编码的字符串(in   unicode)在将字节流作为BLOB写入数据库之前到字节流   (Oracle)或Image(SQL Server)。

还有一个' guid'附加到字符串的开头,即76个字符。

我试图将附件提取并保存到磁盘上的文件而不是数据库中。它的工作时间约占20%。剩余的时间我将字符串传递给System.FormatException: Invalid character in a Base-64 string.时获得FromBase64String

我注意到数据库中的值如下所示,保存成功:

0x7B00350030003100460032003300350046002D00370

失败者总是这样开始:

0x7B35303146323335462D373546302D343936342D394

我在这里没有足够的字符来粘贴完整的示例,因此请参阅this pastebin以获取一个不起作用的示例。它应该代表一个Word文档,该文档仅显示"仅测试文档"。 This one是同一文档,但已转换为PDF。

This可以使用并转换为test font.htm。它必须插入SQL数据库中的image列,然后使用我的代码拉出:

 private const int guidLength = 38 * 2;
 public static byte[] GetAttachment(string folderid, string filename) {
 string queryString = string.Format("SELECT <image column> FROM AttachmentTable WHERE .....",
                      folderid, filename);
                using (SqlConnection connection = new SqlConnection("context connection=true"))
                {
                    connection.Open();
                    using (SqlCommand selectAttachment = new SqlCommand(
                        queryString,
                        connection))
                    {
                        using (SqlDataReader reader = selectAttachment.ExecuteReader())                        {
                            while (reader.Read())
                            {
                                if (reader[0] == System.DBNull.Value)
                                    return new byte[0];
                                byte[] data = (byte[])reader[0];
                                byte[] truncatedData;
                                if (data[data.Length - 2] == 0)
                                    truncatedData = new byte[data.Length - guidLength - 2];
                                else
                                    truncatedData = new byte[data.Length - guidLength];
                                Array.Copy(data, guidLength, truncatedData, 0, truncatedData.Length);
                                // base64 unencode
                                string truncatedString = Encoding.Unicode.GetString(truncatedData);
                                return Convert.FromBase64String(truncatedString);
                            }
                        }

                    }
                } 

             }

然后保存附件:

 public static void SaveAttachmentToFile(string file, string folderid, string fileName)
        {
            byte[] data = GetAttachment(file, folderid);
            if (data == null)
                throw new ArgumentNullException("Attachment has no data, it may have been deleted");
            using (FileStream writer = new FileStream(fileName, FileMode.Create))
            {
                writer.Write(data, 0, data.Length);
            }
        }

SQL CLR功能

   [SqlFunction(IsDeterministic = true,
                     IsPrecise = true,
                     DataAccess = DataAccessKind.Read,
                     SystemDataAccess = SystemDataAccessKind.Read)]
    public static SqlString WriteToFile(SqlString path, SqlString folderid, SqlString fileName)
    {
        try
        {
            if (!path.IsNull && !folderid.IsNull && !fileName.IsNull)
            {
                var dir = Path.GetDirectoryName(path.Value);
                if (!Directory.Exists(dir))
                    Directory.CreateDirectory(dir);
                string filename = Convert.ToString(fileName);
                string folderid = Convert.ToString(efolderid);
                string filepath = Convert.ToString(path);
                SaveAttachmentToFile(filename, folderid, filepath);
                return "Wrote file";
            }
            else
                return "No data passed to method!";
        }
        catch (IOException e)
        {
            return "Make sure the assembly has external access!\n" + e.ToString();
        }
        catch (Exception ex)
        {
            return ex.ToString();
        }
    }

注意,上面的所有C#代码都被编译成一个程序集,然后用作CLR函数:

CREATE FUNCTION [dbo].[WriteToFile](@path [nvarchar](max), @efolderid [nvarchar](max), @filename [nvarchar](max))
RETURNS [nvarchar](max) WITH EXECUTE AS CALLER
AS 
EXTERNAL NAME [ClassLibrary1].[CLR.UserDefinedFunctions].[WriteToFile]
GO

我认为我的问题可能与编码有关。我以为我可以使用Encoding.MIME.GetString,但它不存在。我也试过了UTF-8,但获得了0%的成功率。 Unicode似乎有效,但如上所述,成功率约为20%。

我的问题是,为什么其中一些无法保存(不正确的base64字符..但为什么?)但其他人工作正常?如何确定要使用的正确编码?有一种模式,但我真的不知道如何从这里开始。

1 个答案:

答案 0 :(得分:1)

给定的数据插入方法不明确; “unicode”实际上不是文本编码;它是将符号表示为数字的一般系统。 .Net框架确实有一个名为“Unicode”的编码,但这是一个误称,这个编码实际上是UTF-16。

现在,您提到的数据有两种格式;一个有效,一个无效。这两种格式之间的区别在于其中一种格式在每个数据字节之间有00个字节。这对应于UTF-16-LE,其中所有符号都是16位,也就是2个字节,其中最低部分存储在第一个字节中。没有那些00字节的压缩数据应该是纯ASCII。

这种UTF-16格式实际上是一种非常愚蠢的保存Base64数据的方式,因为根据定义,Base64 总是纯7位ascii;永远不会使用这些额外的字节,只需将保存该数据所需的空间加倍。事实上,当保存为字节时,Base64编码也没有任何优点,因为Base64的目的是将二进制数据转换为纯文本,因此它可以由无法处理存储/传输二进制数据的系统处理。鉴于此Base64文本随后在数据库中保存为二进制LOB,这显然不是这种情况。

除此之外,00字节在这里为您的问题提供解决方案:正如我所说,对于Base64内容,这些中间字节将永远不会被使用,这意味着它们将始终00。另一方面,Base64始终是纯ASCII文本,从不包含00字节。这意味着您可以检查那些00字节并使用它们的存在来选择正确的编码。

请注意,在将字节转换为字符串之后切断GUID 要简单得多,因为它将始终具有38的长度,而不是ASCII中的38字节或UTF-16中的76个字节。

将第一个代码块的阅读器部分改为此应解决问题:

using (SqlDataReader reader = selectAttachment.ExecuteReader())
{
    // only reading one anyway; doesn't need to be a 'while'.
    if (!reader.Read())
        return new byte[0];
    if (reader[0] == System.DBNull.Value)
        return new byte[0];
    byte[] data = (byte[])reader[0];
    if (data.Length == 0)
        return new byte[0];
    String base64String
    if (data.Length > 1 && data[1] == 00)
        base64String = Encoding.Unicode.GetString(data);
    else
        base64String = Encoding.ASCII.GetString(data);
    // Cuts off the GUID, and takes care of any trailing 00 bytes.
    String truncatedString = base64String.Substring(38).TrimEnd('\0');
    return Convert.FromBase64String(truncatedString);
}