blob: 5de987c68a1365dc5bcdba65f7ddb546a7615bd4 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Pub must never run git through a shell. On Windows it used to spawn
/// `cmd /c git ...`, where `cmd.exe` treats `&` in an argument as a command
/// separator. These tests only fail on Windows if that regresses, but they
/// pass on all platforms.
@TestOn('vm')
library;
import 'dart:io';
import 'package:pub/src/exit_codes.dart' as exit_codes;
import 'package:pub/src/path.dart';
import 'package:test/test.dart';
import '../../descriptor.dart' as d;
import '../../test_pub.dart';
/// A relative file name, so the arguments below contain no whitespace
/// regardless of where the sandbox lives (`dart:io` quotes arguments
/// containing whitespace, which would neutralize `&` in `cmd.exe`).
const _proofFile = 'proof.txt';
/// Fails if a file named [_proofFile] exists anywhere in the sandbox.
void _expectNoProofFile() {
expect(
Directory(d.sandbox)
.listSync(recursive: true)
.map((entity) => entity.path)
.where((path) => p.basename(path) == _proofFile),
isEmpty,
);
}
void main() {
setUp(ensureGit);
test(
'passes a git ref containing shell metacharacters verbatim to git',
() async {
await d.git('foo.git', [
d.libDir('foo'),
d.libPubspec('foo', '1.0.0'),
]).create();
const ref = 'HEAD&echo>$_proofFile';
await d
.appDir(
dependencies: {
'foo': {
'git': {'url': '../foo.git', 'ref': ref},
},
},
)
.create();
await pubGet(
error: contains("Could not find git ref '$ref'"),
exitCode: exit_codes.UNAVAILABLE,
);
_expectNoProofFile();
},
);
test(
'passes a git url containing shell metacharacters verbatim to git',
() async {
await d.git('foo.git', [
d.libDir('foo'),
d.libPubspec('foo', '1.0.0'),
]).create();
// `../foo.git` exists, `../foo.git&echo` does not. If `&` were
// interpreted by a shell, the clone of `../foo.git` would succeed.
await d
.appDir(
dependencies: {
'foo': {
'git': {'url': '../foo.git&echo'},
},
},
)
.create();
await pubGet(
error: allOf(
contains('foo.git&echo'),
contains('does not appear to be a git repository'),
),
exitCode: exit_codes.UNAVAILABLE,
);
_expectNoProofFile();
},
);
test('ignores a git batch file on PATH', () async {
// `cmd.exe` would prefer this `git.bat` over the real `git.exe` further
// down PATH. Pub resolves `git` directly to a `git.exe`.
await d.dir('bin', [
d.file('git.bat', '@echo off\r\necho pwned>"%~dp0$_proofFile"\r\n'),
]).create();
await d.git('foo.git', [
d.libDir('foo'),
d.libPubspec('foo', '1.0.0'),
]).create();
await d
.appDir(
dependencies: {
'foo': {'git': '../foo.git'},
},
)
.create();
await pubGet(
environment: {
'PATH': '${p.join(d.sandbox, 'bin')};${Platform.environment['PATH']}',
},
);
expect(packageSpec('foo'), isNotNull);
_expectNoProofFile();
}, skip: Platform.isWindows ? false : 'Batch files only exist on Windows');
test('explains an ignored git batch file when no git.exe is found', () async {
await d.dir('bin', [d.file('git.bat', '@echo off\r\n')]).create();
await d
.appDir(
dependencies: {
'foo': {'git': '../foo.git'},
},
)
.create();
await pubGet(
// Puts `bin/` first on PATH and removes directories containing a
// `git.exe`.
environment: extendedPathEnv(),
error: allOf(
contains('Cannot find a Git executable.'),
contains(
'Found "${p.join(d.sandbox, 'bin', 'git.bat')}", but only a '
'`git.exe` can be used',
),
),
exitCode: 1,
);
}, skip: Platform.isWindows ? false : 'Batch files only exist on Windows');
}