///|
pub(all) enum RulePatternKind {
PatternEmpty
PatternRoot
PatternExactPath
PatternDirectoryPrefix
PatternFilePath
PatternExtensionGlob
PatternWildcardPrefix
PatternWildcardSuffix
PatternWildcardMiddle
PatternAnchored
PatternCatchAll
} derive(Eq, Debug)
///|
pub(all) enum RuleImpact {
ImpactAllowEmpty
ImpactAllowNarrow
ImpactAllowWide
ImpactBlockRoot
ImpactBlockDirectory
ImpactBlockFile
ImpactBlockWildcard
ImpactNeutral
} derive(Eq, Debug)
///|
pub(all) enum RuleRiskBand {
RuleRiskLow
RuleRiskMedium
RuleRiskHigh
RuleRiskCritical
} derive(Eq, Debug)
///|
pub(all) enum RuleRelation {
RelationDuplicate
RelationBroader
RelationNarrower
RelationConflict
RelationIndependent
} derive(Eq, Debug)
///|
pub(all) struct RuleProfile {
rule : Rule
kind : RulePatternKind
impact : RuleImpact
risk : RuleRiskBand
normalized_pattern : String
literal_weight : Int
depth : Int
wildcard_count : Int
has_anchor : Bool
has_query_like_text : Bool
has_fragment_like_text : Bool
is_root_rule : Bool
is_empty_pattern : Bool
is_directory_rule : Bool
is_file_rule : Bool
is_wildcard_rule : Bool
prefix : String
extension : String?
explanation : String
notes : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct GroupRuleSummary {
agents : Array[String]
group_line : Int
rule_count : Int
allow_count : Int
disallow_count : Int
wildcard_rule_count : Int
anchored_rule_count : Int
root_block_count : Int
empty_rule_count : Int
max_depth : Int
max_literal_weight : Int
crawl_delay : Int?
has_sitemap_hint : Bool
notes : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct PolicyProfile {
group_summaries : Array[GroupRuleSummary]
total_rules : Int
total_allows : Int
total_disallows : Int
total_wildcards : Int
total_anchors : Int
total_root_blocks : Int
highest_depth : Int
highest_weight : Int
has_wildcard_group : Bool
has_specific_group : Bool
has_sitemap : Bool
issue_count : Int
notes : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct RulePairAnalysis {
first : RuleProfile
second : RuleProfile
relation : RuleRelation
message : String
same_action : Bool
same_pattern : Bool
first_matches_second_prefix : Bool
second_matches_first_prefix : Bool
} derive(Eq, Debug)
///|
pub fn RulePatternKind::label(self : RulePatternKind) -> String {
match self {
PatternEmpty => "empty"
PatternRoot => "root"
PatternExactPath => "exact-path"
PatternDirectoryPrefix => "directory-prefix"
PatternFilePath => "file-path"
PatternExtensionGlob => "extension-glob"
PatternWildcardPrefix => "wildcard-prefix"
PatternWildcardSuffix => "wildcard-suffix"
PatternWildcardMiddle => "wildcard-middle"
PatternAnchored => "anchored"
PatternCatchAll => "catch-all"
}
}
///|
pub fn RuleImpact::label(self : RuleImpact) -> String {
match self {
ImpactAllowEmpty => "allow-empty"
ImpactAllowNarrow => "allow-narrow"
ImpactAllowWide => "allow-wide"
ImpactBlockRoot => "block-root"
ImpactBlockDirectory => "block-directory"
ImpactBlockFile => "block-file"
ImpactBlockWildcard => "block-wildcard"
ImpactNeutral => "neutral"
}
}
///|
pub fn RuleRiskBand::label(self : RuleRiskBand) -> String {
match self {
RuleRiskLow => "low"
RuleRiskMedium => "medium"
RuleRiskHigh => "high"
RuleRiskCritical => "critical"
}
}
///|
pub fn RuleRelation::label(self : RuleRelation) -> String {
match self {
RelationDuplicate => "duplicate"
RelationBroader => "broader"
RelationNarrower => "narrower"
RelationConflict => "conflict"
RelationIndependent => "independent"
}
}
///|
pub fn analyze_rule(rule : Rule) -> RuleProfile {
let normalized_pattern = normalize_rule_pattern(rule.pattern)
let literal_weight = pattern_weight(normalized_pattern)
let depth = rule_pattern_depth(normalized_pattern)
let wildcard_count = count_char(normalized_pattern, '*')
let has_anchor = normalized_pattern.has_suffix("$")
let is_empty_pattern = normalized_pattern.is_empty()
let is_root_rule = normalized_pattern == "/"
let is_directory_rule = looks_like_directory_rule(normalized_pattern)
let is_file_rule = looks_like_file_rule(normalized_pattern)
let is_wildcard_rule = wildcard_count > 0
let has_query_like_text = normalized_pattern.contains("?")
let has_fragment_like_text = normalized_pattern.contains("#")
let kind = classify_rule_pattern(
normalized_pattern, is_empty_pattern, is_root_rule, is_directory_rule, is_file_rule,
is_wildcard_rule, has_anchor,
)
let impact = classify_rule_impact(
rule.allow,
kind,
is_root_rule,
is_directory_rule,
is_file_rule,
is_wildcard_rule,
)
let risk = classify_rule_risk(
rule.allow,
kind,
impact,
wildcard_count,
literal_weight,
has_query_like_text,
)
let prefix = rule_prefix(normalized_pattern)
let extension = rule_extension(normalized_pattern)
let notes = rule_notes(
rule, normalized_pattern, kind, impact, risk, wildcard_count, has_query_like_text,
has_fragment_like_text,
)
{
rule,
kind,
impact,
risk,
normalized_pattern,
literal_weight,
depth,
wildcard_count,
has_anchor,
has_query_like_text,
has_fragment_like_text,
is_root_rule,
is_empty_pattern,
is_directory_rule,
is_file_rule,
is_wildcard_rule,
prefix,
extension,
explanation: rule_explanation(rule, kind, impact, risk),
notes,
}
}
///|
pub fn analyze_rules(rules : Array[Rule]) -> Array[RuleProfile] {
let profiles : Array[RuleProfile] = []
for rule in rules {
profiles.push(analyze_rule(rule))
}
profiles
}
///|
pub fn AgentGroup::profiles(self : AgentGroup) -> Array[RuleProfile] {
analyze_rules(self.rules)
}
///|
pub fn AgentGroup::summary(
self : AgentGroup,
has_sitemap_hint : Bool,
) -> GroupRuleSummary {
summarize_group(self, has_sitemap_hint)
}
///|
pub fn RobotsPolicy::profile(self : RobotsPolicy) -> PolicyProfile {
profile_policy(self)
}
///|
pub fn profile_policy(policy : RobotsPolicy) -> PolicyProfile {
let summaries : Array[GroupRuleSummary] = []
let notes : Array[String] = []
let mut total_rules = 0
let mut total_allows = 0
let mut total_disallows = 0
let mut total_wildcards = 0
let mut total_anchors = 0
let mut total_root_blocks = 0
let mut highest_depth = 0
let mut highest_weight = 0
let mut has_wildcard_group = false
let mut has_specific_group = false
for group in policy.groups {
let summary = summarize_group(group, !policy.sitemaps.is_empty())
total_rules = total_rules + summary.rule_count
total_allows = total_allows + summary.allow_count
total_disallows = total_disallows + summary.disallow_count
total_wildcards = total_wildcards + summary.wildcard_rule_count
total_anchors = total_anchors + summary.anchored_rule_count
total_root_blocks = total_root_blocks + summary.root_block_count
if summary.max_depth > highest_depth {
highest_depth = summary.max_depth
}
if summary.max_literal_weight > highest_weight {
highest_weight = summary.max_literal_weight
}
if group.agents.contains("*") {
has_wildcard_group = true
} else {
has_specific_group = true
}
summaries.push(summary)
}
if policy.groups.is_empty() {
notes.push("no user-agent group is defined")
}
if policy.sitemaps.is_empty() {
notes.push("no sitemap directive is present")
}
if total_root_blocks > 0 {
notes.push("at least one group blocks the root path")
}
if total_wildcards > 0 {
notes.push("wildcard matching is used")
}
if policy.issues.length() > 0 {
notes.push("parse issues should be reviewed")
}
{
group_summaries: summaries,
total_rules,
total_allows,
total_disallows,
total_wildcards,
total_anchors,
total_root_blocks,
highest_depth,
highest_weight,
has_wildcard_group,
has_specific_group,
has_sitemap: !policy.sitemaps.is_empty(),
issue_count: policy.issues.length(),
notes,
}
}
///|
pub fn summarize_group(
group : AgentGroup,
has_sitemap_hint : Bool,
) -> GroupRuleSummary {
let profiles = analyze_rules(group.rules)
let notes : Array[String] = []
let mut allow_count = 0
let mut disallow_count = 0
let mut wildcard_rule_count = 0
let mut anchored_rule_count = 0
let mut root_block_count = 0
let mut empty_rule_count = 0
let mut max_depth = 0
let mut max_literal_weight = 0
for profile in profiles {
if profile.rule.allow {
allow_count = allow_count + 1
} else {
disallow_count = disallow_count + 1
}
if profile.is_wildcard_rule {
wildcard_rule_count = wildcard_rule_count + 1
}
if profile.has_anchor {
anchored_rule_count = anchored_rule_count + 1
}
if profile.is_root_rule && !profile.rule.allow {
root_block_count = root_block_count + 1
}
if profile.is_empty_pattern {
empty_rule_count = empty_rule_count + 1
}
if profile.depth > max_depth {
max_depth = profile.depth
}
if profile.literal_weight > max_literal_weight {
max_literal_weight = profile.literal_weight
}
}
if group.rules.is_empty() {
notes.push("group has no effective allow/disallow rules")
}
if allow_count > 0 && disallow_count == 0 {
notes.push("group only contains allow rules")
}
if disallow_count > 0 && allow_count == 0 {
notes.push("group only contains disallow rules")
}
if root_block_count > 0 {
notes.push(
"group blocks the whole site unless later allow rules carve it back",
)
}
if wildcard_rule_count > 0 {
notes.push("group uses wildcard rules")
}
if group.crawl_delay is Some(delay) && delay > 60 {
notes.push("crawl-delay is high")
}
{
agents: group.agents,
group_line: group.line,
rule_count: group.rules.length(),
allow_count,
disallow_count,
wildcard_rule_count,
anchored_rule_count,
root_block_count,
empty_rule_count,
max_depth,
max_literal_weight,
crawl_delay: group.crawl_delay,
has_sitemap_hint,
notes,
}
}
///|
pub fn compare_rules(first : Rule, second : Rule) -> RulePairAnalysis {
compare_rule_profiles(analyze_rule(first), analyze_rule(second))
}
///|
pub fn compare_rule_profiles(
first : RuleProfile,
second : RuleProfile,
) -> RulePairAnalysis {
let same_action = first.rule.allow == second.rule.allow
let same_pattern = first.normalized_pattern == second.normalized_pattern
let first_matches_second_prefix = prefix_covers(
first.prefix,
second.normalized_pattern,
)
let second_matches_first_prefix = prefix_covers(
second.prefix,
first.normalized_pattern,
)
let relation = if same_pattern && same_action {
RelationDuplicate
} else if same_pattern && !same_action {
RelationConflict
} else if first_matches_second_prefix && !second_matches_first_prefix {
if same_action {
RelationBroader
} else {
RelationConflict
}
} else if second_matches_first_prefix && !first_matches_second_prefix {
if same_action {
RelationNarrower
} else {
RelationConflict
}
} else {
RelationIndependent
}
{
first,
second,
relation,
message: relation_message(relation, first, second),
same_action,
same_pattern,
first_matches_second_prefix,
second_matches_first_prefix,
}
}
///|
pub fn analyze_rule_pairs(rules : Array[Rule]) -> Array[RulePairAnalysis] {
let profiles = analyze_rules(rules)
let pairs : Array[RulePairAnalysis] = []
for left_index, left in profiles.iter2() {
for right_index, right in profiles.iter2() {
if left_index < right_index {
pairs.push(compare_rule_profiles(left, right))
}
}
}
pairs
}
///|
pub fn AgentGroup::rule_pairs(self : AgentGroup) -> Array[RulePairAnalysis] {
analyze_rule_pairs(self.rules)
}
///|
pub fn RuleProfile::matches_path(self : RuleProfile, path : StringView) -> Bool {
rule_matches(self.rule.pattern, extract_path(path))
}
///|
pub fn RuleProfile::is_broad(self : RuleProfile) -> Bool {
self.is_root_rule || self.kind == PatternCatchAll || self.literal_weight <= 1
}
///|
pub fn RuleProfile::is_narrow(self : RuleProfile) -> Bool {
self.literal_weight >= 8 && self.depth >= 2
}
///|
pub fn RuleProfile::is_risky_block(self : RuleProfile) -> Bool {
!self.rule.allow &&
(self.risk == RuleRiskHigh || self.risk == RuleRiskCritical)
}
///|
pub fn RuleProfile::is_precise_allow(self : RuleProfile) -> Bool {
self.rule.allow && self.is_narrow() && !self.is_wildcard_rule
}
///|
pub fn RuleProfile::action_label(self : RuleProfile) -> String {
if self.rule.allow {
"allow"
} else {
"disallow"
}
}
///|
pub fn RuleProfile::to_line(self : RuleProfile) -> String {
self.action_label() +
" " +
self.normalized_pattern +
" [" +
self.kind.label() +
", " +
self.risk.label() +
"] " +
self.explanation
}
///|
pub fn RuleProfile::to_markdown_row(self : RuleProfile) -> String {
"| " +
self.rule.line.to_string() +
" | " +
self.action_label() +
" | " +
self.normalized_pattern.replace_all(old="|", new="\\|") +
" | " +
self.kind.label() +
" | " +
self.risk.label() +
" | " +
self.literal_weight.to_string() +
" |"
}
///|
pub fn RuleProfile::notes_text(self : RuleProfile) -> String {
if self.notes.is_empty() {
"no notes"
} else {
self.notes.join("; ")
}
}
///|
pub fn GroupRuleSummary::to_line(self : GroupRuleSummary) -> String {
"agents=" +
self.agents.join(",") +
" rules=" +
self.rule_count.to_string() +
" allow=" +
self.allow_count.to_string() +
" disallow=" +
self.disallow_count.to_string() +
" wildcards=" +
self.wildcard_rule_count.to_string()
}
///|
pub fn GroupRuleSummary::to_markdown(self : GroupRuleSummary) -> String {
let lines : Array[String] = []
lines.push("### Group " + self.agents.join(","))
lines.push("")
lines.push("- Start line: " + self.group_line.to_string())
lines.push("- Rules: " + self.rule_count.to_string())
lines.push("- Allow rules: " + self.allow_count.to_string())
lines.push("- Disallow rules: " + self.disallow_count.to_string())
lines.push("- Wildcard rules: " + self.wildcard_rule_count.to_string())
lines.push("- Anchored rules: " + self.anchored_rule_count.to_string())
lines.push("- Root blocks: " + self.root_block_count.to_string())
lines.push("- Max depth: " + self.max_depth.to_string())
lines.push("- Max literal weight: " + self.max_literal_weight.to_string())
match self.crawl_delay {
Some(delay) => lines.push("- Crawl delay: " + delay.to_string())
None => lines.push("- Crawl delay: none")
}
if self.notes.is_empty() {
lines.push("- Notes: none")
} else {
lines.push("- Notes: " + self.notes.join("; "))
}
lines.join("\n")
}
///|
pub fn PolicyProfile::to_line(self : PolicyProfile) -> String {
"rules=" +
self.total_rules.to_string() +
" allow=" +
self.total_allows.to_string() +
" disallow=" +
self.total_disallows.to_string() +
" groups=" +
self.group_summaries.length().to_string() +
" issues=" +
self.issue_count.to_string()
}
///|
pub fn PolicyProfile::to_markdown(self : PolicyProfile) -> String {
let lines : Array[String] = []
lines.push("# RoboPolicy Rule Profile")
lines.push("")
lines.push("- Groups: " + self.group_summaries.length().to_string())
lines.push("- Rules: " + self.total_rules.to_string())
lines.push("- Allow rules: " + self.total_allows.to_string())
lines.push("- Disallow rules: " + self.total_disallows.to_string())
lines.push("- Wildcard rules: " + self.total_wildcards.to_string())
lines.push("- Anchored rules: " + self.total_anchors.to_string())
lines.push("- Root blocks: " + self.total_root_blocks.to_string())
lines.push("- Highest depth: " + self.highest_depth.to_string())
lines.push("- Highest literal weight: " + self.highest_weight.to_string())
lines.push("- Sitemap present: " + yes_no(self.has_sitemap))
lines.push("- Parse issues: " + self.issue_count.to_string())
lines.push("")
if self.notes.is_empty() {
lines.push("No profile notes.")
} else {
lines.push("Profile notes:")
for note in self.notes {
lines.push("- " + note)
}
}
lines.push("")
for summary in self.group_summaries {
lines.push(summary.to_markdown())
lines.push("")
}
lines.join("\n")
}
///|
pub fn RulePairAnalysis::to_line(self : RulePairAnalysis) -> String {
self.relation.label() +
": " +
self.first.normalized_pattern +
" -> " +
self.second.normalized_pattern +
" " +
self.message
}
///|
pub fn find_duplicate_rules(rules : Array[Rule]) -> Array[RulePairAnalysis] {
filter_pairs_by_relation(analyze_rule_pairs(rules), RelationDuplicate)
}
///|
pub fn find_conflicting_rules(rules : Array[Rule]) -> Array[RulePairAnalysis] {
filter_pairs_by_relation(analyze_rule_pairs(rules), RelationConflict)
}
///|
pub fn find_broad_rules(rules : Array[Rule]) -> Array[RuleProfile] {
let profiles : Array[RuleProfile] = []
for profile in analyze_rules(rules) {
if profile.is_broad() {
profiles.push(profile)
}
}
profiles
}
///|
pub fn find_risky_blocks(rules : Array[Rule]) -> Array[RuleProfile] {
let profiles : Array[RuleProfile] = []
for profile in analyze_rules(rules) {
if profile.is_risky_block() {
profiles.push(profile)
}
}
profiles
}
///|
pub fn find_precise_allows(rules : Array[Rule]) -> Array[RuleProfile] {
let profiles : Array[RuleProfile] = []
for profile in analyze_rules(rules) {
if profile.is_precise_allow() {
profiles.push(profile)
}
}
profiles
}
///|
pub fn rules_to_markdown(rules : Array[Rule]) -> String {
let lines : Array[String] = []
lines.push("| Line | Action | Pattern | Kind | Risk | Weight |")
lines.push("| --- | --- | --- | --- | --- | --- |")
for profile in analyze_rules(rules) {
lines.push(profile.to_markdown_row())
}
lines.join("\n")
}
///|
fn filter_pairs_by_relation(
pairs : Array[RulePairAnalysis],
relation : RuleRelation,
) -> Array[RulePairAnalysis] {
let filtered : Array[RulePairAnalysis] = []
for pair in pairs {
if pair.relation == relation {
filtered.push(pair)
}
}
filtered
}
///|
fn normalize_rule_pattern(pattern : String) -> String {
let trimmed = pattern.trim()
if trimmed.is_empty() {
""
} else if trimmed == "*" {
"*"
} else if trimmed == "/*" {
"/*"
} else if trimmed.has_prefix("http://") || trimmed.has_prefix("https://") {
extract_path(trimmed)
} else if trimmed.has_prefix("/") {
trimmed.to_owned()
} else {
"/" + trimmed.to_owned()
}
}
///|
fn classify_rule_pattern(
pattern : String,
is_empty_pattern : Bool,
is_root_rule : Bool,
is_directory_rule : Bool,
is_file_rule : Bool,
is_wildcard_rule : Bool,
has_anchor : Bool,
) -> RulePatternKind {
if is_empty_pattern {
PatternEmpty
} else if is_root_rule {
PatternRoot
} else if pattern == "*" || pattern == "/*" {
PatternCatchAll
} else if has_anchor {
PatternAnchored
} else if is_extension_glob(pattern) {
PatternExtensionGlob
} else if is_wildcard_rule && pattern.has_prefix("*") {
PatternWildcardPrefix
} else if is_wildcard_rule && pattern.has_suffix("*") {
PatternWildcardSuffix
} else if is_wildcard_rule {
PatternWildcardMiddle
} else if is_directory_rule {
PatternDirectoryPrefix
} else if is_file_rule {
PatternFilePath
} else {
PatternExactPath
}
}
///|
fn classify_rule_impact(
allow : Bool,
kind : RulePatternKind,
is_root_rule : Bool,
is_directory_rule : Bool,
is_file_rule : Bool,
is_wildcard_rule : Bool,
) -> RuleImpact {
if allow && kind == PatternEmpty {
ImpactAllowEmpty
} else if allow && (is_directory_rule || is_wildcard_rule) {
ImpactAllowWide
} else if allow {
ImpactAllowNarrow
} else if is_root_rule || kind == PatternCatchAll {
ImpactBlockRoot
} else if is_directory_rule {
ImpactBlockDirectory
} else if is_file_rule {
ImpactBlockFile
} else if is_wildcard_rule {
ImpactBlockWildcard
} else {
ImpactNeutral
}
}
///|
fn classify_rule_risk(
allow : Bool,
kind : RulePatternKind,
impact : RuleImpact,
wildcard_count : Int,
literal_weight : Int,
has_query_like_text : Bool,
) -> RuleRiskBand {
if !allow && impact == ImpactBlockRoot {
RuleRiskCritical
} else if !allow && kind == PatternCatchAll {
RuleRiskCritical
} else if !allow && literal_weight <= 1 {
RuleRiskHigh
} else if wildcard_count >= 2 {
RuleRiskHigh
} else if has_query_like_text {
RuleRiskMedium
} else if impact == ImpactAllowWide || impact == ImpactBlockWildcard {
RuleRiskMedium
} else {
RuleRiskLow
}
}
///|
fn rule_notes(
rule : Rule,
pattern : String,
kind : RulePatternKind,
impact : RuleImpact,
risk : RuleRiskBand,
wildcard_count : Int,
has_query_like_text : Bool,
has_fragment_like_text : Bool,
) -> Array[String] {
let notes : Array[String] = []
if pattern.is_empty() {
notes.push("empty disallow has no blocking effect")
}
if !rule.allow && impact == ImpactBlockRoot {
notes.push("this rule blocks the whole site")
}
if rule.allow && impact == ImpactAllowWide {
notes.push("wide allow rule may override broader disallow rules")
}
if kind == PatternAnchored {
notes.push("end anchor is used")
}
if wildcard_count > 0 {
notes.push("wildcard count=" + wildcard_count.to_string())
}
if wildcard_count >= 2 {
notes.push("multiple wildcards make the rule harder to explain")
}
if has_query_like_text {
notes.push("query marker is present in pattern")
}
if has_fragment_like_text {
notes.push("fragment marker is present in pattern")
}
if risk == RuleRiskCritical {
notes.push("critical review recommended")
}
notes
}
///|
fn rule_explanation(
rule : Rule,
kind : RulePatternKind,
impact : RuleImpact,
risk : RuleRiskBand,
) -> String {
let action = if rule.allow { "allows" } else { "blocks" }
action +
" " +
kind.label() +
" pattern with " +
impact.label() +
" impact and " +
risk.label() +
" risk"
}
///|
fn relation_message(
relation : RuleRelation,
first : RuleProfile,
second : RuleProfile,
) -> String {
match relation {
RelationDuplicate => "rules have the same action and pattern"
RelationBroader =>
first.normalized_pattern + " is broader than " + second.normalized_pattern
RelationNarrower =>
first.normalized_pattern +
" is narrower than " +
second.normalized_pattern
RelationConflict =>
"rules may match overlapping paths with different actions"
RelationIndependent => "rules appear to target independent path ranges"
}
}
///|
fn rule_pattern_depth(pattern : String) -> Int {
let clean = trim_rule_markers(pattern)
if clean.is_empty() || clean == "/" || clean == "*" {
0
} else {
path_depth(clean)
}
}
///|
fn rule_prefix(pattern : String) -> String {
let clean = trim_rule_markers(pattern)
if clean.is_empty() {
""
} else if clean == "*" {
"/"
} else {
match clean.split_once("*") {
Some((left, _)) => if left.is_empty() { "/" } else { left.to_owned() }
None => clean
}
}
}
///|
fn rule_extension(pattern : String) -> String? {
let clean = trim_rule_markers(pattern)
if clean.contains("*") {
match clean.split_once(".") {
Some((_, ext)) => if ext.is_empty() { None } else { Some(ext.to_owned()) }
None => None
}
} else {
path_extension(clean)
}
}
///|
fn trim_rule_markers(pattern : String) -> String {
if pattern.has_suffix("$") {
match pattern.strip_suffix("$") {
Some(body) => body.to_owned()
None => pattern
}
} else {
pattern
}
}
///|
fn looks_like_directory_rule(pattern : String) -> Bool {
!pattern.is_empty() && pattern.has_suffix("/") && pattern != "/"
}
///|
fn looks_like_file_rule(pattern : String) -> Bool {
!pattern.is_empty() &&
!pattern.has_suffix("/") &&
pattern.contains(".") &&
!pattern.contains("*")
}
///|
fn is_extension_glob(pattern : String) -> Bool {
pattern.contains("*.") || pattern.contains("/*.")
}
///|
fn prefix_covers(prefix : String, candidate : String) -> Bool {
if prefix.is_empty() {
false
} else if prefix == "/" {
candidate.has_prefix("/")
} else {
candidate.has_prefix(prefix)
}
}
///|
fn count_char(text : String, needle : Char) -> Int {
let mut count = 0
for ch in text {
if ch == needle {
count = count + 1
}
}
count
}
///|
fn yes_no(value : Bool) -> String {
if value {
"yes"
} else {
"no"
}
}