【问题标题】:Base-N encoding of a byte array字节数组的 Base-N 编码
【发布时间】:2013-01-01 11:25:42
【问题描述】:

几天前,我遇到了this CodeReview,用于对字节数组进行 Base-36 编码。但是,随后的答案并没有涉及解码回字节数组,或者可能重用答案来执行不同基数(基数)的编码。

链接问题的答案使用 BigInteger。因此,就实现而言,基数及其数字可以参数化。

BigInteger 的问题在于,我们将输入视为假定的整数。然而,我们的输入,一个字节数组,只是一系列不透明的值。

  • 如果字节数组以一系列零字节结尾,例如 {0xFF,0x7F,0x00,0x00},则在答案中使用算法时这些字节将丢失(只会编码 {0xFF,0x7F}。李>
  • 如果最后一个非零字节设置了符号位,则使用前面的零字节,因为它被视为 BigInt 的符号分隔符。所以 {0xFF,0xFF,0x00,0x00} 只会编码为 {0xFF,0xFF,0x00}。

.NET 程序员如何使用 BigInteger 创建一个相当高效且与基数无关的编码器,具有解码支持,以及处理字节序的能力,以及“解决”结尾零的能力字节丢失?

【问题讨论】:

  • 最后一个容易定位。
  • 抱歉,这应该是一个自我问答问题,但我不得不在最后 8 分钟左右的时间里弄清楚答案表单认为我有不正确的代码行的地方。一直在发布问题。自我问答帖子不应该在答案也很好之前停止发布问题吗?无论如何,现在发布答案。
  • 不可逆输入:byte[] test = new byte[] {18, 231, 34, 57, 58, 64, 96, 49, 196, 21, 50, 161, 207, 202, 244, 119, 160, 52, 33, 201, 201, 164, 24, 106, 84, 44, 71, 94, 15, 209, 235, 177};
  • codereview.stackexchange.com/a/20014/20654的原因请参考这篇文章的“edit2”部分。如果你用一个额外的“0”字节填充你的字节数组,它将起作用。可悲的是,没有 BigInteger byte[] ctor 允许您告诉它假设字节导致“无符号”值。解决这个问题的唯一真正方法是检查最后一个字节的符号位,如果设置,Array.Resize() 将输入加一(这将隐式添加一个零字节)。
  • 如果您仍然对此感兴趣并且它不必在数学上普遍可移植,我建议考虑分块。我已经在 uint64 算法上实现了数字 div/mod 工作,一次转换 8 个字节(为 base62 生成 11 个字符,需要 10.75 个字符,2.3% 的开销)。不那么节省空间,但几乎,而且速度更快(没有比较,但不涉及缓慢的任意长度整数)。

标签: c# algorithm encoding bit-manipulation radix


【解决方案1】:

编辑 [2020/01/26]:FWIW,code below 及其 unit test 与我的 open source libraries on Github 并存。

edit [2016/04/19]:如果您喜欢异常,您可能希望更改一些 Decode 实现代码以抛出 InvalidDataException 而不是仅仅返回 null。

edit [2014/09/14]:我在 Encode() 中添加了一个“HACK”来处理输入中最后一个字节被签名的情况(如果你要转换为字节)。我现在能想到的唯一明智的解决方案就是将数组大小调整为一个。此案例的其他单元测试通过了,但我没有重新运行 perf 代码来解决这种情况。如果您能提供帮助,请始终让您对 Encode() 的输入在末尾包含一个虚拟 0 字节,以避免额外分配。

用法

我创建了一个 RadixEncoding 类(可在“代码”部分找到),它使用三个参数进行初始化:

  1. 作为字符串的基数(长度当然决定了实际的基数),
  2. 输入字节数组的假定字节顺序(字节序),
  3. 以及用户是否希望编码/解码逻辑确认结束零字节。

创建一个 Base-36 编码,使用 little-endian 输入,并考虑到结束零字节:

const string k_base36_digits = "0123456789abcdefghijklmnopqrstuvwxyz";
var base36_no_zeros = new RadixEncoding(k_base36_digits, EndianFormat.Little, false);

然后实际执行编码/解码:

const string k_input = "A test 1234";
byte[] input_bytes = System.Text.Encoding.UTF8.GetBytes(k_input);
string encoded_string = base36_no_zeros.Encode(input_bytes);
byte[] decoded_bytes = base36_no_zeros.Decode(encoded_string);

性能

使用 Diagnostics.Stopwatch 计时,在 i7 860 @2.80GHz 上运行。 Timing EXE 自己运行,而不是在调试器下运行。

使用与上面相同的 k_base36_digits 字符串 EndianFormat.Little 初始化编码,并且 确认结束零字节(即使 UTF8 字节没有任何额外结束零字节)

将“A test 1234”的 UTF8 字节编码 1,000,000 次需要 2.6567905 秒
解码相同的字符串需要 3.3916248 秒

对“A test 1234. Made稍大!”的UTF8字节进行编码100,000 次需要 1.1577325 秒
解码相同的字符串需要 1.244326 秒

代码

如果您没有CodeContracts generator,则必须使用 if/throw 代码重新实现合同。

using System;
using System.Collections.Generic;
using System.Numerics;
using Contract = System.Diagnostics.Contracts.Contract;

public enum EndianFormat
{
    /// <summary>Least Significant Bit order (lsb)</summary>
    /// <remarks>Right-to-Left</remarks>
    /// <see cref="BitConverter.IsLittleEndian"/>
    Little,
    /// <summary>Most Significant Bit order (msb)</summary>
    /// <remarks>Left-to-Right</remarks>
    Big,
};

/// <summary>Encodes/decodes bytes to/from a string</summary>
/// <remarks>
/// Encoded string is always in big-endian ordering
/// 
/// <p>Encode and Decode take a <b>includeProceedingZeros</b> parameter which acts as a work-around
/// for an edge case with our BigInteger implementation.
/// MSDN says BigInteger byte arrays are in LSB->MSB ordering. So a byte buffer with zeros at the 
/// end will have those zeros ignored in the resulting encoded radix string.
/// If such a loss in precision absolutely cannot occur pass true to <b>includeProceedingZeros</b>
/// and for a tiny bit of extra processing it will handle the padding of zero digits (encoding)
/// or bytes (decoding).</p>
/// <p>Note: doing this for decoding <b>may</b> add an extra byte more than what was originally 
/// given to Encode.</p>
/// </remarks>
// Based on the answers from http://codereview.stackexchange.com/questions/14084/base-36-encoding-of-a-byte-array/
public class RadixEncoding
{
    const int kByteBitCount = 8;

    readonly string kDigits;
    readonly double kBitsPerDigit;
    readonly BigInteger kRadixBig;
    readonly EndianFormat kEndian;
    readonly bool kIncludeProceedingZeros;

    /// <summary>Numerial base of this encoding</summary>
    public int Radix { get { return kDigits.Length; } }
    /// <summary>Endian ordering of bytes input to Encode and output by Decode</summary>
    public EndianFormat Endian { get { return kEndian; } }
    /// <summary>True if we want ending zero bytes to be encoded</summary>
    public bool IncludeProceedingZeros { get { return kIncludeProceedingZeros; } }

    public override string ToString()
    {
        return string.Format("Base-{0} {1}", Radix.ToString(), kDigits);
    }

    /// <summary>Create a radix encoder using the given characters as the digits in the radix</summary>
    /// <param name="digits">Digits to use for the radix-encoded string</param>
    /// <param name="bytesEndian">Endian ordering of bytes input to Encode and output by Decode</param>
    /// <param name="includeProceedingZeros">True if we want ending zero bytes to be encoded</param>
    public RadixEncoding(string digits,
        EndianFormat bytesEndian = EndianFormat.Little, bool includeProceedingZeros = false)
    {
        Contract.Requires<ArgumentNullException>(digits != null);
        int radix = digits.Length;

        kDigits = digits;
        kBitsPerDigit = System.Math.Log(radix, 2);
        kRadixBig = new BigInteger(radix);
        kEndian = bytesEndian;
        kIncludeProceedingZeros = includeProceedingZeros;
    }

    // Number of characters needed for encoding the specified number of bytes
    int EncodingCharsCount(int bytesLength)
    {
        return (int)Math.Ceiling((bytesLength * kByteBitCount) / kBitsPerDigit);
    }
    // Number of bytes needed to decoding the specified number of characters
    int DecodingBytesCount(int charsCount)
    {
        return (int)Math.Ceiling((charsCount * kBitsPerDigit) / kByteBitCount);
    }

    /// <summary>Encode a byte array into a radix-encoded string</summary>
    /// <param name="bytes">byte array to encode</param>
    /// <returns>The bytes in encoded into a radix-encoded string</returns>
    /// <remarks>If <paramref name="bytes"/> is zero length, returns an empty string</remarks>
    public string Encode(byte[] bytes)
    {
        Contract.Requires<ArgumentNullException>(bytes != null);
        Contract.Ensures(Contract.Result<string>() != null);

        // Don't really have to do this, our code will build this result (empty string),
        // but why not catch the condition before doing work?
        if (bytes.Length == 0) return string.Empty;

        // if the array ends with zeros, having the capacity set to this will help us know how much
        // 'padding' we will need to add
        int result_length = EncodingCharsCount(bytes.Length);
        // List<> has a(n in-place) Reverse method. StringBuilder doesn't. That's why.
        var result = new List<char>(result_length);

        // HACK: BigInteger uses the last byte as the 'sign' byte. If that byte's MSB is set, 
        // we need to pad the input with an extra 0 (ie, make it positive)
        if ( (bytes[bytes.Length-1] & 0x80) == 0x80 )
            Array.Resize(ref bytes, bytes.Length+1);

        var dividend = new BigInteger(bytes);
        // IsZero's computation is less complex than evaluating "dividend > 0"
        // which invokes BigInteger.CompareTo(BigInteger)
        while (!dividend.IsZero)
        {
            BigInteger remainder;
            dividend = BigInteger.DivRem(dividend, kRadixBig, out remainder);
            int digit_index = System.Math.Abs((int)remainder);
            result.Add(kDigits[digit_index]);
        }

        if (kIncludeProceedingZeros)
            for (int x = result.Count; x < result.Capacity; x++)
                result.Add(kDigits[0]); // pad with the character that represents 'zero'

        // orientate the characters in big-endian ordering
        if (kEndian == EndianFormat.Little)
            result.Reverse();
        // If we didn't end up adding padding, ToArray will end up returning a TrimExcess'd array, 
        // so nothing wasted
        return new string(result.ToArray());
    }

    void DecodeImplPadResult(ref byte[] result, int padCount)
    {
        if (padCount > 0)
        {
            int new_length = result.Length + DecodingBytesCount(padCount);
            Array.Resize(ref result, new_length); // new bytes will be zero, just the way we want it
        }
    }
    #region Decode (Little Endian)
    byte[] DecodeImpl(string chars, int startIndex = 0)
    {
        var bi = new BigInteger();
        for (int x = startIndex; x < chars.Length; x++)
        {
            int i = kDigits.IndexOf(chars[x]);
            if (i < 0) return null; // invalid character
            bi *= kRadixBig;
            bi += i;
        }

        return bi.ToByteArray();
    }
    byte[] DecodeImplWithPadding(string chars)
    {
        int pad_count = 0;
        for (int x = 0; x < chars.Length; x++, pad_count++)
            if (chars[x] != kDigits[0]) break;

        var result = DecodeImpl(chars, pad_count);
        DecodeImplPadResult(ref result, pad_count);

        return result;
    }
    #endregion
    #region Decode (Big Endian)
    byte[] DecodeImplReversed(string chars, int startIndex = 0)
    {
        var bi = new BigInteger();
        for (int x = (chars.Length-1)-startIndex; x >= 0; x--)
        {
            int i = kDigits.IndexOf(chars[x]);
            if (i < 0) return null; // invalid character
            bi *= kRadixBig;
            bi += i;
        }

        return bi.ToByteArray();
    }
    byte[] DecodeImplReversedWithPadding(string chars)
    {
        int pad_count = 0;
        for (int x = chars.Length - 1; x >= 0; x--, pad_count++)
            if (chars[x] != kDigits[0]) break;

        var result = DecodeImplReversed(chars, pad_count);
        DecodeImplPadResult(ref result, pad_count);

        return result;
    }
    #endregion
    /// <summary>Decode a radix-encoded string into a byte array</summary>
    /// <param name="radixChars">radix string</param>
    /// <returns>The decoded bytes, or null if an invalid character is encountered</returns>
    /// <remarks>
    /// If <paramref name="radixChars"/> is an empty string, returns a zero length array
    /// 
    /// Using <paramref name="IncludeProceedingZeros"/> has the potential to return a buffer with an
    /// additional zero byte that wasn't in the input. So a 4 byte buffer was encoded, this could end up
    /// returning a 5 byte buffer, with the extra byte being null.
    /// </remarks>
    public byte[] Decode(string radixChars)
    {
        Contract.Requires<ArgumentNullException>(radixChars != null);

        if (kEndian == EndianFormat.Big)
            return kIncludeProceedingZeros ? DecodeImplReversedWithPadding(radixChars) : DecodeImplReversed(radixChars);
        else
            return kIncludeProceedingZeros ? DecodeImplWithPadding(radixChars) : DecodeImpl(radixChars);
    }
};

基本单元测试

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

static bool ArraysCompareN<T>(T[] input, T[] output)
    where T : IEquatable<T>
{
    if (output.Length < input.Length) return false;
    for (int x = 0; x < input.Length; x++)
        if(!output[x].Equals(input[x])) return false;

    return true;
}
static bool RadixEncodingTest(RadixEncoding encoding, byte[] bytes)
{
    string encoded = encoding.Encode(bytes);
    byte[] decoded = encoding.Decode(encoded);

    return ArraysCompareN(bytes, decoded);
}
[TestMethod]
public void TestRadixEncoding()
{
    const string k_base36_digits = "0123456789abcdefghijklmnopqrstuvwxyz";
    var base36 = new RadixEncoding(k_base36_digits, EndianFormat.Little, true);
    var base36_no_zeros = new RadixEncoding(k_base36_digits, EndianFormat.Little, true);

    byte[] ends_with_zero_neg = { 0xFF, 0xFF, 0x00, 0x00 };
    byte[] ends_with_zero_pos = { 0xFF, 0x7F, 0x00, 0x00 };
    byte[] text = System.Text.Encoding.ASCII.GetBytes("A test 1234");

    Assert.IsTrue(RadixEncodingTest(base36, ends_with_zero_neg));
    Assert.IsTrue(RadixEncodingTest(base36, ends_with_zero_pos));
    Assert.IsTrue(RadixEncodingTest(base36_no_zeros, text));
}

【讨论】:

  • 感谢您的代码,看起来非常好。有什么方法可以避免解码器在使用“includeProceedingZeros”时生成额外的零字节?我真的很想使用你的代码,但我需要确保'x == Decode(Encode(x))'对于任何'x'都是正确的(没有添加,没有删除,即使它只是'0'字节)。
  • 我已经有一段时间没有处理这段代码的内容了,但如果你绝对需要这个要求,你可能会采用 MS 的 BigInteger 实现(MIT 许可证)并将其更改为采用参数说明BigInt 是无符号的,或者创建 ToByteArray 的重载来完成相同的操作:github.com/dotnet/corefx/blob/master/src/…
  • 感谢您回复我。好吧,您的代码已经在尝试解决填充问题。我想知道,你还记得为什么你的解决方案并不总是有效的问题吗?我找到了另一种解决填充问题的方法here。您认为该解决方案会遇到与您相同的问题吗?
  • 看了一眼,他们也在使用 BigInt,所以我想是的。 BigInt 是小端序,这意味着 byte[] 中的所有后续零都没有意义,就像 0x00112233 中的零一样。但是,它们对 Radix 编码并非毫无意义,因此可以解决。然后是我之前提到的对 BigInt 进行签名的额外解决方法,其中可以引入额外的空字节(我以为你最初问的是什么)。更多类单元测试here
  • 有一个错误。编码000000010AE1C70BC1A5FCCE845FECB09D7FE6FC 并解码回base-36,你会得到尾随零。
【解决方案2】:

有趣的是,我能够将 Kornman 的技术移植到 Java 中,并获得了包括 base36 在内的预期输出。而在运行他的时候?使用 C:\Windows\Microsoft.NET\Framework\v4.0.30319 csc 的 c# 代码,输出不符合预期。

例如,尝试使用 Kornman 的 RadixEncoding 编码为下面的字符串“hello world”对获得的 MD5 hashBytes 进行 base16 编码,我可以看到每个字符的两个字节组的字节顺序错误。

而不是 5eb63bbbe01eeed093cb22bb8f5acdc3

我看到类似 e56bb3bb0ee1....

这是在 Windows 7 上。

const string input = "hello world";

public static void Main(string[] args)
{

  using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
  {
    byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);

    byte[] hashBytes = md5.ComputeHash(inputBytes);

    // Convert the byte array to hexadecimal string
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < hashBytes.Length; i++)
    {
      sb.Append(hashBytes[i].ToString("X2"));
    }
    Console.WriteLine(sb.ToString());
  }
}

Java 代码如下,感兴趣的人可以参考。如上所述,它仅适用于基数 36。

private static final char[] BASE16_CHARS = "0123456789abcdef".toCharArray();
private static final BigInteger BIGINT_16 = BigInteger.valueOf(16);

private static final char[] BASE36_CHARS = "0123456789abcdefghijklmnopqrstuvwxyz".toCharArray();
private static final BigInteger BIGINT_36 = BigInteger.valueOf(36);

public static String toBaseX(byte[] bytes, BigInteger base, char[] chars)
{
    if (bytes == null) {
        return null;
    }

    final int bitsPerByte = 8;
    double bitsPerDigit = Math.log(chars.length) / Math.log(2);

    // Number of chars to encode specified bytes
    int size = (int) Math.ceil((bytes.length * bitsPerByte) / bitsPerDigit);

    StringBuilder sb = new StringBuilder(size);

    for (BigInteger value = new BigInteger(bytes); !value.equals(BigInteger.ZERO);) {
        BigInteger[] quotientAndRemainder = value.divideAndRemainder(base);
        sb.insert(0, chars[Math.abs(quotientAndRemainder[1].intValue())]);
        value = quotientAndRemainder[0];
    }

    return sb.toString();
}

【讨论】:

    猜你喜欢
    • 2012-01-17
    • 2011-10-04
    • 1970-01-01
    • 2010-10-27
    • 1970-01-01
    • 1970-01-01
    • 2016-12-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多