| 1 | import { test } from 'node:test'; |
| 2 | import assert from 'node:assert/strict'; |
| 3 | import path from 'node:path'; |
| 4 | import os from 'node:os'; |
| 5 | import fs from 'node:fs'; |
| 6 | import { create as windows } from '../src/backends/win32.mjs'; |
| 7 | import { create as harmony } from '../src/backends/harmonyos.mjs'; |
| 8 | import { create as linux } from '../src/backends/linux.mjs'; |
| 9 | |
| 10 | test('Windows launch URLs are a single data argument, including PowerShell-looking text', async () => { |
| 11 | const scripts = []; |
| 12 | const b = windows({ exec: { run: async (_, args) => { |
| 13 | scripts.push(Buffer.from(args[args.indexOf('-EncodedCommand') + 1], 'base64').toString('utf16le')); |
| 14 | return {code:0, stdout:'', stderr:''}; |
| 15 | } } }); |
| 16 | for (const [url, quoted] of [ |
| 17 | ['https://example.test/$(Write-Output marker)`whoami`?q=中 😀', '"https://example.test/$(Write-Output marker)`whoami`?q=中 😀"'], |
| 18 | ['https://example.test/"quoted"', '"https://example.test/\\"quoted\\""'], |
| 19 | ['https://example.test/end\\', '"https://example.test/end\\\\"'], |
| 20 | ]) { |
| 21 | assert.equal((await b.open_application({name:'msedge',url})).launched,true); |
| 22 | const script=scripts.at(-1); |
| 23 | assert.ok(!script.includes(url)); |
| 24 | assert.doesNotMatch(script,/Write-Output marker|whoami/); |
| 25 | const encoded=script.match(/FromBase64String\('([^']+)'\)/)?.[1]; |
| 26 | assert.ok(encoded, 'URL data must be carried separately from executable script'); |
| 27 | assert.equal(Buffer.from(encoded,'base64').toString('utf16le'),quoted); |
| 28 | assert.match(script,/Start-Process -FilePath "msedge" -WindowStyle Minimized -ArgumentList \$launchArg/); |
| 29 | } |
| 30 | // activate defaults to background everywhere: an explicit activate:true is |
| 31 | // the only launch that takes the foreground. |
| 32 | assert.match((await b.open_application({name:'msedge'}),scripts.at(-1)),/-WindowStyle Minimized/); |
| 33 | await b.open_application({name:'msedge',activate:true}); |
| 34 | assert.ok(!scripts.at(-1).includes('WindowStyle')); |
| 35 | const before=scripts.length; |
| 36 | for(const url of [123,{},'', '-Command whoami','https://example.test/\nwhoami']) await assert.rejects(b.open_application({name:'msedge',url}), /absolute URL/); |
| 37 | assert.equal(scripts.length,before); |
| 38 | }); |
| 39 | |
| 40 | test('Harmony launch rejects shell syntax before dispatch and quotes admitted identifiers', async () => { |
| 41 | const calls=[]; |
| 42 | const b=harmony({exec:{shell:async args=>{calls.push(args);return {code:0,stdout:'',stderr:''};}}}); |
| 43 | for(const value of ['com.example;id','$(id)','com.example\nid','com.example app','"com.example"','-debug',123,{},'']) { |
| 44 | await assert.rejects(b.open_application({bundle_id:value}),/bundle identifier/); |
| 45 | await assert.rejects(b.open_application({bundle_id:'com.example',ability:value}),/ability identifier/); |
| 46 | } |
| 47 | assert.equal(calls.length,0); |
| 48 | const result=await b.open_application({bundle_id:'com.example.app',ability:'MainAbility'}); |
| 49 | assert.equal(result.launched,true); |
| 50 | assert.deepEqual(calls,[['aa','start','-b',"'com.example.app'",'-a',"'MainAbility'"]]); |
| 51 | }); |
| 52 | |
| 53 | test('Linux screenshot rejects option-shaped destinations before probing or capture', async t => { |
| 54 | const dir=fs.mkdtempSync(path.join(os.tmpdir(),'cu-path-security-')); |
| 55 | t.after(()=>fs.rmSync(dir,{recursive:true,force:true})); |
| 56 | const saved={}; |
| 57 | for(const key of ['DISPLAY','WAYLAND_DISPLAY','XDG_SESSION_TYPE','CODEWHALE_CU_RECORDINGS_DIR']) saved[key]=process.env[key]; |
| 58 | t.after(()=>{for(const [key,value] of Object.entries(saved)) if(value===undefined)delete process.env[key];else process.env[key]=value;}); |
| 59 | process.env.DISPLAY=':fixture';delete process.env.WAYLAND_DISPLAY; |
| 60 | process.env.XDG_SESSION_TYPE='x11';process.env.CODEWHALE_CU_RECORDINGS_DIR=dir; |
| 61 | const calls=[];let probes=0; |
| 62 | const b=linux({exec:{have:async name=>{probes++;return name==='scrot';},run:async (cmd,args)=>{calls.push({cmd,args});return {code:1,stdout:'',stderr:'fixture capture reached'};}}}); |
| 63 | for(const file of ['--exec=anything','relative.png','',123,{},'/tmp/a\0b']) await assert.rejects(b.screenshot({path:file}),/absolute filename/); |
| 64 | assert.equal(probes,0);assert.equal(calls.length,0); |
| 65 | const file=path.join(dir,'--exec=ordinary-filename.png'); |
| 66 | await assert.rejects(b.screenshot({path:file}),/fixture capture reached/); |
| 67 | assert.deepEqual(calls,[{cmd:'scrot',args:['-z',file]}]); |
| 68 | }); |
| 69 |