@@ -48,6 +48,12 @@ const getBufferContents = async remote =>
4848
4949const vimString = value => `'${ value . replace ( / ' / g, "''" ) } '` ;
5050
51+ const vimStringExpr = value =>
52+ value
53+ . split ( '"' )
54+ . map ( vimString )
55+ . join ( ' . nr2char(34) . ' ) ;
56+
5157const removeDirectorySync = dir => {
5258 if ( ! fs . existsSync ( dir ) ) {
5359 return ;
@@ -72,17 +78,41 @@ const cleanupTempProjects = () => {
7278 }
7379} ;
7480
75- const writeFakePrettierExecutable = prettierPath => {
76- fs . writeFileSync (
77- prettierPath ,
78- [
79- '#!/bin/sh' ,
80- 'if [ "$1" = "--version" ]; then' ,
81- " printf '3.0.3\\n'" ,
82- 'fi' ,
83- 'exit 0' ,
84- ] . join ( '\n' ) + '\n'
85- ) ;
81+ const writeFakePrettierExecutable = ( prettierPath , options = { } ) => {
82+ const script = [
83+ '#!/bin/sh' ,
84+ 'if [ "$1" = "--version" ]; then' ,
85+ " printf '3.0.3\\n'" ,
86+ ' exit 0' ,
87+ 'fi' ,
88+ ] ;
89+
90+ if ( options . expectedStdinFilepath ) {
91+ script . push (
92+ `expected_stdin_filepath=${ shellQuote ( options . expectedStdinFilepath ) } ` ,
93+ 'found_stdin_filepath=0' ,
94+ 'for arg in "$@"; do' ,
95+ ' if [ "$arg" = "--stdin-filepath=$expected_stdin_filepath" ]; then' ,
96+ ' found_stdin_filepath=1' ,
97+ ' fi' ,
98+ 'done' ,
99+ 'if [ "$found_stdin_filepath" != "1" ]; then' ,
100+ " printf '%s\\n' 'missing expected stdin filepath' >&2" ,
101+ ' exit 2' ,
102+ 'fi'
103+ ) ;
104+ }
105+
106+ if ( Object . prototype . hasOwnProperty . call ( options , 'formattedOutput' ) ) {
107+ script . push (
108+ 'cat >/dev/null' ,
109+ `printf '%s\\n' ${ shellQuote ( options . formattedOutput ) } `
110+ ) ;
111+ }
112+
113+ script . push ( 'exit 0' ) ;
114+
115+ fs . writeFileSync ( prettierPath , script . join ( '\n' ) + '\n' ) ;
86116 fs . chmodSync ( prettierPath , 0o755 ) ;
87117} ;
88118
@@ -105,8 +135,32 @@ const createProjectLocalPrettierFixture = extension => {
105135 return { root, prettierPath, sourcePath } ;
106136} ;
107137
138+ const createShellSafetyPrettierFixture = ( ) => {
139+ const root = fs . mkdtempSync (
140+ path . join ( fs . realpathSync ( os . tmpdir ( ) ) , 'vim-prettier shell "quote" ' )
141+ ) ;
142+ const binDir = path . join ( root , 'node_modules' , '.bin' ) ;
143+ const sourceDir = path . join ( root , 'src with spaces' ) ;
144+ const prettierPath = path . join ( binDir , 'prettier' ) ;
145+ const sourcePath = path . join ( sourceDir , 'index "quote".js' ) ;
146+
147+ tempProjectRoots . push ( root ) ;
148+ fs . mkdirSync ( binDir , { recursive : true } ) ;
149+ fs . mkdirSync ( sourceDir , { recursive : true } ) ;
150+ writeFakePrettierExecutable ( prettierPath , {
151+ expectedStdinFilepath : sourcePath ,
152+ formattedOutput : 'const formatted = true;' ,
153+ } ) ;
154+ fs . writeFileSync ( sourcePath , 'const formatted=false\n' ) ;
155+
156+ return { root, prettierPath, sourcePath } ;
157+ } ;
158+
108159const setVimCwd = dir =>
109- remote . execute ( `execute 'cd' fnameescape(${ vimString ( dir ) } )` ) ;
160+ remote . execute ( `execute 'cd' fnameescape(${ vimStringExpr ( dir ) } )` ) ;
161+
162+ const editFile = file =>
163+ remote . execute ( `execute 'edit' fnameescape(${ vimStringExpr ( file ) } )` ) ;
110164
111165const resolveConfigFlags = config =>
112166 remote . eval ( `prettier#resolver#config#resolve(${ config } , 0, 1, 1)` ) ;
@@ -430,6 +484,39 @@ if (FORMAT_FIXTURE_LANE === 'all') {
430484 expectNoPluginFlags ( flags ) ;
431485 } ) ;
432486 } ) ;
487+
488+ describe ( 'Prettier command shell safety' , ( ) => {
489+ test ( 'shellescapes stdin filepath for paths containing spaces and quotes' , async ( ) => {
490+ const project = createShellSafetyPrettierFixture ( ) ;
491+
492+ await editFile ( project . sourcePath ) ;
493+
494+ const flags = await resolveConfigFlags ( '{}' ) ;
495+ const expectedPath = await remote . eval (
496+ "shellescape(simplify(expand('%:p')))"
497+ ) ;
498+
499+ expect ( flags ) . toContain ( `--stdin-filepath=${ expectedPath } ` ) ;
500+ expect ( flags ) . not . toContain ( `--stdin-filepath="${ project . sourcePath } "` ) ;
501+ } ) ;
502+
503+ test ( ':Prettier runs a project-local executable from a shell-escaped path' , async ( ) => {
504+ const project = createShellSafetyPrettierFixture ( ) ;
505+
506+ await setVimCwd ( __dirname ) ;
507+ await editFile ( project . sourcePath ) ;
508+
509+ const resolvedPath = await remote . eval (
510+ 'prettier#resolver#executable#getPath()'
511+ ) ;
512+
513+ expect ( resolvedPath ) . toBe ( project . prettierPath ) ;
514+
515+ await remote . execute ( 'Prettier' ) ;
516+
517+ expect ( await getBufferContents ( remote ) ) . toBe ( 'const formatted = true;' ) ;
518+ } ) ;
519+ } ) ;
433520}
434521
435522// run formatting tests in all fixtures
0 commit comments