/* * Copyright (C) 2002-2021 The DOSBox Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include //std::copy #include //std::front_inserter #include #include "logging.h" #include "shell.h" #include "timer.h" #include "bios.h" #include "control.h" #include "regs.h" #include "callback.h" #include "support.h" #include "inout.h" #include "../ints/int10.h" #include "../dos/drives.h" #ifdef _MSC_VER # if !defined(C_SDL2) # include "process.h" # endif # define MIN(a,b) ((a) < (b) ? (a) : (b)) # define MAX(a,b) ((a) > (b) ? (a) : (b)) #else # define MIN(a,b) std::min(a,b) # define MAX(a,b) std::max(a,b) #endif bool clearline=false, inshell=false; int autofixwarn=3; extern int lfn_filefind_handle; extern bool ctrlbrk, gbk; extern bool DOS_BreakFlag; extern bool DOS_BreakConioFlag; void DOS_Shell::ShowPrompt(void) { char dir[DOS_PATHLENGTH]; dir[0] = 0; //DOS_GetCurrentDir doesn't always return something. (if drive is messed up) DOS_GetCurrentDir(0,dir,uselfn); std::string line; const char * promptstr = "\0"; inshell = true; if(GetEnvStr("PROMPT",line)) { std::string::size_type idx = line.find('='); std::string value=line.substr(idx +1 , std::string::npos); line = std::string(promptstr) + value; promptstr = line.c_str(); } while (*promptstr) { if (!strcasecmp(promptstr,"$")) WriteOut("\0"); else if(*promptstr != '$') WriteOut("%c",*promptstr); else switch (toupper(*++promptstr)) { case 'A': WriteOut("&"); break; case 'B': WriteOut("|"); break; case 'C': WriteOut("("); break; case 'D': WriteOut("%02d-%02d-%04d",dos.date.day,dos.date.month,dos.date.year); break; case 'E': WriteOut("%c",27); break; case 'F': WriteOut(")"); break; case 'G': WriteOut(">"); break; case 'H': WriteOut("\b"); break; case 'L': WriteOut("<"); break; case 'N': WriteOut("%c",DOS_GetDefaultDrive()+'A'); break; case 'P': WriteOut("%c:\\%s",DOS_GetDefaultDrive()+'A',dir); break; case 'Q': WriteOut("="); break; case 'S': WriteOut(" "); break; case 'T': { Bitu ticks=(Bitu)(((65536.0 * 100.0)/(double)PIT_TICK_RATE)* mem_readd(BIOS_TIMER)); reg_dl=(uint8_t)((Bitu)ticks % 100); ticks/=100; reg_dh=(uint8_t)((Bitu)ticks % 60); ticks/=60; reg_cl=(uint8_t)((Bitu)ticks % 60); ticks/=60; reg_ch=(uint8_t)((Bitu)ticks % 24); WriteOut("%d:%02d:%02d.%02d",reg_ch,reg_cl,reg_dh,reg_dl); break; } case 'V': WriteOut("DOSBox-X version %s. Reported DOS version %d.%d.",VERSION,dos.version.major,dos.version.minor); break; case '$': WriteOut("$"); break; case '_': WriteOut("\n"); break; case 'M': break; case '+': break; } promptstr++; } inshell = false; } static void outc(uint8_t c) { uint16_t n=1; DOS_WriteFile(STDOUT,&c,&n); } static void backone() { BIOS_NCOLS; uint8_t page=real_readb(BIOSMEM_SEG,BIOSMEM_CURRENT_PAGE); if (CURSOR_POS_COL(page)>0) outc(8); else if (CURSOR_POS_ROW(page)>0) INT10_SetCursorPos(CURSOR_POS_ROW(page)-1, ncols-1, page); } //! \brief Moves the caret to prev row/last column when column is 0 (video mode 0). void MoveCaretBackwards() { uint8_t col, row; const uint8_t page(0); INT10_GetCursorPos(&row, &col, page); if (col != 0) return; uint16_t cols; INT10_GetScreenColumns(&cols); INT10_SetCursorPos(row - 1, static_cast(cols), page); } bool DOS_Shell::BuildCompletions(char * line, uint16_t str_len) { // build new completion list // Lines starting with CD/MD/RD will only get directories in the list bool dir_only = (strncasecmp(ltrim(line),"CD ",3)==0)||(strncasecmp(ltrim(line),"MD ",3)==0)||(strncasecmp(ltrim(line),"RD ",3)==0)|| (strncasecmp(ltrim(line),"CHDIR ",6)==0)||(strncasecmp(ltrim(line),"MKDIR ",3)==0)||(strncasecmp(ltrim(line),"RMDIR ",6)==0); int q=0, r=0, k=0; // get completion mask const char *p_completion_start = strrchr(line, ' '); while (p_completion_start) { q=0; char *i; for (i=line;i','|'}; for (unsigned int j=0; j= DOS_PATHLENGTH) { // TODO: This really should be done in the CON driver so that this code can just print ASCII code 7 instead if (IS_PC98_ARCH) { // TODO: BEEP. I/O PORTS ARE DIFFERENT AS IS THE PIT CLOCK RATE } else { // IBM PC/XT/AT IO_Write(0x43,0xb6); IO_Write(0x42,1750&0xff); IO_Write(0x42,1750>>8); IO_Write(0x61,IO_Read(0x61)|0x3); for(Bitu i=0; i < 333; i++) CALLBACK_Idle(); IO_Write(0x61,IO_Read(0x61)&~0x3); } return false; } if (p_completion_start) { safe_strncpy(mask, p_completion_start,DOS_PATHLENGTH); const char* dot_pos = strrchr(mask, '.'); const char* bs_pos = strrchr_dbcs(mask, '\\'); const char* fs_pos = strrchr(mask, '/'); const char* cl_pos = strrchr(mask, ':'); // not perfect when line already contains wildcards, but works if ((dot_pos-bs_pos>0) && (dot_pos-fs_pos>0) && (dot_pos-cl_pos>0)) strncat(mask, "*",DOS_PATHLENGTH - 1); else strncat(mask, "*.*",DOS_PATHLENGTH - 1); } else { strcpy(mask, "*.*"); } RealPt save_dta=dos.dta(); dos.dta(dos.tables.tempdta); bool res = false; if (DOS_GetSFNPath(mask,smask,false)) { sprintf(mask,"\"%s\"",smask); int fbak=lfn_filefind_handle; lfn_filefind_handle=uselfn?LFN_FILEFIND_INTERNAL:LFN_FILEFIND_NONE; res = DOS_FindFirst(mask, 0xffff & ~DOS_ATTR_VOLUME); lfn_filefind_handle=fbak; } if (!res) { dos.dta(save_dta); // TODO: This really should be done in the CON driver so that this code can just print ASCII code 7 instead if (IS_PC98_ARCH) { // TODO: BEEP. I/O PORTS ARE DIFFERENT AS IS THE PIT CLOCK RATE } else { // IBM PC/XT/AT IO_Write(0x43,0xb6); IO_Write(0x42,1750&0xff); IO_Write(0x42,1750>>8); IO_Write(0x61,IO_Read(0x61)|0x3); for(Bitu i=0; i < 300; i++) CALLBACK_Idle(); IO_Write(0x61,IO_Read(0x61)&~0x3); } return false; } DOS_DTA dta(dos.dta()); char name[DOS_NAMELENGTH_ASCII], lname[LFN_NAMELENGTH], qlname[LFN_NAMELENGTH+2]; uint32_t sz;uint16_t date;uint16_t time;uint8_t att; std::list executable; q=0;r=0; while (*p_completion_start) { k++; if (*p_completion_start++=='\"') { if (k<=completion_index) q++; else r++; } } int fbak=lfn_filefind_handle; lfn_filefind_handle=uselfn?LFN_FILEFIND_INTERNAL:LFN_FILEFIND_NONE; while (res) { dta.GetResult(name,lname,sz,date,time,att); if ((strchr(uselfn?lname:name,' ')!=NULL&&q/2*2==q)||r) sprintf(qlname,q/2*2!=q?"%s\"":"\"%s\"",uselfn?lname:name); else strcpy(qlname,uselfn?lname:name); // add result to completion list if (strcmp(name, ".") && strcmp(name, "..")) { if (dir_only) { //Handle the dir only case different (line starts with cd) if(att & DOS_ATTR_DIRECTORY) l_completion.emplace_back(qlname); } else { const char *ext = strrchr(name, '.'); // file extension if (ext && (strcmp(ext, ".BAT") == 0 || strcmp(ext, ".COM") == 0 || strcmp(ext, ".EXE") == 0)) // we add executables to a separate list and place that list in front of the normal files executable.emplace_front(qlname); else l_completion.emplace_back(qlname); } } res=DOS_FindNext(); } lfn_filefind_handle=fbak; /* Add executable list to front of completion list. */ std::copy(executable.begin(),executable.end(),std::front_inserter(l_completion)); dos.dta(save_dta); return true; } extern bool isDBCSCP(); /* NTS: buffer pointed to by "line" must be at least CMD_MAXLINE+1 large */ void DOS_Shell::InputCommand(char * line) { Bitu size=CMD_MAXLINE-2; //lastcharacter+0 uint8_t c;uint16_t n=1; uint16_t str_len=0;uint16_t str_index=0; uint16_t len=0; bool current_hist=false; // current command stored in history? uint16_t cr; inshell = true; input_eof = false; line[0] = '\0'; std::list::iterator it_history = l_history.begin(), it_completion = l_completion.begin(); while (size) { dos.echo=false; if (!DOS_ReadFile(input_handle,&c,&n)) { LOG(LOG_MISC,LOG_ERROR)("SHELL: Lost the input handle, dropping shell input loop"); n = 0; } if (!n) { input_eof = true; size=0; //Kill the while loop continue; } if (clearline) { clearline = false; *line=0; if (l_completion.size()) l_completion.clear(); //reset the completion list. str_index = 0; str_len = 0; } if (input_handle != STDIN) { /* FIXME: Need DOS_IsATTY() or somesuch */ cr = (uint16_t)c; /* we're not reading from the console */ } else if (IS_PC98_ARCH) { extern uint16_t last_int16_code; /* shift state is needed for some key combinations not directly supported by CON driver. * bit 4 = CTRL * bit 3 = GRPH/ALT * bit 2 = kana * bit 1 = caps * bit 0 = SHIFT */ uint8_t shiftstate = mem_readb(0x52A + 0x0E); /* NTS: PC-98 keyboards lack the US layout HOME / END keys, therefore there is no mapping here */ /* NTS: Since left arrow and backspace map to the same byte value, PC-98 treats it the same at the DOS prompt. * However the PC-98 version of DOSKEY seems to be able to differentiate the two anyway and let the left * arrow move the cursor back (perhaps it's calling INT 18h directly then?) */ if (c == 0x0B) cr = 0x4800; /* IBM extended code up arrow */ else if (c == 0x0A) cr = 0x5000; /* IBM extended code down arrow */ else if (c == 0x0C) { if (shiftstate & 0x10/*CTRL*/) cr = 0x7400; /* IBM extended code CTRL + right arrow */ else cr = 0x4D00; /* IBM extended code right arrow */ } else if (c == 0x08) { /* IBM extended code left arrow OR backspace. use last scancode to tell which as DOSKEY apparently can. */ if (last_int16_code == 0x3B00) { if (shiftstate & 0x10/*CTRL*/) cr = 0x7300; /* CTRL + left arrow */ else cr = 0x4B00; /* left arrow */ } else { cr = 0x08; /* backspace */ } } else if (c == 0x1B) { /* escape */ /* Either it really IS the ESC key, or an ANSI code */ if (last_int16_code != 0x001B) { DOS_ReadFile(input_handle,&c,&n); if (c == 0x44) // DEL cr = 0x5300; else if (c == 0x50) // INS cr = 0x5200; else if (c == 0x53) // F1 cr = 0x3B00; else if (c == 0x54) // F2 cr = 0x3C00; else if (c == 0x55) // F3 cr = 0x3D00; else if (c == 0x56) // F4 cr = 0x3E00; else if (c == 0x57) // F5 cr = 0x3F00; else if (c == 0x45) // F6 cr = 0x4000; else if (c == 0x4A) // F7 cr = 0x4100; else if (c == 0x51) // F9 cr = 0x4300; else if (c == 0x5A) // F10 cr = 0x4400; else cr = 0; } else { cr = (uint16_t)c; } } else { cr = (uint16_t)c; } } else { if (c == 0) { DOS_ReadFile(input_handle,&c,&n); cr = (uint16_t)c << (uint16_t)8; } else { cr = (uint16_t)c; } } switch (cr) { case 0x3d00: /* F3 */ if (!l_history.size()) break; it_history = l_history.begin(); if (it_history != l_history.end() && it_history->length() > str_len) { const char *reader = &(it_history->c_str())[str_len]; while ((c = (uint8_t)(*reader++))) { line[str_index ++] = (char)c; DOS_WriteFile(STDOUT,&c,&n); } str_len = str_index = (uint16_t)it_history->length(); size = (unsigned int)CMD_MAXLINE - str_index - 2u; line[str_len] = 0; } break; case 0x4B00: /* LEFT */ if ((IS_PC98_ARCH||isDBCSCP())&&str_index>1&&(line[str_index-1]<0||(IS_PC98_ARCH||dos.loaded_codepage==932||(dos.loaded_codepage==936&&gbk)||dos.loaded_codepage==950)&&line[str_index-1]>=0x40)&&line[str_index-2]<0) { backone(); str_index --; MoveCaretBackwards(); } if (str_index) { backone(); str_index --; MoveCaretBackwards(); } break; case 0x7400: /*CTRL + RIGHT : cmd.exe-like next word*/ { auto pos = line + str_index; auto spc = *pos == ' '; const auto end = line + str_len; while (pos < end) { if (spc && *pos != ' ') break; if (*pos == ' ') spc = true; pos++; } const auto lgt = MIN(pos, end) - (line + str_index); for (auto i = 0; i < lgt; i++) outc(static_cast(line[str_index++])); } break; case 0x7300: /*CTRL + LEFT : cmd.exe-like previous word*/ { auto pos = line + str_index - 1; const auto beg = line; const auto spc = *pos == ' '; if (spc) { while(*pos == ' ') pos--; while(*pos != ' ') pos--; pos++; } else { while(*pos != ' ') pos--; pos++; } const auto lgt = std::abs(MAX(pos, beg) - (line + str_index)); for (auto i = 0; i < lgt; i++) { backone(); str_index--; MoveCaretBackwards(); } } break; case 0x4D00: /* RIGHT */ if ((IS_PC98_ARCH||isDBCSCP())&&str_index=0x40)) { outc((uint8_t)line[str_index++]); } if (str_index < str_len) { outc((uint8_t)line[str_index++]); } break; case 0x4700: /* HOME */ while (str_index) { backone(); str_index--; } break; case 0x5200: /* INS */ if (IS_PC98_ARCH) { // INS state handled by IBM PC/AT BIOS, faked for PC-98 mode extern bool pc98_doskey_insertmode; // NTS: No visible change to the cursor, just like DOSKEY on PC-98 MS-DOS pc98_doskey_insertmode = !pc98_doskey_insertmode; } break; case 0x4F00: /* END */ while (str_index < str_len) { outc((uint8_t)line[str_index++]); } break; case 0x4800: /* UP */ if (l_history.empty() || it_history == l_history.end()) break; // store current command in history if we are at beginning if (it_history == l_history.begin() && !current_hist) { current_hist=true; l_history.emplace_front(line); } // ensure we're at end to handle all cases while (str_index < str_len) { outc((uint8_t)line[str_index++]); } for (;str_index>0; str_index--) { // removes all characters backone(); outc(' '); backone(); } strcpy(line, it_history->c_str()); len = (uint16_t)it_history->length(); str_len = str_index = len; size = (unsigned int)CMD_MAXLINE - str_index - 2u; DOS_WriteFile(STDOUT, (uint8_t *)line, &len); ++it_history; break; case 0x5000: /* DOWN */ if (l_history.empty() || it_history == l_history.begin()) break; // not very nice but works .. --it_history; if (it_history == l_history.begin()) { // no previous commands in history ++it_history; // remove current command from history if (current_hist) { current_hist=false; l_history.pop_front(); } break; } else --it_history; // ensure we're at end to handle all cases while (str_index < str_len) { outc((uint8_t)line[str_index++]); } for (;str_index>0; str_index--) { // removes all characters backone(); outc(' '); backone(); } strcpy(line, it_history->c_str()); len = (uint16_t)it_history->length(); str_len = str_index = len; size = (unsigned int)CMD_MAXLINE - str_index - 2u; DOS_WriteFile(STDOUT, (uint8_t *)line, &len); ++it_history; break; case 0x5300:/* DELETE */ { if(str_index>=str_len) break; int k=1; if ((IS_PC98_ARCH||isDBCSCP())&&str_index=0x40)) k=2; for (int i=0; i(&line[str_index+1]); DOS_WriteFile(STDOUT,text,&a);//write buffer to screen outc(' ');backone(); for(Bitu i=str_index;i<(str_len-1u);i++) { line[i]=line[i+1u]; backone(); } line[--str_len]=0; size++; } } break; case 0x0F00: /* Shift-Tab */ if (!l_completion.size()) { if (BuildCompletions(line, str_len)) it_completion = l_completion.end(); else break; } if (l_completion.size()) { if (it_completion == l_completion.begin()) it_completion = l_completion.end (); --it_completion; if (it_completion->length()) { for (;str_index > completion_index; str_index--) { // removes all characters backone(); outc(' '); backone(); } strcpy(&line[completion_index], it_completion->c_str()); len = (uint16_t)it_completion->length(); str_len = str_index = (Bitu)(completion_index + len); size = (unsigned int)CMD_MAXLINE - str_index - 2u; DOS_WriteFile(STDOUT, (uint8_t *)it_completion->c_str(), &len); } } break; case 0x08: /* BackSpace */ { int k=1; if ((IS_PC98_ARCH||isDBCSCP())&&str_index>1&&(line[str_index-1]<0||(IS_PC98_ARCH||dos.loaded_codepage==932||(dos.loaded_codepage==936&&gbk)||dos.loaded_codepage==950)&&line[str_index-1]>=0x40)&&line[str_index-2]<0) k=2; for (int i=0; i80?15:10; for (col=mrow; col>0; col--) { for (int i=0; i::iterator source = l_completion.begin(); source != l_completion.end(); ++source) { std::string name = source->c_str(); if (name.size()+2>max[w_count%col]) max[w_count%col]=(unsigned int)(name.size()+2); ++w_count; } total=0; for (size_t i=0; i::iterator source = l_completion.begin(); source != l_completion.end(); ++source) { std::string name = source->c_str(); if (col==1) { WriteOut("%s\n", name.c_str()); lastcr=true; p_count++; } else { WriteOut("%s%-*s", name.c_str(), max[w_count % col]-name.size(), ""); lastcr=false; } if (col>1) { ++w_count; if (w_count % col == 0) {p_count++;WriteOut_NoParsing("\n");lastcr=true;} } size_t GetPauseCount(); if (p_count>GetPauseCount()) { WriteOut(MSG_Get("SHELL_CMD_PAUSE")); lastcr=false; w_count = p_count = 0; uint8_t c;uint16_t n=1; DOS_ReadFile(STDIN,&c,&n); if (c==3) {ctrlbrk=false;WriteOut("^C\r\n");break;} if (c==0) DOS_ReadFile(STDIN,&c,&n); } } if (l_completion.size()) { if (!lastcr) WriteOut_NoParsing("\n"); ShowPrompt(); WriteOut("%s", line); } break; } case'\t': if (l_completion.size()) { ++it_completion; if (it_completion == l_completion.end()) it_completion = l_completion.begin(); } else if (BuildCompletions(line, str_len)) it_completion = l_completion.begin(); else break; if (l_completion.size() && it_completion->length()) { for (;str_index > completion_index; str_index--) { // removes all characters backone(); outc(' '); backone(); } strcpy(&line[completion_index], it_completion->c_str()); len = (uint16_t)it_completion->length(); str_len = str_index = (Bitu)(completion_index + len); size = (unsigned int)CMD_MAXLINE - str_index - 2u; DOS_WriteFile(STDOUT, (uint8_t *)it_completion->c_str(), &len); } break; case 0x1b: /* ESC */ // NTS: According to real PC-98 DOS: // If DOSKEY is loaded, ESC clears the prompt // If DOSKEY is NOT loaded, ESC does nothing. In fact, after ESC, // the next character input is thrown away before resuming normal keyboard input. // // DOSBox / DOSBox-X have always acted as if DOSKEY is loaded in a fashion, so // we'll emulate the PC-98 DOSKEY behavior here. // // DOSKEY on PC-98 is able to clear the whole prompt and even bring the cursor // back up to the first line if the input crosses multiple lines. // NTS: According to real IBM/Microsoft PC/AT DOS: // If DOSKEY is loaded, ESC clears the prompt // If DOSKEY is NOT loaded, ESC prints a backslash and goes to the next line. // The Windows 95 version of DOSKEY puts the cursor at a horizontal position // that matches the DOS prompt (not emulated here). // // DOSBox / DOSBox-X have always acted as if DOSKEY is loaded in a fashion, so // we'll emulate DOSKEY behavior here. while (str_index < str_len) { outc(' '); str_index++; } while (str_index > 0) { backone(); outc(' '); backone(); MoveCaretBackwards(); str_index--; } *line = 0; // reset the line. if (l_completion.size()) l_completion.clear(); //reset the completion list. str_index = 0; str_len = 0; break; default: if (cr >= 0x100) break; if (l_completion.size()) l_completion.clear(); if(str_index < str_len && !INT10_GetInsertState()) { //mem_readb(BIOS_KEYBOARD_FLAGS1)&0x80) dev_con.h ? outc(' ');//move cursor one to the right. uint16_t a = str_len - str_index; uint8_t* text=reinterpret_cast(&line[str_index]); DOS_WriteFile(STDOUT,text,&a);//write buffer to screen backone();//undo the cursor the right. for(Bitu i=str_len;i>str_index;i--) { line[i]=line[i-1]; //move internal buffer backone(); //move cursor back (from write buffer to screen) } line[++str_len]=0;//new end (as the internal buffer moved one place to the right size--; } line[str_index]=(char)(cr&0xFF); str_index ++; if (str_index > str_len){ line[str_index] = '\0'; str_len++; size--; } DOS_WriteFile(STDOUT,&c,&n); break; } } inshell = false; if (!str_len) return; str_len++; // remove current command from history if it's there if (current_hist) { // current_hist=false; l_history.pop_front(); } // add command line to history. Win95 behavior with DOSKey suggests // that the original string is preserved, not the expanded string. l_history.emplace_front(line); it_history = l_history.begin(); if (l_completion.size()) l_completion.clear(); /* DOS %variable% substitution */ ProcessCmdLineEnvVarStitution(line); } void XMS_DOS_LocalA20DisableIfNotEnabled(void); /* Note: Buffer pointed to by "line" must be at least CMD_MAXLINE+1 bytes long! */ void DOS_Shell::ProcessCmdLineEnvVarStitution(char *line) { // Wengier: Rewrote the code using regular expressions (a lot shorter) // Tested by both myself and kcgen with various boundary cases /* DOS7/Win95 testing: * * "%" = "%" * "%%" = "%" * "% %" = "" * "% %" = "" * "% % %" = " %" * * ^ WTF? * * So the below code has funny conditions to match Win95's weird rules on what * consitutes valid or invalid %variable% names. */ /* continue scanning for the ending '%'. variable names are apparently meant to be * alphanumeric, start with a letter, without spaces (if Windows 95 COMMAND.COM is * any good example). If it doesn't end in '%' or is broken by space or starts with * a number, substitution is not carried out. In the middle of the variable name * it seems to be acceptable to use hyphens. * * since spaces break up a variable name to prevent substitution, these commands * act differently from one another: * * C:\>echo %PATH% * C:\DOS;C:\WINDOWS * * C:\>echo %PATH % * %PATH % */ /* initial scan: is there anything to substitute? */ /* if not, then just return without modifying "line" */ /* not really needed but keep this code as is for now */ char *r=line; while (*r != 0 && *r != '%') r++; if (*r != '%') return; /* if the incoming string is already too long, then that's a problem too! */ if (((size_t)(r+1-line)) >= CMD_MAXLINE) { LOG_MSG("DOS_Shell::ProcessCmdLineEnvVarStitution WARNING incoming string to substitute is already too long!\n"); *line = 0; /* clear string (C-string chop with NUL) */ WriteOut("Command input error: string expansion overflow\n"); return; } // Begin the process of shell variable substitutions using regular expressions // Variable names must start with non-digits. Space and special symbols like _ and ~ are apparently valid too (MS-DOS 7/Windows 9x). constexpr char surrogate_percent = 8; const static std::regex re("\\%([^%0-9][^%]*)?%"); std::string text = line; std::smatch match; /* Iterate over potential %var1%, %var2%, etc matches found in the text string */ while (std::regex_search(text, match, re)) { std::string variable_name; variable_name = match[1].str(); if (!variable_name.size()) { /* Replace %% with the character "surrogate_percent", then (eventually) % */ text.replace(match[0].first, match[0].second, std::string(1, surrogate_percent)); continue; } /* Trim preceding spaces from the variable name */ variable_name.erase(0, variable_name.find_first_not_of(' ')); std::string variable_value; if (variable_name.size() && GetEnvStr(variable_name.c_str(), variable_value)) { const size_t equal_pos = variable_value.find_first_of('='); /* Replace the original %var% with its corresponding value from the environment */ const std::string replacement = equal_pos != std::string::npos ? variable_value.substr(equal_pos + 1) : ""; text.replace(match[0].first, match[0].second, replacement); } else text.replace(match[0].first, match[0].second, ""); } std::replace(text.begin(), text.end(), surrogate_percent, '%'); assert(text.size() <= CMD_MAXLINE); strcpy(line, text.c_str()); } int infix=-1; std::string full_arguments = ""; bool dos_a20_disable_on_exec=false; extern bool packerr, mountwarning, nowarn; bool DOS_Shell::Execute(char* name, const char* args) { /* return true => don't check for hardware changes in do_command * return false => check for hardware changes in do_command */ char fullname[DOS_PATHLENGTH+4]; //stores results from Which const char* p_fullname; char line[CMD_MAXLINE]; if(strlen(args)!= 0){ if(*args != ' '){ //put a space in front line[0]=' ';line[1]=0; strncat(line,args,CMD_MAXLINE-2); line[CMD_MAXLINE-1]=0; } else { safe_strncpy(line,args,CMD_MAXLINE); } }else{ line[0]=0; } const Section_prop* sec = static_cast(control->GetSection("dos")); /* check for a drive change */ if (((strcmp(name + 1, ":") == 0) || (strcmp(name + 1, ":\\") == 0)) && isalpha(*name) && !control->SecureMode()) { if (strrchr_dbcs(name,'\\')) { WriteOut(MSG_Get("SHELL_EXECUTE_ILLEGAL_COMMAND"),name); return true; } if (!DOS_SetDrive(toupper(name[0])-'A')) { #ifdef WIN32 if(!sec->Get_bool("automount")) { WriteOut(MSG_Get("SHELL_EXECUTE_DRIVE_NOT_FOUND"),toupper(name[0])); return true; } // automount: attempt direct letter to drive map. int type=GetDriveType(name); if(mountwarning && type==DRIVE_FIXED && (strcasecmp(name,"C:")==0)) WriteOut(MSG_Get("PROGRAM_MOUNT_WARNING_WIN")); first_1: if(type==DRIVE_CDROM) WriteOut(MSG_Get("SHELL_EXECUTE_DRIVE_ACCESS_CDROM"),toupper(name[0])); else if(type==DRIVE_REMOVABLE && (strcasecmp(name,"A:")==0||strcasecmp(name,"B:")==0)) WriteOut(MSG_Get("SHELL_EXECUTE_DRIVE_ACCESS_FLOPPY"),toupper(name[0])); else if(type==DRIVE_REMOVABLE) WriteOut(MSG_Get("SHELL_EXECUTE_DRIVE_ACCESS_REMOVABLE"),toupper(name[0])); else if(type==DRIVE_REMOTE) WriteOut(MSG_Get("SHELL_EXECUTE_DRIVE_ACCESS_NETWORK"),toupper(name[0])); else if((type==DRIVE_FIXED)||(type==DRIVE_RAMDISK)) WriteOut(MSG_Get(mountwarning?"SHELL_EXECUTE_DRIVE_ACCESS_FIXED":"SHELL_EXECUTE_DRIVE_ACCESS_FIXED_LESS"),toupper(name[0])); else { WriteOut(MSG_Get("SHELL_EXECUTE_DRIVE_NOT_FOUND"),toupper(name[0])); return true; } first_2: uint8_t c;uint16_t n=1; DOS_ReadFile (STDIN,&c,&n); do switch (c) { case 'n': case 'N': { DOS_WriteFile (STDOUT,&c, &n); DOS_ReadFile (STDIN,&c,&n); do switch (c) { case 0xD: WriteOut("\n\n"); WriteOut(MSG_Get("SHELL_EXECUTE_DRIVE_NOT_FOUND"),toupper(name[0])); return true; case 0x3: return true; case 0x8: WriteOut("\b \b"); goto first_2; } while (DOS_ReadFile (STDIN,&c,&n)); } case 'y': case 'Y': { DOS_WriteFile (STDOUT,&c, &n); DOS_ReadFile (STDIN,&c,&n); do switch (c) { case 0xD: WriteOut("\n"); goto continue_1; case 0x3: return true; case 0x8: WriteOut("\b \b"); goto first_2; } while (DOS_ReadFile (STDIN,&c,&n)); } case 0x3: return true; case 0xD: WriteOut("\n"); goto first_1; case '\t': case 0x08: goto first_2; default: { DOS_WriteFile (STDOUT,&c, &n); DOS_ReadFile (STDIN,&c,&n); do switch (c) { case 0xD: WriteOut("\n");goto first_1; case 0x3: return true; case 0x8: WriteOut("\b \b"); goto first_2; } while (DOS_ReadFile (STDIN,&c,&n)); goto first_2; } } while (DOS_ReadFile (STDIN,&c,&n)); continue_1: char mountstring[DOS_PATHLENGTH+CROSS_LEN+20]; sprintf(mountstring,"MOUNT %s ",name); if(type==DRIVE_CDROM) strcat(mountstring,"-t cdrom "); else if(type==DRIVE_REMOVABLE && (strcasecmp(name,"A:")==0||strcasecmp(name,"B:")==0)) strcat(mountstring,"-t floppy "); strcat(mountstring,name); strcat(mountstring,"\\"); // if(GetDriveType(name)==5) strcat(mountstring," -ioctl"); nowarn=true; this->ParseLine(mountstring); nowarn=false; //failed: if (!DOS_SetDrive(toupper(name[0])-'A')) #endif WriteOut(MSG_Get("SHELL_EXECUTE_DRIVE_NOT_FOUND"),toupper(name[0])); } return true; } /* Check for a full name */ p_fullname = Which(name); if (!p_fullname) return false; strcpy(fullname,p_fullname); const char* extension = strrchr(fullname,'.'); /*always disallow files without extension from being executed. */ /*only internal commands can be run this way and they never get in this handler */ if(extension == 0) { //Check if the result will fit in the parameters. Else abort if(strlen(fullname) >( DOS_PATHLENGTH - 1) ) return false; char temp_name[DOS_PATHLENGTH + 4]; const char* temp_fullname; //try to add .com, .exe and .bat extensions to filename strcpy(temp_name,fullname); strcat(temp_name,".COM"); temp_fullname=Which(temp_name); if (temp_fullname) { extension=".com";strcpy(fullname,temp_fullname); } else { strcpy(temp_name,fullname); strcat(temp_name,".EXE"); temp_fullname=Which(temp_name); if (temp_fullname) { extension=".exe";strcpy(fullname,temp_fullname);} else { strcpy(temp_name,fullname); strcat(temp_name,".BAT"); temp_fullname=Which(temp_name); if (temp_fullname) { extension=".bat";strcpy(fullname,temp_fullname);} else { return false; } } } } if (strcasecmp(extension, ".bat") == 0) { /* Run the .bat file */ /* delete old batch file if call is not active*/ bool temp_echo=echo; /*keep the current echostate (as delete bf might change it )*/ if(bf && !call) delete bf; bf=new BatchFile(this,fullname,name,line); echo=temp_echo; //restore it. } else { /* only .bat .exe .com extensions maybe be executed by the shell */ if(strcasecmp(extension, ".com") !=0) { if(strcasecmp(extension, ".exe") !=0) return false; } /* Run the .exe or .com file from the shell */ /* Allocate some stack space for tables in physical memory */ reg_sp-=0x200; //Add Parameter block DOS_ParamBlock block(SegPhys(ss)+reg_sp); block.Clear(); //Add a filename RealPt file_name=RealMakeSeg(ss,reg_sp+0x20); MEM_BlockWrite(Real2Phys(file_name),fullname,(Bitu)(strlen(fullname)+1)); /* HACK: Store full commandline for mount and imgmount */ full_arguments.assign(line); /* Fill the command line */ CommandTail cmdtail; cmdtail.count = 0; memset(&cmdtail.buffer,0,CTBUF); //Else some part of the string is unitialized (valgrind) if (strlen(line)>=CTBUF) line[CTBUF-1]=0; cmdtail.count=(uint8_t)strlen(line); memcpy(cmdtail.buffer,line,strlen(line)); cmdtail.buffer[strlen(line)]=0xd; /* Copy command line in stack block too */ MEM_BlockWrite(SegPhys(ss)+reg_sp+0x100,&cmdtail,CTBUF+1); /* Split input line up into parameters, using a few special rules, most notable the one for /AAA => A\0AA * Qbix: It is extremly messy, but this was the only way I could get things like /:aa and :/aa to work correctly */ //Prepare string first char parseline[258] = { 0 }; for(char *pl = line,*q = parseline; *pl ;pl++,q++) { if (*pl == '=' || *pl == ';' || *pl ==',' || *pl == '\t' || *pl == ' ') *q = 0; else *q = *pl; //Replace command seperators with 0. } //No end of string \0 needed as parseline is larger than line for(char* p = parseline; (p-parseline) < 250 ;p++) { //Stay relaxed within boundaries as we have plenty of room if (*p == '/') { //Transform /Hello into H\0ello *p = 0; p++; while ( *p == 0 && (p-parseline) < 250) p++; //Skip empty fields if ((p-parseline) < 250) { //Found something. Lets get the first letter and break it up p++; memmove(static_cast(p + 1),static_cast(p),(250u-(unsigned int)(p-parseline))); if ((p-parseline) < 250) *p = 0; } } } parseline[255] = parseline[256] = parseline[257] = 0; //Just to be safe. /* Parse FCB (first two parameters) and put them into the current DOS_PSP */ uint8_t add; uint16_t skip = 0; //find first argument, we end up at parseline[256] if there is only one argument (similar for the second), which exists and is 0. while(skip < 256 && parseline[skip] == 0) skip++; FCB_Parsename(dos.psp(),0x5C,0x01,parseline + skip,&add); skip += add; //Move to next argument if it exists while(parseline[skip] != 0) skip++; //This is safe as there is always a 0 in parseline at the end. while(skip < 256 && parseline[skip] == 0) skip++; //Which is higher than 256 FCB_Parsename(dos.psp(),0x6C,0x01,parseline + skip,&add); block.exec.fcb1=RealMake(dos.psp(),0x5C); block.exec.fcb2=RealMake(dos.psp(),0x6C); /* Set the command line in the block and save it */ block.exec.cmdtail=RealMakeSeg(ss,reg_sp+0x100); block.SaveData(); #if 0 /* Save CS:IP to some point where i can return them from */ uint32_t oldeip=reg_eip; uint16_t oldcs=SegValue(cs); RealPt newcsip=CALLBACK_RealPointer(call_shellstop); SegSet16(cs,RealSeg(newcsip)); reg_ip=RealOff(newcsip); #endif packerr=false; /* Start up a dos execute interrupt */ reg_ax=0x4b00; //Filename pointer SegSet16(ds,SegValue(ss)); reg_dx=RealOff(file_name); //Paramblock SegSet16(es,SegValue(ss)); reg_bx=reg_sp; SETFLAGBIT(IF,false); CALLBACK_RunRealInt(0x21); /* Restore CS:IP and the stack */ reg_sp+=0x200; #if 0 reg_eip=oldeip; SegSet16(cs,oldcs); #endif if (packerr&&infix<0&&sec->Get_bool("autoa20fix")) { LOG(LOG_DOSMISC,LOG_DEBUG)("Attempting autoa20fix workaround for EXEPACK error"); if (autofixwarn==1||autofixwarn==3) WriteOut("\r\n\033[41;1m\033[1;37;1mDOSBox-X\033[0m Failed to load the executable\r\n\033[41;1m\033[37;1mDOSBox-X\033[0m Now try again with A20 fix...\r\n"); infix=0; dos_a20_disable_on_exec=true; Execute(name, args); dos_a20_disable_on_exec=false; infix=-1; } else if (packerr&&infix<1&&sec->Get_bool("autoloadfix")) { uint16_t segment; uint16_t blocks = (uint16_t)(1); /* start with one paragraph, resize up later. see if it comes up below the 64KB mark */ if (DOS_AllocateMemory(&segment,&blocks)) { DOS_MCB mcb((uint16_t)(segment-1)); if (segment < 0x1000) { uint16_t needed = 0x1000 - segment; DOS_ResizeMemory(segment,&needed); } mcb.SetPSPSeg(0x40); /* FIXME: Wouldn't 0x08, a magic value used to show ownership by MS-DOS, be more appropriate here? */ LOG(LOG_DOSMISC,LOG_DEBUG)("Attempting autoloadfix workaround for EXEPACK error"); if (autofixwarn==2||autofixwarn==3) WriteOut("\r\n\033[41;1m\033[1;37;1mDOSBox-X\033[0m Failed to load the executable\r\n\033[41;1m\033[37;1mDOSBox-X\033[0m Now try again with LOADFIX...\r\n"); infix=1; Execute(name, args); infix=-1; DOS_FreeMemory(segment); } } else if (packerr&&infix<2&&!autofixwarn) { WriteOut("Packed file is corrupt"); } packerr=false; } return true; //Executable started } static const char * bat_ext=".BAT"; static const char * com_ext=".COM"; static const char * exe_ext=".EXE"; static char which_ret[DOS_PATHLENGTH+4], s_ret[DOS_PATHLENGTH+4]; char * DOS_Shell::Which(char * name) { size_t name_len = strlen(name); if(name_len >= DOS_PATHLENGTH) return 0; /* Parse through the Path to find the correct entry */ /* Check if name is already ok but just misses an extension */ if (DOS_FileExists(name)) return name; upcase(name); if (DOS_FileExists(name)) return name; /* try to find .com .exe .bat */ strcpy(which_ret,name); strcat(which_ret,com_ext); if (DOS_FileExists(which_ret)) return which_ret; strcpy(which_ret,name); strcat(which_ret,exe_ext); if (DOS_FileExists(which_ret)) return which_ret; strcpy(which_ret,name); strcat(which_ret,bat_ext); if (DOS_FileExists(which_ret)) return which_ret; /* No Path in filename look through path environment string */ char path[DOS_PATHLENGTH];std::string temp; if (!GetEnvStr("PATH",temp)) return 0; const char * pathenv=temp.c_str(); if (!pathenv) return 0; pathenv = strchr(pathenv,'='); if (!pathenv) return 0; pathenv++; while (*pathenv) { /* remove ; and ;; at the beginning. (and from the second entry etc) */ while(*pathenv == ';') pathenv++; /* get next entry */ Bitu i_path = 0; /* reset writer */ while(*pathenv && (*pathenv !=';') && (i_path < DOS_PATHLENGTH) ) path[i_path++] = *pathenv++; if(i_path == DOS_PATHLENGTH) { /* If max size. move till next ; and terminate path */ while(*pathenv && (*pathenv != ';')) pathenv++; path[DOS_PATHLENGTH - 1] = 0; } else path[i_path] = 0; int k=0; for (int i=0;i<(int)strlen(path);i++) if (path[i]!='\"') path[k++]=path[i]; path[k]=0; /* check entry */ if(size_t len = strlen(path)){ if(len >= (DOS_PATHLENGTH - 2)) continue; if (uselfn&&len>3) { if (path[len - 1]=='\\') path[len - 1]=0; if (DOS_GetSFNPath(("\""+std::string(path)+"\"").c_str(), s_ret, false)) strcpy(path, s_ret); len = strlen(path); } if(path[len - 1] != '\\') { strcat(path,"\\"); len++; } //If name too long =>next if((name_len + len + 1) >= DOS_PATHLENGTH) continue; strcat(path,strchr(name, ' ')?("\""+std::string(name)+"\"").c_str():name); strcpy(which_ret,path); if (DOS_FileExists(which_ret)) return strchr(which_ret, '\"')&&DOS_GetSFNPath(which_ret, s_ret, false)?s_ret:which_ret; strcpy(which_ret,path); strcat(which_ret,com_ext); if (DOS_FileExists(which_ret)) return strchr(which_ret, '\"')&&DOS_GetSFNPath(which_ret, s_ret, false)?s_ret:which_ret; strcpy(which_ret,path); strcat(which_ret,exe_ext); if (DOS_FileExists(which_ret)) return strchr(which_ret, '\"')&&DOS_GetSFNPath(which_ret, s_ret, false)?s_ret:which_ret; strcpy(which_ret,path); strcat(which_ret,bat_ext); if (DOS_FileExists(which_ret)) return strchr(which_ret, '\"')&&DOS_GetSFNPath(which_ret, s_ret, false)?s_ret:which_ret; } } return 0; }