Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 27 additions & 27 deletions app/exec/extension/_lib/vsix-manifest-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ export class VsixManifestBuilder extends ManifestBuilder {

// The vsixmanifest will be responsible for generating the [Content_Types].xml file
// Obviously this is kind of strange, but hey ho.
return this.genContentTypesXml(builders).then(result => {
return this.genContentTypesXml(builders, files).then(result => {
this.addFile({
path: null,
content: result,
Expand All @@ -585,7 +585,7 @@ export class VsixManifestBuilder extends ManifestBuilder {
* This xml contains a <Default> entry for each different file extension
* found in the package, mapping it to the appropriate MIME type.
*/
private genContentTypesXml(builders: ManifestBuilder[]): Promise<string> {
private genContentTypesXml(builders: ManifestBuilder[], packageFiles: PackageFiles): Promise<string> {
let typeMap = VsixManifestBuilder.CONTENT_TYPE_MAP;
trace.debug("Generating [Content_Types].xml");
let contentTypes: any = {
Expand All @@ -605,31 +605,31 @@ export class VsixManifestBuilder extends ManifestBuilder {
let contentTypePromises: Promise<any>[] = [];
let extensionlessFiles = [];
let uniqueExtensions = _.uniq<string>(
Object.keys(this.files).map(f => {
let extName = path.extname(f) || path.extname(this.files[f].partName);
Object.keys(packageFiles).map(f => {
let extName = path.extname(f) || path.extname(packageFiles[f].partName);
const filename = path.basename(f);

// Look in the best guess table. Or, default to text/plain if the file starts with a "."
const bestGuess =
VsixManifestBuilder.BEST_GUESS_CONTENT_TYPES[filename.toUpperCase()] ||
(filename[0] === "." ? "text/plain" : null);
if (!extName && !this.files[f].contentType && this.files[f].addressable && !bestGuess) {
if (!extName && !packageFiles[f].contentType && packageFiles[f].addressable && !bestGuess) {
trace.warn(
"File %s does not have an extension, and its content-type is not declared. Defaulting to application/octet-stream.",
path.resolve(f),
);
this.files[f].contentType = "application/octet-stream";
packageFiles[f].contentType = "application/octet-stream";
} else if (bestGuess) {
this.files[f].contentType = bestGuess;
packageFiles[f].contentType = bestGuess;
}
if (this.files[f].contentType) {
if (packageFiles[f].contentType) {
// If there is an override for this file, ignore its extension
return "";
}

// Later, we will show warnings for extensions with unknown content types if there
// was at least one file with this extension that was addressable.
if (!showWarningForExtensionMap[extName] && this.files[f].addressable) {
if (!showWarningForExtensionMap[extName] && packageFiles[f].addressable) {
showWarningForExtensionMap[extName] = true;
}
return extName.toLowerCase();
Expand Down Expand Up @@ -699,9 +699,9 @@ export class VsixManifestBuilder extends ManifestBuilder {

let contentTypePromises: Promise<any>[] = [];
let extTypeCounter: { [ext: string]: { [type: string]: string[] } } = {};
Object.keys(this.files)
Object.keys(packageFiles)
.filter(fileName => {
return !this.files[fileName].contentType;
return !packageFiles[fileName].contentType;
})
.forEach(fileName => {
let extension = path.extname(fileName).toLowerCase();
Expand All @@ -721,10 +721,10 @@ export class VsixManifestBuilder extends ManifestBuilder {
let child = childProcess.exec('file --mime-type "' + fileName + '"', (err, stdout, stderr) => {
try {
if (err) {
if (this.files[fileName].addressable) {
if (packageFiles[fileName].addressable) {
reject(err);
} else {
this.files[fileName].contentType = "application/octet-stream";
packageFiles[fileName].contentType = "application/octet-stream";
}
} else {
if (typeof stdout === "string") {
Expand All @@ -741,25 +741,25 @@ export class VsixManifestBuilder extends ManifestBuilder {
}
hitCounters[magicMime].push(fileName);
} else {
if (!this.files[fileName].contentType) {
this.files[fileName].contentType = magicMime;
if (!packageFiles[fileName].contentType) {
packageFiles[fileName].contentType = magicMime;
}
}
} else {
if (stderr) {
if (this.files[fileName].addressable) {
if (packageFiles[fileName].addressable) {
reject(stderr);
} else {
this.files[fileName].contentType = "application/octet-stream";
packageFiles[fileName].contentType = "application/octet-stream";
}
} else {
if (this.files[fileName].addressable) {
if (packageFiles[fileName].addressable) {
trace.warn(
"Could not determine content type for %s. Defaulting to application/octet-stream. To override this, add a contentType property to this file entry in the manifest.",
fileName,
);
}
this.files[fileName].contentType = "application/octet-stream";
packageFiles[fileName].contentType = "application/octet-stream";
}
}
}
Expand All @@ -781,7 +781,7 @@ export class VsixManifestBuilder extends ManifestBuilder {
return;
}
hitCounts[type].forEach(fileName => {
this.files[fileName].contentType = type;
packageFiles[fileName].contentType = type;
});
});
contentTypes.Types.Default.push({
Expand All @@ -795,25 +795,25 @@ export class VsixManifestBuilder extends ManifestBuilder {
}
return contentTypePromise.then(() => {
let seenPartNames = new Set();
Object.keys(this.files).forEach(filePath => {
if (this.files[filePath].contentType) {
let partName = "/" + toZipItemName(this.files[filePath].partName);
Object.keys(packageFiles).forEach(filePath => {
if (packageFiles[filePath].contentType) {
let partName = "/" + toZipItemName(packageFiles[filePath].partName);
if (!seenPartNames.has(partName)) {
contentTypes.Types.Override.push({
$: {
ContentType: this.files[filePath].contentType,
ContentType: packageFiles[filePath].contentType,
PartName: partName,
},
});
seenPartNames.add(partName);
}
if ((this.files[filePath] as any)._additionalPackagePaths) {
for (const additionalPath of (this.files[filePath] as any)._additionalPackagePaths) {
if ((packageFiles[filePath] as any)._additionalPackagePaths) {
for (const additionalPath of (packageFiles[filePath] as any)._additionalPackagePaths) {
let additionalPartName = "/" + toZipItemName(additionalPath);
if (!seenPartNames.has(additionalPartName)) {
contentTypes.Types.Override.push({
$: {
ContentType: this.files[filePath].contentType,
ContentType: packageFiles[filePath].contentType,
PartName: additionalPartName,
},
});
Expand Down
95 changes: 95 additions & 0 deletions tests/content-types-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import assert = require('assert');
import path = require('path');
import fs = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AdmZip = require('adm-zip');
import { execAsyncWithLogging } from './test-utils/debug-exec';

// Basic test framework functions
declare function describe(name: string, fn: Function): void;
declare function it(name: string, fn: Function): void;
declare function after(fn: Function): void;

const tfxPath = path.resolve(__dirname, '../../_build/tfx-cli.js');
const samplesPath = path.resolve(__dirname, '../extension-samples');

describe('Content_Types.xml Generation', function() {
this.timeout(30000);

after(function() {
// cleanup - remove generated .vsix files
const testExtensionPath = path.join(samplesPath, 'extensionless-exec-test');
const testVsixPath = path.join(testExtensionPath, 'test.vsix');
try {
if (fs.existsSync(testVsixPath)) {
fs.unlinkSync(testVsixPath);
}
} catch (e) {
// Ignore cleanup errors
}
});

it('should include extensionless executable in Content_Types.xml Override section', function(done) {
const testExtensionPath = path.join(samplesPath, 'extensionless-exec-test');
const outputPath = path.join(testExtensionPath, 'test.vsix');

// Verify the test extension exists
if (!fs.existsSync(testExtensionPath)) {
done(new Error('Test extension directory not found: ' + testExtensionPath));
return;
}

execAsyncWithLogging(`node "${tfxPath}" extension create --root "${testExtensionPath}" --output-path "${outputPath}"`, 'extension create with extensionless executable')
.then(({ stdout }) => {
// Verify .vsix was created
assert(fs.existsSync(outputPath), 'Should create .vsix file');

// Extract and check Content_Types.xml
const zip = new AdmZip(outputPath);
const zipEntries = zip.getEntries();

// Find Content_Types.xml
let contentTypesEntry = null;
for (const entry of zipEntries) {
if (entry.entryName === '[Content_Types].xml') {
contentTypesEntry = entry;
break;
}
}

assert(contentTypesEntry, 'Should contain [Content_Types].xml');

// Read and parse Content_Types.xml
const contentTypesXml = contentTypesEntry.getData().toString('utf8');
console.log('Content_Types.xml content:\n', contentTypesXml);

// Check that linux-executable is included as an Override element
// The file should be listed with its full path
assert(
contentTypesXml.includes('linux-executable') || contentTypesXml.includes('Override'),
'Content_Types.xml should contain Override elements for extensionless files'
);

// More specific check: look for the Override element with linux-executable
// The path should match the file location in the package
const overridePattern = /Override.*PartName="[^"]*linux-executable[^"]*"/;
const hasOverrideForExecutable = overridePattern.test(contentTypesXml);

assert(
hasOverrideForExecutable,
'Content_Types.xml should have Override element with PartName containing linux-executable.\nContent: ' + contentTypesXml
);

// Verify that the Override element has a ContentType attribute
if (contentTypesXml.includes('linux-executable')) {
assert(
contentTypesXml.match(/ContentType="[^"]+"/),
'Override element should have ContentType attribute'
);
}

done();
})
.catch(done);
});
});
3 changes: 3 additions & 0 deletions tests/extension-samples/extensionless-exec-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Extensionless Executable Test

This extension contains a Linux executable without a file extension.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
echo "This is a Linux executable without a file extension"
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"manifestVersion": 1,
"id": "extensionless-exec-test",
"name": "Extensionless Executable Test",
"version": "1.0.0",
"publisher": "test-publisher",
"description": "Test extension with extensionless Linux executable",
"categories": [
"Azure Pipelines"
],
"targets": [
{
"id": "Microsoft.VisualStudio.Services"
}
],
"files": [
{
"path": "linux-executable",
"addressable": true
},
{
"path": "README.md",
"addressable": true
}
]
}