atproto libraries implementation in ocaml
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a9bd69a4f5d5f12ed678763e7f1b250129ed71fb 152 lines 5.1 kB view raw
1(** Handle validation for AT Protocol. 2 3 Handles are domain-name-based identifiers for AT Protocol users. They follow 4 DNS hostname rules with some restrictions. 5 6 Format: <label>.<label>...<label> 7 - At least 2 labels (no "dotless" TLDs) 8 - Each label: 1-63 ASCII alphanumeric characters or hyphens 9 - Labels cannot start or end with hyphens 10 - Total length: max 253 characters (including dots) 11 - Case-insensitive (normalized to lowercase) 12 13 Note: This does NOT use regex - all validation is hand-written. *) 14 15type t = string 16 17type error = 18 [ `Empty 19 | `Too_long 20 | `Invalid_char of char 21 | `Label_empty 22 | `Label_too_long 23 | `Label_starts_with_hyphen 24 | `Label_ends_with_hyphen 25 | `Single_label (* No dot = not a valid handle *) 26 | `Trailing_dot 27 | `Leading_dot 28 | `Consecutive_dots 29 | `Numeric_tld (* TLD cannot be purely numeric *) ] 30 31let pp_error fmt = function 32 | `Empty -> Format.fprintf fmt "handle is empty" 33 | `Too_long -> Format.fprintf fmt "handle exceeds 253 characters" 34 | `Invalid_char c -> Format.fprintf fmt "invalid character: %c" c 35 | `Label_empty -> Format.fprintf fmt "empty label" 36 | `Label_too_long -> Format.fprintf fmt "label exceeds 63 characters" 37 | `Label_starts_with_hyphen -> Format.fprintf fmt "label starts with hyphen" 38 | `Label_ends_with_hyphen -> Format.fprintf fmt "label ends with hyphen" 39 | `Single_label -> Format.fprintf fmt "handle must have at least two labels" 40 | `Trailing_dot -> Format.fprintf fmt "handle has trailing dot" 41 | `Leading_dot -> Format.fprintf fmt "handle has leading dot" 42 | `Consecutive_dots -> Format.fprintf fmt "handle has consecutive dots" 43 | `Numeric_tld -> Format.fprintf fmt "TLD cannot start with a digit" 44 45let error_to_string e = Format.asprintf "%a" pp_error e 46 47(** Check if a character is valid in a handle label *) 48let is_valid_char c = 49 (c >= 'a' && c <= 'z') 50 || (c >= 'A' && c <= 'Z') 51 || (c >= '0' && c <= '9') 52 || c = '-' 53 54(** Check if a string is purely numeric (all digits) *) 55let is_all_numeric s = 56 let len = String.length s in 57 if len = 0 then false 58 else begin 59 let rec check i = 60 if i >= len then true 61 else 62 let c = s.[i] in 63 if c >= '0' && c <= '9' then check (i + 1) else false 64 in 65 check 0 66 end 67 68(** Check if a string starts with a digit *) 69let starts_with_digit s = String.length s > 0 && s.[0] >= '0' && s.[0] <= '9' 70 71(** Validate a single label *) 72let validate_label (label : string) : (unit, error) result = 73 let len = String.length label in 74 if len = 0 then Error `Label_empty 75 else if len > 63 then Error `Label_too_long 76 else if label.[0] = '-' then Error `Label_starts_with_hyphen 77 else if label.[len - 1] = '-' then Error `Label_ends_with_hyphen 78 else begin 79 let rec check_chars i = 80 if i >= len then Ok () 81 else 82 let c = label.[i] in 83 if is_valid_char c then check_chars (i + 1) else Error (`Invalid_char c) 84 in 85 check_chars 0 86 end 87 88(** Split a string into labels by '.' *) 89let split_labels (s : string) : string list = String.split_on_char '.' s 90 91(** Parse and validate a handle string *) 92let of_string (s : string) : (t, error) result = 93 let len = String.length s in 94 if len = 0 then Error `Empty 95 else if len > 253 then Error `Too_long 96 else if s.[0] = '.' then Error `Leading_dot 97 else if s.[len - 1] = '.' then Error `Trailing_dot 98 else if String.contains s ' ' then Error (`Invalid_char ' ') 99 else begin 100 (* Check for consecutive dots *) 101 let has_consecutive_dots = 102 let rec check i = 103 if i >= len - 1 then false 104 else if s.[i] = '.' && s.[i + 1] = '.' then true 105 else check (i + 1) 106 in 107 check 0 108 in 109 if has_consecutive_dots then Error `Consecutive_dots 110 else begin 111 let labels = split_labels s in 112 if List.length labels < 2 then Error `Single_label 113 else begin 114 (* Get the TLD (last label) and check it doesn't start with a digit *) 115 let tld = List.hd (List.rev labels) in 116 if starts_with_digit tld then Error `Numeric_tld 117 else begin 118 let rec validate_all = function 119 | [] -> Ok (String.lowercase_ascii s) 120 | label :: rest -> ( 121 match validate_label label with 122 | Ok () -> validate_all rest 123 | Error e -> Error e) 124 in 125 validate_all labels 126 end 127 end 128 end 129 end 130 131(** Create a handle, raising Invalid_argument on failure *) 132let of_string_exn (s : string) : t = 133 match of_string s with 134 | Ok h -> h 135 | Error e -> invalid_arg (error_to_string e) 136 137(** Convert handle to string *) 138let to_string (h : t) : string = h 139 140(** Normalize a handle (lowercase) *) 141let normalize (h : t) : t = String.lowercase_ascii h 142 143(** Check if a string is a valid handle *) 144let is_valid (s : string) : bool = 145 match of_string s with Ok _ -> true | Error _ -> false 146 147(** Compare handles (case-insensitive) *) 148let compare (a : t) (b : t) : int = 149 String.compare (String.lowercase_ascii a) (String.lowercase_ascii b) 150 151(** Check handles for equality (case-insensitive) *) 152let equal (a : t) (b : t) : bool = compare a b = 0