internal_validation.odin 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #+private
  2. package flags
  3. @require import "base:runtime"
  4. @require import "core:container/bit_array"
  5. @require import "core:fmt"
  6. @require import "core:mem"
  7. @require import "core:os"
  8. @require import "core:reflect"
  9. @require import "core:strconv"
  10. @require import "core:strings"
  11. // This proc is used to assert that `T` meets the expectations of the library.
  12. @(optimization_mode="favor_size", disabled=ODIN_DISABLE_ASSERT)
  13. validate_structure :: proc(model_type: $T, style: Parsing_Style, loc := #caller_location) {
  14. positionals_assigned_so_far: bit_array.Bit_Array
  15. defer bit_array.destroy(&positionals_assigned_so_far)
  16. check_fields: for field in reflect.struct_fields_zipped(T) {
  17. if style == .Unix {
  18. #partial switch specific_type_info in field.type.variant {
  19. case runtime.Type_Info_Map:
  20. fmt.panicf("%T.%s is a map type, and these are not supported in UNIX-style parsing mode.",
  21. model_type, field.name, loc = loc)
  22. }
  23. }
  24. name_is_safe := true
  25. defer {
  26. fmt.assertf(name_is_safe, "%T.%s is using a reserved name.",
  27. model_type, field.name, loc = loc)
  28. }
  29. switch field.name {
  30. case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
  31. name_is_safe = false
  32. }
  33. args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
  34. if !ok {
  35. // If it has no args tag, then we've checked all we need to.
  36. // Most of this proc is validating that the subtags are sane.
  37. continue
  38. }
  39. if name, has_name := get_struct_subtag(args_tag, SUBTAG_NAME); has_name {
  40. fmt.assertf(len(name) > 0, "%T.%s has a zero-length `%s`.",
  41. model_type, field.name, SUBTAG_NAME, loc = loc)
  42. fmt.assertf(strings.index(name, " ") == -1, "%T.%s has a `%s` with spaces in it.",
  43. model_type, field.name, SUBTAG_NAME, loc = loc)
  44. switch name {
  45. case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
  46. name_is_safe = false
  47. continue check_fields
  48. case:
  49. name_is_safe = true
  50. }
  51. }
  52. if pos_str, has_pos := get_struct_subtag(args_tag, SUBTAG_POS); has_pos {
  53. #partial switch specific_type_info in field.type.variant {
  54. case runtime.Type_Info_Map:
  55. fmt.panicf("%T.%s has `%s` defined, and this does not make sense on a map type.",
  56. model_type, field.name, SUBTAG_POS, loc = loc)
  57. }
  58. pos_value, pos_ok := strconv.parse_u64_of_base(pos_str, 10)
  59. fmt.assertf(pos_ok, "%T.%s has `%s` defined as %q but cannot be parsed a base-10 integer >= 0.",
  60. model_type, field.name, SUBTAG_POS, pos_str, loc = loc)
  61. fmt.assertf(!bit_array.get(&positionals_assigned_so_far, pos_value), "%T.%s has `%s` set to #%i, but that position has already been assigned to another flag.",
  62. model_type, field.name, SUBTAG_POS, pos_value, loc = loc)
  63. bit_array.set(&positionals_assigned_so_far, pos_value)
  64. }
  65. required_min, required_max: int
  66. if requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
  67. fmt.assertf(!reflect.is_boolean(field.type), "%T.%s is a required boolean. This is disallowed.",
  68. model_type, field.name, loc = loc)
  69. fmt.assertf(field.name != INTERNAL_VARIADIC_FLAG, "%T.%s is defined as required. This is disallowed.",
  70. model_type, field.name, loc = loc)
  71. if len(requirement) > 0 {
  72. if required_min, required_max, ok = parse_requirements(requirement); ok {
  73. #partial switch specific_type_info in field.type.variant {
  74. case runtime.Type_Info_Dynamic_Array:
  75. fmt.assertf(required_min != required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are the same. Increase the maximum by 1 for an exact number of arguments: (%i<%i)",
  76. model_type,
  77. field.name,
  78. SUBTAG_REQUIRED,
  79. requirement,
  80. required_min,
  81. 1 + required_max,
  82. loc = loc)
  83. fmt.assertf(required_min < required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are swapped.",
  84. model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
  85. case:
  86. fmt.panicf("%T.%s has `%s` defined as %q, but ranges are only supported on dynamic arrays.",
  87. model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
  88. }
  89. } else {
  90. fmt.panicf("%T.%s has `%s` defined as %q, but it cannot be parsed as a valid range.",
  91. model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
  92. }
  93. }
  94. }
  95. if length, is_variadic := get_struct_subtag(args_tag, SUBTAG_VARIADIC); is_variadic {
  96. if value, parse_ok := strconv.parse_u64_of_base(length, 10); parse_ok {
  97. fmt.assertf(value > 0,
  98. "%T.%s has `%s` set to %i. It must be greater than zero.",
  99. model_type, field.name, value, SUBTAG_VARIADIC, loc = loc)
  100. fmt.assertf(value != 1,
  101. "%T.%s has `%s` set to 1. This has no effect.",
  102. model_type, field.name, SUBTAG_VARIADIC, loc = loc)
  103. }
  104. #partial switch specific_type_info in field.type.variant {
  105. case runtime.Type_Info_Dynamic_Array:
  106. fmt.assertf(style != .Odin,
  107. "%T.%s has `%s` defined, but this only makes sense in UNIX-style parsing mode.",
  108. model_type, field.name, SUBTAG_VARIADIC, loc = loc)
  109. case:
  110. fmt.panicf("%T.%s has `%s` defined, but this only makes sense on dynamic arrays.",
  111. model_type, field.name, SUBTAG_VARIADIC, loc = loc)
  112. }
  113. }
  114. allowed_to_define_file_perms: bool = ---
  115. #partial switch specific_type_info in field.type.variant {
  116. case runtime.Type_Info_Map:
  117. allowed_to_define_file_perms = specific_type_info.value.id == os.Handle
  118. case runtime.Type_Info_Dynamic_Array:
  119. allowed_to_define_file_perms = specific_type_info.elem.id == os.Handle
  120. case:
  121. allowed_to_define_file_perms = field.type.id == os.Handle
  122. }
  123. if _, has_file := get_struct_subtag(args_tag, SUBTAG_FILE); has_file {
  124. fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
  125. model_type, field.name, SUBTAG_FILE, loc = loc)
  126. }
  127. if _, has_perms := get_struct_subtag(args_tag, SUBTAG_PERMS); has_perms {
  128. fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
  129. model_type, field.name, SUBTAG_PERMS, loc = loc)
  130. }
  131. #partial switch specific_type_info in field.type.variant {
  132. case runtime.Type_Info_Map:
  133. fmt.assertf(reflect.is_string(specific_type_info.key), "%T.%s is defined as a map[%T]. Only string types are currently supported as map keys.",
  134. model_type,
  135. field.name,
  136. specific_type_info.key)
  137. }
  138. }
  139. }
  140. // Validate that all the required arguments are set and that the set arguments
  141. // are up to the program's expectations.
  142. @(optimization_mode="favor_size")
  143. validate_arguments :: proc(model: ^$T, parser: ^Parser) -> Error {
  144. check_fields: for field, index in reflect.struct_fields_zipped(T) {
  145. was_set := bit_array.get(&parser.fields_set, index)
  146. field_name := get_field_name(field)
  147. args_tag := reflect.struct_tag_get(field.tag, TAG_ARGS)
  148. requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED)
  149. required_min, required_max: int
  150. has_requirements: bool
  151. if is_required {
  152. required_min, required_max, has_requirements = parse_requirements(requirement)
  153. }
  154. if has_requirements && required_min == 0 {
  155. // Allow `0<n` or `<n` to bypass the required condition.
  156. is_required = false
  157. }
  158. if _, is_array := field.type.variant.(runtime.Type_Info_Dynamic_Array); is_array && has_requirements {
  159. // If it's an array, make sure it meets the required number of arguments.
  160. ptr := cast(^runtime.Raw_Dynamic_Array)(cast(uintptr)model + field.offset)
  161. if required_min == required_max - 1 && ptr.len != required_min {
  162. return Validation_Error {
  163. fmt.tprintf("The flag `%s` had %i option%s set, but it requires exactly %i.",
  164. field_name,
  165. ptr.len,
  166. "" if ptr.len == 1 else "s",
  167. required_min),
  168. }
  169. } else if required_min > ptr.len || ptr.len >= required_max {
  170. if required_max == max(int) {
  171. return Validation_Error {
  172. fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i.",
  173. field_name,
  174. ptr.len,
  175. "" if ptr.len == 1 else "s",
  176. required_min),
  177. }
  178. } else {
  179. return Validation_Error {
  180. fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i and at most %i.",
  181. field_name,
  182. ptr.len,
  183. "" if ptr.len == 1 else "s",
  184. required_min,
  185. required_max - 1),
  186. }
  187. }
  188. }
  189. } else if !was_set {
  190. if is_required {
  191. return Validation_Error {
  192. fmt.tprintf("The required flag `%s` was not set.", field_name),
  193. }
  194. }
  195. // Not set, not required; moving on.
  196. continue
  197. }
  198. // All default checks have passed. The program gets a look at it now.
  199. if global_custom_flag_checker != nil {
  200. ptr := cast(rawptr)(cast(uintptr)model + field.offset)
  201. error := global_custom_flag_checker(model,
  202. field.name,
  203. mem.make_any(ptr, field.type.id),
  204. args_tag)
  205. if len(error) > 0 {
  206. // The program reported an error message.
  207. return Validation_Error { error }
  208. }
  209. }
  210. }
  211. return nil
  212. }