Существует ли стандартная техника для упаковки двоичных данных в строку UTF-16? - PullRequest
10 голосов
/ 15 марта 2009

(в .NET) У меня есть произвольные двоичные данные, хранящиеся в байте [] (например, изображение). Теперь мне нужно сохранить эти данные в строке (поле «Комментарий» старого API). Существует ли стандартная методика для упаковки этих двоичных данных в строку ? Под «упаковкой» я подразумеваю, что для любого достаточно большого и случайного набора данных байтов. Длина / 2 примерно равна pack.Length ; потому что два байта более или менее одного символа.

Два «очевидных» ответа не соответствуют всем критериям:

string base64 = System.Convert.ToBase64String(bytes)

не очень эффективно использует строку , поскольку она использует только 64 символа из примерно 60 000 доступных (мое хранилище - System.String ). Идя с

string utf16 = System.Text.Encoding.Unicode.GetString(bytes)

лучше использует строку , но она не будет работать для данных, которые содержат недопустимые символы Unicode (скажем, несовпадающие суррогатные пары). Эта статья MSDN показывает эту точную (плохую) технику.

Давайте рассмотрим простой пример:

byte[] bytes = new byte[] { 0x41, 0x00, 0x31, 0x00};
string utf16 = System.Text.Encoding.Unicode.GetString(bytes);
byte[] utf16_bytes = System.Text.Encoding.Unicode.GetBytes(utf16);

В этом случае байтов и utf16_bytes одинаковы, поскольку оригинальные байты были строкой UTF-16. Выполнение этой же процедуры с кодировкой base64 дает 16-членный base64_bytes массив.

Теперь повторите процедуру с неверными данными UTF-16:

byte[] bytes = new byte[] { 0x41, 0x00, 0x00, 0xD8};

Вы обнаружите, что utf16_bytes не соответствуют исходным данным.

Я написал код, который использует U + FFFD в качестве экранирования перед недопустимыми символами Юникода; это работает, но я хотел бы знать, есть ли более стандартная техника, чем то, что я только что приготовил самостоятельно. Не говоря уже о том, что catch не нравится DecoderFallbackException как способ обнаружения недопустимых символов.

Полагаю, вы могли бы назвать это кодировкой "базовый BMP" или "базовый UTF-16" (используя все символы в базовой многоязычной плоскости Unicode). Да, в идеале я бы следовал совету Шона Стила и передавал бы байт [] .


Я собираюсь согласиться с предложением Питера Хауса в качестве «правильного» ответа, потому что он единственный, кто приблизился к предложению «стандартной техники».


Редактировать base16k выглядит еще лучше. Джим Беверидж имеет реализацию .

Ответы [ 7 ]

12 голосов
/ 19 марта 2009

Могу ли я предложить вам сделать использовать base64? Возможно, это не самый эффективный способ хранения данных, но он имеет свои преимущества:

  1. Ваше беспокойство по поводу кода прошло.
  2. У вас будет меньше проблем с совместимостью с другими плеерами, если они есть.
  3. Если закодированная строка когда-либо будет рассматриваться как ASCII во время преобразования, экспорта, импорта, резервного копирования, восстановления и т. Д., У вас также не возникнет никаких проблем.
  4. Если вы когда-нибудь упадете замертво или окажетесь под шиной или чем-то еще, любой программист, который когда-либо попадет в поле комментариев, сразу узнает, что это base64, и не будет считать, что он весь зашифрован или что-то в этом роде.
5 голосов
/ 27 апреля 2009

Я наткнулся на Base16k после прочтения вашего вопроса. Не совсем стандарт, но, похоже, работает хорошо, и его было достаточно просто реализовать в C #.

3 голосов
/ 21 марта 2009

Во-первых, помните, что Unicode не означает 16 бит. Тот факт, что System.String использует UTF-16 для внутреннего использования, ни здесь, ни там. Символы Unicode являются абстрактными - они получают только битовые представления через кодировки.

Вы говорите «мое хранилище - System.String» - если это так, вы не можете говорить о битах и ​​байтах, только о символах Unicode. System.String, конечно, имеет свою собственную внутреннюю кодировку, но (теоретически) она может отличаться.

Кстати, если вы считаете, что внутреннее представление System.String слишком неэффективно для памяти для данных в кодировке Base64, почему вы не беспокоитесь о латинских / западных строках?

Если вы хотите хранить двоичные данные в System.String, вам необходимо сопоставить наборы битов и символов.

Вариант A: есть готовый в форме кодировки Base64. Как вы указали, это кодирует шесть бит данных на символ.

Вариант B: если вы хотите упаковать больше битов на символ, то вам нужно создать массив (или кодировку) из 128, 256, 512 и т. Д. Символов Unicode, а также пакет 7, 8, 9 и т. Д. данных на символ. Эти символы должны быть настоящими символами Юникода.

Чтобы ответить на ваш вопрос просто, да, есть стандарт, это Base64-кодировка.

Это настоящая проблема? У вас есть данные, подтверждающие вашу идею не использовать Base64?

2 голосов
/ 15 марта 2009

Вы можете рассматривать двоичные данные как UTF-8b . Кодировка UTF-8b предполагает, что байты являются многобайтовыми последовательностями UTF-8, но имеет запасную кодировку для вещей, которые не являются.

1 голос
/ 24 февраля 2015

Вот C # версия реализации C ++ Джима Бевериджа :

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Linq;


//
// Base16k.cpp : Variant of base64 used to efficiently encode  binary into Unicode UTF16 strings. Based on work by
// Markus Scherer at https://sites.google.com/site/markusicu/unicode/base16k
//
// This code is hereby placed in the Public Domain.
// Jim Beveridge, November 29, 2011.
//
// C# port of http://qualapps.blogspot.com/2011/11/base64-for-unicode-utf16.html
// This code is hereby placed in the Public Domain.
// J. Daniel Smith, February 23, 2015
//

namespace JDanielSmith
{
    public static partial class Convert
    {
        /// <summary>
        /// Encode a binary array into a Base16k string for Unicode.
        /// </summary>
        public static string ToBase16kString(byte[] inArray)
        {
            int len = inArray.Length;

            var sb = new StringBuilder(len*6/5);
            sb.Append(len);

            int code = 0;

            for (int i=0; i<len; ++i)
            {
                byte byteValue = inArray[i];
                switch (i%7)
                {
                case 0:
                    code = byteValue<<6;
                    break;

                case 1:
                    code |= byteValue>>2;
                    code += 0x5000;
                    sb.Append(System.Convert.ToChar(code));
                    code = (byteValue&3)<<12;
                    break;

                case 2:
                    code |= byteValue<<4;
                    break;

                case 3:
                    code |= byteValue>>4;
                    code+=0x5000;
                    sb.Append(System.Convert.ToChar(code));
                    code = (byteValue&0xf)<<10;
                    break;

                case 4:
                    code |= byteValue<<2;
                    break;

                case 5:
                    code|=byteValue>>6;
                    code+=0x5000;
                    sb.Append(System.Convert.ToChar(code));
                    code=(byteValue&0x3f)<<8;
                    break;

                case 6:
                    code|=byteValue;
                    code+=0x5000;
                    sb.Append(System.Convert.ToChar(code));
                    code=0;
                    break;
                }
            }

            // emit a character for remaining bits
            if (len%7 != 0) {
                code += 0x5000;
                sb.Append(System.Convert.ToChar(code));
            }

            return sb.ToString();
        }

        /// <summary>
        ///  Decode a Base16k string for Unicode into a binary array.
        /// </summary>
        public static byte[] FromBase16kString(string s)
        {
            // read the length
            var r = new Regex(@"^\d+", RegexOptions.None, matchTimeout: TimeSpan.FromMilliseconds(100));
            Match m = r.Match(s);
            if (!m.Success)
                return null;

            int length;
            if (!Int32.TryParse(m.Value, out length))
                return null;

            var buf = new List<byte>(length);

            int pos=0;  // position in s
            while ((pos < s.Length) && (s[pos] >= '0' && s[pos] <= '9'))
                ++pos;

            // decode characters to bytes
            int i = 0;    // byte position modulo 7 (0..6 wrapping around)
            int code=0;
            byte byteValue=0;

            while (length-- > 0)
            {
                if (((1<<i)&0x2b)!=0)
                {
                    // fetch another Han character at i=0, 1, 3, 5
                    if(pos >= s.Length)
                    {
                        // Too few Han characters representing binary data.
                        System.Diagnostics.Debug.Assert(pos < s.Length);
                        return null;
                    }

                    code=s[pos++]-0x5000;
                }

                switch (i%7)
                {
                case 0:
                    byteValue = System.Convert.ToByte(code>>6);
                    buf.Add(byteValue);
                    byteValue = System.Convert.ToByte((code&0x3f)<<2);
                    break;

                case 1:
                    byteValue |= System.Convert.ToByte(code>>12);
                    buf.Add(byteValue);
                    break;

                case 2:
                    byteValue = System.Convert.ToByte((code>>4)&0xff);
                    buf.Add(byteValue);
                    byteValue = System.Convert.ToByte((code&0xf)<<4);
                    break;

                case 3:
                    byteValue |= System.Convert.ToByte(code>>10);
                    buf.Add(byteValue);
                    break;

                case 4:
                    byteValue = System.Convert.ToByte((code>>2)&0xff);
                    buf.Add(byteValue);
                    byteValue = System.Convert.ToByte((code&3)<<6);
                    break;

                case 5:
                    byteValue |= System.Convert.ToByte(code>>8);
                    buf.Add(byteValue);
                    break;

                case 6:
                    byteValue = System.Convert.ToByte(code&0xff);
                    buf.Add(byteValue);
                    break;
                }

                // advance to the next byte position
                if(++i==7)
                    i=0;
            }

            return buf.ToArray();
        }
    }
}

namespace Base16kCS
{
    class Program
    {
        static void Main(string[] args)
        {
            var drand = new Random();

            // Create 500 different binary objects, then encode and decode them.
            // The first 16 objects will have length 0,1,2 ... 16 to test boundary conditions.
            for (int loop = 0; loop < 500; ++loop)
            {
                Console.WriteLine("{0}", loop);

                int dw = drand.Next(128000);
                var org = new List<byte>(dw);
                for (int i = 0; i < dw; ++i)
                    org.Add(Convert.ToByte(drand.Next(256)));

                if (loop < 16)
                    org = org.Take(loop).ToList();

                string wstr = JDanielSmith.Convert.ToBase16kString(org.ToArray());

                byte[] bin = JDanielSmith.Convert.FromBase16kString(wstr);

                System.Diagnostics.Debug.Assert(org.SequenceEqual(bin));
            }
        }
    }
}
0 голосов
/ 19 марта 2009

Есть еще один способ обойти это ограничение: хотя я не уверен, насколько хорошо оно будет работать.

Во-первых, вам необходимо выяснить, какой тип строки ожидает вызов API - и какова структура этой строки. Если я возьму простой пример, давайте рассмотрим строку .Net:

  • Int32 _length;
  • byte [] _data;
  • байт _terminator = 0;

Добавьте перегрузку к вашему вызову API, таким образом:

[DllImport("legacy.dll")]
private static extern void MyLegacyFunction(byte[] data);

[DllImport("legacy.dll")]
private static extern void MyLegacyFunction(string comment);

Тогда, когда вам нужно вызвать байтовую версию, вы можете сделать следующее:

    public static void TheLegacyWisperer(byte[] data)
    {
        byte[] realData = new byte[data.Length + 4 /* _length */ + 1 /* _terminator */ ];
        byte[] lengthBytes = BitConverter.GetBytes(data.Length);
        Array.Copy(lengthBytes, realData, 4);
        Array.Copy(data, 0, realData, 4, data.Length);
        // realData[end] is equal to 0 in any case.
        MyLegacyFunction(realData);
    }
0 голосов
/ 19 марта 2009

Я дурачился с прямыми массивами символов, и ваш единственный неудачный случай работает с моей реализацией. Код был хорошо протестирован: сначала сделайте ваши тесты.

Вы можете ускорить это, используя небезопасный код. Но я уверен, что UnicodeEncoding так же медленно (если не медленнее).

/// <summary>
/// Represents an encoding that packs bytes tightly into a string.
/// </summary>
public class ByteEncoding : Encoding
{
    /// <summary>
    /// Gets the Byte Encoding instance.
    /// </summary>
    public static readonly Encoding Encoding = new ByteEncoding();

    private ByteEncoding()
    {
    }

    public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
    {
        for (int i = 0; i < chars.Length; i++)
        {
            // Work out some indicies.
            int j = i * 2;
            int k = byteIndex + j;

            // Get the bytes.
            byte[] packedBytes = BitConverter.GetBytes((short) chars[charIndex + i]);

            // Unpack them.
            bytes[k] = packedBytes[0];
            bytes[k + 1] = packedBytes[1];
        }

        return chars.Length * 2;
    }

    public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
    {
        for (int i = 0; i < byteCount; i += 2)
        {
            // Work out some indicies.
            int j = i / 2;
            int k = byteIndex + i;

            // Make sure we don't read too many bytes.
            byte byteB = 0;
            if (i + 1 < byteCount)
            {
                byteB = bytes[k + 1];
            }

            // Add it to the array.
            chars[charIndex + j] = (char) BitConverter.ToInt16(new byte[] { bytes[k], byteB }, 0);
        }

        return (byteCount / 2) + (byteCount % 2); // Round up.
    }

    public override int GetByteCount(char[] chars, int index, int count)
    {
        return count * 2;
    }

    public override int GetCharCount(byte[] bytes, int index, int count)
    {
        return (count / 2) + (count % 2);
    }

    public override int GetMaxByteCount(int charCount)
    {
        return charCount * 2;
    }

    public override int GetMaxCharCount(int byteCount)
    {
        return (byteCount / 2) + (byteCount % 2);
    }
}

Вот код теста:

    static void Main(string[] args)
    {
        byte[] original = new byte[256];

        // Note that we can't tell on the decode side how
        // long the array was if the original length is
        // an odd number. This will result in an
        // inconclusive result.
        for (int i = 0; i < original.Length; i++)
            original[i] = (byte) Math.Abs(i - 1);

        string packed = ByteEncoding.Encoding.GetString(original);
        byte[] unpacked = ByteEncoding.Encoding.GetBytes(packed);

        bool pass = true;

        if (original.Length != unpacked.Length)
        {
            Console.WriteLine("Inconclusive: Lengths differ.");
            pass = false;
        }

        int min = Math.Min(original.Length, unpacked.Length);
        for (int i = 0; i < min; i++)
        {
            if (original[i] != unpacked[i])
            {
                Console.WriteLine("Fail: Invalid at a position {0}.", i);
                pass = false;
            }
        }

        Console.WriteLine(pass ? "All Passed" : "Failure Present");

        Console.ReadLine();
    }

Тест работает, но вам придется протестировать его с помощью функции API.

...