blob: 80e48a0178a9465a89f9f6104f7acc72f5e682ab [file] [log] [blame] [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.
import 'package:dartdoc/src/model/documentation_comment.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
void main() {
group('DocumentationComment.resolveExamplePath', () {
late p.Context pathContext;
late String packagePath;
setUp(() {
// Set current to a directory far away from the package root to ensure
// resolution does not accidentally depend on it.
pathContext = p.Context(
style: p.Style.posix, current: '/completely/different/path');
packagePath = '/project';
});
String? resolve(String filepath, {String? sourceFileName}) {
return DocumentationComment.resolveExamplePath(
filepath,
packagePath: packagePath,
sourceFilePath: sourceFileName,
pathContext: pathContext,
warn: (kind, {message}) {},
);
}
test('absolute path from root', () {
expect(resolve('/examples/hello.dart'), '/project/examples/hello.dart');
});
test('relative path from root library', () {
expect(
resolve('examples/hello.dart', sourceFileName: '/project/lib/a.dart'),
'/project/lib/examples/hello.dart',
);
});
test('relative path with ..', () {
expect(
resolve('../examples/hello.dart',
sourceFileName: '/project/lib/a.dart'),
'/project/examples/hello.dart',
);
});
test('relative path from nested library', () {
expect(
resolve('util.dart', sourceFileName: '/project/lib/src/a.dart'),
'/project/lib/src/util.dart',
);
});
test('relative path with null sourceFileName (resolved from root)', () {
expect(
resolve('examples/hello.dart', sourceFileName: null),
'/project/examples/hello.dart',
);
});
test('path with complex URI encoded characters', () {
expect(
resolve('/examples/100%25effective.dart'),
'/project/examples/100%effective.dart',
);
expect(
resolve('/examples/a%5cb.dart'),
'/project/examples/a\\b.dart',
);
expect(
resolve('/examples/a%20b.dart'),
'/project/examples/a b.dart',
);
});
test('path with fragment and query (ignored)', () {
expect(
resolve('/examples/hello.dart?q=1#region'),
'/project/examples/hello.dart',
);
expect(
resolve('/examples/hello.dart?#'),
'/project/examples/hello.dart',
);
});
test('special characters in package and source paths', () {
var specialContext = p.Context(style: p.Style.posix, current: '/other');
var specialPackagePath = '/project %25 space';
var specialSourcePath = '/project %25 space/lib/a.dart';
expect(
DocumentationComment.resolveExamplePath(
'hello.dart',
packagePath: specialPackagePath,
sourceFilePath: specialSourcePath,
pathContext: specialContext,
warn: (kind, {message}) {},
),
'/project %25 space/lib/hello.dart',
);
});
test('clamping: relative traversal cannot escape root', () {
expect(
resolve('../../../examples/hello.dart',
sourceFileName: '/project/lib/a.dart'),
'/project/examples/hello.dart',
);
});
test('clamping: absolute traversal cannot escape root', () {
expect(
resolve('/../outside.dart'),
'/project/outside.dart',
);
});
test('absolute URI (rejected)', () {
expect(
resolve('https://example.com/hello.dart'),
isNull,
);
});
test('path with backslashes', () {
// Uri.resolve converts backslashes to forward slashes.
var result = resolve('examples\\hello.dart',
sourceFileName: '/project/lib/a.dart');
expect(result, '/project/lib/examples/hello.dart');
});
test('Windows-style absolute paths and resolution', () {
var windowsContext =
p.Context(style: p.Style.windows, current: r'C:\project');
var windowsPackagePath = r'C:\project';
String? resolveWindows(String filepath, {String? sourceFileName}) {
return DocumentationComment.resolveExamplePath(
filepath,
packagePath: windowsPackagePath,
sourceFilePath: sourceFileName,
pathContext: windowsContext,
warn: (kind, {message}) {},
);
}
expect(
resolveWindows('hello.dart', sourceFileName: r'C:\project\lib\a.dart'),
r'C:\project\lib\hello.dart',
);
expect(
resolveWindows('../examples/hello.dart',
sourceFileName: r'C:\project\lib\a.dart'),
r'C:\project\examples\hello.dart',
);
expect(
resolveWindows('../../../outside.dart',
sourceFileName: r'C:\project\lib\a.dart'),
r'C:\project\outside.dart',
);
});
group('unusual path inputs (no crash)', () {
var inputs = [
'\x00',
' ',
'%%',
'',
'..',
'//',
r'\..\..\',
'C:\\Windows\\System32',
'http://[::1]/',
'a' * 10000,
];
for (var input in inputs) {
test(': $input', () {
DocumentationComment.resolveExamplePath(
input,
packagePath: packagePath,
sourceFilePath: '/project/lib/a.dart',
pathContext: pathContext,
warn: (kind, {message}) {},
);
});
}
});
group('invalid URI inputs (trigger warnings)', () {
var inputs = [
': [', // Invalid URI
'http://[::1]]', // Invalid IPv6
'/%ff%fe%fd', // Invalid UTF-8 encoding
];
for (var input in inputs) {
test(': $input', () {
var warned = false;
DocumentationComment.resolveExamplePath(
input,
packagePath: packagePath,
sourceFilePath: '/project/lib/a.dart',
pathContext: pathContext,
warn: (kind, {message}) => warned = true,
);
expect(warned, isTrue,
reason: 'Expected a warning for input "$input"');
});
}
});
group('rejects paths with schemes or authorities (trigger warnings)', () {
var inputs = [
'file:///something',
'file:///etc/passwd',
'http://example.com/script.dart',
'https://pub.dev/packages',
];
for (var input in inputs) {
test('rejects: $input', () {
var warned = false;
DocumentationComment.resolveExamplePath(
input,
packagePath: packagePath,
sourceFilePath: '/project/lib/a.dart',
pathContext: pathContext,
warn: (kind, {message}) {
warned = true;
expect(message,
contains('Schemes and authorities are not allowed.'));
},
);
expect(warned, isTrue,
reason: 'Expected a warning for input "$input"');
});
}
});
});
}