Skip to content

Commit 131201c

Browse files
Fix: Symlink Resolution for Cross-Platform Copy Operations (#1103)
* fix: resolve symlinks relative to their target to handle relative paths on Linux * minor version update * added unit test to copy symlink (created using relative path) to a file * minor text update * test: verify relative and absolute symlink resolution in recursive directory copy
1 parent b5a0010 commit 131201c

File tree

4 files changed

+41
-7
lines changed

4 files changed

+41
-7
lines changed

node/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "azure-pipelines-task-lib",
3-
"version": "5.1.0",
3+
"version": "5.2.0",
44
"description": "Azure Pipelines Task SDK",
55
"main": "./task.js",
66
"typings": "./task.d.ts",

node/task.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,7 +1188,8 @@ export function cp(sourceOrOptions: unknown, destinationOrSource: string, option
11881188

11891189
try {
11901190
if (lstatSource.isSymbolicLink()) {
1191-
source = fs.readlinkSync(source);
1191+
const symlinkTarget = fs.readlinkSync(source);
1192+
source = path.resolve(path.dirname(source), symlinkTarget);
11921193
lstatSource = fs.lstatSync(source);
11931194
}
11941195
if (lstatSource.isFile()) {
@@ -1226,7 +1227,8 @@ const copyDirectoryWithResolvedSymlinks = (src: string, dest: string, force: boo
12261227

12271228
if (entry.isSymbolicLink()) {
12281229
// Resolve the symbolic link and copy the target
1229-
const resolvedPath = fs.readlinkSync(srcPath);
1230+
const symlinkTarget = fs.readlinkSync(srcPath);
1231+
const resolvedPath = path.resolve(path.dirname(srcPath), symlinkTarget);
12301232
const stat = fs.lstatSync(resolvedPath);
12311233

12321234
if (stat.isFile()) {

node/test/cp.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,23 @@ describe('cp cases', () => {
1717
const TEST_SRC_DIR = 'test-src';
1818
const TEST_DEST_DIR = 'test-dest';
1919
const OUTSIDE_FILE = path.resolve(DIRNAME, 'outside-file.txt');
20-
const SYMLINK_NAME = 'symlink-outside.txt';
20+
const RELATIVE_TARGET_FILE = path.resolve(DIRNAME, 'outside-file2.txt');
21+
const ABSOLUTE_SYMLINK = 'symlink-outside.txt';
22+
const RELATIVE_SYMLINK = 'symlink-outside2.txt';
2123

2224
before((done) => {
2325
tl.mkdirP(TEMP_DIR_1);
2426
tl.mkdirP(TEMP_DIR_2);
2527
fs.mkdirSync(TEST_SRC_DIR, { recursive: true });
26-
const symlinkPath = path.join(TEST_SRC_DIR, SYMLINK_NAME);
28+
const symlinkPath = path.join(TEST_SRC_DIR, ABSOLUTE_SYMLINK);
2729
fs.writeFileSync(OUTSIDE_FILE, 'This is a file outside the source folder.');
30+
fs.writeFileSync(RELATIVE_TARGET_FILE, 'This is the second file outside the source folder.');
2831
fs.symlinkSync(OUTSIDE_FILE, symlinkPath);
2932
fs.mkdirSync(TEST_DEST_DIR, { recursive: true });
3033
fs.writeFileSync(TEMP_DIR_2_FILE_1, 'file1');
34+
const symlinkPath2 = path.join(TEST_SRC_DIR, RELATIVE_SYMLINK);
35+
const targetPath = path.relative(TEST_SRC_DIR, RELATIVE_TARGET_FILE);
36+
fs.symlinkSync(targetPath, symlinkPath2, 'file');
3137

3238
try {
3339
testutil.initialize();
@@ -154,10 +160,36 @@ describe('cp cases', () => {
154160

155161
tl.cp(TEST_SRC_DIR, TEST_DEST_DIR, '-r', false, 0);
156162

163+
// Verify first symlink
157164
assert(fs.existsSync(path.join(TEST_DEST_DIR, TEST_SRC_DIR)), 'Directory was not copied');
158165
assert(fs.existsSync(path.join(TEST_DEST_DIR, TEST_SRC_DIR, 'outside-file.txt')), 'File was not copied');
159166
assert.equal(fs.readFileSync(path.join(TEST_DEST_DIR, TEST_SRC_DIR, 'outside-file.txt'), 'utf8'), 'This is a file outside the source folder.', 'File content is incorrect');
160-
assert(!fs.existsSync(path.join(TEST_DEST_DIR, TEST_SRC_DIR, SYMLINK_NAME)), 'Symbolic link should not be copied');
167+
assert(!fs.existsSync(path.join(TEST_DEST_DIR, TEST_SRC_DIR, ABSOLUTE_SYMLINK)), 'First symbolic link should not be copied');
168+
169+
// Verify second symlink with relative path
170+
assert(fs.existsSync(path.join(TEST_DEST_DIR, TEST_SRC_DIR, 'outside-file2.txt')), 'Second file was not copied');
171+
assert.equal(
172+
fs.readFileSync(path.join(TEST_DEST_DIR, TEST_SRC_DIR, 'outside-file2.txt'), 'utf8'),
173+
'This is the second file outside the source folder.',
174+
'Second file content is incorrect'
175+
);
176+
assert(!fs.existsSync(path.join(TEST_DEST_DIR, TEST_SRC_DIR, RELATIVE_SYMLINK)), 'Second symbolic link should not be copied');
177+
178+
done();
179+
});
180+
181+
it('copy symlink pointing to file where symlink is created using relative path to target file', (done) => {
182+
const rootSymlinkName = 'root-symlink-to-temp2-file';
183+
const symlinkPath = path.join(DIRNAME, rootSymlinkName);
184+
const targetPath = path.relative(DIRNAME, TEMP_DIR_2_FILE_1);
185+
186+
fs.symlinkSync(targetPath, symlinkPath, 'file');
187+
188+
const destPath = path.join(TEST_DEST_DIR, rootSymlinkName);
189+
tl.cp(symlinkPath, destPath);
190+
assert(fs.existsSync(destPath), 'Destination file was not created');
191+
assert(!fs.lstatSync(destPath).isSymbolicLink(), 'Destination should not be a symlink');
192+
assert.equal(fs.readFileSync(destPath, 'utf8'), 'file1', 'File content does not match source');
161193

162194
done();
163195
});

0 commit comments

Comments
 (0)