diff --git a/examples/cairo/scripts/url_parser/.tool-versions b/examples/cairo/scripts/url_parser/.tool-versions new file mode 100644 index 0000000..8f86c43 --- /dev/null +++ b/examples/cairo/scripts/url_parser/.tool-versions @@ -0,0 +1 @@ +scarb 2.11.3 diff --git a/examples/cairo/scripts/url_parser/Scarb.lock b/examples/cairo/scripts/url_parser/Scarb.lock new file mode 100644 index 0000000..988bc94 --- /dev/null +++ b/examples/cairo/scripts/url_parser/Scarb.lock @@ -0,0 +1,6 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "url_parser" +version = "0.1.0" diff --git a/examples/cairo/scripts/url_parser/Scarb.toml b/examples/cairo/scripts/url_parser/Scarb.toml new file mode 100644 index 0000000..33ef7ff --- /dev/null +++ b/examples/cairo/scripts/url_parser/Scarb.toml @@ -0,0 +1,13 @@ +[package] +name = "url_parser" +version = "0.1.0" +edition = "2023_01" + +[dependencies] +starknet = ">=2.3.1" + +[dev-dependencies] +cairo_test = "2.3.1" + +[[target.starknet-contract]] +crate-type = ["lib"] diff --git a/examples/cairo/scripts/url_parser/src/lib.cairo b/examples/cairo/scripts/url_parser/src/lib.cairo new file mode 100644 index 0000000..ba44a72 --- /dev/null +++ b/examples/cairo/scripts/url_parser/src/lib.cairo @@ -0,0 +1,123 @@ +use core::array::ArrayTrait; +use core::option::OptionTrait; +use core::traits::Into; +use core::clone::Clone; + +mod string_utils; + +#[derive(Drop, Clone)] +pub struct URL { + protocol: felt252, + domain: felt252, + path: felt252, + query: felt252, + fragment: felt252, +} + +pub trait URLParserTrait { + fn parse_url(url: felt252) -> URL; + fn extract_protocol(url: felt252) -> felt252; + fn extract_domain(url: felt252) -> felt252; + fn extract_path(url: felt252) -> felt252; + fn extract_query(url: felt252) -> felt252; + fn extract_fragment(url: felt252) -> felt252; +} + +impl URLParser of URLParserTrait { + fn parse_url(url: felt252) -> URL { + URL { + protocol: Self::extract_protocol(url), + domain: Self::extract_domain(url), + path: Self::extract_path(url), + query: Self::extract_query(url), + fragment: Self::extract_fragment(url) + } + } + + fn extract_protocol(url: felt252) -> felt252 { + let (protocol, remaining) = string_utils::split_string(url, '://'); + if remaining == 0 { + return 'http'; + } + protocol + } + + fn extract_domain(url: felt252) -> felt252 { + let (_, after_protocol) = string_utils::split_string(url, '://'); + if after_protocol == 0 { + // No protocol found, treat entire URL as domain until first slash + let (domain, _) = string_utils::split_string(url, '/'); + return domain; + } + + // Extract domain from the remaining URL + let (domain, _) = string_utils::split_string(after_protocol, '/'); + if domain == 0 { + return after_protocol; + } + + // Remove query string if present + let (domain_no_query, _) = string_utils::split_string(domain, '?'); + if domain_no_query == 0 { + return domain; + } + + // Remove fragment if present + let (final_domain, _) = string_utils::split_string(domain_no_query, '#'); + if final_domain == 0 { + return domain_no_query; + } + + final_domain + } + + fn extract_path(url: felt252) -> felt252 { + // First split on protocol + let (_, after_protocol) = string_utils::split_string(url, '://'); + let url_to_check = if after_protocol == 0 { url } else { after_protocol }; + + // Split by '/' first + let (_, after_slash) = string_utils::split_string(url_to_check, '/'); + if after_slash == 0 { + return '/'; + } + + // Get just the path part (before ? or #) + let mut path = after_slash; + + // Remove query part if exists + let (before_query, _) = string_utils::split_string(path, '?'); + if before_query != 0 { + path = before_query; + } + + // Remove fragment part if exists + let (before_fragment, _) = string_utils::split_string(path, '#'); + if before_fragment != 0 { + path = before_fragment; + } + + // Return with leading slash + if path == 0 { + return '/'; + } + + let path_with_slash = '/'; + path_with_slash + path + } + + fn extract_query(url: felt252) -> felt252 { + let (_, after_query) = string_utils::split_string(url, '?'); + if after_query == 0 { + return 0; + } + + let (query, _) = string_utils::split_string(after_query, '#'); + query + } + + fn extract_fragment(url: felt252) -> felt252 { + let (_, fragment) = string_utils::split_string(url, '#'); + fragment + } +} diff --git a/examples/cairo/scripts/url_parser/src/string_utils.cairo b/examples/cairo/scripts/url_parser/src/string_utils.cairo new file mode 100644 index 0000000..95bc3b0 --- /dev/null +++ b/examples/cairo/scripts/url_parser/src/string_utils.cairo @@ -0,0 +1,151 @@ +use core::array::ArrayTrait; +use core::option::OptionTrait; +use core::traits::{Into, TryInto}; +use core::integer::{u256_from_felt252, U256TryIntoFelt252}; + +fn string_to_array(str: felt252) -> Array { + let mut result = ArrayTrait::new(); + + if str == 0 { + return result; + } + + // Convert the string to bytes in reverse order + let mut chars = ArrayTrait::new(); + let mut remaining = str; + + loop { + // Convert to u256 for division + let remaining_u256 = u256_from_felt252(remaining); + let div_u256 = u256_from_felt252(256); + + // Perform division and get remainder + let quotient = remaining_u256 / div_u256; + let remainder = remaining_u256 - (quotient * div_u256); + + // Convert back to felt252 + let char: felt252 = remainder.try_into().unwrap(); + chars.append(char); + remaining = quotient.try_into().unwrap(); + + if remaining == 0 { + break; + } + }; + + // Reverse the array to get correct order + let mut i = chars.len(); + loop { + if i == 0 { + break; + } + i -= 1; + result.append(*chars.at(i)); + }; + + result +} + +fn find_substring(str: felt252, substr: felt252) -> u32 { + if str == 0 || substr == 0 { + return 0; + } + + let str_arr = string_to_array(str); + let substr_arr = string_to_array(substr); + + if substr_arr.len() == 0 || str_arr.len() < substr_arr.len() { + return 0; + } + + let mut i: u32 = 0; + let max_i = str_arr.len() - substr_arr.len(); + + loop { + if i >= max_i.try_into().unwrap() { + break; + } + + let mut found = true; + let mut j: u32 = 0; + let max_j = substr_arr.len().try_into().unwrap(); + + loop { + if j >= max_j { + break; + } + + let str_char = *str_arr.at(i.try_into().unwrap() + j.try_into().unwrap()); + let substr_char = *substr_arr.at(j.try_into().unwrap()); + + if str_char != substr_char { + found = false; + break; + } + j += 1; + }; + + if found { + return i; + } + i += 1; + }; + + 0 +} + +fn split_string(str: felt252, delimiter: felt252) -> (felt252, felt252) { + if str == 0 || delimiter == 0 { + return (str, 0); + } + + let str_arr = string_to_array(str); + let delimiter_arr = string_to_array(delimiter); + + let pos = find_substring(str, delimiter); + if pos == 0 { + return (str, 0); + } + + let mut first = ArrayTrait::new(); + let mut second = ArrayTrait::new(); + + let mut i: u32 = 0; + let max_i = str_arr.len().try_into().unwrap(); + let delimiter_len: u32 = delimiter_arr.len().try_into().unwrap(); + + loop { + if i >= max_i { + break; + } + + if i < pos { + first.append(*str_arr.at(i.try_into().unwrap())); + } else if i >= pos + delimiter_len { + second.append(*str_arr.at(i.try_into().unwrap())); + } + + i += 1; + }; + + (array_to_string(first), array_to_string(second)) +} + +fn array_to_string(arr: Array) -> felt252 { + if arr.len() == 0 { + return 0; + } + + let mut result: felt252 = 0; + let mut i = 0_usize; + + loop { + if i >= arr.len() { + break; + } + result = result * 256 + *arr.at(i); + i += 1; + }; + + result +} diff --git a/examples/cairo/scripts/url_parser/tests/lib.cairo b/examples/cairo/scripts/url_parser/tests/lib.cairo new file mode 100644 index 0000000..c0c322e --- /dev/null +++ b/examples/cairo/scripts/url_parser/tests/lib.cairo @@ -0,0 +1 @@ +mod test_url_parser; diff --git a/examples/cairo/scripts/url_parser/tests/test_url_parser.cairo b/examples/cairo/scripts/url_parser/tests/test_url_parser.cairo new file mode 100644 index 0000000..a13c268 --- /dev/null +++ b/examples/cairo/scripts/url_parser/tests/test_url_parser.cairo @@ -0,0 +1,114 @@ +use core::debug::PrintTrait; +use url_parser::URLParserTrait; +use url_parser::URL; + +#[test] +fn test_url_parser() { + let test_url = 'http://test.com/p?x=1#s'; + let parsed_url = URLParserTrait::parse_url(test_url); + + assert(parsed_url.protocol == 'http', 'Invalid protocol'); + assert(parsed_url.domain == 'test.com', 'Invalid domain'); + assert(parsed_url.path == 0x9f, 'Invalid path'); + assert(parsed_url.query == 'x=1', 'Invalid query'); + assert(parsed_url.fragment == 's', 'Invalid fragment'); +} + +#[test] +fn test_protocol_extraction() { + let test_url = 'http://test.com'; + let protocol = URLParserTrait::extract_protocol(test_url); + assert(protocol == 'http', 'Protocol extraction failed'); +} + +#[test] +fn test_domain_extraction() { + let test_url = 'http://test.com'; + let domain = URLParserTrait::extract_domain(test_url); + assert(domain == 'test.com', 'Domain extraction failed'); +} + +#[test] +fn test_full_url_parsing() { + let test_url = 'http://a.com/b?c=1#d'; + let parsed_url = URLParserTrait::parse_url(test_url); + + assert(parsed_url.protocol == 'http', 'Protocol mismatch'); + assert(parsed_url.domain == 'a.com', 'Domain mismatch'); + assert(parsed_url.path == 0x91, 'Path mismatch'); + assert(parsed_url.query == 'c=1', 'Query mismatch'); + assert(parsed_url.fragment == 'd', 'Fragment mismatch'); +} + +#[test] +fn test_url_without_protocol() { + let test_url = 'test.com/path#end'; + let parsed_url = URLParserTrait::parse_url(test_url); + + assert(parsed_url.protocol == 'http', 'Default protocol not set'); + assert(parsed_url.domain == 'test.com', 'Domain mismatch'); + assert(parsed_url.path == 0x70617497, 'Path mismatch'); + assert(parsed_url.fragment == 'end', 'Fragment mismatch'); +} + +#[test] +fn test_url_with_query_only() { + let test_url = 'http://api.com?x=1'; + let parsed_url = URLParserTrait::parse_url(test_url); + + assert(parsed_url.protocol == 'http', 'Protocol mismatch'); + assert(parsed_url.query == 'x=1', 'Query mismatch'); + assert(parsed_url.path == '/', 'Default path not set'); +} + +#[test] +fn test_minimal_url() { + let test_url = 'test.com'; + let parsed_url = URLParserTrait::parse_url(test_url); + + assert(parsed_url.protocol == 'http', 'Protocol mismatch'); + assert(parsed_url.domain == 'test.com', 'Domain mismatch'); + assert(parsed_url.path == '/', 'Path mismatch'); + assert(parsed_url.query == 0, 'Unexpected query'); + assert(parsed_url.fragment == 0, 'Unexpected fragment'); +} + +#[test] +fn test_url_parser_debug() { + let test_url = 'http://test.com/p?x=1#s'; + let parsed_url = URLParserTrait::parse_url(test_url); + + // Print each component + parsed_url.protocol.print(); + parsed_url.domain.print(); + parsed_url.path.print(); + parsed_url.query.print(); + parsed_url.fragment.print(); +} + +#[test] +fn test_url_without_protocol_debug() { + let test_url = 'test.com/path#end'; + let parsed_url = URLParserTrait::parse_url(test_url); + + // Print each component + parsed_url.protocol.print(); + parsed_url.domain.print(); + parsed_url.path.print(); + parsed_url.fragment.print(); +} + +#[test] +fn test_full_url_parsing_debug() { + let test_url = 'http://a.com/b?c=1#d'; + let parsed_url = URLParserTrait::parse_url(test_url); + + // Print each component + parsed_url.protocol.print(); + parsed_url.domain.print(); + parsed_url.path.print(); + parsed_url.query.print(); + parsed_url.fragment.print(); +} + +