409 lines
6.5 KiB
Go
409 lines
6.5 KiB
Go
package binarystr
|
|
|
|
import (
|
|
`bytes`
|
|
`errors`
|
|
`io`
|
|
`strconv`
|
|
`strings`
|
|
`unicode`
|
|
)
|
|
|
|
/*
|
|
BoolToBytes provides the inverse of [BytesToBool].
|
|
|
|
Each boolean in bools indicates a 0 (false) or 1 (true).
|
|
|
|
e.g.:
|
|
|
|
[]bool{
|
|
false, true, true, true,
|
|
false, true, false, false,
|
|
|
|
false, true, true, false,
|
|
false, true, false, true,
|
|
|
|
false, true, true, true,
|
|
false, false, true, true,
|
|
|
|
false, true, true, true,
|
|
false, true, false, false,
|
|
}
|
|
|
|
to:
|
|
|
|
[]byte{
|
|
0x74, 0x65, 0x73, 0x74,
|
|
}
|
|
|
|
If bools is nil, then b, rem, and err will also be nil and this function will no-op.
|
|
|
|
If len(bools) % 8 != 0, then err will be [ErrBadNumOct].
|
|
The remaining booleans can be found in rem.
|
|
*/
|
|
func BoolToBytes(bools []bool) (b []byte, rem []bool, err error) {
|
|
|
|
var idx int
|
|
var loc []bool
|
|
var sb *strings.Builder = new(strings.Builder)
|
|
|
|
if bools == nil {
|
|
return
|
|
}
|
|
if len(bools) == 0 {
|
|
b = make([]byte, 0)
|
|
return
|
|
}
|
|
|
|
loc = make([]bool, len(bools))
|
|
copy(loc, bools)
|
|
|
|
for idx = 0; idx < len(loc); idx += 8 {
|
|
|
|
if len(loc) < 8 {
|
|
break
|
|
}
|
|
|
|
writeBoolChunk(loc[idx:idx+8], sb)
|
|
|
|
loc = loc[idx+8:]
|
|
}
|
|
|
|
if b, err = StrToBytes(sb.String()); err != nil {
|
|
return
|
|
}
|
|
|
|
if len(loc) > 1 {
|
|
rem = append(rem, loc...)
|
|
err = ErrBadNumOct
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
BytesToStr converts bytes to a bitstring.
|
|
|
|
e.g.:
|
|
|
|
[]byte{
|
|
0x74, 0x65, 0x73, 0x74,
|
|
}
|
|
|
|
to:
|
|
|
|
"01110100011001010111001101110100"
|
|
|
|
For the inverse, see [StrToBytes].
|
|
To format the bitstring, see [FmtBinStr].
|
|
*/
|
|
func BytesToStr(b []byte) (binStr string) {
|
|
|
|
var cb byte
|
|
var sb *strings.Builder
|
|
|
|
if b == nil {
|
|
return
|
|
}
|
|
|
|
sb = new(strings.Builder)
|
|
|
|
for _, cb = range b {
|
|
// This will never actually return an error.
|
|
_, _ = sb.WriteString(
|
|
strconv.FormatUint(
|
|
uint64(uint8(cb)),
|
|
2,
|
|
),
|
|
)
|
|
}
|
|
|
|
binStr = sb.String()
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
BytesToBool returns a slice of "binary booleans".
|
|
|
|
For each bit of data in b, a 0 is false and a 1 is true (as one would expect).
|
|
|
|
For the inverse of this, see [BoolToBytes].
|
|
*/
|
|
func BytesToBool(b []byte) (bools []bool) {
|
|
|
|
var err error
|
|
var r rune
|
|
var rSz int
|
|
var idx int
|
|
var s *strings.Reader
|
|
|
|
if len(b) == 0 {
|
|
return
|
|
}
|
|
|
|
// s will always be a valid bitstring.
|
|
s = strings.NewReader(BytesToStr(b))
|
|
|
|
bools = make([]bool, s.Len())
|
|
|
|
for idx = 0; idx < s.Len(); idx++ {
|
|
r, rSz, err = s.ReadRune()
|
|
|
|
if rSz == 0 {
|
|
break
|
|
}
|
|
if err != nil {
|
|
// It'll only be an io.EOF if anything at all.
|
|
err = nil
|
|
break
|
|
}
|
|
|
|
switch r {
|
|
case '0':
|
|
bools[idx] = false
|
|
case '1':
|
|
bools[idx] = true
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
FmtBinStr formats bitstring s to more comfortable representations.
|
|
|
|
bitSep is a string to separate each bit boundary.
|
|
|
|
nybSep is a string to separate each "nibble"/"nybble" (4-bit "word") boundary.
|
|
|
|
bytSep is a string to separate each byte boundary.
|
|
|
|
Note that if you only explicitly use whitespace separators, then the output
|
|
can still be parsed back to native []byte{} data via [StrToBytes].
|
|
|
|
Note that NO checking is done against the validity of s being a bitstring;
|
|
the only content-checking done in s is if a given rune is a whitespace character or not
|
|
(whitespace present in s is skipped over, which lets you run FmtBinStr to reformat a bitstring).
|
|
*/
|
|
func FmtBinStr(s, bitSep, nybSep, bytSep string) (out string) {
|
|
|
|
var idx int
|
|
var lastIdx int
|
|
var sb *strings.Builder
|
|
|
|
if strings.TrimSpace(s) == "" {
|
|
return
|
|
}
|
|
|
|
sb = new(strings.Builder)
|
|
|
|
lastIdx = len(s) - 1
|
|
|
|
for idx = 0; idx < len(s); idx++ {
|
|
_ = sb.WriteByte(s[idx])
|
|
if idx != lastIdx {
|
|
if (idx+1)%8 == 0 {
|
|
if bytSep != "" {
|
|
_, _ = sb.WriteString(bytSep)
|
|
}
|
|
continue
|
|
} else if (idx+1)%4 == 0 {
|
|
if nybSep != "" {
|
|
_, _ = sb.WriteString(nybSep)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if bitSep != "" {
|
|
_, _ = sb.WriteString(bitSep)
|
|
}
|
|
}
|
|
|
|
out = sb.String()
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
NewDecoder returns a [Decoder] from [io.Reader] r.
|
|
|
|
Data from r is expected to be BITSTRING data (strings);
|
|
doing read operations from a [Decoder] returns PLAIN data.
|
|
*/
|
|
func NewDecoder(r io.Reader) (e *Decoder) {
|
|
|
|
e = &Decoder{
|
|
r: r,
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
NewEncoder returns an [Encoder] from [io.Reader] r.
|
|
|
|
Data from r is expected to be PLAIN data;
|
|
doing read operations from an [Encoder] returns BITSTRING data (strings).
|
|
*/
|
|
func NewEncoder(r io.Reader) (e *Encoder) {
|
|
|
|
e = &Encoder{
|
|
r: r,
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
StrToBytes converts a bitstring to bytes.
|
|
|
|
Whitespace is ignored.
|
|
|
|
e.g.:
|
|
|
|
"01110100011001010111001101110100"
|
|
|
|
to:
|
|
|
|
[]byte("test")
|
|
|
|
or:
|
|
|
|
[]byte{0x74, 0x65, 0x73, 0x74}
|
|
|
|
For the inverse, see [BytesToStr].
|
|
|
|
If the number of characters in binStr is not modulo 8 (`len() % 8 != 0`),
|
|
then err will be of type [BadNumOctErr].
|
|
|
|
If there are any characters encountered in binStr that are not "0" or "1"
|
|
(other than whitespace), then err will be of type [BadBinCharErr].
|
|
*/
|
|
func StrToBytes(binStr string) (b []byte, err error) {
|
|
|
|
var r rune
|
|
var cb byte
|
|
var rSz int
|
|
var pos uint64
|
|
var rErr error
|
|
var posR uint64
|
|
var chunk []rune
|
|
var buf *strings.Reader
|
|
var bytBuf *bytes.Buffer = new(bytes.Buffer)
|
|
|
|
defer func() {
|
|
b = bytBuf.Bytes()
|
|
}()
|
|
|
|
if strings.TrimSpace(binStr) == "" {
|
|
return
|
|
}
|
|
buf = strings.NewReader(binStr)
|
|
|
|
chunk = make([]rune, 0, 8)
|
|
for {
|
|
r, rSz, rErr = buf.ReadRune()
|
|
if rSz == 0 {
|
|
break
|
|
}
|
|
pos += uint64(rSz)
|
|
if rSz != 0 {
|
|
posR++
|
|
}
|
|
if len(chunk) == 8 {
|
|
if cb, err = chunkToByte(chunk); err != nil {
|
|
return
|
|
}
|
|
if err = bytBuf.WriteByte(cb); err != nil {
|
|
return
|
|
}
|
|
chunk = make([]rune, 0, 8)
|
|
}
|
|
if unicode.IsSpace(r) {
|
|
continue
|
|
}
|
|
if rSz != 0 {
|
|
switch r {
|
|
case '0', '1':
|
|
chunk = append(chunk, r)
|
|
default:
|
|
err = &BadBinCharErr{
|
|
posByte: pos,
|
|
posRune: posR,
|
|
r: r,
|
|
}
|
|
return
|
|
}
|
|
}
|
|
if rErr != nil {
|
|
if errors.Is(rErr, io.EOF) {
|
|
break
|
|
} else {
|
|
err = rErr
|
|
return
|
|
}
|
|
}
|
|
if rSz == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(chunk) != 0 {
|
|
if len(chunk) == 8 {
|
|
if cb, err = chunkToByte(chunk); err != nil {
|
|
return
|
|
}
|
|
if err = bytBuf.WriteByte(cb); err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
err = &BadNumOctErr{
|
|
posByte: pos,
|
|
posRune: posR,
|
|
rem: string(chunk),
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func chunkToByte(chunk []rune) (b byte, err error) {
|
|
|
|
var u8 uint64
|
|
|
|
if len(chunk) == 0 {
|
|
err = ErrNoChunk
|
|
return
|
|
}
|
|
|
|
if u8, err = strconv.ParseUint(string(chunk), 2, 8); err != nil {
|
|
return
|
|
}
|
|
|
|
b = byte(uint8(u8))
|
|
|
|
return
|
|
}
|
|
|
|
func writeBoolChunk(chunk []bool, sb io.StringWriter) {
|
|
|
|
var b bool
|
|
|
|
if len(chunk) == 0 {
|
|
return
|
|
}
|
|
|
|
for _, b = range chunk {
|
|
switch b {
|
|
case false:
|
|
sb.WriteString("0")
|
|
case true:
|
|
sb.WriteString("1")
|
|
}
|
|
}
|
|
|
|
}
|