arrow_tiberius/mssql/
identifier.rs1use crate::Result;
4
5const MAX_IDENTIFIER_CHARS: usize = 128;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[non_exhaustive]
10pub enum IdentifierPolicy {
11 BracketQuoted,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct Identifier {
19 value: String,
20}
21
22impl Identifier {
23 pub fn new(value: impl Into<String>) -> Result<Self> {
25 Self::with_policy(value, IdentifierPolicy::BracketQuoted)
26 }
27
28 pub fn with_policy(value: impl Into<String>, policy: IdentifierPolicy) -> Result<Self> {
30 let value = value.into();
31 validate_identifier(&value, policy)?;
32 Ok(Self { value })
33 }
34
35 pub fn as_str(&self) -> &str {
37 &self.value
38 }
39
40 pub fn quoted_sql(&self) -> String {
42 bracket_quote(&self.value)
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct TableName {
49 schema: Option<Identifier>,
50 table: Identifier,
51}
52
53impl TableName {
54 pub fn new(schema: impl Into<String>, table: impl Into<String>) -> Result<Self> {
56 Ok(Self {
57 schema: Some(Identifier::new(schema)?),
58 table: Identifier::new(table)?,
59 })
60 }
61
62 pub fn unqualified(table: impl Into<String>) -> Result<Self> {
64 Ok(Self {
65 schema: None,
66 table: Identifier::new(table)?,
67 })
68 }
69
70 pub fn schema(&self) -> Option<&Identifier> {
72 self.schema.as_ref()
73 }
74
75 pub fn table(&self) -> &Identifier {
77 &self.table
78 }
79
80 pub fn quoted_sql(&self) -> String {
82 match &self.schema {
83 Some(schema) => format!("{}.{}", schema.quoted_sql(), self.table.quoted_sql()),
84 None => self.table.quoted_sql(),
85 }
86 }
87}
88
89fn validate_identifier(value: &str, policy: IdentifierPolicy) -> Result<()> {
90 match policy {
91 IdentifierPolicy::BracketQuoted => {
92 if value.is_empty() {
93 return Err(crate::Error::InvalidIdentifier {
94 reason: "identifier cannot be empty".to_owned(),
95 });
96 }
97
98 if value.chars().any(char::is_control) {
99 return Err(crate::Error::InvalidIdentifier {
100 reason: "identifier cannot contain control characters".to_owned(),
101 });
102 }
103
104 let len = value.chars().count();
105 if len > MAX_IDENTIFIER_CHARS {
106 return Err(crate::Error::InvalidIdentifier {
107 reason: format!(
108 "identifier is {len} characters; maximum is {MAX_IDENTIFIER_CHARS}"
109 ),
110 });
111 }
112
113 Ok(())
114 }
115 }
116}
117
118fn bracket_quote(value: &str) -> String {
119 let escaped = value.replace(']', "]]");
120 format!("[{escaped}]")
121}
122
123#[cfg(test)]
124mod tests {
125 use super::{Identifier, TableName};
126
127 #[test]
128 fn quotes_ordinary_identifier() {
129 let ident = Identifier::new("target_table").unwrap();
130
131 assert_eq!(ident.as_str(), "target_table");
132 assert_eq!(ident.quoted_sql(), "[target_table]");
133 }
134
135 #[test]
136 fn quotes_identifier_with_spaces() {
137 let ident = Identifier::new("target table").unwrap();
138
139 assert_eq!(ident.quoted_sql(), "[target table]");
140 }
141
142 #[test]
143 fn quotes_reserved_like_identifier() {
144 let ident = Identifier::new("select").unwrap();
145
146 assert_eq!(ident.quoted_sql(), "[select]");
147 }
148
149 #[test]
150 fn treats_dot_as_literal_identifier_content() {
151 let ident = Identifier::new("dbo.target").unwrap();
152
153 assert_eq!(ident.quoted_sql(), "[dbo.target]");
154 }
155
156 #[test]
157 fn escapes_brackets() {
158 let ident = Identifier::new("a]b").unwrap();
159
160 assert_eq!(ident.quoted_sql(), "[a]]b]");
161 }
162
163 #[test]
164 fn quotes_injection_shaped_identifier_as_one_identifier() {
165 let ident = Identifier::new("dbo].[target]; DROP TABLE [prod];--").unwrap();
166
167 assert_eq!(
168 ident.quoted_sql(),
169 "[dbo]].[target]]; DROP TABLE [prod]];--]"
170 );
171 }
172
173 #[test]
174 fn accepts_exactly_128_unicode_scalar_values() {
175 let value = "表".repeat(128);
176 let ident = Identifier::new(value.clone()).unwrap();
177
178 assert_eq!(ident.as_str(), value);
179 }
180
181 #[test]
182 fn rejects_empty_identifier() {
183 let err = Identifier::new("").expect_err("empty identifiers should be rejected");
184
185 assert!(
186 err.to_string().contains("identifier cannot be empty"),
187 "unexpected error: {err}"
188 );
189 }
190
191 #[test]
192 fn rejects_control_characters() {
193 for value in ["line\nbreak", "tab\tname", "nul\0name"] {
194 let err = Identifier::new(value).expect_err("control characters should be rejected");
195
196 assert!(
197 err.to_string().contains("control characters"),
198 "unexpected error: {err}"
199 );
200 }
201 }
202
203 #[test]
204 fn rejects_over_length_identifier() {
205 let value = "x".repeat(129);
206 let err = Identifier::new(value).expect_err("over-length identifiers should be rejected");
207
208 assert!(
209 err.to_string().contains("maximum is 128"),
210 "unexpected error: {err}"
211 );
212 }
213
214 #[test]
215 fn rejects_over_length_unicode_identifier_by_character_count() {
216 let value = "表".repeat(129);
217 let err = Identifier::new(value).expect_err("over-length identifiers should be rejected");
218
219 assert!(
220 err.to_string().contains("identifier is 129 characters"),
221 "unexpected error: {err}"
222 );
223 }
224
225 #[test]
226 fn renders_schema_qualified_table_name() {
227 let table = TableName::new("dbo", "target").unwrap();
228
229 assert_eq!(table.quoted_sql(), "[dbo].[target]");
230 assert_eq!(table.schema().unwrap().as_str(), "dbo");
231 assert_eq!(table.table().as_str(), "target");
232 }
233
234 #[test]
235 fn renders_unqualified_table_name() {
236 let table = TableName::unqualified("target").unwrap();
237
238 assert_eq!(table.quoted_sql(), "[target]");
239 assert!(table.schema().is_none());
240 assert_eq!(table.table().as_str(), "target");
241 }
242
243 #[test]
244 fn table_name_does_not_split_dots_inside_parts() {
245 let table = TableName::new("dbo.part", "target.part").unwrap();
246
247 assert_eq!(table.quoted_sql(), "[dbo.part].[target.part]");
248 }
249
250 #[test]
251 fn rejects_invalid_schema_or_table_part() {
252 let err = TableName::new("", "target").expect_err("empty schema should be rejected");
253 assert!(
254 err.to_string().contains("identifier cannot be empty"),
255 "unexpected error: {err}"
256 );
257
258 let err = TableName::new("dbo", "").expect_err("empty table should be rejected");
259 assert!(
260 err.to_string().contains("identifier cannot be empty"),
261 "unexpected error: {err}"
262 );
263
264 let err = TableName::unqualified("").expect_err("empty table should be rejected");
265 assert!(
266 err.to_string().contains("identifier cannot be empty"),
267 "unexpected error: {err}"
268 );
269 }
270}