1use crate::{EngineError, Result};
6use asc_findrefs::{ClassQuery, MemberQuery, MemberSignature, Query, TypeMatch};
7
8mod denormalize;
9mod mappings;
10mod proguard;
11
12pub use denormalize::{
13 denormalize_class_candidates, denormalize_identifier, denormalize_method_candidates, denormalize_query, is_java_reserved,
14};
15pub(crate) use denormalize::{partial_proto, proto_matches};
16
17pub use mappings::{FieldRef, MappingError, MappingsHash, MethodRef, NameMappings, NameResolver};
18pub use proguard::{ProguardError, ProguardMapping, ProguardWarning, WarningKind};
19
20pub fn format_class_name(name: &str) -> Result<String> {
28 if name.is_empty() {
29 return Err(EngineError::Query("Class name cannot be empty".into()));
30 }
31 if (name.starts_with('L') && name.ends_with(';') && name.contains('/')) || name.starts_with('[') {
32 return Ok(name.to_string());
33 }
34 let mut n = name.replace('.', "/");
35 if !n.starts_with('L') {
36 n.insert(0, 'L');
37 }
38 if !n.ends_with(';') {
39 n.push(';');
40 }
41 Ok(n)
42}
43
44pub(crate) fn class_inner(descriptor: &str) -> &str {
48 let d = descriptor.strip_prefix('L').unwrap_or(descriptor);
49 d.strip_suffix(';').unwrap_or(d)
50}
51
52pub(crate) fn class_prefix(prefix: &str) -> Result<String> {
55 let p = prefix.trim();
56 if p.is_empty() {
57 return Err(EngineError::Query("Class prefix cannot be empty".into()));
58 }
59 if p.starts_with('L') {
60 return Ok(p.to_string());
61 }
62 Ok(format!("L{}", p.replace('.', "/")))
63}
64
65pub(crate) fn normalize_class_query(name: &str, fuzzy: bool) -> Result<String> {
68 if fuzzy {
69 if name.contains('.') && !name.contains('/') {
70 return Ok(name.replace('.', "/"));
71 }
72 return Ok(name.to_string());
73 }
74 format_class_name(name)
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub(crate) struct MemberTarget<'a> {
81 pub class: Option<&'a str>,
83 pub name: &'a str,
85 pub signature: Option<&'a str>,
87}
88
89impl<'a> MemberTarget<'a> {
90 pub(crate) fn parse(spec: &'a str) -> MemberTarget<'a> {
96 let s = spec.trim();
97 let (class, member) = if let Some((c, m)) = s.split_once("->") {
98 (Some(c.trim()), m.trim())
99 } else if let Some((c, m)) = s.rsplit_once('#') {
100 (Some(c.trim()), m.trim())
101 } else if let Some(i) = dotted_member_split(s) {
102 (Some(&s[..i]), &s[i + 1..])
103 } else {
104 (None, s)
105 };
106 let (name, signature) = match member.find(['(', ':']) {
107 Some(i) if is_member_name(&member[..i]) => (&member[..i], Some(&member[i..])),
108 _ => (member, None),
109 };
110 MemberTarget { class: class.filter(|c| !c.is_empty()), name, signature }
111 }
112}
113
114fn is_identifier(s: &str) -> bool {
118 if let Some(inner) = s.strip_prefix('`').and_then(|r| r.strip_suffix('`')) {
119 return !inner.is_empty() && !inner.contains('`');
120 }
121 let mut chars = s.chars();
122 chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
123 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
124}
125
126fn is_member_name(s: &str) -> bool {
127 is_identifier(s) || s == "<init>" || s == "<clinit>"
128}
129
130fn dotted_member_split(spec: &str) -> Option<usize> {
133 let head_end = spec.find(['(', ':']).unwrap_or(spec.len());
134 let head = &spec[..head_end];
135 let i = head.rfind('.')?;
136 let (class, name) = (&head[..i], &head[i + 1..]);
137 let segments: Vec<&str> = class.split(['.', '/']).collect();
138 let capitalised = segments.last().is_some_and(|l| l.trim_matches('`').starts_with(|c: char| c.is_ascii_uppercase()));
139 let classlike = segments.iter().all(|s| is_identifier(s));
140 let member_evident = head_end < spec.len() || name.starts_with('<');
142 (classlike && is_member_name(name) && (capitalised || member_evident)).then_some(i)
143}
144
145pub(crate) fn looks_like_class(spec: &str) -> bool {
148 let s = spec.trim();
149 if s.starts_with('L') && s.ends_with(';') && !s.contains("->") {
150 return true;
151 }
152 let segments: Vec<&str> = s.split('.').collect();
153 segments.len() > 1
154 && segments.iter().all(|seg| is_identifier(seg))
155 && segments.last().is_some_and(|l| l.trim_matches('`').starts_with(|c: char| c.is_ascii_uppercase()))
156}
157
158fn simple_class_pattern(name: &str) -> Option<String> {
163 (is_identifier(name) && name != "L").then(|| format!("[/L]{};\\x00", regex::escape(name)))
164}
165
166pub fn member_query(kind: &str, class: Option<&str>, fuzzy_class: bool, name: Option<&str>, exact_name: bool) -> Result<MemberQuery> {
188 let mut class = class.map(str::trim).filter(|c| !c.is_empty());
189 let mut name = name.map(str::trim).filter(|n| !n.is_empty());
190 let mut exact_name = exact_name;
191 let mut signature = None;
192 if let (Some(c), None) = (class, name)
194 && (c.contains("->") || c.contains('#'))
195 {
196 (class, name) = (None, Some(c));
197 }
198 if let Some(n) = name {
199 let t = MemberTarget::parse(n);
200 match t.class {
201 Some(c) => {
202 class = class.or(Some(c));
203 name = Some(t.name).filter(|n| !n.is_empty());
204 }
205 None if class.is_none() && looks_like_class(n) => (class, name) = (Some(n), None),
206 None => name = Some(t.name),
207 }
208 if let Some(sig) = t.signature {
209 exact_name = true;
210 signature = Some(parse_signature(sig, kind == "method")?);
211 }
212 }
213 if let Some(n) = name.and_then(|n| n.strip_prefix('=')) {
214 (name, exact_name) = (Some(n), true);
215 }
216 if class.is_none() && name.is_none() {
217 return Err(EngineError::Query(format!(
218 "{kind} query needs a class or a {kind} name, e.g. Landroid/webkit/WebView;->loadUrl, android.webkit.WebView.loadUrl or just loadUrl"
219 )));
220 }
221 let class = match class {
222 Some(c) => Some(match c.strip_prefix('~') {
223 Some(f) => ClassQuery::fuzzy(normalize_class_query(f, true)?),
224 None if fuzzy_class => ClassQuery::fuzzy(normalize_class_query(c, true)?),
225 None => match simple_class_pattern(c) {
226 Some(pattern) => ClassQuery::fuzzy(pattern),
227 None => ClassQuery::exact(format_class_name(c)?),
228 },
229 }),
230 None => None,
231 };
232 Ok(MemberQuery::default().with_class(class).with_name(name.map(String::from)).with_exact_name(exact_name).with_signature(signature))
233}
234
235pub const KINDS: &[&str] = &["string", "literal", "type", "method", "field", "extends", "impl", "override", "annotation", "flag", "in"];
238
239pub fn parse_kind(kind: &str) -> Result<&'static str> {
247 let k = kind.trim().to_ascii_lowercase();
248 let alias = |k: &str| -> Option<&'static str> {
249 Some(match k {
250 "string" | "str" | "text" => "string",
251 "literal" => "literal",
252 "type" | "class" => "type",
253 "method" | "call" | "invoke" => "method",
254 "field" => "field",
255 "extends" | "extend" | "subclass" | "superclass" => "extends",
256 "impl" | "implements" | "implement" | "interface" | "implementer" => "impl",
257 "override" | "overrides" | "implementor" => "override",
258 "annotation" | "annotated" | "anno" => "annotation",
259 "flag" | "flags" | "access" | "modifier" => "flag",
260 "in" | "inclass" => "in",
261 _ => return None,
262 })
263 };
264 alias(&k).or_else(|| k.strip_suffix("es").and_then(alias)).or_else(|| k.strip_suffix('s').and_then(alias)).ok_or_else(|| {
265 let closest = KINDS.iter().min_by_key(|c| edit_distance(&k, c, usize::MAX).unwrap_or(usize::MAX)).copied().unwrap_or("string");
266 EngineError::Query(format!("unknown kind {kind:?}: use string, type, method, field, extends or impl (closest: {closest})"))
267 })
268}
269
270pub(crate) fn edit_distance(a: &str, b: &str, max: usize) -> Option<usize> {
272 let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
273 if a.len().abs_diff(b.len()) > max {
274 return None;
275 }
276 let mut row: Vec<usize> = (0..=b.len()).collect();
277 for (i, ca) in a.iter().enumerate() {
278 let mut prev = row[0];
279 row[0] = i + 1;
280 let mut best = row[0];
281 for (j, cb) in b.iter().enumerate() {
282 let cur = row[j + 1];
283 row[j + 1] = if ca == cb { prev } else { 1 + prev.min(cur).min(row[j]) };
284 prev = cur;
285 best = best.min(row[j + 1]);
286 }
287 if best > max {
288 return None;
289 }
290 }
291 Some(row[b.len()]).filter(|&d| d <= max)
292}
293
294pub fn java_signature_to_dalvik(signature: &str) -> Option<String> {
299 let (name, rest) = signature.trim().split_once('(')?;
300 let (params, ret) = rest.split_once(')')?;
301 let params: String = params.split(',').map(str::trim).filter(|p| !p.is_empty()).map(java_type).collect::<Option<_>>()?;
302 let ret = match ret.trim() {
303 "" => String::new(),
304 r => java_type(r)?,
305 };
306 Some(format!("{}({params}){ret}", name.trim()))
307}
308
309fn java_type(t: &str) -> Option<String> {
311 let mut t = t.trim();
313 let mut dims = 0;
314 while let Some(inner) = t.strip_suffix("[]") {
315 t = inner.trim();
316 dims += 1;
317 }
318 let element = java_element_type(t)?;
319 Some("[".repeat(dims) + &element)
320}
321
322fn java_element_type(t: &str) -> Option<String> {
324 let primitive = match t {
325 "void" => "V",
326 "boolean" => "Z",
327 "byte" => "B",
328 "short" => "S",
329 "char" => "C",
330 "int" => "I",
331 "long" => "J",
332 "float" => "F",
333 "double" => "D",
334 _ => "",
335 };
336 if !primitive.is_empty() {
337 return Some(primitive.to_string());
338 }
339 if (t.starts_with('L') && t.ends_with(';')) || t.starts_with('[') || (t.len() == 1 && "VZBSCIJFD".contains(t)) {
340 return Some(t.to_string());
341 }
342 t.split('.').all(is_identifier).then(|| format!("L{};", t.replace('.', "/")))
343}
344
345pub(crate) fn parse_signature(signature: &str, method: bool) -> Result<MemberSignature> {
351 let bad = |why: &str| {
352 EngineError::Query(format!(
353 "bad signature {signature:?}: {why}; write a method's as (params)ret, e.g. (ILjava/lang/String;)V or (int, String), \
354 a field's as :T, e.g. :I"
355 ))
356 };
357 let s = signature.trim();
358 if !method {
359 let t = s.strip_prefix(':').ok_or_else(|| bad("a field takes :T"))?;
360 return type_match(t).map(MemberSignature::Field).ok_or_else(|| bad("unknown type"));
361 }
362 let (params, ret) = s.strip_prefix('(').and_then(|r| r.split_once(')')).ok_or_else(|| bad("a method takes (params)"))?;
363 let params = params.trim();
364 let params = if params.is_empty() {
365 Vec::new()
366 } else if let Some(descriptors) = (!params.contains([',', ' '])).then(|| dalvik_types(params)).flatten() {
367 descriptors.into_iter().map(|d| TypeMatch::Descriptor(d.to_string())).collect()
368 } else {
369 params.split(',').map(type_match).collect::<Option<_>>().ok_or_else(|| bad("unknown parameter type"))?
370 };
371 let ret = match ret.trim() {
372 "" => None,
373 r => Some(type_match(r).ok_or_else(|| bad("unknown return type"))?),
374 };
375 Ok(MemberSignature::Method { params, ret })
376}
377
378pub(crate) fn dalvik_types(s: &str) -> Option<Vec<&str>> {
381 let mut out = Vec::new();
382 let mut rest = s;
383 while !rest.is_empty() {
384 let dims = rest.len() - rest.trim_start_matches('[').len();
385 let end = match rest[dims..].chars().next()? {
386 'Z' | 'B' | 'S' | 'C' | 'I' | 'J' | 'F' | 'D' | 'V' => dims + 1,
387 'L' => dims + rest[dims..].find(';')? + 1,
388 _ => return None,
389 };
390 out.push(&rest[..end]);
391 rest = &rest[end..];
392 }
393 Some(out)
394}
395
396fn type_match(t: &str) -> Option<TypeMatch> {
400 let t = t.trim();
401 if let Some([d]) = dalvik_types(t).as_deref() {
402 return Some(TypeMatch::Descriptor(d.to_string()));
403 }
404 let (mut base, mut dims) = (t, 0);
405 while let Some(b) = base.strip_suffix("[]") {
406 (base, dims) = (b.trim_end(), dims + 1);
407 }
408 let arrays = "[".repeat(dims);
409 let primitive = match base {
410 "void" => "V",
411 "boolean" => "Z",
412 "byte" => "B",
413 "short" => "S",
414 "char" => "C",
415 "int" => "I",
416 "long" => "J",
417 "float" => "F",
418 "double" => "D",
419 _ => "",
420 };
421 if !primitive.is_empty() {
422 return Some(TypeMatch::Descriptor(format!("{arrays}{primitive}")));
423 }
424 if base.contains(['.', '/']) {
425 return base.split(['.', '/']).all(is_identifier).then(|| TypeMatch::Descriptor(format!("{arrays}L{};", base.replace('.', "/"))));
426 }
427 is_identifier(base).then(|| TypeMatch::Simple { name: base.to_string(), dims })
428}
429
430pub(crate) fn exact_method(signature: &str) -> Result<MemberQuery> {
433 let bad = || EngineError::Query(format!("not a method signature: {signature:?} (want Lcls;->name(params)ret)"));
434 let (class, member) = signature.trim().split_once("->").ok_or_else(bad)?;
435 let p = member.find('(').ok_or_else(bad)?;
436 Ok(MemberQuery::default()
437 .with_class(Some(ClassQuery::exact(class.to_string())))
438 .with_name(Some(member[..p].to_string()))
439 .with_exact_name(true)
440 .with_signature(Some(parse_signature(&member[p..], true)?)))
441}
442
443pub fn parse_condition(spec: &str) -> Result<Query> {
464 crate::condition::parse_condition_in(spec, None)
465}
466
467pub fn parse_rule(spec: &str) -> Result<Query> {
476 crate::condition::parse_rule_in(spec, None)
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn class_names() {
485 assert_eq!(format_class_name("com.poc.Main").unwrap(), "Lcom/poc/Main;");
486 assert_eq!(format_class_name("Lcom/poc/Main;").unwrap(), "Lcom/poc/Main;");
487 assert_eq!(format_class_name("com/poc/Main").unwrap(), "Lcom/poc/Main;");
488 assert_eq!(format_class_name("[Ljava/lang/Object;").unwrap(), "[Ljava/lang/Object;");
489 assert_eq!(normalize_class_query("com.poc", true).unwrap(), "com/poc");
490 assert_eq!(normalize_class_query("poc", true).unwrap(), "poc");
491 assert!(format_class_name("").is_err());
492 for p in ["com.xxx", "com/xxx", "Lcom/xxx", " com.xxx\t"] {
493 assert_eq!(class_prefix(p).unwrap(), "Lcom/xxx");
494 }
495 assert_eq!(class_prefix("a").unwrap(), "La");
496 assert_eq!(class_prefix(" ").unwrap_err().to_string(), "Class prefix cannot be empty");
497 assert!(member_query("method", None, false, None, false).is_err());
498 let q = member_query("method", Some("com.poc.Main"), false, Some("on"), false).unwrap();
499 assert!(!q.exact_name);
500 assert_eq!(q.class.unwrap(), ClassQuery::exact("Lcom/poc/Main;"));
501 assert!(member_query("field", None, false, Some("f"), true).unwrap().exact_name);
502 }
503
504 fn precise(c: &str) -> Option<ClassQuery> {
505 Some(ClassQuery::exact(c))
506 }
507
508 #[test]
509 fn member_targets_in_every_spelling() {
510 let t = |s| MemberTarget::parse(s);
511 assert_eq!(t("Lcom/a/B;->run(I)V"), MemberTarget { class: Some("Lcom/a/B;"), name: "run", signature: Some("(I)V") });
512 assert_eq!(t("Lc;->f:I"), MemberTarget { class: Some("Lc;"), name: "f", signature: Some(":I") });
513 assert_eq!(t("com.a.B->run"), MemberTarget { class: Some("com.a.B"), name: "run", signature: None });
514 assert_eq!(t("com.a.B#run"), MemberTarget { class: Some("com.a.B"), name: "run", signature: None });
515 assert_eq!(
516 t("android.webkit.WebView.loadUrl"),
517 MemberTarget { class: Some("android.webkit.WebView"), name: "loadUrl", signature: None }
518 );
519 assert_eq!(t("com/a/B.run(I)V"), MemberTarget { class: Some("com/a/B"), name: "run", signature: Some("(I)V") });
520 assert_eq!(t("java.lang.Thread.<init>"), MemberTarget { class: Some("java.lang.Thread"), name: "<init>", signature: None });
521 assert_eq!(t("Build$VERSION.SDK_INT").class, Some("Build$VERSION"));
522 for name in ["get.*Id", "(get|set)Foo", "a.b", "exec"] {
524 assert_eq!(t(name), MemberTarget { class: None, name, signature: None }, "{name}");
525 }
526 assert!(looks_like_class("android.webkit.WebView") && looks_like_class("Lcom/a/B;"));
527 assert!(!looks_like_class("loadUrl") && !looks_like_class("com.a.b") && !looks_like_class("Lc;->m"));
528 }
529
530 #[test]
531 fn member_queries_from_any_spelling_agree() {
532 let want = MemberQuery::default().with_class(precise("Landroid/webkit/WebView;")).with_name(Some("loadUrl".into()));
533 for spec in [
534 "Landroid/webkit/WebView;->loadUrl",
535 "android.webkit.WebView.loadUrl",
536 "android.webkit.WebView->loadUrl",
537 "android.webkit.WebView#loadUrl",
538 ] {
539 assert_eq!(member_query("method", None, false, Some(spec), false).unwrap(), want, "{spec}");
540 }
541 assert_eq!(member_query("method", Some("Landroid/webkit/WebView;->loadUrl"), false, None, false).unwrap(), want);
543 assert_eq!(member_query("method", Some("android.webkit.WebView"), false, Some("loadUrl"), false).unwrap(), want);
544 let class_only = member_query("method", None, false, Some("android.webkit.WebView"), false).unwrap();
545 assert_eq!(class_only, MemberQuery::default().with_class(precise("Landroid/webkit/WebView;")));
546 let q = member_query("method", None, false, Some("Lc;->run(I)V"), false).unwrap();
548 assert_eq!((q.name.as_deref(), q.exact_name), (Some("run"), true));
549 let q = member_query("field", Some("Lx;"), false, Some("Lc;->f:I"), false).unwrap();
550 assert_eq!((q.class, q.name.as_deref(), q.exact_name), (precise("Lx;"), Some("f"), true));
551 let q = member_query("method", None, false, Some("~Foo->=bar"), false).unwrap();
552 assert_eq!(
553 q,
554 MemberQuery::default().with_class(Some(ClassQuery::fuzzy("Foo"))).with_name(Some("bar".into())).with_exact_name(true)
555 );
556 let q = member_query("method", None, false, Some("WebView.loadUrl"), false).unwrap();
558 assert_eq!(q.class, Some(ClassQuery::fuzzy("[/L]WebView;\\x00")));
559 let e = member_query("method", None, false, Some(" "), false).unwrap_err().to_string();
560 assert!(e.contains("needs a class or a method name"), "{e}");
561 }
562
563 #[test]
564 fn kinds_tolerate_case_plurals_and_synonyms() {
565 for (k, want) in [
566 ("Methods", "method"),
567 ("strings", "string"),
568 ("TYPE", "type"),
569 ("implements", "impl"),
570 ("classes", "type"),
571 ("fields", "field"),
572 ("literal", "literal"),
573 ] {
574 assert_eq!(parse_kind(k).unwrap(), want, "{k}");
575 }
576 let e = parse_kind("metod").unwrap_err().to_string();
577 assert!(e.contains("unknown kind \"metod\"") && e.contains("closest: method"), "{e}");
578 assert_eq!(
579 parse_condition("Methods:android.webkit.WebView.loadUrl").unwrap(),
580 parse_condition("method:Landroid/webkit/WebView;->loadUrl").unwrap()
581 );
582 assert!(matches!(parse_rule("string:a & method:b").unwrap(), Query::All { all, .. } if all.len() == 2));
583 }
584
585 #[test]
586 fn signatures_in_dalvik_and_java_form() {
587 let d = |s: &str| TypeMatch::Descriptor(s.into());
588 let method = |params: Vec<TypeMatch>, ret: Option<TypeMatch>| MemberSignature::Method { params, ret };
589 let sig = |s| parse_signature(s, true).unwrap();
590 assert_eq!(sig("(I)V"), method(vec![d("I")], Some(d("V"))));
591 assert_eq!(sig("(I)"), method(vec![d("I")], None));
592 assert_eq!(sig("()"), method(vec![], None));
593 assert_eq!(sig("(ILjava/lang/String;[B)Z"), method(vec![d("I"), d("Ljava/lang/String;"), d("[B")], Some(d("Z"))));
594 assert_eq!(sig("(int)"), method(vec![d("I")], None));
595 assert_eq!(sig("(int, java.lang.String[]) void"), method(vec![d("I"), d("[Ljava/lang/String;")], Some(d("V"))));
596 assert_eq!(
597 sig("(String, Map[][])"),
598 method(vec![TypeMatch::Simple { name: "String".into(), dims: 0 }, TypeMatch::Simple { name: "Map".into(), dims: 2 }], None)
599 );
600 assert_eq!(parse_signature(":I", false).unwrap(), MemberSignature::Field(d("I")));
601 assert_eq!(parse_signature(":java.lang.String", false).unwrap(), MemberSignature::Field(d("Ljava/lang/String;")));
602 for (bad, method) in [("(I", true), ("(%)V", true), (":I", true), ("(I)V", false), (":", false)] {
603 let e = parse_signature(bad, method).unwrap_err().to_string();
604 assert!(e.contains("bad signature"), "{bad}: {e}");
605 }
606 for spec in ["Lc;->run(I)V", "c.run(int)void", "Lc;->run(I)V "] {
608 let q = member_query("method", None, false, Some(spec), false).unwrap();
609 assert_eq!(q.signature, Some(method(vec![d("I")], Some(d("V")))), "{spec}");
610 }
611 assert_eq!(
612 member_query("method", None, false, Some("WebView.loadUrl(String)"), false).unwrap().signature.unwrap().to_string(),
613 "(String)"
614 );
615 assert_eq!(member_query("method", None, false, Some("loadUrl"), false).unwrap().signature, None);
616 assert!(member_query("field", None, false, Some("Lc;->f(I)V"), false).is_err());
617 assert_eq!(
618 parse_condition("method:Lc;->m(I)").unwrap(),
619 Query::Method(member_query("method", None, false, Some("Lc;->m(I)"), false).unwrap())
620 );
621 }
622
623 #[test]
624 fn java_signatures_and_edit_distance() {
625 assert_eq!(java_signature_to_dalvik("loadUrl(java.lang.String, int)").as_deref(), Some("loadUrl(Ljava/lang/String;I)"));
626 assert_eq!(java_signature_to_dalvik("f(byte[], Lx;) void").as_deref(), Some("f([BLx;)V"));
627 assert_eq!(java_signature_to_dalvik("run()").as_deref(), Some("run()"));
628 assert_eq!(java_signature_to_dalvik("run"), None);
629 assert_eq!(java_signature_to_dalvik("f(int [] [])").as_deref(), Some("f([[I)"));
630 let deep = format!("f(int{})", "[]".repeat(1_000_000));
632 assert_eq!(java_signature_to_dalvik(&deep).map(|s| s.len()), Some("f()I".len() + 1_000_000));
633 assert_eq!(edit_distance("kitten", "sitting", 5), Some(3));
634 assert_eq!(edit_distance("kitten", "sitting", 2), None);
635 assert_eq!(edit_distance("a", "a", 0), Some(0));
636 }
637}