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") } } }