Skip to content

Commit

Permalink
Create SourceFile lazily through cache
Browse files Browse the repository at this point in the history
  • Loading branch information
sigurdm committed Sep 26, 2023
1 parent 1bc4462 commit 5e7ee25
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 21 deletions.
57 changes: 42 additions & 15 deletions lib/src/validator/leak_detection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ import 'package:source_span/source_span.dart';
import '../ignore.dart';
import '../validator.dart';

/// [Utf8Codec] which allows malformed strings.
const _utf8AllowMalformed = Utf8Codec(allowMalformed: true);
/// All recognized secrets fit in ASCII (first seven bits). So for speed we
/// decode as ASCII.
const _asciiAllowInvalid = AsciiCodec(allowInvalid: true);

/// Link to the documentation for the `false_secrets` key in `pubspec.yaml`.
const _falseSecretsDocumentationLink = 'https://dart.dev/go/false-secrets';

/// A validator that validates attempts to find secrets that are about to be
/// accidentally leaked.
final class LeakDetectionValidator extends Validator {
// Creating a [SourceFile] is expensive, so we only do it on demand.
final sourceFileCache = <String, SourceFile>{};

@override
Future<void> validate() async {
// Load `false_secrets` from `pubspec.yaml`.
Expand All @@ -48,15 +52,15 @@ final class LeakDetectionValidator extends Validator {
// On Windows, we can't open some files without normalizing them
final file = File(p.normalize(p.absolute(f)));
text = await pool.withResource(
() async => await file.readAsString(encoding: _utf8AllowMalformed),
() async => await file.readAsString(encoding: _asciiAllowInvalid),
);
} on IOException {
// Pass, ignore files we can't read, let something else error later!
return <LeakMatch>[];
}

return leakPatterns
.map((p) => p.findPossibleLeaks(relPath, text))
.map((p) => p.findPossibleLeaks(relPath, text, sourceFileCache))
.expand((i) => i);
}),
).then((lists) => lists.expand((i) => i).toList());
Expand All @@ -70,11 +74,8 @@ final class LeakDetectionValidator extends Validator {
if (leaks.length > 3) {
errors.addAll(leaks.take(2).map((leak) => leak.toString()));

final files = leaks
.map((leak) => leak.span.sourceUrl!.toFilePath(windows: false))
.toSet()
.toList(growable: false)
..sort();
final files =
leaks.map((leak) => leak.url).toSet().toList(growable: false)..sort();
final s = files.length > 1 ? 's' : '';

errors.add(
Expand Down Expand Up @@ -105,14 +106,33 @@ final class LeakDetectionValidator extends Validator {

/// Instance of a match against a [LeakPattern].
final class LeakMatch {
final Map<String, SourceFile> sourceFileCache;

final LeakPattern pattern;
final SourceSpan span;

LeakMatch(this.pattern, this.span);
final String content;
final String url;
final int start;
final int end;

SourceSpan span() {
final sourceFile =
sourceFileCache[url] ??= SourceFile.fromString(content, url: url);
return sourceFile.span(start, end);
}

LeakMatch(
this.pattern, {
required this.url,
required this.content,
required this.start,
required this.end,
required this.sourceFileCache,
});

@override
String toString() =>
span.message('Potential leak of ${pattern.kind} detected.');
span().message('Potential leak of ${pattern.kind} detected.');
}

/// Definition of a pattern for detecting accidentally leaked secrets.
Expand Down Expand Up @@ -170,7 +190,11 @@ final class LeakPattern {
/// * no pattern in [_allowed] is matched,
/// * Captured group have a entropy higher than [_entropyThresholds] requires
/// for the given _group identifier_, and,
Iterable<LeakMatch> findPossibleLeaks(String file, String content) sync* {
Iterable<LeakMatch> findPossibleLeaks(
String file,
String content,
Map<String, SourceFile> sourceFileCache,
) sync* {
for (final m in _pattern.allMatches(content)) {
if (_allowed.any((s) => m.group(0)!.contains(s))) {
continue;
Expand All @@ -179,10 +203,13 @@ final class LeakPattern {
.any((entry) => _entropy(m.group(entry.key)!) < entry.value)) {
continue;
}
final source = SourceFile.fromString(content, url: file);
yield LeakMatch(
this,
source.span(m.start, m.start + m.group(0)!.length),
url: file,
content: content,
start: m.start,
end: m.start + m.group(0)!.length,
sourceFileCache: sourceFileCache,
);
}
}
Expand Down
16 changes: 10 additions & 6 deletions test/validator/leak_detection_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,22 @@ void main() {
group('for "${pattern.kind}"', () {
for (var i = 0; i < pattern.testsWithLeaks.length; i++) {
test('finds leak in testWithLeaks[$i]', () {
final leaks = pattern
.findPossibleLeaks('source.dart', pattern.testsWithLeaks[i])
.toList(growable: false);
final leaks = pattern.findPossibleLeaks(
'source.dart',
pattern.testsWithLeaks[i],
{},
).toList(growable: false);
expect(leaks, hasLength(equals(1)));
});
}

for (var i = 0; i < pattern.testsWithNoLeaks.length; i++) {
test('finds no leak in testsWithNoLeaks[$i]', () {
final leaks = pattern
.findPossibleLeaks('source.dart', pattern.testsWithNoLeaks[i])
.toList(growable: false);
final leaks = pattern.findPossibleLeaks(
'source.dart',
pattern.testsWithNoLeaks[i],
{},
).toList(growable: false);
expect(leaks, isEmpty);
});
}
Expand Down

0 comments on commit 5e7ee25

Please sign in to comment.