Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement encode/decode #2

Merged
merged 2 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: ~"
Loading