Skip to content

Commit

Permalink
Merge pull request #2 from nao1215/nchika/imple-encode-decode
Browse files Browse the repository at this point in the history
Implement encode/decode
  • Loading branch information
nao1215 authored Jan 15, 2025
2 parents 6b606d1 + e5c8ad1 commit f4c2fac
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 17 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ permissions:
contents: read

jobs:
build:
test:
runs-on: ${{ matrix.os }}

strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
ghc: ["9.8", "9.6"]
ghc: ['9.12', '9.10', '9.8', '9.6', '9.4', '9.2', '9.0']

steps:
- name: Check out repository
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ and this project adheres to the

## Unreleased

## 0.1.0.0 - YYYY-MM-DD
## 0.1.0.0 - 2025-01-15
- First release of `clockwork-base32` library.
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Contributing as a Developer

- When creating a bug report: Please follow the template and provide detailed information.
- When fixing a feature: Create a Pull Request (PR) with accompanying test code.
- When adding a feature: First, propose the feature in an Issue.

## Contributing Outside of Coding

The following actions help boost my motivation:

- Giving a GitHub Star
- Promoting the application
- Becoming a GitHub Sponsor
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
[![Test](https://github.com/nao1215/clockwork-base32/actions/workflows/test.yml/badge.svg)](https://github.com/nao1215/clockwork-base32/actions/workflows/test.yml)

# clockwork-base32

Clockwork Base32 is a simple variant of Base32 inspired by Crockford's Base32. See [Clockwork Base32 Specification](https://gist.github.com/szktty/228f85794e4187882a77734c89c384a8).

## Supported Haskell Versions & OS
- GHC 9.0 or later
- Linux, macOS, Windows

## Usage

```haskell
-- TODO: Write usage examples here
module Main where

import ClockworkBase32 (encode, decode)

main :: IO ()
main = do
-- encode example
let originalString = "foobar"
putStrLn $ "Original string: " ++ originalString
let encodedString = encode originalString
putStrLn $ "Encoded (Base32): " ++ encodedString

-- decode example
let decodedResult = decode encodedString
case decodedResult of
Right decodedString -> putStrLn $ "Decoded string: " ++ decodedString
Left err -> putStrLn $ "Error decoding: " ++ err
```

## Other Implementations
Expand All @@ -27,3 +49,20 @@ Clockwork Base32 is a simple variant of Base32 inspired by Crockford's Base32. S
- Rust: [hnakamur/rs-clockwork-base32](https://github.com/hnakamur/rs-clockwork-base32)
- Go: [shogo82148/go-clockwork-base32](https://github.com/shogo82148/go-clockwork-base32)
- TypeScript: [niyari/base32-ts](https://github.com/niyari/base32-ts)


## Contributing

First off, thanks for taking the time to contribute! ❤️ See [CONTRIBUTING.md](./CONTRIBUTING.md) for more information.
Contributions are not only related to development. For example, GitHub Star motivates me to develop!

## Contact

If you would like to send comments such as "find a bug" or "request for additional features" to the developer, please use one of the following contacts.

- [GitHub Issue](https://github.com/nao1215/clockwork-base32/issues)


## LICENSE

[MIT License](./LICENSE)
2 changes: 1 addition & 1 deletion clockwork-base32.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ license-file: LICENSE
author: Naohiro CHIKAMATSU
maintainer: [email protected]
copyright: 2025 Naohiro CHIKAMATSU
category: Web
category: Base32
build-type: Simple
extra-source-files: README.md
CHANGELOG.md
Expand Down
90 changes: 85 additions & 5 deletions src/ClockworkBase32.hs
Original file line number Diff line number Diff line change
@@ -1,8 +1,88 @@
module ClockworkBase32
( maxInList
( encode,
decode
) where

-- returns the maximum element in a list, if it exists
maxInList :: (Ord a) => [a] -> Maybe a
maxInList [] = Nothing
maxInList xs = Just (maximum xs)
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import Data.Word (Word8)

-- clockwork base32 symbols
base32Symbols :: String
base32Symbols = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"

-- | Encode a string into Clockwork Base32 format.
-- This function encodes a given input string into the Clockwork Base32 encoding
-- by processing it in chunks of 5 bytes. The input is converted into a sequence
-- of Base32 symbols using the Clockwork Base32 alphabet.
--
-- For example:
-- encode "foobar" => "CSQPYRK1E8"
encode :: String -> String
encode input = processChunks inputBytes
where
inputBytes = BS.unpack $ BSC.pack input
processChunks [] = ""
processChunks bytes = encodeChunk (take 5 bytes) ++ processChunks (drop 5 bytes)

-- | Encode a chunk of 5 bytes into Base32 symbols.
-- This function takes a chunk of 1 to 5 bytes and converts it into the corresponding
-- Base32 symbols. If the chunk is smaller than 5 bytes, the number of output symbols
-- will be adjusted accordingly.
--
-- For example:
-- encodeChunk [102] => "CR"
encodeChunk :: [Word8] -> String
encodeChunk chunk = take outputLength $ map (base32Symbols !!) indices
where
-- 5 bytes -> 40 bits
val :: Integer
val = foldl (\acc (b, i) -> acc .|. (fromIntegral b `shiftL` (32 - 8 * i))) 0 (zip chunk [0..])
-- split 40 bits into 8 5-bit indices
indices = [fromIntegral ((val `shiftR` (35 - 5 * i)) .&. 0x1F) | i <- [0..7]]
-- determine the number of symbols to output based on the input size
outputLength = case length chunk of
1 -> 2
2 -> 4
3 -> 5
4 -> 7
5 -> 8
_ -> 0

-- | Decode a Clockwork Base32 encoded string into the original string.
decode :: String -> Either String String
decode input = case mapM decodeChar input of
Left err -> Left err
Right symbols -> processChunks symbols []
where
-- convert a Base32 symbol into a value
decodeChar :: Char -> Either String Word8
decodeChar c = case lookup c decodeMap of
Just val -> Right val
Nothing -> Left ("Invalid character: " ++ [c])

-- process the input symbols in chunks of 8 characters
processChunks :: [Word8] -> [Word8] -> Either String String
processChunks [] acc = Right (BSC.unpack (BS.pack (reverse acc)))
processChunks symbols acc = case decodeChunk (take 8 symbols) of
[] -> processChunks (drop 8 symbols) acc
chunk -> processChunks (drop 8 symbols) (reverse chunk ++ acc)

-- decode a chunk of 8 Base32 symbols into 5 bytes
decodeChunk :: [Word8] -> [Word8]
decodeChunk chunk = [fromIntegral byte | byte <- [v1, v2, v3, v4, v5], byte /= 0]
where
-- 8 symbols -> 40 bits
val :: Integer
val = foldl (\acc (b, i) -> acc .|. (fromIntegral b `shiftL` (35 - 5 * i))) 0 (zip chunk [0..])
-- split 40 bits into 5 8-bit values
v1 = (val `shiftR` 32) .&. 0xFF
v2 = (val `shiftR` 24) .&. 0xFF
v3 = (val `shiftR` 16) .&. 0xFF
v4 = (val `shiftR` 8) .&. 0xFF
v5 = val .&. 0xFF

-- create a map for decoding Base32 symbols
decodeMap :: [(Char, Word8)]
decodeMap = zip base32Symbols [0..]
80 changes: 73 additions & 7 deletions test/Spec.hs
Original file line number Diff line number Diff line change
@@ -1,14 +1,80 @@
module Main (main) where

import ClockworkBase32 (maxInList)
import ClockworkBase32 (decode, encode)
import Test.Hspec


main :: IO ()
main = hspec $ do
describe "maxInList" $ do
it "returns Nothing for an empty list" $ do
maxInList ([] :: [Int]) `shouldBe` Nothing
-- testcase from
-- https://github.com/szktty/go-clockwork-base32/blob/c2cac4daa7ad2045089b943b377b12ac57e3254e/base32_test.go#L36-L44
-- https://github.com/shiguredo/erlang-base32/blob/0cc88a702ce1d8ca345e516a05a9a85f7f23a718/test/base32_clockwork_test.erl#L7-L18
describe "encode" $ do
it "returns '' when input value is ''" $ do
encode "" `shouldBe` ""

it "returns 'CR' when input value is 'f'" $ do
encode "f" `shouldBe` "CR"

it "returns 'CSQG' when input value is 'fo'" $ do
encode "fo" `shouldBe` "CSQG"

it "returns 'CSQPY' when input value is 'foo'" $ do
encode "foo" `shouldBe` "CSQPY"

it "returns 'CSQPYRG' when input value is 'foob'" $ do
encode "foob" `shouldBe` "CSQPYRG"

it "returns 'CSQPYRK1' when input value is 'fooba'" $ do
encode "fooba" `shouldBe` "CSQPYRK1"

it "returns 'CSQPYRK1E8' when input value is 'foobar'" $ do
encode "foobar" `shouldBe` "CSQPYRK1E8"

it "returns '91JPRV3F5GG7EVVJDHJ22' when input value is 'Hello, world!'" $ do
encode "Hello, world!" `shouldBe` "91JPRV3F5GG7EVVJDHJ22"

it "returns 'AHM6A83HENMP6TS0C9S6YXVE41K6YY10D9TPTW3K41QQCSBJ41T6GS90DHGQMY90CHQPEBG' when input value is 'The quick brown fox jumps over the lazy dog.'" $ do
encode "The quick brown fox jumps over the lazy dog." `shouldBe` "AHM6A83HENMP6TS0C9S6YXVE41K6YY10D9TPTW3K41QQCSBJ41T6GS90DHGQMY90CHQPEBG"

it "returns '07EKWRQY2N7DEAVD5MJ3JX36KM' when input value is '\x01\xdd\x3e\x62\xfe\x15\x4e\xd7\x2b\x6d\x2d\x24\x39\x74\x66\x9d'" $ do
encode "\x01\xdd\x3e\x62\xfe\x15\x4e\xd7\x2b\x6d\x2d\x24\x39\x74\x66\x9d" `shouldBe` "07EKWRQY2N7DEAVD5MJ3JX36KM"

it "returns 'AXQQEB10D5T20WK5C5P6RY90EXQQ4TVK44' when input value is 'Wow, it really works!'" $ do
encode "Wow, it really works!" `shouldBe` "AXQQEB10D5T20WK5C5P6RY90EXQQ4TVK44"

describe "decode" $ do
it "returns Right '' when input value is ''" $ do
decode "" `shouldBe` Right ""

it "returns Right 'f' when input value is 'CR'" $ do
decode "CR" `shouldBe` Right "f"

it "returns Right 'fo' when input value is 'CSQG'" $ do
decode "CSQG" `shouldBe` Right "fo"

it "returns Right 'foo' when input value is 'CSQPY'" $ do
decode "CSQPY" `shouldBe` Right "foo"

it "returns Right 'foob' when input value is 'CSQPYRG'" $ do
decode "CSQPYRG" `shouldBe` Right "foob"

it "returns Right 'fooba' when input value is 'CSQPYRK1'" $ do
decode "CSQPYRK1" `shouldBe` Right "fooba"

it "returns Right 'foobar' when input value is 'CSQPYRK1E8'" $ do
decode "CSQPYRK1E8" `shouldBe` Right "foobar"

it "returns Right 'Hello, world!' when input value is '91JPRV3F5GG7EVVJDHJ22'" $ do
decode "91JPRV3F5GG7EVVJDHJ22" `shouldBe` Right "Hello, world!"

it "returns Right 'The quick brown fox jumps over the lazy dog.' when input value is 'AHM6A83HENMP6TS0C9S6YXVE41K6YY10D9TPTW3K41QQCSBJ41T6GS90DHGQMY90CHQPEBG'" $ do
decode "AHM6A83HENMP6TS0C9S6YXVE41K6YY10D9TPTW3K41QQCSBJ41T6GS90DHGQMY90CHQPEBG" `shouldBe` Right "The quick brown fox jumps over the lazy dog."

it "returns Right '\x01\xdd\x3e\x62\xfe\x15\x4e\xd7\x2b\x6d\x2d\x24\x39\x74\x66\x9d' when input value is '07EKWRQY2N7DEAVD5MJ3JX36KM'" $ do
decode "07EKWRQY2N7DEAVD5MJ3JX36KM" `shouldBe` Right "\x01\xdd\x3e\x62\xfe\x15\x4e\xd7\x2b\x6d\x2d\x24\x39\x74\x66\x9d"

it "returns Right 'Wow, it really works!' when input value is 'AXQQEB10D5T20WK5C5P6RY90EXQQ4TVK44'" $ do
decode "AXQQEB10D5T20WK5C5P6RY90EXQQ4TVK44" `shouldBe` Right "Wow, it really works!"

it "returns the maximum element for a non-empty list" $ do
maxInList ([1, 2, 3] :: [Int]) `shouldBe` Just 3
it "returns Left 'Invalid character: ~' when input value is '~'" $ do
decode "~" `shouldBe` Left "Invalid character: ~"

0 comments on commit f4c2fac

Please sign in to comment.