Skip to content

Commit 7201bb0

Browse files
committed
Initial commit
1 parent 5ac4a02 commit 7201bb0

File tree

4 files changed

+228
-0
lines changed

4 files changed

+228
-0
lines changed

README

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Basic string utilities for Solidity, optimized for low gas usage.

StringUtils.sol

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* @title String utility functions for Solidity contracts.
3+
* @author Nick Johnson <[email protected]>
4+
*
5+
* @dev All functions are UTF-8 friendly, if input strings are valid UTF-8.
6+
* Offsets and sizes are specified in bytes, not characters, and so will
7+
* not respect UTF-8 character boundaries; be careful to only pass values
8+
* that you know are between characters.
9+
*/
10+
contract StringUtils {
11+
function readWord(bytes a, uint idx) private returns (bytes32 word) {
12+
assembly {
13+
word := mload(add(add(a, idx), 32))
14+
}
15+
}
16+
17+
/**
18+
* @dev Compares two strings, returning a negative number if a is smaller,
19+
* a positive number if a is larger, and zero if the strings are equal.
20+
* @param a The first string to compare.
21+
* @param b The second string to compare.
22+
* @return An integer whose sign indicates the value of the comparison.
23+
*/
24+
function strcmp(string a, string b) internal returns (int) {
25+
uint shortest = bytes(a).length;
26+
if (bytes(b).length < bytes(a).length)
27+
shortest = bytes(b).length;
28+
29+
for (uint idx = 0; idx < shortest; idx += 32) {
30+
var diff = int(
31+
uint(readWord(bytes(a), idx)) - uint(readWord(bytes(b), idx)));
32+
if (diff != 0)
33+
return diff;
34+
}
35+
return int(bytes(a).length - bytes(b).length);
36+
}
37+
38+
/**
39+
* @dev Finds an occurrence of a substring in a string, returning its index,
40+
* or -1 if the substring is not found.
41+
* @param haystack The string to search.
42+
* @param needle The string to look for.
43+
* @param idx The string index at which to start searching.
44+
* @return The index of the first character of the substring, or -1 if not
45+
* found.
46+
*/
47+
function strstr(string haystack, string needle, uint idx) internal
48+
returns (int)
49+
{
50+
uint needleSize = bytes(needle).length;
51+
bytes32 hash;
52+
assembly {
53+
hash := sha3(add(needle, 32), needleSize)
54+
}
55+
for(; idx <= bytes(haystack).length - needleSize; idx++) {
56+
bytes32 testHash;
57+
assembly {
58+
testHash := sha3(add(add(haystack, idx), 32), needleSize)
59+
}
60+
if (hash == testHash)
61+
return int(idx);
62+
}
63+
return -1;
64+
}
65+
66+
/**
67+
* @dev Copies part of one string into another. If the requested range
68+
* extends past the end of the source or target strings, the range will
69+
* be truncated. If src and dest are the same, the ranges must either
70+
* not overlap, or idx must be less than start.
71+
* @param dest The destination string to copy into.
72+
* @param idx The start index in the destination string.
73+
* @param src The string to copy from.
74+
* @param start The index into the source string to start copying.
75+
* @param len The number of bytes to copy.
76+
*/
77+
function strncpy(string dest, uint idx, string src, uint start, uint len)
78+
internal
79+
{
80+
if (idx + len > bytes(dest).length)
81+
len = bytes(dest).length - idx;
82+
if (start > bytes(src).length)
83+
return;
84+
if (start + len > bytes(src).length)
85+
len = bytes(src).length - start;
86+
87+
// From here, we treat idx and start as memory offsets for dest and idx.
88+
// Skip over the first word, which contains the length of each string.
89+
idx += 32;
90+
start += 32;
91+
92+
// Copy word-length chunks while possible
93+
for(; len >= 32; len -= 32) {
94+
assembly {
95+
mstore(add(dest, idx), mload(add(src, start)))
96+
}
97+
idx += 32;
98+
start += 32;
99+
}
100+
101+
// Copy remaining bytes
102+
uint mask = 256 ** (32 - len) - 1;
103+
assembly {
104+
let destaddr := add(dest, idx)
105+
let srcpart := and(mload(add(src, start)), bnot(mask))
106+
let destpart := and(mload(destaddr), mask)
107+
mstore(destaddr, or(destpart, srcpart))
108+
}
109+
}
110+
111+
/**
112+
* @dev Returns a substring starting at idx and continuing until the first
113+
* occurrence of delim. If delim is not found, returns the remainder of
114+
* the string.
115+
* @param str The string to return a substring of.
116+
* @param delim The delimiter to search for.
117+
* @param idx The start index.
118+
* @return A newly allocated string consisting of bytes between idx and the
119+
* first occurrence of delim.
120+
*/
121+
function strsep(string str, string delim, uint idx) internal
122+
returns (string ret)
123+
{
124+
int endIdx = strstr(str, delim, idx);
125+
if (endIdx == -1) {
126+
endIdx = int(bytes(str).length);
127+
}
128+
ret = new string(uint(endIdx) - idx);
129+
strncpy(ret, 0, str, idx, uint(endIdx) - idx);
130+
}
131+
132+
/**
133+
* @dev Returns the length of a string, in characters.
134+
* @param str The string to return the length of.
135+
* @return The length of the string, in characters.
136+
*/
137+
function strchrlen(string str) internal returns (uint len) {
138+
bytes memory strdata = bytes(str);
139+
for (uint i = 0; i < strdata.length; i++)
140+
// Don't count continuation bytes, of the form 0b10xxxxxx
141+
if (strdata[i] & 0xC0 != 0x80)
142+
len += 1;
143+
}
144+
}

StringUtils_test.sol

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import 'dapple/test.sol';
2+
import 'StringUtils.sol';
3+
4+
contract StringUtilsTest is Test, StringUtils {
5+
function abs(int x) returns (int) {
6+
if(x < 0)
7+
return -x;
8+
return x;
9+
}
10+
11+
function sign(int x) returns (int) {
12+
return x/abs(x);
13+
}
14+
15+
function assertEq(string a, string b) {
16+
assertEq(strcmp(a, b), 0);
17+
}
18+
19+
function testStrcmp() logs_gas {
20+
assertEq(sign(strcmp("foobie", "foobie")), 0);
21+
assertEq(sign(strcmp("foobie", "foobif")), -1);
22+
assertEq(sign(strcmp("foobie", "foobid")), 1);
23+
assertEq(sign(strcmp("foobie", "foobies")), -1);
24+
assertEq(sign(strcmp("foobie", "foobi")), 1);
25+
assertEq(sign(strcmp("foobie", "doobie")), 1);
26+
assertEq(sign(strcmp("01234567890123456789012345678901", "012345678901234567890123456789012")), -1);
27+
}
28+
29+
function testStrstr() logs_gas {
30+
assertEq(strstr("abracadabra", "bra", 0), 1);
31+
assertEq(strstr("abracadabra", "bra", 2), 8);
32+
assertEq(strstr("abracadabra", "rab", 0), -1);
33+
assertEq(strstr("ABC ABCDAB ABCDABCDABDE", "ABCDABD", 0), 15);
34+
}
35+
36+
function testStrncpy() logs_gas {
37+
string memory target = "0123456789";
38+
39+
// Basic nonoverlapping copy
40+
strncpy(target, 0, target, 5, 5);
41+
assertEq(target, "5678956789");
42+
43+
// Truncate input range
44+
strncpy(target, 0, target, 8, 5);
45+
assertEq(target, "8978956789");
46+
47+
// Truncate output range
48+
strncpy(target, 8, target, 1, 5);
49+
assertEq(target, "8978956797");
50+
51+
// Overlapping copy
52+
strncpy(target, 0, target, 2, 8);
53+
assertEq(target, "7895679797");
54+
55+
// Copy a longer string
56+
string memory longer = "0123456789012345678901234567890123456789012345";
57+
strncpy(longer, 0, longer, 1, 45);
58+
assertEq(longer, "1234567890123456789012345678901234567890123455");
59+
}
60+
61+
function testStrsep() logs_gas {
62+
assertEq(strsep("www.google.com", ".", 0), "www");
63+
assertEq(strsep("www.google.com", ".", 4), "google");
64+
assertEq(strsep("www.google.com", ".", 11), "com");
65+
assertEq(strsep("www.google.com", ".", 15), "");
66+
assertEq(strsep("foo->bar->baz", "->", 0), "foo");
67+
assertEq(strsep("foo->bar->baz", "->", 5), "bar");
68+
}
69+
70+
function testStrchrlen() logs_gas {
71+
assertEq(strchrlen(""), 0);
72+
assertEq(strchrlen("foobar"), 6);
73+
assertEq(strchrlen("I ♥ ethereum"), 12);
74+
}
75+
}

dappfile

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: 1.0.0
2+
tags: []
3+
layout:
4+
sol_sources: .
5+
build_dir: build
6+
dependencies: {}
7+
ignore: []
8+
name: ethereum-stringutils

0 commit comments

Comments
 (0)