parsePubspecLock function
Parses a pubspec.lock file's content into every resolved package,
sorted by name — this ordering is what makes the resulting
SbomDocument reproducible: the same lock file always produces
byte-identical JSON, regardless of the lock file's own key order or
any Map iteration order.
A line-based parser rather than a real YAML parser — deliberately:
this package has no yaml dependency (see pubspec.lock itself,
verified before writing this), and pubspec.lock's own shape (fixed
2-space-indented top-level package keys, fixed field names) is simple
enough that a small state machine is honest, not a fragile shortcut.
A line this parser doesn't recognize is skipped, never thrown on —
pub's own lock file format is a stable, first-party contract this
package doesn't control, so surviving a future minor format tweak
(an added field, say) matters more than being maximally strict.
Implementation
List<DependencyEntry> parsePubspecLock(String content) {
final entries = <DependencyEntry>[];
String? currentName;
DependencyType? currentType;
DependencySource? currentSource;
String? currentVersion;
void flush() {
if (currentName != null && currentType != null && currentVersion != null) {
entries.add(
DependencyEntry(
name: currentName!,
version: currentVersion!,
type: currentType!,
source: currentSource ?? DependencySource.unknown,
),
);
}
currentName = null;
currentType = null;
currentSource = null;
currentVersion = null;
}
final topLevelKey = RegExp(r'^ (\S+):\s*$');
final dependencyLine = RegExp(r'^\s{4}dependency:\s*(.+)$');
final sourceLine = RegExp(r'^\s{4}source:\s*(\S+)$');
final versionLine = RegExp(r'^\s{4}version:\s*"?([^"\n]+)"?$');
for (final line in content.split('\n')) {
final keyMatch = topLevelKey.firstMatch(line);
if (keyMatch != null) {
flush();
currentName = keyMatch.group(1);
continue;
}
final depMatch = dependencyLine.firstMatch(line);
if (depMatch != null) {
currentType = _parseDependencyType(depMatch.group(1)!.trim());
continue;
}
final sourceMatch = sourceLine.firstMatch(line);
if (sourceMatch != null) {
currentSource = _parseSource(sourceMatch.group(1)!.trim());
continue;
}
final versionMatch = versionLine.firstMatch(line);
if (versionMatch != null) {
currentVersion = versionMatch.group(1)!.trim();
continue;
}
}
flush();
entries.sort((a, b) => a.name.compareTo(b.name));
return entries;
}