diff --git a/package.json b/package.json index 1b76f00d2a..a9cff46205 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "got": "^11.8.6", "graceful-fs": "^4.2.11", "ignore": "^5.3.2", + "isbinaryfile": "^5.0.2", "jszip": "^3.10.1", "mime": "2.6.0", "minimatch": "^9.0.5", diff --git a/src/convert/replacements.ts b/src/convert/replacements.ts index cba3f2e23e..e583b32419 100644 --- a/src/convert/replacements.ts +++ b/src/convert/replacements.ts @@ -6,11 +6,12 @@ */ import { readFile } from 'node:fs/promises'; import { Transform, Readable } from 'node:stream'; -import { sep, posix, join, isAbsolute } from 'node:path'; +import { sep, posix, join, isAbsolute, extname } from 'node:path'; import { Lifecycle, Messages, SfError, SfProject } from '@salesforce/core'; import { minimatch } from 'minimatch'; import { Env } from '@salesforce/kit'; import { ensureString, isString } from '@salesforce/ts-types'; +import { isBinaryFileSync } from 'isbinaryfile'; import { SourcePath } from '../common/types'; import { SourceComponent } from '../resolve/sourceComponent'; import { MarkedReplacement, ReplacementConfig, ReplacementEvent } from './types'; @@ -20,14 +21,20 @@ const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sd const fileContentsCache = new Map(); +// First do a quick check for common text extensions +// If that fails, confirm that it is not a binary file +const textExtensions = new Set(['.cls', '.xml', '.json', '.js', '.css', '.html', '.htm', '.txt', '.md']); +const isTextFile = (path: string): boolean => textExtensions.has(extname(path)) || !isBinaryFileSync(path); + /** If a component has replacements, you get it piped through the replacementStream * Otherwise, you'll get the original readable stream + * Ignore binary files, they will get corrupted in the replacement process */ export const getReplacementStreamForReadable = ( component: SourceComponent, path: SourcePath ): Readable | ReplacementStream => - component.replacements?.[path] + component.replacements?.[path] && isTextFile(path) ? component.tree.stream(path).pipe(new ReplacementStream(component.replacements?.[path])) : component.tree.stream(path); diff --git a/test/collections/componentSet.test.ts b/test/collections/componentSet.test.ts index 0190af5541..9030c8408c 100644 --- a/test/collections/componentSet.test.ts +++ b/test/collections/componentSet.test.ts @@ -92,7 +92,7 @@ describe('ComponentSet', () => { { members: ['replaceStuff'], name: 'ApexClass' }, { members: ['TestObj__c.FieldA__c'], name: 'CustomField' }, { members: ['TestObj__c'], name: 'CustomObject' }, - { members: ['Test'], name: 'StaticResource' }, + { members: ['ImageTest', 'Test'], name: 'StaticResource' }, ], version, }); diff --git a/test/nuts/local/replacements/replacements.nut.ts b/test/nuts/local/replacements/replacements.nut.ts index bd58563bad..26faf35858 100644 --- a/test/nuts/local/replacements/replacements.nut.ts +++ b/test/nuts/local/replacements/replacements.nut.ts @@ -95,5 +95,29 @@ describe('e2e replacements test', () => { } } }); + it('skips images in static resources to prevent file corruption', async () => { + const srZipPath = path.join(session.project.dir, 'unzipped', 'staticresources', 'ImageTest.resource'); + expect(fs.existsSync(srZipPath)).to.be.true; + const srZip = await JSZip.loadAsync(fs.readFileSync(srZipPath)); + + // static resource zip should have 2 files: + // 1. test-image.png, 2. test-image.resource-meta.xml + expect(Object.entries(srZip.files).length).to.equal(2); + + const imageMeta = srZip.file('test-image.resource-meta.xml'); + if (imageMeta && !imageMeta.dir) { + const content = await imageMeta.async('nodebuffer'); + const imageMetaAsString = content.toString(); + expect(imageMetaAsString).to.not.include('placeholder'); + expect(imageMetaAsString).to.include('foo'); + } + + const image = srZip.file('test-image.png'); + if (image && !image.dir) { + const content = await image.async('nodebuffer'); + // The file size would be much larger if it was corrupted via the string replacement method + expect(content.byteLength).to.equal(1562121); + } + }); }); }); diff --git a/test/nuts/local/replacements/testProj/force-app/main/default/staticresources/ImageTest.resource-meta.xml b/test/nuts/local/replacements/testProj/force-app/main/default/staticresources/ImageTest.resource-meta.xml new file mode 100644 index 0000000000..1e72ff0a9b --- /dev/null +++ b/test/nuts/local/replacements/testProj/force-app/main/default/staticresources/ImageTest.resource-meta.xml @@ -0,0 +1,7 @@ + + + Private + application/zip + added from sfdx plugin + ImageTest + \ No newline at end of file diff --git a/test/nuts/local/replacements/testProj/force-app/main/default/staticresources/ImageTest/test-image.png b/test/nuts/local/replacements/testProj/force-app/main/default/staticresources/ImageTest/test-image.png new file mode 100644 index 0000000000..61f1e126b5 Binary files /dev/null and b/test/nuts/local/replacements/testProj/force-app/main/default/staticresources/ImageTest/test-image.png differ diff --git a/test/nuts/local/replacements/testProj/force-app/main/default/staticresources/ImageTest/test-image.resource-meta.xml b/test/nuts/local/replacements/testProj/force-app/main/default/staticresources/ImageTest/test-image.resource-meta.xml new file mode 100644 index 0000000000..a8dd419d4b --- /dev/null +++ b/test/nuts/local/replacements/testProj/force-app/main/default/staticresources/ImageTest/test-image.resource-meta.xml @@ -0,0 +1,6 @@ + + + Private + image/png + Test placeholder + diff --git a/test/nuts/local/replacements/testProj/sfdx-project.json b/test/nuts/local/replacements/testProj/sfdx-project.json index 469a60cbf0..2877e8ed99 100644 --- a/test/nuts/local/replacements/testProj/sfdx-project.json +++ b/test/nuts/local/replacements/testProj/sfdx-project.json @@ -59,6 +59,11 @@ "glob": "**/*.css", "replaceWithEnv": "THE_REPLACEMENT", "stringToReplace": "placeholder" + }, + { + "glob": "force-app/main/default/staticresources/ImageTest/**/**", + "replaceWithEnv": "THE_REPLACEMENT", + "stringToReplace": "placeholder" } ], "sfdcLoginUrl": "https://login.salesforce.com", diff --git a/yarn.lock b/yarn.lock index cc5ec1e06d..9583daef92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3233,6 +3233,11 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isbinaryfile@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.2.tgz#fe6e4dfe2e34e947ffa240c113444876ba393ae0" + integrity sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -5066,16 +5071,7 @@ srcset@^5.0.0: resolved "https://registry.yarnpkg.com/srcset/-/srcset-5.0.0.tgz#9df6c3961b5b44a02532ce6ae4544832609e2e3f" integrity sha512-SqEZaAEhe0A6ETEa9O1IhSPC7MdvehZtCnTR0AftXk3QhY2UNgb+NApFOUPZILXk/YTDfFxMTNJOBpzrJsEdIA== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5134,14 +5130,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5631,7 +5620,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -5649,15 +5638,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"