in app/exec/extension/_lib/vsix-manifest-builder.ts [588:840]
private genContentTypesXml(builders: ManifestBuilder[]): Promise<string> {
let typeMap = VsixManifestBuilder.CONTENT_TYPE_MAP;
trace.debug("Generating [Content_Types].xml");
let contentTypes: any = {
Types: {
$: {
xmlns: "http://schemas.openxmlformats.org/package/2006/content-types",
},
Default: [],
Override: [],
},
};
let windows = /^win/.test(process.platform);
let contentTypePromise;
const showWarningForExtensionMap: { [ext: string]: boolean } = {};
if (windows) {
// On windows, check HKCR to get the content type of the file based on the extension
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);
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) {
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";
} else if (bestGuess) {
this.files[f].contentType = bestGuess;
}
if (this.files[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) {
showWarningForExtensionMap[extName] = true;
}
return extName.toLowerCase();
}),
);
uniqueExtensions.forEach(ext => {
if (!ext.trim()) {
return;
}
if (typeMap[ext]) {
contentTypes.Types.Default.push({
$: {
Extension: ext,
ContentType: typeMap[ext],
},
});
return;
}
let hkcrKey = new winreg({
hive: winreg.HKCR,
key: "\\" + ext,
});
const regPromise = new Promise((resolve, reject) => {
hkcrKey.get("Content Type", (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
})
.then((type: winreg.RegistryItem) => {
trace.debug("Found content type for %s: %s.", ext, type.value);
let contentType = "application/octet-stream";
if (type) {
contentType = type.value;
}
return contentType;
})
.catch(err => {
if (showWarningForExtensionMap[ext]) {
trace.warn(
"Could not determine content type for extension %s. Defaulting to application/octet-stream. To override this, add a contentType property to this file entry in the manifest.",
ext,
);
}
return "application/octet-stream";
})
.then(contentType => {
contentTypes.Types.Default.push({
$: {
Extension: ext,
ContentType: contentType,
},
});
});
contentTypePromises.push(regPromise);
});
contentTypePromise = Promise.all(contentTypePromises);
} else {
// If not on windows, run the file --mime-type command to use magic to get the content type.
// If the file has an extension, rev a hit counter for that extension and the extension
// If there is no extension, create an <Override> element for the element
// For each file with an extension that doesn't match the most common type for that extension
// (tracked by the hit counter), create an <Override> element.
// Finally, add a <Default> element for each extension mapped to the most common type.
let contentTypePromises: Promise<any>[] = [];
let extTypeCounter: { [ext: string]: { [type: string]: string[] } } = {};
Object.keys(this.files)
.filter(fileName => {
return !this.files[fileName].contentType;
})
.forEach(fileName => {
let extension = path.extname(fileName).toLowerCase();
let mimePromise;
if (typeMap[extension]) {
if (!extTypeCounter[extension]) {
extTypeCounter[extension] = {};
}
if (!extTypeCounter[extension][typeMap[extension]]) {
extTypeCounter[extension][typeMap[extension]] = [];
}
extTypeCounter[extension][typeMap[extension]].push(fileName);
mimePromise = Promise.resolve(null);
return;
}
mimePromise = new Promise((resolve, reject) => {
let child = childProcess.exec('file --mime-type "' + fileName + '"', (err, stdout, stderr) => {
try {
if (err) {
if (this.files[fileName].addressable) {
reject(err);
} else {
this.files[fileName].contentType = "application/octet-stream";
}
} else {
if (typeof stdout === "string") {
let magicMime = _.trimEnd(stdout.substr(stdout.lastIndexOf(" ") + 1), "\n");
trace.debug("Magic mime type for %s is %s.", fileName, magicMime);
if (magicMime) {
if (extension) {
if (!extTypeCounter[extension]) {
extTypeCounter[extension] = {};
}
let hitCounters = extTypeCounter[extension];
if (!hitCounters[magicMime]) {
hitCounters[magicMime] = [];
}
hitCounters[magicMime].push(fileName);
} else {
if (!this.files[fileName].contentType) {
this.files[fileName].contentType = magicMime;
}
}
} else {
if (stderr) {
if (this.files[fileName].addressable) {
reject(stderr);
} else {
this.files[fileName].contentType = "application/octet-stream";
}
} else {
if (this.files[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";
}
}
}
}
resolve(null);
} catch (e) {
reject(e);
}
});
});
contentTypePromises.push(mimePromise);
});
contentTypePromise = Promise.all(contentTypePromises).then(() => {
Object.keys(extTypeCounter).forEach(ext => {
let hitCounts = extTypeCounter[ext];
let bestMatch = maxKey<string[]>(hitCounts, i => i.length);
Object.keys(hitCounts).forEach(type => {
if (type === bestMatch) {
return;
}
hitCounts[type].forEach(fileName => {
this.files[fileName].contentType = type;
});
});
contentTypes.Types.Default.push({
$: {
Extension: ext,
ContentType: bestMatch,
},
});
});
});
}
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);
if (!seenPartNames.has(partName)) {
contentTypes.Types.Override.push({
$: {
ContentType: this.files[filePath].contentType,
PartName: partName,
},
});
seenPartNames.add(partName);
}
if ((this.files[filePath] as any)._additionalPackagePaths) {
for (const additionalPath of (this.files[filePath] as any)._additionalPackagePaths) {
let additionalPartName = "/" + toZipItemName(additionalPath);
if (!seenPartNames.has(additionalPartName)) {
contentTypes.Types.Override.push({
$: {
ContentType: this.files[filePath].contentType,
PartName: additionalPartName,
},
});
seenPartNames.add(additionalPartName);
}
}
}
}
});
// Add the Default entries for manifests.
builders.forEach(builder => {
let manifestExt = path.extname(builder.getPath());
if (contentTypes.Types.Default.filter(t => t.$.Extension === manifestExt).length === 0) {
contentTypes.Types.Default.push({
$: {
Extension: manifestExt,
ContentType: builder.getContentType(),
},
});
}
});
return jsonToXml(contentTypes).replace(/\n/g, os.EOL);
});
}