diff --git a/heck.go b/heck.go new file mode 100644 index 0000000..5053c1e --- /dev/null +++ b/heck.go @@ -0,0 +1,348 @@ +package bin + +import ( + "strings" + "unicode" +) + +// Ported from https://github.com/withoutboats/heck +// https://github.com/withoutboats/heck/blob/master/LICENSE-APACHE +// https://github.com/withoutboats/heck/blob/master/LICENSE-MIT + +// ToPascalCase converts a string to upper camel case. +func ToPascalCase(s string) string { + return transform( + s, + capitalize, + func(*strings.Builder) {}, + ) +} + +func transform( + s string, + with_word func(string, *strings.Builder), + boundary func(*strings.Builder), +) string { + builder := new(strings.Builder) + + first_word := true + words := splitIntoWords(s) + for _, word := range words { + char_indices := newReader(word) + init := 0 + mode := _Boundary + + for char_indices.Move() { + i, c := char_indices.This() + + // Skip underscore characters + if c == '_' { + if init == i { + init += 1 + } + continue + } + + if next_i, next := char_indices.Peek(); next_i != -1 { + + // The mode including the current character, assuming the + // current character does not result in a word boundary. + next_mode := func() _WordMode { + if unicode.IsLower(c) { + return _Lowercase + } else if unicode.IsUpper(c) { + return _Uppercase + } else { + return mode + } + }() + + // Word boundary after if next is underscore or current is + // not uppercase and next is uppercase + if next == '_' || (next_mode == _Lowercase && unicode.IsUpper(next)) { + if !first_word { + // boundary(f)?; + boundary(builder) + } + { + // with_word(&word[init..next_i], f)?; + with_word(word[init:next_i], builder) + } + + first_word = false + init = next_i + mode = _Boundary + + // Otherwise if current and previous are uppercase and next + // is lowercase, word boundary before + } else if mode == _Uppercase && unicode.IsUpper(c) && unicode.IsLower(next) { + if !first_word { + // boundary(f)?; + boundary(builder) + } else { + first_word = false + } + { + // with_word(&word[init..i], f)?; + with_word(word[init:i], builder) + } + init = i + mode = _Boundary + + // Otherwise no word boundary, just update the mode + } else { + mode = next_mode + } + + } else { + // Collect trailing characters as a word + if !first_word { + // boundary(f)?; + boundary(builder) + } else { + first_word = false + } + { + // with_word(&word[init..], f)?; + with_word(word[init:], builder) + } + break + } + } + } + + return builder.String() +} + +// fn capitalize(s: &str, f: &mut fmt::Formatter) -> fmt::Result { +// let mut char_indices = s.char_indices(); +// if let Some((_, c)) = char_indices.next() { +// write!(f, "{}", c.to_uppercase())?; +// if let Some((i, _)) = char_indices.next() { +// lowercase(&s[i..], f)?; +// } +// } + +// Ok(()) +// } + +func capitalize(s string, f *strings.Builder) { + char_indices := newReader(s) + if i, c := char_indices.Next(); i != -1 { + f.WriteString(strings.ToUpper(string(c))) + if i, _ := char_indices.Next(); i != -1 { + lowercase(s[i:], f) + } + } +} + +// fn lowercase(s: &str, f: &mut fmt::Formatter) -> fmt::Result { +// let mut chars = s.chars().peekable(); +// while let Some(c) = chars.next() { +// if c == 'Σ' && chars.peek().is_none() { +// write!(f, "ς")?; +// } else { +// write!(f, "{}", c.to_lowercase())?; +// } +// } + +// Ok(()) +// } + +func lowercase(s string, f *strings.Builder) { + chars := newReader(s) + for chars.Move() { + _, c := chars.This() + if c == 'Σ' && chars.PeekNext() == 0 { + f.WriteString("ς") + } else { + f.WriteString(strings.ToLower(string(c))) + } + } +} + +func ToSnakeForSighash(s string) string { + return ToRustSnakeCase(s) +} + +type reader struct { + runes []rune + index int +} + +func splitStringByRune(s string) []rune { + var res []rune + iterateStringAsRunes(s, func(r rune) bool { + res = append(res, r) + return true + }) + return res +} + +func iterateStringAsRunes(s string, callback func(r rune) bool) { + for _, rn := range s { + doContinue := callback(rn) + if !doContinue { + return + } + } +} + +func newReader(s string) *reader { + return &reader{ + runes: splitStringByRune(s), + index: -1, + } +} + +func (r reader) This() (int, rune) { + return r.index, r.runes[r.index] +} + +func (r reader) HasNext() bool { + return r.index < len(r.runes)-1 +} + +func (r *reader) Next() (int, rune) { + if r.HasNext() { + r.index++ + return r.index, r.runes[r.index] + } + return -1, rune(0) +} + +func (r reader) Peek() (int, rune) { + if r.HasNext() { + return r.index + 1, r.runes[r.index+1] + } + return -1, rune(0) +} + +func (r reader) PeekNext() rune { + if r.HasNext() { + return r.runes[r.index+1] + } + return rune(0) +} + +func (r *reader) Move() bool { + if r.HasNext() { + r.index++ + return true + } + return false +} + +// #[cfg(not(feature = "unicode"))] +func splitIntoWords(s string) []string { + parts := strings.FieldsFunc(s, func(r rune) bool { + return !(unicode.IsLetter(r) || unicode.IsDigit(r)) + }) + return parts +} + +type _WordMode int + +const ( + /// There have been no lowercase or uppercase characters in the current + /// word. + _Boundary _WordMode = iota + /// The previous cased character in the current word is lowercase. + _Lowercase + /// The previous cased character in the current word is uppercase. + _Uppercase +) + +// ToRustSnakeCase converts the given string to a snake_case string. +// Ported from https://github.com/withoutboats/heck/blob/c501fc95db91ce20eaef248a511caec7142208b4/src/lib.rs#L75 as used by Anchor. +func ToRustSnakeCase(s string) string { + builder := new(strings.Builder) + + first_word := true + words := splitIntoWords(s) + for _, word := range words { + char_indices := newReader(word) + init := 0 + mode := _Boundary + + for char_indices.Move() { + i, c := char_indices.This() + + // Skip underscore characters + if c == '_' { + if init == i { + init += 1 + } + continue + } + + if next_i, next := char_indices.Peek(); next_i != -1 { + + // The mode including the current character, assuming the + // current character does not result in a word boundary. + next_mode := func() _WordMode { + if unicode.IsLower(c) { + return _Lowercase + } else if unicode.IsUpper(c) { + return _Uppercase + } else { + return mode + } + }() + + // Word boundary after if next is underscore or current is + // not uppercase and next is uppercase + if next == '_' || (next_mode == _Lowercase && unicode.IsUpper(next)) { + if !first_word { + // boundary(f)?; + builder.WriteRune('_') + } + { + // with_word(&word[init..next_i], f)?; + builder.WriteString(strings.ToLower(word[init:next_i])) + } + + first_word = false + init = next_i + mode = _Boundary + + // Otherwise if current and previous are uppercase and next + // is lowercase, word boundary before + } else if mode == _Uppercase && unicode.IsUpper(c) && unicode.IsLower(next) { + if !first_word { + // boundary(f)?; + builder.WriteRune('_') + } else { + first_word = false + } + { + // with_word(&word[init..i], f)?; + builder.WriteString(strings.ToLower(word[init:i])) + } + init = i + mode = _Boundary + + // Otherwise no word boundary, just update the mode + } else { + mode = next_mode + } + + } else { + // Collect trailing characters as a word + if !first_word { + // boundary(f)?; + builder.WriteRune('_') + } else { + first_word = false + } + { + // with_word(&word[init..], f)?; + builder.WriteString(strings.ToLower(word[init:])) + } + break + } + } + } + + return builder.String() +} diff --git a/heck_test.go b/heck_test.go new file mode 100644 index 0000000..96bcb89 --- /dev/null +++ b/heck_test.go @@ -0,0 +1,78 @@ +package bin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCamelCase(t *testing.T) { + type item struct { + input string + want string + } + tests := []item{ + // TODO: find out if need to fix, and if yes, then fix. + // {"1hello", "1Hello"}, // actual: `1hello` + // {"1Hello", "1Hello"}, // actual: `1hello` + // {"hello1world", "Hello1World"}, // actual: `Hello1world` + // {"mGridCol6@md", "MGridCol6md"}, // actual: `MGridCol6Md` + // {"A::a", "Aa"}, // actual: `AA` + // {"foìBar-baz", "FoìBarBaz"}, + // + {"hello1World", "Hello1World"}, + {"Hello1World", "Hello1World"}, + {"foo", "Foo"}, + {"foo-bar", "FooBar"}, + {"foo-bar-baz", "FooBarBaz"}, + {"foo--bar", "FooBar"}, + {"--foo-bar", "FooBar"}, + {"--foo--bar", "FooBar"}, + {"FOO-BAR", "FooBar"}, + {"FOÈ-BAR", "FoèBar"}, + {"-foo-bar-", "FooBar"}, + {"--foo--bar--", "FooBar"}, + {"foo-1", "Foo1"}, + {"foo.bar", "FooBar"}, + {"foo..bar", "FooBar"}, + {"..foo..bar..", "FooBar"}, + {"foo_bar", "FooBar"}, + {"__foo__bar__", "FooBar"}, + {"__foo__bar__", "FooBar"}, + {"foo bar", "FooBar"}, + {" foo bar ", "FooBar"}, + {"-", ""}, + {" - ", ""}, + {"fooBar", "FooBar"}, + {"fooBar-baz", "FooBarBaz"}, + {"fooBarBaz-bazzy", "FooBarBazBazzy"}, + {"FBBazzy", "FbBazzy"}, + {"F", "F"}, + {"FooBar", "FooBar"}, + {"Foo", "Foo"}, + {"FOO", "Foo"}, + {"--", ""}, + {"", ""}, + {"--__--_--_", ""}, + {"foo bar?", "FooBar"}, + {"foo bar!", "FooBar"}, + {"foo bar$", "FooBar"}, + {"foo-bar#", "FooBar"}, + {"XMLHttpRequest", "XmlHttpRequest"}, + {"AjaxXMLHttpRequest", "AjaxXmlHttpRequest"}, + {"Ajax-XMLHttpRequest", "AjaxXmlHttpRequest"}, + {"Hello11World", "Hello11World"}, + {"hello1", "Hello1"}, + {"Hello1", "Hello1"}, + {"h1W", "H1W"}, + // TODO: add support to non-alphanumeric characters (non-latin, non-ascii). + } + + for i := range tests { + test := tests[i] + t.Run(test.input, func(t *testing.T) { + t.Parallel() + assert.Equal(t, test.want, ToPascalCase(test.input)) + }) + } +} diff --git a/sighash.go b/sighash.go index 7902764..b920e3e 100644 --- a/sighash.go +++ b/sighash.go @@ -16,8 +16,6 @@ package bin import ( "crypto/sha256" - "strings" - "unicode" ) // Sighash creates an anchor sighash for the provided namespace and element. @@ -30,15 +28,21 @@ func Sighash(namespace string, name string) []byte { } func SighashInstruction(name string) []byte { + // Instruction sighash are the first 8 bytes of the sha256 of + // {SIGHASH_INSTRUCTION_NAMESPACE}:{snake_case(name)} return Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name)) } func SighashAccount(name string) []byte { - return Sighash(SIGHASH_ACCOUNT_NAMESPACE, ToSnakeForSighash(name)) + // Account sighash are the first 8 bytes of the sha256 of + // {SIGHASH_ACCOUNT_NAMESPACE}:{camelCase(name)} + return Sighash(SIGHASH_ACCOUNT_NAMESPACE, ToPascalCase(name)) } +// NOTE: no casing conversion is done here, it's up to the caller to +// provide the correct casing. func SighashTypeID(namespace string, name string) TypeID { - return TypeIDFromBytes(Sighash(namespace, ToSnakeForSighash(name))) + return TypeIDFromBytes(Sighash(namespace, (name))) } // Namespace for calculating state instruction sighash signatures. @@ -58,174 +62,3 @@ const ACCOUNT_DISCRIMINATOR_SIZE = 8 // TODO: // https://github.com/project-serum/anchor/blob/84a2b8200cc3c7cb51d7127918e6cbbd836f0e99/ts/src/error.ts#L48 - -func ToSnakeForSighash(s string) string { - return ToRustSnakeCase(s) -} - -type reader struct { - runes []rune - index int -} - -func splitStringByRune(s string) []rune { - var res []rune - iterateStringAsRunes(s, func(r rune) bool { - res = append(res, r) - return true - }) - return res -} - -func iterateStringAsRunes(s string, callback func(r rune) bool) { - for _, rn := range s { - doContinue := callback(rn) - if !doContinue { - return - } - } -} - -func newReader(s string) *reader { - return &reader{ - runes: splitStringByRune(s), - index: -1, - } -} - -func (r reader) This() (int, rune) { - return r.index, r.runes[r.index] -} - -func (r reader) HasNext() bool { - return r.index < len(r.runes)-1 -} - -func (r reader) Peek() (int, rune) { - if r.HasNext() { - return r.index + 1, r.runes[r.index+1] - } - return -1, rune(0) -} - -func (r *reader) Move() bool { - if r.HasNext() { - r.index++ - return true - } - return false -} - -// #[cfg(not(feature = "unicode"))] -func splitIntoWords(s string) []string { - parts := strings.FieldsFunc(s, func(r rune) bool { - return !(unicode.IsLetter(r) || unicode.IsDigit(r)) - }) - return parts -} - -type _WordMode int - -const ( - /// There have been no lowercase or uppercase characters in the current - /// word. - _Boundary _WordMode = iota - /// The previous cased character in the current word is lowercase. - _Lowercase - /// The previous cased character in the current word is uppercase. - _Uppercase -) - -// ToRustSnakeCase converts the given string to a snake_case string. -// Ported from https://github.com/withoutboats/heck/blob/c501fc95db91ce20eaef248a511caec7142208b4/src/lib.rs#L75 as used by Anchor. -func ToRustSnakeCase(s string) string { - builder := new(strings.Builder) - - first_word := true - words := splitIntoWords(s) - for _, word := range words { - char_indices := newReader(word) - init := 0 - mode := _Boundary - - for char_indices.Move() { - i, c := char_indices.This() - - // Skip underscore characters - if c == '_' { - if init == i { - init += 1 - } - continue - } - - if next_i, next := char_indices.Peek(); next_i != -1 { - - // The mode including the current character, assuming the - // current character does not result in a word boundary. - next_mode := func() _WordMode { - if unicode.IsLower(c) { - return _Lowercase - } else if unicode.IsUpper(c) { - return _Uppercase - } else { - return mode - } - }() - - // Word boundary after if next is underscore or current is - // not uppercase and next is uppercase - if next == '_' || (next_mode == _Lowercase && unicode.IsUpper(next)) { - if !first_word { - // boundary(f)?; - builder.WriteRune('_') - } - { - // with_word(&word[init..next_i], f)?; - builder.WriteString(strings.ToLower(word[init:next_i])) - } - - first_word = false - init = next_i - mode = _Boundary - - // Otherwise if current and previous are uppercase and next - // is lowercase, word boundary before - } else if mode == _Uppercase && unicode.IsUpper(c) && unicode.IsLower(next) { - if !first_word { - // boundary(f)?; - builder.WriteRune('_') - } else { - first_word = false - } - { - // with_word(&word[init..i], f)?; - builder.WriteString(strings.ToLower(word[init:i])) - } - init = i - mode = _Boundary - - // Otherwise no word boundary, just update the mode - } else { - mode = next_mode - } - - } else { - // Collect trailing characters as a word - if !first_word { - // boundary(f)?; - builder.WriteRune('_') - } else { - first_word = false - } - { - // with_word(&word[init..], f)?; - builder.WriteString(strings.ToLower(word[init:])) - } - break - } - } - } - - return builder.String() -} diff --git a/sighash_test.go b/sighash_test.go index cdade26..abab2b1 100644 --- a/sighash_test.go +++ b/sighash_test.go @@ -23,9 +23,13 @@ import ( func TestSighash(t *testing.T) { { - name := "hello" - got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name)) + ixName := "hello" + got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(ixName)) require.NotEmpty(t, got) + require.Equal(t, + got, + SighashInstruction(ixName), + ) expected := []byte{149, 118, 59, 220, 196, 127, 161, 179} require.Equal(t, @@ -34,9 +38,13 @@ func TestSighash(t *testing.T) { ) } { - name := "serumSwap" - got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name)) + ixName := "serumSwap" + got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(ixName)) require.NotEmpty(t, got) + require.Equal(t, + got, + SighashInstruction(ixName), + ) expected := []byte{88, 183, 70, 249, 214, 118, 82, 210} require.Equal(t, @@ -45,13 +53,17 @@ func TestSighash(t *testing.T) { ) require.Equal(t, expected, - SighashInstruction(name), + SighashInstruction(ixName), ) } { - name := "aldrinV2Swap" - got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name)) + ixName := "aldrinV2Swap" + got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(ixName)) require.NotEmpty(t, got) + require.Equal(t, + got, + SighashInstruction(ixName), + ) expected := []byte{190, 166, 89, 139, 33, 152, 16, 10} require.Equal(t, @@ -60,13 +72,17 @@ func TestSighash(t *testing.T) { ) require.Equal(t, expected, - SighashInstruction(name), + SighashInstruction(ixName), ) } { - name := "raydiumSwapV2" - got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name)) + ixName := "raydiumSwapV2" + got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(ixName)) require.NotEmpty(t, got) + require.Equal(t, + got, + SighashInstruction(ixName), + ) expected := []byte{69, 227, 98, 93, 237, 202, 223, 140} require.Equal(t, @@ -75,7 +91,26 @@ func TestSighash(t *testing.T) { ) require.Equal(t, expected, - SighashInstruction(name), + SighashInstruction(ixName), + ) + } + { + accountName := "DialectAccount" + got := Sighash(SIGHASH_ACCOUNT_NAMESPACE, (accountName)) + require.NotEmpty(t, got) + require.Equal(t, + got, + SighashAccount(accountName), + ) + + expected := []byte{157, 38, 120, 189, 93, 204, 119, 18} + require.Equal(t, + expected, + got, + ) + require.Equal(t, + expected, + SighashAccount(accountName), ) } } @@ -160,5 +195,4 @@ func TestToSnakeForSighash(t *testing.T) { }) } }) - }