Created
December 6, 2024 19:47
-
-
Save bogdan/f6029ed94afce0b5262ad1b82f93f85e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: UNLICENSED | |
pragma solidity ^0.8.20; | |
library IpfsCid { | |
bytes internal constant BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567"; | |
/// @notice Converts a CID string to a `bytes32` hash. | |
/// @param cidString The Base32-encoded CID string (e.g., "bafy..."). | |
/// @return hash The `bytes32` representation of the multihash digest. | |
function cidToHash(string memory cidString) internal pure returns (bytes32) { | |
bytes memory cidBytes = IpfsCid.decodeBase32(bytes(IpfsCid.dropFirstByte(cidString))); | |
// CID must have at least 36 bytes: 1-byte version, 1-byte codec, 34-byte multihash | |
require(cidBytes.length >= 36, "Invalid CID length"); | |
// Validate CIDv1 and DAG-PB codec | |
require(cidBytes[0] == 0x01 && cidBytes[1] == 0x70, "Invalid CID prefix"); | |
// Validate SHA-256 multihash prefix | |
require(cidBytes[2] == 0x12 && cidBytes[3] == 0x20, "Invalid multihash prefix"); | |
// Extract and return the 32-byte digest | |
bytes32 digest; | |
assembly { | |
digest := mload(add(cidBytes, 36)) // Load 32 bytes starting from the 5th byte | |
} | |
return digest; | |
} | |
/// @dev Encodes arbitrary bytes into a Base32 string. | |
/// @param data The input bytes. | |
/// @return base32 The Base32 encoded string. | |
function toBase32(bytes memory data) internal pure returns (string memory) { | |
uint256 dataLength = data.length; | |
uint256 encodedLength = (dataLength * 8 + 4) / 5; // Base32 expands data by ~1.6x | |
bytes memory result = new bytes(encodedLength); | |
uint256 buffer; | |
uint256 bits; | |
uint256 index = 0; | |
for (uint256 i = 0; i < dataLength; i++) { | |
buffer = (buffer << 8) | uint8(data[i]); | |
bits += 8; | |
while (bits >= 5) { | |
bits -= 5; | |
result[index++] = bytes1( | |
BASE32_ALPHABET[(buffer >> bits) & 0x1F] | |
); | |
} | |
} | |
if (bits > 0) { | |
result[index++] = bytes1( | |
BASE32_ALPHABET[(buffer << (5 - bits)) & 0x1F] | |
); | |
} | |
return string(result); | |
} | |
/// @dev Decodes a Base32-encoded string into bytes. | |
/// @param input The Base32 string to decode. | |
/// @return The decoded bytes. | |
function decodeBase32(bytes memory input) internal pure returns (bytes memory) { | |
bytes memory inputBytes = bytes(input); | |
uint256 inputLength = inputBytes.length; | |
require(inputLength > 0, "Input string is empty"); | |
// Base32 expands data by 8/5, so decoded length is inputLength * 5 / 8 (rounded up) | |
uint256 decodedLength = (inputLength * 5 + 7) / 8; | |
bytes memory result = new bytes(decodedLength); | |
uint256 buffer = 0; | |
uint256 bits = 0; | |
uint256 index = 0; | |
for (uint256 i = 0; i < inputLength; i++) { | |
uint256 value = indexOf(BASE32_ALPHABET, inputBytes[i]); | |
require(value < 32, "Invalid Base32 character"); | |
buffer = (buffer << 5) | value; | |
bits += 5; | |
if (bits >= 8) { | |
bits -= 8; | |
result[index++] = bytes1(uint8(buffer >> bits)); | |
} | |
} | |
return result; | |
} | |
function dropFirstByte(string memory input) internal pure returns (string memory) { | |
return string(dropFirstByte(bytes(input))); | |
} | |
function dropFirstByte(bytes memory input) internal pure returns (bytes memory) { | |
require(input.length > 0, "Input is empty"); | |
// Create a new bytes array of length input.length - 1 | |
bytes memory result = new bytes(input.length - 1); | |
// Copy all bytes except the first one | |
for (uint256 i = 1; i < input.length; i++) { | |
result[i - 1] = input[i]; | |
} | |
return result; | |
} | |
/// @dev Finds the index of a character in the Base32 alphabet. | |
/// @param alphabet The Base32 alphabet. | |
/// @param char The character to find. | |
/// @return The index of the character in the alphabet. | |
function indexOf(bytes memory alphabet, bytes1 char) internal pure returns (uint256) { | |
for (uint256 i = 0; i < alphabet.length; i++) { | |
if (alphabet[i] == char) { | |
return i; | |
} | |
} | |
revert("Character not in Base32 alphabet"); | |
} | |
/// @notice Converts a `bytes32` hash to a IPFS CIDv1 Base32 string. | |
/// @param hash The SHA-256 hash in `bytes32` format. | |
/// @return cid The CIDv1 Base32 string. | |
function bytes32ToCid(bytes32 hash) internal pure returns (string memory) { | |
// Multihash prefix for SHA-256: 0x12 (hash function), 0x20 (length of digest = 32 bytes) | |
bytes memory multihash = abi.encodePacked(hex"1220", hash); | |
// CIDv1 prefix for dag-pb: 0x01 (CIDv1), 0x70 (codec dag-pb) | |
bytes memory cidv1 = abi.encodePacked(hex"0170", multihash); | |
// Encode the CIDv1 bytes into Base32 | |
return string.concat("b", IpfsCid.toBase32(cidv1)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment