Skip to content

Commit 6d154cc

Browse files
authored
Merge pull request #6326 from Jiloc/feat/move-clarity-serialization-to-root
feat: enable `clarity-serialization` in cargo workspace
2 parents 799953e + e3bdb4c commit 6d154cc

File tree

19 files changed

+247
-1417
lines changed

19 files changed

+247
-1417
lines changed

.cargo/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[alias]
22
stacks-node = "run --package stacks-node --"
33
fmt-stacks = "fmt -- --config group_imports=StdExternalCrate,imports_granularity=Module"
4-
clippy-stacks = "clippy -p stx-genesis -p libstackerdb -p stacks-signer -p pox-locking -p clarity -p libsigner -p stacks-common --no-deps --tests --all-features -- -D warnings"
4+
clippy-stacks = "clippy -p stx-genesis -p libstackerdb -p stacks-signer -p pox-locking -p clarity-serialization -p clarity -p libsigner -p stacks-common --no-deps --tests --all-features -- -D warnings"
55
clippy-stackslib = "clippy -p stackslib --no-deps -- -Aclippy::all -Wclippy::indexing_slicing"
66

77
# Uncomment to improve performance slightly, at the cost of portability

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ members = [
44
"stackslib",
55
"stacks-common",
66
"pox-locking",
7+
"clarity-serialization",
78
"clarity",
89
"stx-genesis",
910
"libstackerdb",
@@ -12,8 +13,6 @@ members = [
1213
"stacks-node",
1314
"contrib/tools/config-docs-generator"]
1415

15-
exclude = ["contrib/clarity-serialization"]
16-
1716
# Dependencies we want to keep the same between workspace members
1817
[workspace.dependencies]
1918
ed25519-dalek = { version = "2.1.1", default-features = false }

contrib/clarity-serialization/Cargo.toml renamed to clarity-serialization/Cargo.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ keywords = [ "stacks", "stx", "bitcoin", "crypto", "blockstack", "decentralized"
1010
readme = "README.md"
1111

1212
[dependencies]
13-
lazy_static = "1.4.0"
13+
lazy_static = { workspace = true }
1414
regex = { version = "1", default-features = false }
15-
serde = { version = "1", features = ["derive"] }
16-
serde_derive = { version = "1" }
17-
slog = { version = "2.5.2", features = [ "max_level_trace" ] }
18-
stacks_common = { package = "stacks-common", path = "../../stacks-common", default-features = false }
19-
thiserror = { version = "1.0.65" }
15+
serde = { workspace = true }
16+
serde_derive = { workspace = true }
17+
slog = { workspace = true }
18+
stacks_common = { package = "stacks-common", path = "../stacks-common", default-features = false }
19+
thiserror = { workspace = true }
2020

2121
[dev-dependencies]
2222
mutants = "0.0.3"
23-
test-case = { version = "3.3.1", default-features = false }
23+
rstest = "0.17.0"
2424

2525
[features]
2626
default = []
File renamed without changes.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright (C) 2025 Stacks Open Internet Foundation
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
use rstest::rstest;
17+
18+
use crate::errors::CodecError;
19+
use crate::representations::{
20+
CONTRACT_MAX_NAME_LENGTH, CONTRACT_MIN_NAME_LENGTH, ClarityName, ContractName, MAX_STRING_LEN,
21+
};
22+
use crate::stacks_common::codec::StacksMessageCodec;
23+
24+
#[rstest]
25+
#[case::valid_name("hello")]
26+
#[case::dash("hello-dash")]
27+
#[case::underscore("hello_underscore")]
28+
#[case::numbers("test123")]
29+
#[case::single_letter("a")]
30+
#[case::exclamation_mark("set-token-uri!")]
31+
#[case::question_mark("is-owner?")]
32+
#[case::plus("math+")]
33+
#[case::less_than("greater-than<")]
34+
#[case::greater_than("less-than>")]
35+
#[case::less_than_or_equal_to("<=")]
36+
#[case::greater_than_or_equal_to(">=")]
37+
#[case::asterisk("*")]
38+
#[case::slash("/")]
39+
#[case::dash_only("-")]
40+
#[case::equals("=")]
41+
fn test_clarity_name_valid(#[case] name: &str) {
42+
let clarity_name = ClarityName::try_from(name.to_string())
43+
.unwrap_or_else(|_| panic!("Should parse valid clarity name: {name}"));
44+
assert_eq!(clarity_name.as_str(), name);
45+
}
46+
47+
#[rstest]
48+
#[case::empty("")]
49+
#[case::starts_with_number("123abc")]
50+
#[case::contains_space("hello world")]
51+
#[case::contains_at("hello@world")]
52+
#[case::contains_hash("hello#world")]
53+
#[case::contains_dollar("hello$world")]
54+
#[case::contains_percent("hello%world")]
55+
#[case::contains_ampersand("hello&world")]
56+
#[case::contains_dot("hello.world")]
57+
#[case::contains_comma("hello,world")]
58+
#[case::contains_semicolon("hello;world")]
59+
#[case::contains_colon("hello:world")]
60+
#[case::contains_pipe("hello|world")]
61+
#[case::contains_backslash("hello\\world")]
62+
#[case::contains_quote("hello\"world")]
63+
#[case::contains_apostrophe("hello'world")]
64+
#[case::contains_bracket_open("hello[world")]
65+
#[case::contains_bracket_close("hello]world")]
66+
#[case::contains_curly_open("hello{world")]
67+
#[case::contains_curly_close("hello}world")]
68+
#[case::contains_parenthesis_open("hello(world")]
69+
#[case::contains_parenthesis_close("hello)world")]
70+
#[case::too_long(&"a".repeat(MAX_STRING_LEN as usize + 1))]
71+
fn test_clarity_name_invalid(#[case] name: &str) {
72+
let result = ClarityName::try_from(name.to_string());
73+
assert!(result.is_err());
74+
assert!(matches!(
75+
result.unwrap_err(),
76+
CodecError::InvalidClarityName(_, _)
77+
));
78+
}
79+
80+
#[rstest]
81+
#[case("test-name")]
82+
#[case::max_length(&"a".repeat(MAX_STRING_LEN as usize))]
83+
fn test_clarity_name_serialization(#[case] name: &str) {
84+
let name = ClarityName::try_from(name.to_string()).unwrap();
85+
86+
let mut buffer = Vec::new();
87+
name.consensus_serialize(&mut buffer)
88+
.unwrap_or_else(|_| panic!("Serialization should succeed for name: {name}"));
89+
90+
// Should have length byte followed by the string bytes
91+
assert_eq!(buffer[0], name.len());
92+
assert_eq!(&buffer[1..], name.as_bytes());
93+
94+
// Test deserialization
95+
let deserialized = ClarityName::consensus_deserialize(&mut buffer.as_slice()).unwrap();
96+
assert_eq!(deserialized, name);
97+
}
98+
99+
// the first byte is the length of the buffer.
100+
#[rstest]
101+
#[case::invalid_utf8(vec![4, 0xFF, 0xFE, 0xFD, 0xFC], "Failed to parse Clarity name: could not contruct from utf8")]
102+
#[case::invalid_name(vec![2, b'2', b'i'], "Failed to parse Clarity name: InvalidClarityName(\"ClarityName\", \"2i\")")] // starts with number
103+
#[case::too_long(vec![MAX_STRING_LEN + 1], "Failed to deserialize clarity name: too long")]
104+
#[case::wrong_length(vec![3, b'a'], "failed to fill whole buffer")]
105+
fn test_clarity_name_deserialization_errors(#[case] buffer: Vec<u8>, #[case] error_message: &str) {
106+
let result = ClarityName::consensus_deserialize(&mut buffer.as_slice());
107+
assert!(result.is_err());
108+
assert_eq!(result.unwrap_err().to_string(), error_message);
109+
}
110+
111+
#[rstest]
112+
#[case::valid_name("hello")]
113+
#[case::dash("contract-name")]
114+
#[case::underscore("hello_world")]
115+
#[case::numbers("test123")]
116+
#[case::transient("__transient")]
117+
#[case::min_length("a")]
118+
#[case::max_length(&"a".repeat(CONTRACT_MAX_NAME_LENGTH))]
119+
#[case::max_string_len(&"a".repeat(MAX_STRING_LEN as usize))]
120+
fn test_contract_name_valid(#[case] name: &str) {
121+
let contract_name = ContractName::try_from(name.to_string())
122+
.unwrap_or_else(|_| panic!("Should parse valid contract name: {name}"));
123+
assert_eq!(contract_name.as_str(), name);
124+
}
125+
126+
#[rstest]
127+
#[case::empty("")]
128+
#[case::starts_with_number("123contract")]
129+
#[case::contains_space("hello world")]
130+
#[case::contains_at("hello@world")]
131+
#[case::contains_dot("hello.world")]
132+
#[case::contains_exclamation("hello!world")]
133+
#[case::contains_question("hello?world")]
134+
#[case::contains_plus("hello+world")]
135+
#[case::contains_asterisk("hello*world")]
136+
#[case::contains_equals("hello=world")]
137+
#[case::contains_slash("hello/world")]
138+
#[case::contains_less_than("hello<world")]
139+
#[case::contains_greater_than("hello>world")]
140+
#[case::contains_comma("hello,world")]
141+
#[case::contains_semicolon("hello;world")]
142+
#[case::contains_colon("hello:world")]
143+
#[case::contains_pipe("hello|world")]
144+
#[case::contains_backslash("hello\\world")]
145+
#[case::contains_quote("hello\"world")]
146+
#[case::contains_apostrophe("hello'world")]
147+
#[case::contains_bracket_open("hello[world")]
148+
#[case::contains_bracket_close("hello]world")]
149+
#[case::contains_curly_open("hello{world")]
150+
#[case::contains_curly_close("hello}world")]
151+
#[case::contains_parenthesis_open("hello(world")]
152+
#[case::contains_parenthesis_close("hello)world")]
153+
#[case::too_short(&"a".repeat(CONTRACT_MIN_NAME_LENGTH - 1))]
154+
#[case::too_long(&"a".repeat(MAX_STRING_LEN as usize + 1))]
155+
fn test_contract_name_invalid(#[case] name: &str) {
156+
let result = ContractName::try_from(name.to_string());
157+
assert!(result.is_err());
158+
assert!(matches!(
159+
result.unwrap_err(),
160+
CodecError::InvalidContractName(_, _)
161+
));
162+
}
163+
164+
#[rstest]
165+
#[case::valid_name("test-contract")]
166+
#[case::dash("contract-name")]
167+
#[case::underscore("hello_world")]
168+
#[case::numbers("test123")]
169+
#[case::transient("__transient")]
170+
#[case::min_length("a")]
171+
#[case::max_length(&"a".repeat(CONTRACT_MAX_NAME_LENGTH))]
172+
fn test_contract_name_serialization(#[case] name: &str) {
173+
let name = ContractName::try_from(name.to_string()).unwrap();
174+
let mut buffer = Vec::with_capacity((name.len() + 1) as usize);
175+
name.consensus_serialize(&mut buffer)
176+
.unwrap_or_else(|_| panic!("Serialization should succeed for name: {name}"));
177+
assert_eq!(buffer[0], name.len());
178+
assert_eq!(&buffer[1..], name.as_bytes());
179+
180+
// Test deserialization
181+
let deserialized = ContractName::consensus_deserialize(&mut buffer.as_slice()).unwrap();
182+
assert_eq!(deserialized, name);
183+
}
184+
185+
#[test]
186+
fn test_contract_name_serialization_too_long() {
187+
let name =
188+
ContractName::try_from("a".repeat(CONTRACT_MAX_NAME_LENGTH + 1)).expect("should parse");
189+
let mut buffer = Vec::with_capacity((name.len() + 1) as usize);
190+
let result = name.consensus_serialize(&mut buffer);
191+
assert!(result.is_err());
192+
assert_eq!(
193+
result.unwrap_err().to_string(),
194+
format!(
195+
"Failed to serialize contract name: too short or too long: {}",
196+
name.len()
197+
)
198+
);
199+
}
200+
201+
// the first byte is the length of the buffer.
202+
#[rstest]
203+
#[case::invalid_utf8(vec![4, 0xFF, 0xFE, 0xFD, 0xFC], "Failed to parse Contract name: could not construct from utf8")]
204+
#[case::invalid_name(vec![2, b'2', b'i'], "Failed to parse Contract name: InvalidContractName(\"ContractName\", \"2i\")")] // starts with number
205+
#[case::too_long(vec![MAX_STRING_LEN + 1], &format!("Failed to deserialize contract name: too short or too long: {}", MAX_STRING_LEN + 1))]
206+
#[case::wrong_length(vec![3, b'a'], "failed to fill whole buffer")]
207+
fn test_contract_name_deserialization_errors(#[case] buffer: Vec<u8>, #[case] error_message: &str) {
208+
let result = ContractName::consensus_deserialize(&mut buffer.as_slice());
209+
assert!(result.is_err());
210+
assert_eq!(result.unwrap_err().to_string(), error_message);
211+
}

0 commit comments

Comments
 (0)