1 import Base 2 import ./type.bend as Rl 3 4 # ---- the hub's rules: a name is 12 to 64 characters of a-z, 0-9 and -; a version is four numbers ---- 5 def digits_only(cs: List<&2, Char>) -> Bool: 6 match cs: 7 case []: 8 True{} 9 case c <> rest: 10 Char.is_digit(c) && digits_only(rest) 11 12 def all_numbers(xs: List<&2, String>) -> Bool: 13 match xs: 14 case []: 15 True{} 16 case +x <> rest: 17 Bool.not(String.is_empty(x)) && digits_only(String.to_list(x)) && all_numbers(rest) 18 19 def name_chars(cs: List<&2, Char>) -> Bool: 20 match cs: 21 case []: 22 True{} 23 case +c <> rest: 24 (Char.is_lower(c) || Char.is_digit(c) || Char.is_eq(c, '-')) && name_chars(rest) 25 26 def package_ok(p: Rl.Package) -> Bool: 27 match p: 28 case Rl.Package{+n, +v, _, _}: 29 Nat.is_ge(String.length(n), 12n) && Nat.is_le(String.length(n), 64n) && name_chars(String.to_list(n)) && 30 Nat.is_eq(List.length(&2, String, String.split(v, '.')), 4n) && all_numbers(String.split(v, '.')) 31 32 def packages_ok(ps: List<&2, Rl.Package>) -> Bool: 33 match ps: 34 case []: 35 True{} 36 case p <> rest: 37 package_ok(p) && packages_ok(rest) 38 39 def packages(r: Rl.Release) -> List<&2, Rl.Package>: 40 match r: 41 case Rl.Release{_, ps, _}: 42 ps 43 44 def plugins(r: Rl.Release) -> List<&2, Rl.Plugin>: 45 match r: 46 case Rl.Release{_, _, gs}: 47 gs 48 49 def repository(r: Rl.Release) -> Rl.Repository: 50 match r: 51 case Rl.Release{repo, _, _}: 52 repo 53 54 def is_public(r: Rl.Release) -> Bool: 55 match r: 56 case Rl.Release{repo, _, _}: 57 match repo: 58 case Rl.Repository{_, p}: 59 p 60 61 # Every component any package ships. 62 def shipped_components(ps: List<&2, Rl.Package>) -> List<&2, String>: 63 match ps: 64 case []: 65 [] 66 case p <> rest: 67 match p: 68 case Rl.Package{_, _, _, cs}: 69 List.append(&2, String, cs, shipped_components(rest)) 70 71 # Every package's entry file. 72 def entries(ps: List<&2, Rl.Package>) -> List<&2, String>: 73 match ps: 74 case []: 75 [] 76 case p <> rest: 77 match p: 78 case Rl.Package{_, _, e, _}: 79 e <> entries(rest) 80 81 # Every plugin's entry file. 82 def plugin_entries(gs: List<&2, Rl.Plugin>) -> List<&2, String>: 83 match gs: 84 case []: 85 [] 86 case g <> rest: 87 match g: 88 case Rl.Plugin{_, _, e, _, _}: 89 e <> plugin_entries(rest) 90 91 # Every component any plugin ships. 92 def plugin_components(gs: List<&2, Rl.Plugin>) -> List<&2, String>: 93 match gs: 94 case []: 95 [] 96 case g <> rest: 97 match g: 98 case Rl.Plugin{_, _, _, cs, _}: 99 List.append(&2, String, cs, plugin_components(rest)) 100 101 # ---- the repository: Bend only, and no dead code ---- 102 # Bend, or a foreign effect in C or JavaScript (Bend's two lanes). 103 def is_source(+f: String) -> Bool: 104 String.ends_with(f, ".bend") || String.ends_with(f, ".c") || String.ends_with(f, ".js") 105 106 def base_name(f: String) -> String: 107 Maybe.default(&2, String, List.head(&2, String, List.reverse(&2, String, String.split(f, '/'))), "") 108 109 # Every source file is reachable from an entry: a package's, or the system's own. 110 def no_dead_code(fs: List<&2, String>, +reached: List<&2, String>) -> Bool: 111 match fs: 112 case []: 113 True{} 114 case +f <> rest: 115 (Bool.not(is_source(f)) || List.contains(~String, ~String.eq, reached, f)) && no_dead_code(rest, reached) 116 117 # The first source file nothing reaches, "" when there is none. 118 def dead(fs: List<&2, String>, +reached: List<&2, String>) -> String: 119 match fs: 120 case []: 121 "" 122 case +f <> rest: 123 Bool.pick(String, is_source(f) && Bool.not(List.contains(~String, ~String.eq, reached, f)), f, dead(rest, reached)) 124 125 # The first file that is not Bend, "" when there is none. 126 # ---- the hub's path rule: every segment is [A-Za-z_][A-Za-z0-9_]* (the file name also ends in .bend, .c or 127 # .js, or is a LICENSE), under 256 characters ---- 128 def name_char(+c: Char, first: Bool) -> Bool: 129 Char.is_lower(c) || Char.is_upper(c) || Char.is_eq(c, '_') || (Bool.not(first) && Char.is_digit(c)) 130 131 def name_chars_ok(cs: List<&2, Char>, first: Bool) -> Bool: 132 match cs: 133 case []: 134 Bool.not(first) 135 case +c <> rest: 136 name_char(c, first) && name_chars_ok(rest, False{}) 137 138 def stem(+f: String) -> String: 139 Bool.pick(String, String.ends_with(f, ".bend"), String.take(f, Nat.sub(String.length(f), 5n)), 140 Bool.pick(String, String.ends_with(f, ".js"), String.take(f, Nat.sub(String.length(f), 3n)), 141 Bool.pick(String, String.ends_with(f, ".c"), String.take(f, Nat.sub(String.length(f), 2n)), f))) 142 143 def segments_ok(ss: List<&2, String>) -> Bool: 144 match ss: 145 case []: 146 True{} 147 case +s <> rest: 148 match rest: 149 case []: 150 String.eq(s, "LICENSE") || (is_source(s) && name_chars_ok(String.to_list(stem(s)), True{})) 151 case _ <> _: 152 name_chars_ok(String.to_list(s), True{}) && segments_ok(rest) 153 154 def hub_path_ok(+f: String) -> Bool: 155 Nat.is_lt(String.length(f), 256n) && segments_ok(String.split(f, '/')) 156 157 # The first file the hub would refuse, "" when there is none. 158 def hub_refused(fs: List<&2, String>) -> String: 159 match fs: 160 case []: 161 "" 162 case +f <> rest: 163 Bool.pick(String, hub_path_ok(f), hub_refused(rest), f)