smol/src/files.c

3258 lines
92 KiB
C

/* $Id$ */
/**************************************************************************
* files.c *
* *
* Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, *
* 2008, 2009 Free Software Foundation, Inc. *
* 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 3, 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 St, Fifth Floor, Boston, MA *
* 02110-1301, USA. *
* *
**************************************************************************/
#include "proto.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <utime.h>
#include <fcntl.h>
#include <errno.h>
#include <ctype.h>
#include <pwd.h>
#include <libgen.h>
/* Add an entry to the openfile openfilestruct. This should only be
* called from open_buffer(). */
void make_new_buffer(void)
{
/* If there are no entries in openfile, make the first one and
* move to it. */
if (openfile == NULL) {
openfile = make_new_opennode();
splice_opennode(openfile, openfile, openfile);
/* Otherwise, make a new entry for openfile, splice it in after
* the current entry, and move to it. */
} else {
splice_opennode(openfile, make_new_opennode(), openfile->next);
openfile = openfile->next;
}
/* Initialize the new buffer. */
initialize_buffer();
}
/* Initialize the current entry of the openfile openfilestruct. */
void initialize_buffer(void)
{
assert(openfile != NULL);
openfile->filename = mallocstrcpy(NULL, "");
initialize_buffer_text();
openfile->current_x = 0;
openfile->placewewant = 0;
openfile->current_y = 0;
openfile->modified = FALSE;
#ifndef NANO_TINY
openfile->mark_set = FALSE;
openfile->mark_begin = NULL;
openfile->mark_begin_x = 0;
openfile->fmt = NIX_FILE;
openfile->current_stat = NULL;
openfile->undotop = NULL;
openfile->current_undo = NULL;
openfile->lock_filename = NULL;
#endif
#ifdef ENABLE_COLOR
openfile->colorstrings = NULL;
#endif
}
/* Initialize the text of the current entry of the openfile
* openfilestruct. */
void initialize_buffer_text(void)
{
assert(openfile != NULL);
openfile->fileage = make_new_node(NULL);
openfile->fileage->data = mallocstrcpy(NULL, "");
openfile->filebot = openfile->fileage;
openfile->edittop = openfile->fileage;
openfile->current = openfile->fileage;
#ifdef ENABLE_COLOR
openfile->fileage->multidata = NULL;
#endif
openfile->totsize = 0;
}
#ifndef NANO_TINY
/* Actyally write the lock file. This function will
ALWAYS annihilate any previous version of the file.
We'll borrow INSECURE_BACKUP here to decide about lock file
paranoia here as well...
Args:
lockfilename: file name for lock
origfilename: name of the file the lock is for
modified: whether to set the modified bit in the file
Returns: 1 on success, 0 on failure (but continue loading), -1 on failure and abort
*/
int write_lockfile(const char *lockfilename, const char *origfilename, bool modified)
{
int cflags, fd;
FILE *filestream;
pid_t mypid;
uid_t myuid;
struct passwd *mypwuid;
char *lockdata = charalloc(1024);
char myhostname[32];
ssize_t lockdatalen = 1024;
ssize_t wroteamt;
/* Run things which might fail first before we try and blow away
the old state */
myuid = geteuid();
if ((mypwuid = getpwuid(myuid)) == NULL) {
statusbar(_("Couldn't determine my identity for lock file (getpwuid() failed)"));
return -1;
}
mypid = getpid();
if (gethostname(myhostname, 31) < 0) {
statusbar(_("Couldn't determine hostname for lock file: %s"), strerror(errno));
return -1;
}
if (delete_lockfile(lockfilename) < 0)
return -1;
if (ISSET(INSECURE_BACKUP))
cflags = O_WRONLY | O_CREAT | O_APPEND;
else
cflags = O_WRONLY | O_CREAT | O_EXCL | O_APPEND;
fd = open(lockfilename, cflags,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
/* Maybe we just don't have write access, don't stop us from
opening the file at all, just don't set the lock_filename
and return success */
if (fd < 0 && errno == EACCES)
return 1;
/* Now we've got a safe file stream. If the previous open()
call failed, this will return NULL. */
filestream = fdopen(fd, "wb");
if (fd < 0 || filestream == NULL) {
statusbar(_("Error writing lock file %s: %s"), lockfilename,
strerror(errno));
return -1;
}
/* Okay. so at the moment we're following this state for how
to store the lock data:
byte 0 - 0x62
byte 1 - 0x30
bytes 2-12 - program name which created the lock
bytes 24,25 - little endian store of creator program's PID
(b24 = 256^0 column, b25 = 256^1 column)
bytes 28-44 - username of who created the lock
bytes 68-100 - hostname of where the lock was created
bytes 108-876 - filename the lock is for
byte 1007 - 0x55 if file is modified
Looks like VIM also stores undo state in this file so we're
gonna have to figure out how to slap a 'OMG don't use recover
on our lockfile' message in here...
This is likely very wrong, so this is a WIP
*/
null_at(&lockdata, lockdatalen);
lockdata[0] = 0x62;
lockdata[1] = 0x30;
lockdata[24] = mypid % 256;
lockdata[25] = mypid / 256;
snprintf(&lockdata[2], 10, "nano %s", VERSION);
strncpy(&lockdata[28], mypwuid->pw_name, 16);
strncpy(&lockdata[68], myhostname, 31);
strncpy(&lockdata[108], origfilename, 768);
if (modified == TRUE)
lockdata[1007] = 0x55;
wroteamt = fwrite(lockdata, sizeof(char), lockdatalen, filestream);
if (wroteamt < lockdatalen) {
statusbar(_("Error writing lock file %s: %s"),
lockfilename, ferror(filestream));
return -1;
}
#ifdef DEBUG
fprintf(stderr, "In write_lockfile(), write successful (wrote %d bytes)\n", wroteamt);
#endif /* DEBUG */
if (fclose(filestream) == EOF) {
statusbar(_("Error writing lock file %s: %s"),
lockfilename, strerror(errno));
return -1;
}
openfile->lock_filename = lockfilename;
return 1;
}
/* Less exciting, delete the lock file.
Return -1 if successful and complain on the statusbar, 1 otherwite
*/
int delete_lockfile(const char *lockfilename)
{
if (unlink(lockfilename) < 0 && errno != ENOENT) {
statusbar(_("Error deleting lock file %s: %s"), lockfilename,
strerror(errno));
return -1;
}
return 1;
}
/* Deal with lockfiles. Return -1 on refusing to override
the lock file, and 1 on successfully created the lockfile, 0 means
we were not successful on creating the lockfile but we should
continue to load the file and complain to the user.
*/
int do_lockfile(const char *filename)
{
char *lockdir = dirname((char *) mallocstrcpy(NULL, filename));
char *lockbase = basename((char *) mallocstrcpy(NULL, filename));
ssize_t lockfilesize = (sizeof (char *) * (strlen(filename)
+ strlen(locking_prefix) + strlen(locking_suffix) + 3));
char *lockfilename = (char *) nmalloc(lockfilesize);
char lockprog[12], lockuser[16];
struct stat fileinfo;
int lockfd, lockpid;
snprintf(lockfilename, lockfilesize, "%s/%s%s%s", lockdir,
locking_prefix, lockbase, locking_suffix);
#ifdef DEBUG
fprintf(stderr, "lock file name is %s\n", lockfilename);
#endif /* DEBUG */
if (stat(lockfilename, &fileinfo) != -1) {
ssize_t readtot = 0;
ssize_t readamt = 0;
char *lockbuf = (char *) nmalloc(8192);
char *promptstr = (char *) nmalloc(128);
int ans;
if ((lockfd = open(lockfilename, O_RDONLY)) < 0) {
statusbar(_("Error opening lock file %s: %s"),
lockfilename, strerror(errno));
return -1;
}
do {
readamt = read(lockfd, &lockbuf[readtot], BUFSIZ);
readtot += readamt;
} while (readtot < 8192 && readamt > 0);
if (readtot < 48) {
statusbar(_("Error reading lock file %s: Not enough data read"),
lockfilename);
return -1;
}
strncpy(lockprog, &lockbuf[2], 10);
lockpid = lockbuf[25] * 256 + lockbuf[24];
strncpy(lockuser, &lockbuf[28], 16);
#ifdef DEBUG
fprintf(stderr, "lockpid = %d\n", lockpid);
fprintf(stderr, "program name which created this lock file should be %s\n",
lockprog);
fprintf(stderr, "user which created this lock file should be %s\n",
lockuser);
#endif /* DEBUG */
sprintf(promptstr, "File being edited (by %s, PID %d, user %s), continue?",
lockprog, lockpid, lockuser);
ans = do_yesno_prompt(FALSE, promptstr);
if (ans < 1) {
blank_statusbar();
return -1;
}
}
return write_lockfile(lockfilename, filename, FALSE);
}
#endif /* NANO_TINY */
/* If it's not "", filename is a file to open. We make a new buffer, if
* necessary, and then open and read the file, if applicable. */
void open_buffer(const char *filename, bool undoable)
{
bool new_buffer = (openfile == NULL
#ifdef ENABLE_MULTIBUFFER
|| ISSET(MULTIBUFFER)
#endif
);
/* Whether we load into this buffer or a new one. */
FILE *f;
int rc;
/* rc == -2 means that we have a new file. -1 means that the
* open() failed. 0 means that the open() succeeded. */
assert(filename != NULL);
#ifndef DISABLE_OPERATINGDIR
if (check_operating_dir(filename, FALSE)) {
statusbar(_("Can't insert file from outside of %s"),
operating_dir);
return;
}
#endif
/* If we're loading into a new buffer, add a new entry to
* openfile. */
if (new_buffer)
make_new_buffer();
/* If the filename isn't blank, open the file. Otherwise, treat it
* as a new file. */
rc = (filename[0] != '\0') ? open_file(filename, new_buffer, &f) :
-2;
/* If we have a file, and we're loading into a new buffer, update
* the filename. */
if (rc != -1 && new_buffer)
openfile->filename = mallocstrcpy(openfile->filename, filename);
/* If we have a non-new file, read it in. Then, if the buffer has
* no stat, update the stat, if applicable. */
if (rc > 0) {
read_file(f, rc, filename, undoable, new_buffer);
#ifndef NANO_TINY
if (openfile->current_stat == NULL) {
openfile->current_stat =
(struct stat *)nmalloc(sizeof(struct stat));
stat(filename, openfile->current_stat);
}
#endif
}
/* If we have a file, and we're loading into a new buffer, move back
* to the beginning of the first line of the buffer. */
if (rc != -1 && new_buffer) {
openfile->current = openfile->fileage;
openfile->current_x = 0;
openfile->placewewant = 0;
}
#ifdef ENABLE_COLOR
/* If we're loading into a new buffer, update the colors to account
* for it, if applicable. */
if (new_buffer)
color_update();
#endif
}
#ifndef DISABLE_SPELLER
/* If it's not "", filename is a file to open. We blow away the text of
* the current buffer, and then open and read the file, if
* applicable. Note that we skip the operating directory test when
* doing this. */
void replace_buffer(const char *filename)
{
FILE *f;
int rc;
/* rc == -2 means that we have a new file. -1 means that the
* open() failed. 0 means that the open() succeeded. */
assert(filename != NULL);
/* If the filename isn't blank, open the file. Otherwise, treat it
* as a new file. */
rc = (filename[0] != '\0') ? open_file(filename, TRUE, &f) : -2;
/* Reinitialize the text of the current buffer. */
free_filestruct(openfile->fileage);
initialize_buffer_text();
/* If we have a non-new file, read it in. */
if (rc > 0)
read_file(f, rc, filename, FALSE, TRUE);
/* Move back to the beginning of the first line of the buffer. */
openfile->current = openfile->fileage;
openfile->current_x = 0;
openfile->placewewant = 0;
}
#endif /* !DISABLE_SPELLER */
/* Update the screen to account for the current buffer. */
void display_buffer(void)
{
/* Update the titlebar, since the filename may have changed. */
titlebar(NULL);
#ifdef ENABLE_COLOR
/* Make sure we're using the buffer's associated colors, if
* applicable. */
color_init();
#endif
/* Update the edit window. */
edit_refresh();
}
#ifdef ENABLE_MULTIBUFFER
/* Switch to the next file buffer if next_buf is TRUE. Otherwise,
* switch to the previous file buffer. */
void switch_to_prevnext_buffer(bool next_buf)
{
assert(openfile != NULL);
/* If only one file buffer is open, indicate it on the statusbar and
* get out. */
if (openfile == openfile->next) {
statusbar(_("No more open file buffers"));
return;
}
/* Switch to the next or previous file buffer, depending on the
* value of next_buf. */
openfile = next_buf ? openfile->next : openfile->prev;
#ifdef DEBUG
fprintf(stderr, "filename is %s\n", openfile->filename);
#endif
/* Update the screen to account for the current buffer. */
display_buffer();
/* Indicate the switch on the statusbar. */
statusbar(_("Switched to %s"),
((openfile->filename[0] == '\0') ? _("New Buffer") :
openfile->filename));
#ifdef DEBUG
dump_filestruct(openfile->current);
#endif
display_main_list();
}
/* Switch to the previous entry in the openfile filebuffer. */
void switch_to_prev_buffer_void(void)
{
switch_to_prevnext_buffer(FALSE);
}
/* Switch to the next entry in the openfile filebuffer. */
void switch_to_next_buffer_void(void)
{
switch_to_prevnext_buffer(TRUE);
}
/* Delete an entry from the openfile filebuffer, and switch to the one
* after it. Return TRUE on success, or FALSE if there are no more open
* file buffers. */
bool close_buffer(void)
{
assert(openfile != NULL);
/* If only one file buffer is open, get out. */
if (openfile == openfile->next)
return FALSE;
#if !defined(NANO_TINY) && defined(ENABLE_NANORC)
update_poshistory(openfile->filename, openfile->current->lineno, xplustabs()+1);
#endif
/* Switch to the next file buffer. */
switch_to_next_buffer_void();
/* Close the file buffer we had open before. */
unlink_opennode(openfile->prev);
display_main_list();
return TRUE;
}
#endif /* ENABLE_MULTIBUFFER */
/* A bit of a copy and paste from open_file(), is_file_writable()
* just checks whether the file is appendable as a quick
* permissions check, and we tend to err on the side of permissiveness
* (reporting TRUE when it might be wrong) to not fluster users
* editing on odd filesystems by printing incorrect warnings.
*/
int is_file_writable(const char *filename)
{
struct stat fileinfo, fileinfo2;
int fd;
FILE *f;
char *full_filename;
bool ans = TRUE;
if (ISSET(VIEW_MODE))
return TRUE;
assert(filename != NULL);
/* Get the specified file's full path. */
full_filename = get_full_path(filename);
/* Okay, if we can't stat the path due to a component's
permissions, just try the relative one */
if (full_filename == NULL
|| (stat(full_filename, &fileinfo) == -1 && stat(filename, &fileinfo2) != -1))
full_filename = mallocstrcpy(NULL, filename);
if ((fd = open(full_filename, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR |
S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)) == -1
|| (f = fdopen(fd, "a")) == NULL)
ans = FALSE;
else
fclose(f);
close(fd);
free(full_filename);
return ans;
}
/* We make a new line of text from buf. buf is length buf_len. If
* first_line_ins is TRUE, then we put the new line at the top of the
* file. Otherwise, we assume prevnode is the last line of the file,
* and put our line after prevnode. */
filestruct *read_line(char *buf, filestruct *prevnode, bool
*first_line_ins, size_t buf_len)
{
filestruct *fileptr = (filestruct *)nmalloc(sizeof(filestruct));
/* Convert nulls to newlines. buf_len is the string's real
* length. */
unsunder(buf, buf_len);
assert(openfile->fileage != NULL && strlen(buf) == buf_len);
fileptr->data = mallocstrcpy(NULL, buf);
#ifndef NANO_TINY
/* If it's a DOS file ("\r\n"), and file conversion isn't disabled,
* strip the '\r' part from fileptr->data. */
if (!ISSET(NO_CONVERT) && buf_len > 0 && buf[buf_len - 1] == '\r')
fileptr->data[buf_len - 1] = '\0';
#endif
#ifdef ENABLE_COLOR
fileptr->multidata = NULL;
#endif
if (*first_line_ins) {
/* Special case: We're inserting with the cursor on the first
* line. */
fileptr->prev = NULL;
fileptr->next = openfile->fileage;
fileptr->lineno = 1;
if (*first_line_ins) {
*first_line_ins = FALSE;
/* If we're inserting into the first line of the file, then
* we want to make sure that our edit buffer stays on the
* first line and that fileage stays up to date. */
openfile->edittop = fileptr;
} else
openfile->filebot = fileptr;
openfile->fileage = fileptr;
} else {
assert(prevnode != NULL);
fileptr->prev = prevnode;
fileptr->next = NULL;
fileptr->lineno = prevnode->lineno + 1;
prevnode->next = fileptr;
}
return fileptr;
}
/* Read an open file into the current buffer. f should be set to the
* open file, and filename should be set to the name of the file.
* undoable means do we want to create undo records to try and undo this.
* Will also attempt to check file writability if fd > 0 and checkwritable == TRUE
*/
void read_file(FILE *f, int fd, const char *filename, bool undoable, bool checkwritable)
{
size_t num_lines = 0;
/* The number of lines in the file. */
size_t len = 0;
/* The length of the current line of the file. */
size_t i = 0;
/* The position in the current line of the file. */
size_t bufx = MAX_BUF_SIZE;
/* The size of each chunk of the file that we read. */
char input = '\0';
/* The current input character. */
char *buf;
/* The buffer where we store chunks of the file. */
filestruct *fileptr = openfile->current;
/* The current line of the file. */
bool first_line_ins = FALSE;
/* Whether we're inserting with the cursor on the first line. */
int input_int;
/* The current value we read from the file, whether an input
* character or EOF. */
bool writable = TRUE;
/* Is the file writable (if we care) */
#ifndef NANO_TINY
int format = 0;
/* 0 = *nix, 1 = DOS, 2 = Mac, 3 = both DOS and Mac. */
#endif
assert(openfile->fileage != NULL && openfile->current != NULL);
buf = charalloc(bufx);
buf[0] = '\0';
#ifndef NANO_TINY
if (undoable)
add_undo(INSERT);
#endif
if (openfile->current == openfile->fileage)
first_line_ins = TRUE;
else
fileptr = openfile->current->prev;
/* Read the entire file into the filestruct. */
while ((input_int = getc(f)) != EOF) {
input = (char)input_int;
/* If it's a *nix file ("\n") or a DOS file ("\r\n"), and file
* conversion isn't disabled, handle it! */
if (input == '\n') {
#ifndef NANO_TINY
/* If it's a DOS file or a DOS/Mac file ('\r' before '\n' on
* the first line if we think it's a *nix file, or on any
* line otherwise), and file conversion isn't disabled,
* handle it! */
if (!ISSET(NO_CONVERT) && (num_lines == 0 || format != 0) &&
i > 0 && buf[i - 1] == '\r') {
if (format == 0 || format == 2)
format++;
}
#endif
/* Read in the line properly. */
fileptr = read_line(buf, fileptr, &first_line_ins, len);
/* Reset the line length in preparation for the next
* line. */
len = 0;
num_lines++;
buf[0] = '\0';
i = 0;
#ifndef NANO_TINY
/* If it's a Mac file ('\r' without '\n' on the first line if we
* think it's a *nix file, or on any line otherwise), and file
* conversion isn't disabled, handle it! */
} else if (!ISSET(NO_CONVERT) && (num_lines == 0 ||
format != 0) && i > 0 && buf[i - 1] == '\r') {
/* If we currently think the file is a *nix file, set format
* to Mac. If we currently think the file is a DOS file,
* set format to both DOS and Mac. */
if (format == 0 || format == 1)
format += 2;
/* Read in the line properly. */
fileptr = read_line(buf, fileptr, &first_line_ins, len);
/* Reset the line length in preparation for the next line.
* Since we've already read in the next character, reset it
* to 1 instead of 0. */
len = 1;
num_lines++;
buf[0] = input;
buf[1] = '\0';
i = 1;
#endif
} else {
/* Calculate the total length of the line. It might have
* nulls in it, so we can't just use strlen() here. */
len++;
/* Now we allocate a bigger buffer MAX_BUF_SIZE characters
* at a time. If we allocate a lot of space for one line,
* we may indeed have to use a buffer this big later on, so
* we don't decrease it at all. We do free it at the end,
* though. */
if (i >= bufx - 1) {
bufx += MAX_BUF_SIZE;
buf = charealloc(buf, bufx);
}
buf[i] = input;
buf[i + 1] = '\0';
i++;
}
}
/* Perhaps this could use some better handling. */
if (ferror(f))
nperror(filename);
fclose(f);
if (fd > 0 && checkwritable) {
close(fd);
writable = is_file_writable(filename);
}
#ifndef NANO_TINY
/* If file conversion isn't disabled and the last character in this
* file is '\r', read it in properly as a Mac format line. */
if (len == 0 && !ISSET(NO_CONVERT) && input == '\r') {
len = 1;
buf[0] = input;
buf[1] = '\0';
}
#endif
/* Did we not get a newline and still have stuff to do? */
if (len > 0) {
#ifndef NANO_TINY
/* If file conversion isn't disabled and the last character in
* this file is '\r', set format to Mac if we currently think
* the file is a *nix file, or to both DOS and Mac if we
* currently think the file is a DOS file. */
if (!ISSET(NO_CONVERT) && buf[len - 1] == '\r' &&
(format == 0 || format == 1))
format += 2;
#endif
/* Read in the last line properly. */
fileptr = read_line(buf, fileptr, &first_line_ins, len);
num_lines++;
}
free(buf);
/* If we didn't get a file and we don't already have one, open a
* blank buffer. */
if (fileptr == NULL)
open_buffer("", FALSE);
/* Attach the file we got to the filestruct. If we got a file of
* zero bytes, don't do anything. */
if (num_lines > 0) {
/* If the file we got doesn't end in a newline, tack its last
* line onto the beginning of the line at current. */
if (len > 0) {
size_t current_len = strlen(openfile->current->data);
/* Adjust the current x-coordinate to compensate for the
* change in the current line. */
if (num_lines == 1)
openfile->current_x += len;
else
openfile->current_x = len;
/* Tack the text at fileptr onto the beginning of the text
* at current. */
openfile->current->data =
charealloc(openfile->current->data, len +
current_len + 1);
charmove(openfile->current->data + len,
openfile->current->data, current_len + 1);
strncpy(openfile->current->data, fileptr->data, len);
/* Don't destroy fileage, edittop, or filebot! */
if (fileptr == openfile->fileage)
openfile->fileage = openfile->current;
if (fileptr == openfile->edittop)
openfile->edittop = openfile->current;
if (fileptr == openfile->filebot)
openfile->filebot = openfile->current;
/* Move fileptr back one line and blow away the old fileptr,
* since its text has been saved. */
fileptr = fileptr->prev;
if (fileptr != NULL) {
if (fileptr->next != NULL)
free(fileptr->next);
}
}
/* Attach the line at current after the line at fileptr. */
if (fileptr != NULL) {
fileptr->next = openfile->current;
openfile->current->prev = fileptr;
}
/* Renumber starting with the last line of the file we
* inserted. */
renumber(openfile->current);
}
openfile->totsize += get_totsize(openfile->fileage,
openfile->filebot);
/* If the NO_NEWLINES flag isn't set, and text has been added to
* the magicline (i.e. a file that doesn't end in a newline has been
* inserted at the end of the current buffer), add a new magicline,
* and move the current line down to it. */
if (!ISSET(NO_NEWLINES) && openfile->filebot->data[0] != '\0') {
new_magicline();
openfile->current = openfile->filebot;
openfile->current_x = 0;
}
/* Set the current place we want to the end of the last line of the
* file we inserted. */
openfile->placewewant = xplustabs();
#ifndef NANO_TINY
if (undoable)
update_undo(INSERT);
if (format == 3) {
if (writable)
statusbar(
P_("Read %lu line (Converted from DOS and Mac format)",
"Read %lu lines (Converted from DOS and Mac format)",
(unsigned long)num_lines), (unsigned long)num_lines);
else
statusbar(
P_("Read %lu line (Converted from DOS and Mac format - Warning: No write permission)",
"Read %lu lines (Converted from DOS and Mac format - Warning: No write permission)",
(unsigned long)num_lines), (unsigned long)num_lines);
} else if (format == 2) {
openfile->fmt = MAC_FILE;
if (writable)
statusbar(P_("Read %lu line (Converted from Mac format)",
"Read %lu lines (Converted from Mac format)",
(unsigned long)num_lines), (unsigned long)num_lines);
else
statusbar(P_("Read %lu line (Converted from Mac format - Warning: No write permission)",
"Read %lu lines (Converted from Mac format - Warning: No write permission)",
(unsigned long)num_lines), (unsigned long)num_lines);
} else if (format == 1) {
openfile->fmt = DOS_FILE;
if (writable)
statusbar(P_("Read %lu line (Converted from DOS format)",
"Read %lu lines (Converted from DOS format)",
(unsigned long)num_lines), (unsigned long)num_lines);
else
statusbar(P_("Read %lu line (Converted from DOS format - Warning: No write permission)",
"Read %lu lines (Converted from DOS format - Warning: No write permission)",
(unsigned long)num_lines), (unsigned long)num_lines);
} else
#endif
if (writable)
statusbar(P_("Read %lu line", "Read %lu lines",
(unsigned long)num_lines), (unsigned long)num_lines);
else
statusbar(P_("Read %lu line ( Warning: No write permission)",
"Read %lu lines (Warning: No write permission)",
(unsigned long)num_lines), (unsigned long)num_lines);
}
/* Open the file (and decide if it exists). If newfie is TRUE, display
* "New File" if the file is missing. Otherwise, say "[filename] not
* found".
*
* Return -2 if we say "New File", -1 if the file isn't opened, and the
* fd opened otherwise. The file might still have an error while reading
* with a 0 return value. *f is set to the opened file. */
int open_file(const char *filename, bool newfie, FILE **f)
{
struct stat fileinfo, fileinfo2;
int fd;
char *full_filename;
assert(filename != NULL && f != NULL);
/* Get the specified file's full path. */
full_filename = get_full_path(filename);
/* Okay, if we can't stat the path due to a component's
permissions, just try the relative one */
if (full_filename == NULL
|| (stat(full_filename, &fileinfo) == -1 && stat(filename, &fileinfo2) != -1))
full_filename = mallocstrcpy(NULL, filename);
#ifndef NANO_TINY
if (ISSET(LOCKING))
if (do_lockfile(full_filename) < 0)
return -1;
#endif
if (stat(full_filename, &fileinfo) == -1) {
/* Well, maybe we can open the file even if the OS
says its not there */
if ((fd = open(filename, O_RDONLY)) != -1) {
statusbar(_("Reading File"));
free(full_filename);
return 0;
}
if (newfie) {
statusbar(_("New File"));
return -2;
}
statusbar(_("\"%s\" not found"), filename);
beep();
return -1;
} else if (S_ISDIR(fileinfo.st_mode) || S_ISCHR(fileinfo.st_mode) ||
S_ISBLK(fileinfo.st_mode)) {
/* Don't open directories, character files, or block files.
* Sorry, /dev/sndstat! */
statusbar(S_ISDIR(fileinfo.st_mode) ?
_("\"%s\" is a directory") :
_("\"%s\" is a device file"), filename);
beep();
return -1;
} else if ((fd = open(full_filename, O_RDONLY)) == -1) {
statusbar(_("Error reading %s: %s"), filename,
strerror(errno));
beep();
return -1;
} else {
/* The file is A-OK. Open it. */
*f = fdopen(fd, "rb");
if (*f == NULL) {
statusbar(_("Error reading %s: %s"), filename,
strerror(errno));
beep();
close(fd);
} else
statusbar(_("Reading File"));
}
free(full_filename);
return fd;
}
/* This function will return the name of the first available extension
* of a filename (starting with [name][suffix], then [name][suffix].1,
* etc.). Memory is allocated for the return value. If no writable
* extension exists, we return "". */
char *get_next_filename(const char *name, const char *suffix)
{
static int ulmax_digits = -1;
unsigned long i = 0;
char *buf;
size_t namelen, suffixlen;
assert(name != NULL && suffix != NULL);
if (ulmax_digits == -1)
ulmax_digits = digits(ULONG_MAX);
namelen = strlen(name);
suffixlen = strlen(suffix);
buf = charalloc(namelen + suffixlen + ulmax_digits + 2);
sprintf(buf, "%s%s", name, suffix);
while (TRUE) {
struct stat fs;
if (stat(buf, &fs) == -1)
return buf;
if (i == ULONG_MAX)
break;
i++;
sprintf(buf + namelen + suffixlen, ".%lu", i);
}
/* We get here only if there is no possible save file. Blank out
* the filename to indicate this. */
null_at(&buf, 0);
return buf;
}
/* Insert a file into a new buffer if the MULTIBUFFER flag is set, or
* into the current buffer if it isn't. If execute is TRUE, insert the
* output of an executed command instead of a file. */
void do_insertfile(
#ifndef NANO_TINY
bool execute
#else
void
#endif
)
{
int i;
const char *msg;
char *ans = mallocstrcpy(NULL, "");
/* The last answer the user typed at the statusbar prompt. */
filestruct *edittop_save = openfile->edittop;
size_t current_x_save = openfile->current_x;
ssize_t current_y_save = openfile->current_y;
bool edittop_inside = FALSE, meta_key = FALSE, func_key = FALSE;
const sc *s;
#ifndef NANO_TINY
bool right_side_up = FALSE, single_line = FALSE;
#endif
currmenu = MINSERTFILE;
while (TRUE) {
#ifndef NANO_TINY
if (execute) {
msg =
#ifdef ENABLE_MULTIBUFFER
ISSET(MULTIBUFFER) ?
_("Command to execute in new buffer [from %s] ") :
#endif
_("Command to execute [from %s] ");
} else {
#endif
msg =
#ifdef ENABLE_MULTIBUFFER
ISSET(MULTIBUFFER) ?
_("File to insert into new buffer [from %s] ") :
#endif
_("File to insert [from %s] ");
#ifndef NANO_TINY
}
#endif
i = do_prompt(TRUE,
#ifndef DISABLE_TABCOMP
TRUE,
#endif
#ifndef NANO_TINY
execute ? MEXTCMD :
#endif
MINSERTFILE, ans,
&meta_key, &func_key,
#ifndef NANO_TINY
NULL,
#endif
edit_refresh, msg,
#ifndef DISABLE_OPERATINGDIR
operating_dir != NULL && strcmp(operating_dir,
".") != 0 ? operating_dir :
#endif
"./");
/* If we're in multibuffer mode and the filename or command is
* blank, open a new buffer instead of canceling. If the
* filename or command begins with a newline (i.e. an encoded
* null), treat it as though it's blank. */
if (i == -1 || ((i == -2 || *answer == '\n')
#ifdef ENABLE_MULTIBUFFER
&& !ISSET(MULTIBUFFER)
#endif
)) {
statusbar(_("Cancelled"));
break;
} else {
size_t pww_save = openfile->placewewant;
ans = mallocstrcpy(ans, answer);
s = get_shortcut(currmenu, &i, &meta_key, &func_key);
#ifndef NANO_TINY
#ifdef ENABLE_MULTIBUFFER
if (s && s->scfunc == new_buffer_void) {
/* Don't allow toggling if we're in view mode. */
if (!ISSET(VIEW_MODE))
TOGGLE(MULTIBUFFER);
continue;
} else
#endif
if (s && s->scfunc == ext_cmd_void) {
execute = !execute;
continue;
}
#ifndef DISABLE_BROWSER
else
#endif
#endif /* !NANO_TINY */
#ifndef DISABLE_BROWSER
if (s && s->scfunc == to_files_void) {
char *tmp = do_browse_from(answer);
if (tmp == NULL)
continue;
/* We have a file now. Indicate this. */
free(answer);
answer = tmp;
i = 0;
}
#endif
/* If we don't have a file yet, go back to the statusbar
* prompt. */
if (i != 0
#ifdef ENABLE_MULTIBUFFER
&& (i != -2 || !ISSET(MULTIBUFFER))
#endif
)
continue;
#ifndef NANO_TINY
/* Keep track of whether the mark begins inside the
* partition and will need adjustment. */
if (openfile->mark_set) {
filestruct *top, *bot;
size_t top_x, bot_x;
mark_order((const filestruct **)&top, &top_x,
(const filestruct **)&bot, &bot_x,
&right_side_up);
single_line = (top == bot);
}
#endif
#ifdef ENABLE_MULTIBUFFER
if (!ISSET(MULTIBUFFER)) {
#endif
/* If we're not inserting into a new buffer, partition
* the filestruct so that it contains no text and hence
* looks like a new buffer, and keep track of whether
* the top of the edit window is inside the
* partition. */
filepart = partition_filestruct(openfile->current,
openfile->current_x, openfile->current,
openfile->current_x);
edittop_inside =
(openfile->edittop == openfile->fileage);
#ifdef ENABLE_MULTIBUFFER
}
#endif
/* Convert newlines to nulls, just before we insert the file
* or execute the command. */
sunder(answer);
align(&answer);
#ifndef NANO_TINY
if (execute) {
#ifdef ENABLE_MULTIBUFFER
if (ISSET(MULTIBUFFER))
/* Open a blank buffer. */
open_buffer("", FALSE);
#endif
/* Save the command's output in the current buffer. */
execute_command(answer);
#ifdef ENABLE_MULTIBUFFER
if (ISSET(MULTIBUFFER)) {
/* Move back to the beginning of the first line of
* the buffer. */
openfile->current = openfile->fileage;
openfile->current_x = 0;
openfile->placewewant = 0;
}
#endif
} else {
#endif /* !NANO_TINY */
/* Make sure the path to the file specified in answer is
* tilde-expanded. */
answer = mallocstrassn(answer,
real_dir_from_tilde(answer));
/* Save the file specified in answer in the current
* buffer. */
open_buffer(answer, TRUE);
#ifndef NANO_TINY
}
#endif
#if defined(ENABLE_MULTIBUFFER) && defined(ENABLE_NANORC)
if (ISSET(MULTIBUFFER)) {
/* Update the screen to account for the current
* buffer. */
ssize_t savedposline, savedposcol;
display_buffer();
#ifndef NANO_TINY
if (!execute && ISSET(POS_HISTORY)
&& check_poshistory(answer, &savedposline, &savedposcol))
do_gotolinecolumn(savedposline, savedposcol, FALSE, FALSE, FALSE, FALSE);
#endif
} else
#endif
{
filestruct *top_save = openfile->fileage;
/* If we were at the top of the edit window before, set
* the saved value of edittop to the new top of the edit
* window. */
if (edittop_inside)
edittop_save = openfile->fileage;
/* Update the current x-coordinate to account for the
* number of characters inserted on the current line.
* If the mark begins inside the partition, adjust the
* mark coordinates to compensate for the change in the
* current line. */
openfile->current_x = strlen(openfile->filebot->data);
if (openfile->fileage == openfile->filebot) {
#ifndef NANO_TINY
if (openfile->mark_set) {
openfile->mark_begin = openfile->current;
if (!right_side_up)
openfile->mark_begin_x +=
openfile->current_x;
}
#endif
openfile->current_x += current_x_save;
}
#ifndef NANO_TINY
else if (openfile->mark_set) {
if (!right_side_up) {
if (single_line) {
openfile->mark_begin = openfile->current;
openfile->mark_begin_x -= current_x_save;
} else
openfile->mark_begin_x -=
openfile->current_x;
}
}
#endif
/* Update the current y-coordinate to account for the
* number of lines inserted. */
openfile->current_y += current_y_save;
/* Unpartition the filestruct so that it contains all
* the text again. Note that we've replaced the
* non-text originally in the partition with the text in
* the inserted file/executed command output. */
unpartition_filestruct(&filepart);
/* Renumber starting with the beginning line of the old
* partition. */
renumber(top_save);
/* Restore the old edittop. */
openfile->edittop = edittop_save;
/* Restore the old place we want. */
openfile->placewewant = pww_save;
/* Mark the file as modified. */
set_modified();
/* Update the screen. */
edit_refresh();
}
break;
}
}
shortcut_init(FALSE);
free(ans);
}
/* Insert a file into a new buffer or the current buffer, depending on
* whether the MULTIBUFFER flag is set. If we're in view mode, only
* allow inserting a file into a new buffer. */
void do_insertfile_void(void)
{
if (ISSET(RESTRICTED)) {
nano_disabled_msg();
return;
}
#ifdef ENABLE_MULTIBUFFER
if (ISSET(VIEW_MODE) && !ISSET(MULTIBUFFER))
statusbar(_("Key invalid in non-multibuffer mode"));
else
#endif
do_insertfile(
#ifndef NANO_TINY
FALSE
#endif
);
display_main_list();
}
/* When passed "[relative path]" or "[relative path][filename]" in
* origpath, return "[full path]" or "[full path][filename]" on success,
* or NULL on error. Do this if the file doesn't exist but the relative
* path does, since the file could exist in memory but not yet on disk).
* Don't do this if the relative path doesn't exist, since we won't be
* able to go there. */
char *get_full_path(const char *origpath)
{
struct stat fileinfo;
char *d_here, *d_there, *d_there_file = NULL;
const char *last_slash;
bool path_only;
if (origpath == NULL)
return NULL;
/* Get the current directory. If it doesn't exist, back up and try
* again until we get a directory that does, and use that as the
* current directory. */
d_here = charalloc(PATH_MAX + 1);
d_here = getcwd(d_here, PATH_MAX + 1);
while (d_here == NULL) {
if (chdir("..") == -1)
break;
d_here = getcwd(d_here, PATH_MAX + 1);
}
/* If we succeeded, canonicalize it in d_here. */
if (d_here != NULL) {
align(&d_here);
/* If the current directory isn't "/", tack a slash onto the end
* of it. */
if (strcmp(d_here, "/") != 0) {
d_here = charealloc(d_here, strlen(d_here) + 2);
strcat(d_here, "/");
}
/* Otherwise, set d_here to "". */
} else
d_here = mallocstrcpy(NULL, "");
d_there = real_dir_from_tilde(origpath);
/* If stat()ing d_there fails, assume that d_there refers to a new
* file that hasn't been saved to disk yet. Set path_only to TRUE
* if d_there refers to a directory, and FALSE otherwise. */
path_only = (stat(d_there, &fileinfo) != -1 &&
S_ISDIR(fileinfo.st_mode));
/* If path_only is TRUE, make sure d_there ends in a slash. */
if (path_only) {
size_t d_there_len = strlen(d_there);
if (d_there[d_there_len - 1] != '/') {
d_there = charealloc(d_there, d_there_len + 2);
strcat(d_there, "/");
}
}
/* Search for the last slash in d_there. */
last_slash = strrchr(d_there, '/');
/* If we didn't find one, then make sure the answer is in the format
* "d_here/d_there". */
if (last_slash == NULL) {
assert(!path_only);
d_there_file = d_there;
d_there = d_here;
} else {
/* If path_only is FALSE, then save the filename portion of the
* answer (everything after the last slash) in d_there_file. */
if (!path_only)
d_there_file = mallocstrcpy(NULL, last_slash + 1);
/* Remove the filename portion of the answer from d_there. */
null_at(&d_there, last_slash - d_there + 1);
/* Go to the path specified in d_there. */
if (chdir(d_there) == -1) {
free(d_there);
d_there = NULL;
} else {
free(d_there);
/* Get the full path. */
d_there = charalloc(PATH_MAX + 1);
d_there = getcwd(d_there, PATH_MAX + 1);
/* If we succeeded, canonicalize it in d_there. */
if (d_there != NULL) {
align(&d_there);
/* If the current directory isn't "/", tack a slash onto
* the end of it. */
if (strcmp(d_there, "/") != 0) {
d_there = charealloc(d_there, strlen(d_there) + 2);
strcat(d_there, "/");
}
} else
/* Otherwise, set path_only to TRUE, so that we clean up
* correctly, free all allocated memory, and return
* NULL. */
path_only = TRUE;
/* Finally, go back to the path specified in d_here,
* where we were before. We don't check for a chdir()
* error, since we can do nothing if we get one. */
IGNORE_CALL_RESULT(chdir(d_here));
/* Free d_here, since we're done using it. */
free(d_here);
}
}
/* At this point, if path_only is FALSE and d_there isn't NULL,
* d_there contains the path portion of the answer and d_there_file
* contains the filename portion of the answer. If this is the
* case, tack the latter onto the end of the former. d_there will
* then contain the complete answer. */
if (!path_only && d_there != NULL) {
d_there = charealloc(d_there, strlen(d_there) +
strlen(d_there_file) + 1);
strcat(d_there, d_there_file);
}
/* Free d_there_file, since we're done using it. */
if (d_there_file != NULL)
free(d_there_file);
return d_there;
}
/* Return the full version of path, as returned by get_full_path(). On
* error, if path doesn't reference a directory, or if the directory
* isn't writable, return NULL. */
char *check_writable_directory(const char *path)
{
char *full_path = get_full_path(path);
/* If get_full_path() fails, return NULL. */
if (full_path == NULL)
return NULL;
/* If we can't write to path or path isn't a directory, return
* NULL. */
if (access(full_path, W_OK) != 0 ||
full_path[strlen(full_path) - 1] != '/') {
free(full_path);
return NULL;
}
/* Otherwise, return the full path. */
return full_path;
}
/* This function calls mkstemp(($TMPDIR|P_tmpdir|/tmp/)"nano.XXXXXX").
* On success, it returns the malloc()ed filename and corresponding FILE
* stream, opened in "r+b" mode. On error, it returns NULL for the
* filename and leaves the FILE stream unchanged. */
char *safe_tempfile(FILE **f)
{
char *full_tempdir = NULL;
const char *tmpdir_env;
int fd;
mode_t original_umask = 0;
assert(f != NULL);
/* If $TMPDIR is set, set tempdir to it, run it through
* get_full_path(), and save the result in full_tempdir. Otherwise,
* leave full_tempdir set to NULL. */
tmpdir_env = getenv("TMPDIR");
if (tmpdir_env != NULL)
full_tempdir = check_writable_directory(tmpdir_env);
/* If $TMPDIR is unset, empty, or not a writable directory, and
* full_tempdir is NULL, try P_tmpdir instead. */
if (full_tempdir == NULL)
full_tempdir = check_writable_directory(P_tmpdir);
/* if P_tmpdir is NULL, use /tmp. */
if (full_tempdir == NULL)
full_tempdir = mallocstrcpy(NULL, "/tmp/");
full_tempdir = charealloc(full_tempdir, strlen(full_tempdir) + 12);
strcat(full_tempdir, "nano.XXXXXX");
original_umask = umask(0);
umask(S_IRWXG | S_IRWXO);
fd = mkstemp(full_tempdir);
if (fd != -1)
*f = fdopen(fd, "r+b");
else {
free(full_tempdir);
full_tempdir = NULL;
}
umask(original_umask);
return full_tempdir;
}
#ifndef DISABLE_OPERATINGDIR
/* Initialize full_operating_dir based on operating_dir. */
void init_operating_dir(void)
{
assert(full_operating_dir == NULL);
if (operating_dir == NULL)
return;
full_operating_dir = get_full_path(operating_dir);
/* If get_full_path() failed or the operating directory is
* inaccessible, unset operating_dir. */
if (full_operating_dir == NULL || chdir(full_operating_dir) == -1) {
free(full_operating_dir);
full_operating_dir = NULL;
free(operating_dir);
operating_dir = NULL;
}
}
/* Check to see if we're inside the operating directory. Return FALSE
* if we are, or TRUE otherwise. If allow_tabcomp is TRUE, allow
* incomplete names that would be matches for the operating directory,
* so that tab completion will work. */
bool check_operating_dir(const char *currpath, bool allow_tabcomp)
{
/* full_operating_dir is global for memory cleanup. It should have
* already been initialized by init_operating_dir(). Also, a
* relative operating directory path will only be handled properly
* if this is done. */
char *fullpath;
bool retval = FALSE;
const char *whereami1, *whereami2 = NULL;
/* If no operating directory is set, don't bother doing anything. */
if (operating_dir == NULL)
return FALSE;
assert(full_operating_dir != NULL);
fullpath = get_full_path(currpath);
/* If fullpath is NULL, it means some directory in the path doesn't
* exist or is unreadable. If allow_tabcomp is FALSE, then currpath
* is what the user typed somewhere. We don't want to report a
* non-existent directory as being outside the operating directory,
* so we return FALSE. If allow_tabcomp is TRUE, then currpath
* exists, but is not executable. So we say it isn't in the
* operating directory. */
if (fullpath == NULL)
return allow_tabcomp;
whereami1 = strstr(fullpath, full_operating_dir);
if (allow_tabcomp)
whereami2 = strstr(full_operating_dir, fullpath);
/* If both searches failed, we're outside the operating directory.
* Otherwise, check the search results. If the full operating
* directory path is not at the beginning of the full current path
* (for normal usage) and vice versa (for tab completion, if we're
* allowing it), we're outside the operating directory. */
if (whereami1 != fullpath && whereami2 != full_operating_dir)
retval = TRUE;
free(fullpath);
/* Otherwise, we're still inside it. */
return retval;
}
#endif
#ifndef NANO_TINY
/* Although this sucks, it sucks less than having a single 'my system is messed up
* and I'm blanket allowing insecure file writing operations.
*/
int prompt_failed_backupwrite(const char *filename)
{
static int i;
static char *prevfile = NULL; /* What was the laast file we were paased so we don't keep asking this?
though maybe we should.... */
if (prevfile == NULL || strcmp(filename, prevfile)) {
i = do_yesno_prompt(FALSE,
_("Failed to write backup file, continue saving? (Say N if unsure) "));
prevfile = mallocstrcpy(prevfile, filename);
}
return i;
}
void init_backup_dir(void)
{
char *full_backup_dir;
if (backup_dir == NULL)
return;
full_backup_dir = get_full_path(backup_dir);
/* If get_full_path() failed or the backup directory is
* inaccessible, unset backup_dir. */
if (full_backup_dir == NULL ||
full_backup_dir[strlen(full_backup_dir) - 1] != '/') {
free(full_backup_dir);
free(backup_dir);
backup_dir = NULL;
} else {
free(backup_dir);
backup_dir = full_backup_dir;
}
}
#endif
/* Read from inn, write to out. We assume inn is opened for reading,
* and out for writing. We return 0 on success, -1 on read error, or -2
* on write error. */
int copy_file(FILE *inn, FILE *out)
{
int retval = 0;
char buf[BUFSIZ];
size_t charsread;
assert(inn != NULL && out != NULL && inn != out);
do {
charsread = fread(buf, sizeof(char), BUFSIZ, inn);
if (charsread == 0 && ferror(inn)) {
retval = -1;
break;
}
if (fwrite(buf, sizeof(char), charsread, out) < charsread) {
retval = -2;
break;
}
} while (charsread > 0);
if (fclose(inn) == EOF)
retval = -1;
if (fclose(out) == EOF)
retval = -2;
return retval;
}
/* Write a file out to disk. If f_open isn't NULL, we assume that it is
* a stream associated with the file, and we don't try to open it
* ourselves. If tmp is TRUE, we set the umask to disallow anyone else
* from accessing the file, we don't set the filename to its name, and
* we don't print out how many lines we wrote on the statusbar.
*
* tmp means we are writing a temporary file in a secure fashion. We
* use it when spell checking or dumping the file on an error. If
* append is APPEND, it means we are appending instead of overwriting.
* If append is PREPEND, it means we are prepending instead of
* overwriting. If nonamechange is TRUE, we don't change the current
* filename. nonamechange is ignored if tmp is FALSE, we're appending,
* or we're prepending.
*
* Return TRUE on success or FALSE on error. */
bool write_file(const char *name, FILE *f_open, bool tmp, append_type
append, bool nonamechange)
{
bool retval = FALSE;
/* Instead of returning in this function, you should always
* set retval and then goto cleanup_and_exit. */
size_t lineswritten = 0;
const filestruct *fileptr = openfile->fileage;
int fd;
/* The file descriptor we use. */
mode_t original_umask = 0;
/* Our umask, from when nano started. */
bool realexists;
/* The result of stat(). TRUE if the file exists, FALSE
* otherwise. If name is a link that points nowhere, realexists
* is FALSE. */
struct stat st;
/* The status fields filled in by stat(). */
bool anyexists;
/* The result of lstat(). The same as realexists, unless name
* is a link. */
struct stat lst;
/* The status fields filled in by lstat(). */
char *realname;
/* name after tilde expansion. */
FILE *f = NULL;
/* The actual file, realname, we are writing to. */
char *tempname = NULL;
/* The temp file name we write to on prepend. */
assert(name != NULL);
if (*name == '\0')
return -1;
if (f_open != NULL)
f = f_open;
if (!tmp)
titlebar(NULL);
realname = real_dir_from_tilde(name);
#ifndef DISABLE_OPERATINGDIR
/* If we're writing a temporary file, we're probably going outside
* the operating directory, so skip the operating directory test. */
if (!tmp && check_operating_dir(realname, FALSE)) {
statusbar(_("Can't write outside of %s"), operating_dir);
goto cleanup_and_exit;
}
#endif
anyexists = (lstat(realname, &lst) != -1);
/* If the temp file exists and isn't already open, give up. */
if (tmp && anyexists && f_open == NULL)
goto cleanup_and_exit;
/* If NOFOLLOW_SYMLINKS is set, it doesn't make sense to prepend or
* append to a symlink. Here we warn about the contradiction. */
if (ISSET(NOFOLLOW_SYMLINKS) && anyexists && S_ISLNK(lst.st_mode)) {
statusbar(
_("Cannot prepend or append to a symlink with --nofollow set"));
goto cleanup_and_exit;
}
/* Save the state of the file at the end of the symlink (if there is
* one). */
realexists = (stat(realname, &st) != -1);
#ifndef NANO_TINY
/* if we have not stat()d this file before (say, the user just
* specified it interactively), stat and save the value
* or else we will chase null pointers when we do
* modtime checks, preserve file times, etc. during backup */
if (openfile->current_stat == NULL && !tmp && realexists) {
openfile->current_stat = (struct stat *)nmalloc(sizeof(struct stat));
stat(realname, openfile->current_stat);
}
/* We backup only if the backup toggle is set, the file isn't
* temporary, and the file already exists. Furthermore, if we
* aren't appending, prepending, or writing a selection, we backup
* only if the file has not been modified by someone else since nano
* opened it. */
if (ISSET(BACKUP_FILE) && !tmp && realexists && ((append !=
OVERWRITE || openfile->mark_set) || (openfile->current_stat &&
openfile->current_stat->st_mtime == st.st_mtime))) {
int backup_fd;
FILE *backup_file;
char *backupname;
struct utimbuf filetime;
int copy_status;
int backup_cflags;
/* Save the original file's access and modification times. */
filetime.actime = openfile->current_stat->st_atime;
filetime.modtime = openfile->current_stat->st_mtime;
if (f_open == NULL) {
/* Open the original file to copy to the backup. */
f = fopen(realname, "rb");
if (f == NULL) {
statusbar(_("Error reading %s: %s"), realname,
strerror(errno));
beep();
/* If we can't read from the original file, go on, since
* only saving the original file is better than saving
* nothing. */
goto skip_backup;
}
}
/* If backup_dir is set, we set backupname to
* backup_dir/backupname~[.number], where backupname is the
* canonicalized absolute pathname of realname with every '/'
* replaced with a '!'. This means that /home/foo/file is
* backed up in backup_dir/!home!foo!file~[.number]. */
if (backup_dir != NULL) {
char *backuptemp = get_full_path(realname);
if (backuptemp == NULL)
/* If get_full_path() failed, we don't have a
* canonicalized absolute pathname, so just use the
* filename portion of the pathname. We use tail() so
* that e.g. ../backupname will be backed up in
* backupdir/backupname~ instead of
* backupdir/../backupname~. */
backuptemp = mallocstrcpy(NULL, tail(realname));
else {
size_t i = 0;
for (; backuptemp[i] != '\0'; i++) {
if (backuptemp[i] == '/')
backuptemp[i] = '!';
}
}
backupname = charalloc(strlen(backup_dir) +
strlen(backuptemp) + 1);
sprintf(backupname, "%s%s", backup_dir, backuptemp);
free(backuptemp);
backuptemp = get_next_filename(backupname, "~");
if (*backuptemp == '\0') {
statusbar(_("Error writing backup file %s: %s"), backupname,
_("Too many backup files?"));
free(backuptemp);
free(backupname);
/* If we can't write to the backup, DONT go on, since
whatever caused the backup file to fail (e.g. disk
full may well cause the real file write to fail, which
means we could lose both the backup and the original! */
goto cleanup_and_exit;
} else {
free(backupname);
backupname = backuptemp;
}
} else {
backupname = charalloc(strlen(realname) + 2);
sprintf(backupname, "%s~", realname);
}
/* First, unlink any existing backups. Next, open the backup
file with O_CREAT and O_EXCL. If it succeeds, we
have a file descriptor to a new backup file. */
if (unlink(backupname) < 0 && errno != ENOENT && !ISSET(INSECURE_BACKUP)) {
if (prompt_failed_backupwrite(backupname))
goto skip_backup;
statusbar(_("Error writing backup file %s: %s"), backupname,
strerror(errno));
free(backupname);
goto cleanup_and_exit;
}
if (ISSET(INSECURE_BACKUP))
backup_cflags = O_WRONLY | O_CREAT | O_APPEND;
else
backup_cflags = O_WRONLY | O_CREAT | O_EXCL | O_APPEND;
backup_fd = open(backupname, backup_cflags,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
/* Now we've got a safe file stream. If the previous open()
call failed, this will return NULL. */
backup_file = fdopen(backup_fd, "wb");
if (backup_fd < 0 || backup_file == NULL) {
statusbar(_("Error writing backup file %s: %s"), backupname,
strerror(errno));
free(backupname);
goto cleanup_and_exit;
}
/* We shouldn't worry about chown()ing something if we're not
root, since it's likely to fail! */
if (geteuid() == NANO_ROOT_UID && fchown(backup_fd,
openfile->current_stat->st_uid, openfile->current_stat->st_gid) == -1
&& !ISSET(INSECURE_BACKUP)) {
if (prompt_failed_backupwrite(backupname))
goto skip_backup;
statusbar(_("Error writing backup file %s: %s"), backupname,
strerror(errno));
free(backupname);
fclose(backup_file);
goto cleanup_and_exit;
}
if (fchmod(backup_fd, openfile->current_stat->st_mode) == -1
&& !ISSET(INSECURE_BACKUP)) {
if (prompt_failed_backupwrite(backupname))
goto skip_backup;
statusbar(_("Error writing backup file %s: %s"), backupname,
strerror(errno));
free(backupname);
fclose(backup_file);
/* If we can't write to the backup, DONT go on, since
whatever caused the backup file to fail (e.g. disk
full may well cause the real file write to fail, which
means we could lose both the backup and the original! */
goto cleanup_and_exit;
}
#ifdef DEBUG
fprintf(stderr, "Backing up %s to %s\n", realname, backupname);
#endif
/* Copy the file. */
copy_status = copy_file(f, backup_file);
if (copy_status != 0) {
statusbar(_("Error reading %s: %s"), realname,
strerror(errno));
beep();
goto cleanup_and_exit;
}
/* And set its metadata. */
if (utime(backupname, &filetime) == -1 && !ISSET(INSECURE_BACKUP)) {
if (prompt_failed_backupwrite(backupname))
goto skip_backup;
statusbar(_("Error writing backup file %s: %s"), backupname,
strerror(errno));
/* If we can't write to the backup, DONT go on, since
whatever caused the backup file to fail (e.g. disk
full may well cause the real file write to fail, which
means we could lose both the backup and the original! */
goto cleanup_and_exit;
}
free(backupname);
}
skip_backup:
#endif /* !NANO_TINY */
/* If NOFOLLOW_SYMLINKS is set and the file is a link, we aren't
* doing prepend or append. So we delete the link first, and just
* overwrite. */
if (ISSET(NOFOLLOW_SYMLINKS) && anyexists && S_ISLNK(lst.st_mode) &&
unlink(realname) == -1) {
statusbar(_("Error writing %s: %s"), realname, strerror(errno));
goto cleanup_and_exit;
}
if (f_open == NULL) {
original_umask = umask(0);
/* If we create a temp file, we don't let anyone else access it.
* We create a temp file if tmp is TRUE. */
if (tmp)
umask(S_IRWXG | S_IRWXO);
else
umask(original_umask);
}
/* If we're prepending, copy the file to a temp file. */
if (append == PREPEND) {
int fd_source;
FILE *f_source = NULL;
if (f == NULL) {
f = fopen(realname, "rb");
if (f == NULL) {
statusbar(_("Error reading %s: %s"), realname,
strerror(errno));
beep();
goto cleanup_and_exit;
}
}
tempname = safe_tempfile(&f);
if (tempname == NULL) {
statusbar(_("Error writing temp file: %s"),
strerror(errno));
goto cleanup_and_exit;
}
if (f_open == NULL) {
fd_source = open(realname, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd_source != -1) {
f_source = fdopen(fd_source, "rb");
if (f_source == NULL) {
statusbar(_("Error reading %s: %s"), realname,
strerror(errno));
beep();
close(fd_source);
fclose(f);
unlink(tempname);
goto cleanup_and_exit;
}
}
}
if (copy_file(f_source, f) != 0) {
statusbar(_("Error writing %s: %s"), tempname,
strerror(errno));
unlink(tempname);
goto cleanup_and_exit;
}
}
if (f_open == NULL) {
/* Now open the file in place. Use O_EXCL if tmp is TRUE. This
* is copied from joe, because wiggy says so *shrug*. */
fd = open(realname, O_WRONLY | O_CREAT | ((append == APPEND) ?
O_APPEND : (tmp ? O_EXCL : O_TRUNC)), S_IRUSR |
S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
/* Set the umask back to the user's original value. */
umask(original_umask);
/* If we couldn't open the file, give up. */
if (fd == -1) {
statusbar(_("Error writing %s: %s"), realname,
strerror(errno));
/* tempname has been set only if we're prepending. */
if (tempname != NULL)
unlink(tempname);
goto cleanup_and_exit;
}
f = fdopen(fd, (append == APPEND) ? "ab" : "wb");
if (f == NULL) {
statusbar(_("Error writing %s: %s"), realname,
strerror(errno));
close(fd);
goto cleanup_and_exit;
}
}
/* There might not be a magicline. There won't be when writing out
* a selection. */
assert(openfile->fileage != NULL && openfile->filebot != NULL);
while (fileptr != NULL) {
size_t data_len = strlen(fileptr->data), size;
/* Convert newlines to nulls, just before we write to disk. */
sunder(fileptr->data);
size = fwrite(fileptr->data, sizeof(char), data_len, f);
/* Convert nulls to newlines. data_len is the string's real
* length. */
unsunder(fileptr->data, data_len);
if (size < data_len) {
statusbar(_("Error writing %s: %s"), realname,
strerror(errno));
fclose(f);
goto cleanup_and_exit;
}
/* If we're on the last line of the file, don't write a newline
* character after it. If the last line of the file is blank,
* this means that zero bytes are written, in which case we
* don't count the last line in the total lines written. */
if (fileptr == openfile->filebot) {
if (fileptr->data[0] == '\0')
lineswritten--;
} else {
#ifndef NANO_TINY
if (openfile->fmt == DOS_FILE || openfile->fmt ==
MAC_FILE) {
if (putc('\r', f) == EOF) {
statusbar(_("Error writing %s: %s"), realname,
strerror(errno));
fclose(f);
goto cleanup_and_exit;
}
}
if (openfile->fmt != MAC_FILE) {
#endif
if (putc('\n', f) == EOF) {
statusbar(_("Error writing %s: %s"), realname,
strerror(errno));
fclose(f);
goto cleanup_and_exit;
}
#ifndef NANO_TINY
}
#endif
}
fileptr = fileptr->next;
lineswritten++;
}
/* If we're prepending, open the temp file, and append it to f. */
if (append == PREPEND) {
int fd_source;
FILE *f_source = NULL;
fd_source = open(tempname, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd_source != -1) {
f_source = fdopen(fd_source, "rb");
if (f_source == NULL)
close(fd_source);
}
if (f_source == NULL) {
statusbar(_("Error reading %s: %s"), tempname,
strerror(errno));
beep();
fclose(f);
goto cleanup_and_exit;
}
if (copy_file(f_source, f) == -1 || unlink(tempname) == -1) {
statusbar(_("Error writing %s: %s"), realname,
strerror(errno));
goto cleanup_and_exit;
}
} else if (fclose(f) != 0) {
statusbar(_("Error writing %s: %s"), realname,
strerror(errno));
goto cleanup_and_exit;
}
if (!tmp && append == OVERWRITE) {
if (!nonamechange) {
openfile->filename = mallocstrcpy(openfile->filename,
realname);
#ifdef ENABLE_COLOR
/* We might have changed the filename, so update the colors
* to account for it, and then make sure we're using
* them. */
color_update();
color_init();
/* If color syntaxes are available and turned on, we need to
* call edit_refresh(). */
if (openfile->colorstrings != NULL &&
!ISSET(NO_COLOR_SYNTAX))
edit_refresh();
#endif
}
#ifndef NANO_TINY
/* Update current_stat to reference the file as it is now. */
if (openfile->current_stat == NULL)
openfile->current_stat =
(struct stat *)nmalloc(sizeof(struct stat));
if (!openfile->mark_set)
stat(realname, openfile->current_stat);
#endif
statusbar(P_("Wrote %lu line", "Wrote %lu lines",
(unsigned long)lineswritten),
(unsigned long)lineswritten);
openfile->modified = FALSE;
titlebar(NULL);
}
retval = TRUE;
cleanup_and_exit:
free(realname);
if (tempname != NULL)
free(tempname);
return retval;
}
#ifndef NANO_TINY
/* Write a marked selection from a file out to disk. Return TRUE on
* success or FALSE on error. */
bool write_marked_file(const char *name, FILE *f_open, bool tmp,
append_type append)
{
bool retval;
bool old_modified = openfile->modified;
/* write_file() unsets the modified flag. */
bool added_magicline = FALSE;
/* Whether we added a magicline after filebot. */
filestruct *top, *bot;
size_t top_x, bot_x;
assert(openfile->mark_set);
/* Partition the filestruct so that it contains only the marked
* text. */
mark_order((const filestruct **)&top, &top_x,
(const filestruct **)&bot, &bot_x, NULL);
filepart = partition_filestruct(top, top_x, bot, bot_x);
/* Handle the magicline if the NO_NEWLINES flag isn't set. If the
* line at filebot is blank, treat it as the magicline and hence the
* end of the file. Otherwise, add a magicline and treat it as the
* end of the file. */
if (!ISSET(NO_NEWLINES) &&
(added_magicline = (openfile->filebot->data[0] != '\0')))
new_magicline();
retval = write_file(name, f_open, tmp, append, TRUE);
/* If the NO_NEWLINES flag isn't set, and we added a magicline,
* remove it now. */
if (!ISSET(NO_NEWLINES) && added_magicline)
remove_magicline();
/* Unpartition the filestruct so that it contains all the text
* again. */
unpartition_filestruct(&filepart);
if (old_modified)
set_modified();
return retval;
}
#endif /* !NANO_TINY */
/* Write the current file to disk. If the mark is on, write the current
* marked selection to disk. If exiting is TRUE, write the file to disk
* regardless of whether the mark is on, and without prompting if the
* TEMP_FILE flag is set. Return TRUE on success or FALSE on error. */
bool do_writeout(bool exiting)
{
int i;
append_type append = OVERWRITE;
char *ans;
/* The last answer the user typed at the statusbar prompt. */
#ifdef NANO_EXTRA
static bool did_credits = FALSE;
#endif
bool retval = FALSE, meta_key = FALSE, func_key = FALSE;
const sc *s;
currmenu = MWRITEFILE;
if (exiting && openfile->filename[0] != '\0' && ISSET(TEMP_FILE)) {
retval = write_file(openfile->filename, NULL, FALSE, OVERWRITE,
FALSE);
/* Write succeeded. */
if (retval)
return retval;
}
ans = mallocstrcpy(NULL,
#ifndef NANO_TINY
(!exiting && openfile->mark_set) ? "" :
#endif
openfile->filename);
while (TRUE) {
const char *msg;
#ifndef NANO_TINY
const char *formatstr, *backupstr;
formatstr = (openfile->fmt == DOS_FILE) ?
_(" [DOS Format]") : (openfile->fmt == MAC_FILE) ?
_(" [Mac Format]") : "";
backupstr = ISSET(BACKUP_FILE) ? _(" [Backup]") : "";
/* If we're using restricted mode, don't display the "Write
* Selection to File" prompt. This function is disabled, since
* it allows reading from or writing to files not specified on
* the command line. */
if (!ISSET(RESTRICTED) && !exiting && openfile->mark_set)
msg = (append == PREPEND) ?
_("Prepend Selection to File") : (append == APPEND) ?
_("Append Selection to File") :
_("Write Selection to File");
else
#endif /* !NANO_TINY */
msg = (append == PREPEND) ? _("File Name to Prepend to") :
(append == APPEND) ? _("File Name to Append to") :
_("File Name to Write");
/* If we're using restricted mode, the filename isn't blank,
* and we're at the "Write File" prompt, disable tab
* completion. */
i = do_prompt(!ISSET(RESTRICTED) ||
openfile->filename[0] == '\0',
#ifndef DISABLE_TABCOMP
TRUE,
#endif
MWRITEFILE, ans,
&meta_key, &func_key,
#ifndef NANO_TINY
NULL,
#endif
edit_refresh, "%s%s%s", msg,
#ifndef NANO_TINY
formatstr, backupstr
#else
"", ""
#endif
);
/* If the filename or command begins with a newline (i.e. an
* encoded null), treat it as though it's blank. */
if (i < 0 || *answer == '\n') {
statusbar(_("Cancelled"));
retval = FALSE;
break;
} else {
ans = mallocstrcpy(ans, answer);
s = get_shortcut(currmenu, &i, &meta_key, &func_key);
#ifndef DISABLE_BROWSER
if (s && s->scfunc == to_files_void) {
char *tmp = do_browse_from(answer);
if (tmp == NULL)
continue;
/* We have a file now. Indicate this. */
free(answer);
answer = tmp;
} else
#endif /* !DISABLE_BROWSER */
#ifndef NANO_TINY
if (s && s->scfunc == dos_format_void) {
openfile->fmt = (openfile->fmt == DOS_FILE) ? NIX_FILE :
DOS_FILE;
continue;
} else if (s && s->scfunc == mac_format_void) {
openfile->fmt = (openfile->fmt == MAC_FILE) ? NIX_FILE :
MAC_FILE;
continue;
} else if (s && s->scfunc == backup_file_void) {
TOGGLE(BACKUP_FILE);
continue;
} else
#endif /* !NANO_TINY */
if (s && s->scfunc == prepend_void) {
append = (append == PREPEND) ? OVERWRITE : PREPEND;
continue;
} else if (s && s->scfunc == append_void) {
append = (append == APPEND) ? OVERWRITE : APPEND;
continue;
} else if (s && s->scfunc == do_help_void) {
continue;
}
#ifdef DEBUG
fprintf(stderr, "filename is %s\n", answer);
#endif
#ifdef NANO_EXTRA
/* If the current file has been modified, we've pressed
* Ctrl-X at the edit window to exit, we've pressed "y" at
* the "Save modified buffer" prompt to save, we've entered
* "zzy" as the filename to save under (hence "xyzzy"), and
* this is the first time we've done this, show an Easter
* egg. Display the credits. */
if (!did_credits && exiting && !ISSET(TEMP_FILE) &&
strcasecmp(answer, "zzy") == 0) {
do_credits();
did_credits = TRUE;
retval = FALSE;
break;
}
#endif
if (append == OVERWRITE) {
size_t answer_len = strlen(answer);
bool name_exists, do_warning;
char *full_answer, *full_filename;
struct stat st;
/* Convert newlines to nulls, just before we get the
* full path. */
sunder(answer);
full_answer = get_full_path(answer);
full_filename = get_full_path(openfile->filename);
name_exists = (stat((full_answer == NULL) ? answer :
full_answer, &st) != -1);
if (openfile->filename[0] == '\0')
do_warning = name_exists;
else
do_warning = (strcmp((full_answer == NULL) ?
answer : full_answer, (full_filename == NULL) ?
openfile->filename : full_filename) != 0);
/* Convert nulls to newlines. answer_len is the
* string's real length. */
unsunder(answer, answer_len);
if (full_filename != NULL)
free(full_filename);
if (full_answer != NULL)
free(full_answer);
if (do_warning) {
/* If we're using restricted mode, we aren't allowed
* to overwrite an existing file with the current
* file. We also aren't allowed to change the name
* of the current file if it has one, because that
* would allow reading from or writing to files not
* specified on the command line. */
if (ISSET(RESTRICTED))
continue;
if (name_exists) {
i = do_yesno_prompt(FALSE,
_("File exists, OVERWRITE ? "));
if (i == 0 || i == -1)
continue;
} else
#ifndef NANO_TINY
if (exiting || !openfile->mark_set)
#endif
{
i = do_yesno_prompt(FALSE,
_("Save file under DIFFERENT NAME ? "));
if (i == 0 || i == -1)
continue;
}
}
#ifndef NANO_TINY
/* Complain if the file exists, the name hasn't changed, and the
stat information we had before does not match what we have now */
else if (name_exists && openfile->current_stat && (openfile->current_stat->st_mtime < st.st_mtime ||
openfile->current_stat->st_dev != st.st_dev || openfile->current_stat->st_ino != st.st_ino)) {
i = do_yesno_prompt(FALSE,
_("File was modified since you opened it, continue saving ? "));
if (i == 0 || i == -1)
continue;
}
#endif
}
/* Convert newlines to nulls, just before we save the
* file. */
sunder(answer);
align(&answer);
/* Here's where we allow the selected text to be written to
* a separate file. If we're using restricted mode, this
* function is disabled, since it allows reading from or
* writing to files not specified on the command line. */
retval =
#ifndef NANO_TINY
(!ISSET(RESTRICTED) && !exiting && openfile->mark_set) ?
write_marked_file(answer, NULL, FALSE, append) :
#endif
write_file(answer, NULL, FALSE, append, FALSE);
break;
}
}
free(ans);
return retval;
}
/* Write the current file to disk. If the mark is on, write the current
* marked selection to disk. */
void do_writeout_void(void)
{
do_writeout(FALSE);
display_main_list();
}
/* Return a malloc()ed string containing the actual directory, used to
* convert ~user/ and ~/ notation. */
char *real_dir_from_tilde(const char *buf)
{
char *retval;
assert(buf != NULL);
if (*buf == '~') {
size_t i = 1;
char *tilde_dir;
/* Figure out how much of the string we need to compare. */
for (; buf[i] != '/' && buf[i] != '\0'; i++)
;
/* Get the home directory. */
if (i == 1) {
get_homedir();
tilde_dir = mallocstrcpy(NULL, homedir);
} else {
const struct passwd *userdata;
tilde_dir = mallocstrncpy(NULL, buf, i + 1);
tilde_dir[i] = '\0';
do {
userdata = getpwent();
} while (userdata != NULL && strcmp(userdata->pw_name,
tilde_dir + 1) != 0);
endpwent();
if (userdata != NULL)
tilde_dir = mallocstrcpy(tilde_dir, userdata->pw_dir);
}
retval = charalloc(strlen(tilde_dir) + strlen(buf + i) + 1);
sprintf(retval, "%s%s", tilde_dir, buf + i);
free(tilde_dir);
} else
retval = mallocstrcpy(NULL, buf);
return retval;
}
#if !defined(DISABLE_TABCOMP) || !defined(DISABLE_BROWSER)
/* Our sort routine for file listings. Sort alphabetically and
* case-insensitively, and sort directories before filenames. */
int diralphasort(const void *va, const void *vb)
{
struct stat fileinfo;
const char *a = *(const char *const *)va;
const char *b = *(const char *const *)vb;
bool aisdir = stat(a, &fileinfo) != -1 && S_ISDIR(fileinfo.st_mode);
bool bisdir = stat(b, &fileinfo) != -1 && S_ISDIR(fileinfo.st_mode);
if (aisdir && !bisdir)
return -1;
if (!aisdir && bisdir)
return 1;
/* Standard function brain damage: We should be sorting
* alphabetically and case-insensitively according to the current
* locale, but there's no standard strcasecoll() function, so we
* have to use multibyte strcasecmp() instead, */
return mbstrcasecmp(a, b);
}
/* Free the memory allocated for array, which should contain len
* elements. */
void free_chararray(char **array, size_t len)
{
assert(array != NULL);
for (; len > 0; len--)
free(array[len - 1]);
free(array);
}
#endif
#ifndef DISABLE_TABCOMP
/* Is the given path a directory? */
bool is_dir(const char *buf)
{
char *dirptr;
struct stat fileinfo;
bool retval;
assert(buf != NULL);
dirptr = real_dir_from_tilde(buf);
retval = (stat(dirptr, &fileinfo) != -1 &&
S_ISDIR(fileinfo.st_mode));
free(dirptr);
return retval;
}
/* These functions, username_tab_completion(), cwd_tab_completion()
* (originally exe_n_cwd_tab_completion()), and input_tab(), were
* adapted from busybox 0.46 (cmdedit.c). Here is the notice from that
* file, with the copyright years updated:
*
* Termios command line History and Editting, originally
* intended for NetBSD sh (ash)
* Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007
* Main code: Adam Rogoyski <rogoyski@cs.utexas.edu>
* Etc: Dave Cinege <dcinege@psychosis.com>
* Majorly adjusted/re-written for busybox:
* Erik Andersen <andersee@debian.org>
*
* You may use this code as you wish, so long as the original author(s)
* are attributed in any redistributions of the source code.
* This code is 'as is' with no warranty.
* This code may safely be consumed by a BSD or GPL license. */
/* We consider the first buf_len characters of buf for ~username tab
* completion. */
char **username_tab_completion(const char *buf, size_t *num_matches,
size_t buf_len)
{
char **matches = NULL;
const struct passwd *userdata;
assert(buf != NULL && num_matches != NULL && buf_len > 0);
*num_matches = 0;
while ((userdata = getpwent()) != NULL) {
if (strncmp(userdata->pw_name, buf + 1, buf_len - 1) == 0) {
/* Cool, found a match. Add it to the list. This makes a
* lot more sense to me (Chris) this way... */
#ifndef DISABLE_OPERATINGDIR
/* ...unless the match exists outside the operating
* directory, in which case just go to the next match. */
if (check_operating_dir(userdata->pw_dir, TRUE))
continue;
#endif
matches = (char **)nrealloc(matches, (*num_matches + 1) *
sizeof(char *));
matches[*num_matches] =
charalloc(strlen(userdata->pw_name) + 2);
sprintf(matches[*num_matches], "~%s", userdata->pw_name);
++(*num_matches);
}
}
endpwent();
return matches;
}
/* We consider the first buf_len characters of buf for filename tab
* completion. */
char **cwd_tab_completion(const char *buf, bool allow_files, size_t
*num_matches, size_t buf_len)
{
char *dirname = mallocstrcpy(NULL, buf), *filename;
size_t filenamelen;
char **matches = NULL;
DIR *dir;
const struct dirent *nextdir;
assert(dirname != NULL && num_matches != NULL);
*num_matches = 0;
null_at(&dirname, buf_len);
/* Okie, if there's a / in the buffer, strip out the directory
* part. */
filename = strrchr(dirname, '/');
if (filename != NULL) {
char *tmpdirname = filename + 1;
filename = mallocstrcpy(NULL, tmpdirname);
*tmpdirname = '\0';
tmpdirname = dirname;
dirname = real_dir_from_tilde(dirname);
free(tmpdirname);
} else {
filename = dirname;
dirname = mallocstrcpy(NULL, "./");
}
assert(dirname[strlen(dirname) - 1] == '/');
dir = opendir(dirname);
if (dir == NULL) {
/* Don't print an error, just shut up and return. */
beep();
free(filename);
free(dirname);
return NULL;
}
filenamelen = strlen(filename);
while ((nextdir = readdir(dir)) != NULL) {
bool skip_match = FALSE;
#ifdef DEBUG
fprintf(stderr, "Comparing \'%s\'\n", nextdir->d_name);
#endif
/* See if this matches. */
if (strncmp(nextdir->d_name, filename, filenamelen) == 0 &&
(*filename == '.' || (strcmp(nextdir->d_name, ".") !=
0 && strcmp(nextdir->d_name, "..") != 0))) {
/* Cool, found a match. Add it to the list. This makes a
* lot more sense to me (Chris) this way... */
char *tmp = charalloc(strlen(dirname) +
strlen(nextdir->d_name) + 1);
sprintf(tmp, "%s%s", dirname, nextdir->d_name);
#ifndef DISABLE_OPERATINGDIR
/* ...unless the match exists outside the operating
* directory, in which case just go to the next match. */
if (check_operating_dir(tmp, TRUE))
skip_match = TRUE;
#endif
/* ...or unless the match isn't a directory and allow_files
* isn't set, in which case just go to the next match. */
if (!allow_files && !is_dir(tmp))
skip_match = TRUE;
free(tmp);
if (skip_match)
continue;
matches = (char **)nrealloc(matches, (*num_matches + 1) *
sizeof(char *));
matches[*num_matches] = mallocstrcpy(NULL, nextdir->d_name);
++(*num_matches);
}
}
closedir(dir);
free(dirname);
free(filename);
return matches;
}
/* Do tab completion. place refers to how much the statusbar cursor
* position should be advanced. refresh_func is the function we will
* call to refresh the edit window. */
char *input_tab(char *buf, bool allow_files, size_t *place, bool
*lastwastab, void (*refresh_func)(void), bool *list)
{
size_t num_matches = 0, buf_len;
char **matches = NULL;
assert(buf != NULL && place != NULL && *place <= strlen(buf) && lastwastab != NULL && refresh_func != NULL && list != NULL);
*list = FALSE;
/* If the word starts with `~' and there is no slash in the word,
* then try completing this word as a username. */
if (*place > 0 && *buf == '~') {
const char *bob = strchr(buf, '/');
if (bob == NULL || bob >= buf + *place)
matches = username_tab_completion(buf, &num_matches,
*place);
}
/* Match against files relative to the current working directory. */
if (matches == NULL)
matches = cwd_tab_completion(buf, allow_files, &num_matches,
*place);
buf_len = strlen(buf);
if (num_matches == 0 || *place != buf_len)
beep();
else {
size_t match, common_len = 0;
char *mzero;
const char *lastslash = revstrstr(buf, "/", buf + *place);
size_t lastslash_len = (lastslash == NULL) ? 0 :
lastslash - buf + 1;
char *match1_mb = charalloc(mb_cur_max() + 1);
char *match2_mb = charalloc(mb_cur_max() + 1);
int match1_mb_len, match2_mb_len;
while (TRUE) {
for (match = 1; match < num_matches; match++) {
/* Get the number of single-byte characters that all the
* matches have in common. */
match1_mb_len = parse_mbchar(matches[0] + common_len,
match1_mb, NULL);
match2_mb_len = parse_mbchar(matches[match] +
common_len, match2_mb, NULL);
match1_mb[match1_mb_len] = '\0';
match2_mb[match2_mb_len] = '\0';
if (strcmp(match1_mb, match2_mb) != 0)
break;
}
if (match < num_matches || matches[0][common_len] == '\0')
break;
common_len += parse_mbchar(buf + common_len, NULL, NULL);
}
free(match1_mb);
free(match2_mb);
mzero = charalloc(lastslash_len + common_len + 1);
strncpy(mzero, buf, lastslash_len);
strncpy(mzero + lastslash_len, matches[0], common_len);
common_len += lastslash_len;
mzero[common_len] = '\0';
assert(common_len >= *place);
if (num_matches == 1 && is_dir(mzero)) {
mzero[common_len++] = '/';
assert(common_len > *place);
}
if (num_matches > 1 && (common_len != *place || !*lastwastab))
beep();
/* If there is more of a match to display on the statusbar, show
* it. We reset lastwastab to FALSE: it requires pressing Tab
* twice in succession with no statusbar changes to see a match
* list. */
if (common_len != *place) {
*lastwastab = FALSE;
buf = charealloc(buf, common_len + buf_len - *place + 1);
charmove(buf + common_len, buf + *place, buf_len -
*place + 1);
strncpy(buf, mzero, common_len);
*place = common_len;
} else if (!*lastwastab || num_matches < 2)
*lastwastab = TRUE;
else {
int longest_name = 0, ncols, editline = 0;
/* Now we show a list of the available choices. */
assert(num_matches > 1);
/* Sort the list. */
qsort(matches, num_matches, sizeof(char *), diralphasort);
for (match = 0; match < num_matches; match++) {
common_len = strnlenpt(matches[match], COLS - 1);
if (common_len > COLS - 1) {
longest_name = COLS - 1;
break;
}
if (common_len > longest_name)
longest_name = common_len;
}
assert(longest_name <= COLS - 1);
/* Each column will be (longest_name + 2) columns wide, i.e.
* two spaces between columns, except that there will be
* only one space after the last column. */
ncols = (COLS + 1) / (longest_name + 2);
/* Blank the edit window, and print the matches out
* there. */
blank_edit();
wmove(edit, 0, 0);
/* Disable el cursor. */
curs_set(0);
for (match = 0; match < num_matches; match++) {
char *disp;
wmove(edit, editline, (longest_name + 2) *
(match % ncols));
if (match % ncols == 0 &&
editline == editwinrows - 1 &&
num_matches - match > ncols) {
waddstr(edit, _("(more)"));
break;
}
disp = display_string(matches[match], 0, longest_name,
FALSE);
waddstr(edit, disp);
free(disp);
if ((match + 1) % ncols == 0)
editline++;
}
wnoutrefresh(edit);
*list = TRUE;
}
free(mzero);
}
free_chararray(matches, num_matches);
/* Only refresh the edit window if we don't have a list of filename
* matches on it. */
if (!*list)
refresh_func();
/* Enable el cursor. */
curs_set(1);
return buf;
}
#endif /* !DISABLE_TABCOMP */
/* Only print the last part of a path. Isn't there a shell command for
* this? */
const char *tail(const char *foo)
{
const char *tmp = strrchr(foo, '/');
if (tmp == NULL)
tmp = foo;
else
tmp++;
return tmp;
}
#if !defined(NANO_TINY) && defined(ENABLE_NANORC)
/* Return the constructed dorfile path, or NULL if we can't find the home
* directory. The string is dynamically allocated, and should be
* freed. */
char *construct_filename(const char *str)
{
char *newstr = NULL;
if (homedir != NULL) {
size_t homelen = strlen(homedir);
newstr = charalloc(homelen + strlen(str) + 1);
strcpy(newstr, homedir);
strcpy(newstr + homelen, str);
}
return newstr;
}
char *histfilename(void)
{
return construct_filename("/.nano/search_history");
}
/* Construct the legacy history filename
* (Deprecate in 2.5, delete later
*/
char *legacyhistfilename(void)
{
return construct_filename("/.nano_history");
}
char *poshistfilename(void)
{
return construct_filename("/.nano/filepos_history");
}
void history_error(const char *msg, ...)
{
va_list ap;
va_start(ap, msg);
vfprintf(stderr, _(msg), ap);
va_end(ap);
fprintf(stderr, _("\nPress Enter to continue\n"));
while (getchar() != '\n')
;
}
/* Now that we have more than one history file, let's just rely
on a .nano dir for this stuff. Return 1 if the dir exists
or was successfully created, and return 0 otherwise.
*/
int check_dotnano(void)
{
struct stat dirstat;
char *nanodir = construct_filename("/.nano");
if (stat(nanodir, &dirstat) == -1) {
if (mkdir(nanodir, S_IRWXU | S_IRWXG | S_IRWXO) == -1) {
history_error(N_("Unable to create directory %s: %s\nIt is required for saving/loading search history or cursor position\n"),
nanodir, strerror(errno));
return 0;
}
} else if (!S_ISDIR(dirstat.st_mode)) {
history_error(N_("Path %s is not a directory and needs to be.\nNano will be unable to load or save search or cursor position history\n"));
return 0;
}
return 1;
}
/* Load histories from ~/.nano_history. */
void load_history(void)
{
char *nanohist = histfilename();
char *legacyhist = legacyhistfilename();
struct stat hstat;
if (stat(legacyhist, &hstat) != -1 && stat(nanohist, &hstat) == -1) {
if (rename(legacyhist, nanohist) == -1)
history_error(N_("Detected a legacy nano history file (%s) which I tried to move\nto the preferred location (%s) but encountered an error: %s"),
legacyhist, nanohist, strerror(errno));
else
history_error(N_("Detected a legacy nano history file (%s) which I moved\nto the preferred location (%s)\n(see the nano FAQ about this change)"),
legacyhist, nanohist);
}
/* Assume do_rcfile() has reported a missing home directory. */
if (nanohist != NULL) {
FILE *hist = fopen(nanohist, "rb");
if (hist == NULL) {
if (errno != ENOENT) {
/* Don't save history when we quit. */
UNSET(HISTORYLOG);
history_error(N_("Error reading %s: %s"), nanohist,
strerror(errno));
}
} else {
/* Load a history list (first the search history, then the
* replace history) from the oldest entry to the newest.
* Assume the last history entry is a blank line. */
filestruct **history = &search_history;
char *line = NULL;
size_t buf_len = 0;
ssize_t read;
while ((read = getline(&line, &buf_len, hist)) >= 0) {
if (read > 0 && line[read - 1] == '\n') {
read--;
line[read] = '\0';
}
if (read > 0) {
unsunder(line, read);
update_history(history, line);
} else
history = &replace_history;
}
fclose(hist);
free(line);
if (search_history->prev != NULL)
last_search = mallocstrcpy(NULL, search_history->prev->data);
}
free(nanohist);
free(legacyhist);
}
}
/* Write the lines of a history list, starting with the line at h, to
* the open file at hist. Return TRUE if the write succeeded, and FALSE
* otherwise. */
bool writehist(FILE *hist, filestruct *h)
{
filestruct *p;
/* Write a history list from the oldest entry to the newest. Assume
* the last history entry is a blank line. */
for (p = h; p != NULL; p = p->next) {
size_t p_len = strlen(p->data);
sunder(p->data);
if (fwrite(p->data, sizeof(char), p_len, hist) < p_len ||
putc('\n', hist) == EOF)
return FALSE;
}
return TRUE;
}
/* Save histories to ~/.nano/search_history. */
void save_history(void)
{
char *nanohist;
/* Don't save unchanged or empty histories. */
if (!history_has_changed() || (searchbot->lineno == 1 &&
replacebot->lineno == 1))
return;
nanohist = histfilename();
if (nanohist != NULL) {
FILE *hist = fopen(nanohist, "wb");
if (hist == NULL)
history_error(N_("Error writing %s: %s"), nanohist,
strerror(errno));
else {
/* Make sure no one else can read from or write to the
* history file. */
chmod(nanohist, S_IRUSR | S_IWUSR);
if (!writehist(hist, searchage) || !writehist(hist,
replaceage))
history_error(N_("Error writing %s: %s"), nanohist,
strerror(errno));
fclose(hist);
}
free(nanohist);
}
}
/* Analogs for the POS history */
void save_poshistory(void)
{
char *poshist;
char *statusstr = NULL;
poshiststruct *posptr;
poshist = poshistfilename();
if (poshist != NULL) {
FILE *hist = fopen(poshist, "wb");
if (hist == NULL)
history_error(N_("Error writing %s: %s"), poshist,
strerror(errno));
else {
/* Make sure no one else can read from or write to the
* history file. */
chmod(poshist, S_IRUSR | S_IWUSR);
for (posptr = poshistory; posptr != NULL; posptr = posptr->next) {
statusstr = charalloc(strlen(posptr->filename) + 2 * sizeof(ssize_t) + 4);
sprintf(statusstr, "%s %d %d\n", posptr->filename, (int) posptr->lineno,
(int) posptr->xno);
if (fwrite(statusstr, sizeof(char), strlen(statusstr), hist) < strlen(statusstr))
history_error(N_("Error writing %s: %s"), poshist,
strerror(errno));
free(statusstr);
}
fclose(hist);
}
free(poshist);
}
}
/* Update the POS history, given a filename line and column.
* If no entry is found, add a new entry on the end
*/
void update_poshistory(char *filename, ssize_t lineno, ssize_t xpos)
{
poshiststruct *posptr, *posprev = NULL;
char *fullpath = get_full_path(filename);
if (fullpath == NULL)
return;
for (posptr = poshistory; posptr != NULL; posptr = posptr->next) {
if (!strcmp(posptr->filename, fullpath)) {
posptr->lineno = lineno;
posptr->xno = xpos;
return;
}
posprev = posptr;
}
/* Didn't find it, make a new node yo! */
posptr = (poshiststruct *) nmalloc(sizeof(poshiststruct));
posptr->filename = mallocstrcpy(NULL, fullpath);
posptr->lineno = lineno;
posptr->xno = xpos;
posptr->next = NULL;
if (!poshistory)
poshistory = posptr;
else
posprev->next = posptr;
free(fullpath);
}
/* Check the POS history to see if file matches
* an existing entry. If so return 1 and set line and column
* to the right values Otherwise return 0
*/
int check_poshistory(const char *file, ssize_t *line, ssize_t *column)
{
poshiststruct *posptr;
char *fullpath = get_full_path(file);
if (fullpath == NULL)
return 0;
for (posptr = poshistory; posptr != NULL; posptr = posptr->next) {
if (!strcmp(posptr->filename, fullpath)) {
*line = posptr->lineno;
*column = posptr->xno;
free(fullpath);
return 1;
}
}
free(fullpath);
return 0;
}
/* Load histories from ~/.nano_history. */
void load_poshistory(void)
{
char *nanohist = poshistfilename();
/* Assume do_rcfile() has reported a missing home directory. */
if (nanohist != NULL) {
FILE *hist = fopen(nanohist, "rb");
if (hist == NULL) {
if (errno != ENOENT) {
/* Don't save history when we quit. */
UNSET(POS_HISTORY);
history_error(N_("Error reading %s: %s"), nanohist,
strerror(errno));
}
} else {
char *line = NULL, *lineptr, *xptr;
size_t buf_len = 0;
ssize_t read, lineno, xno;
poshiststruct *posptr;
/* See if we can find the file we're currently editing */
while ((read = getline(&line, &buf_len, hist)) >= 0) {
if (read > 0 && line[read - 1] == '\n') {
read--;
line[read] = '\0';
}
if (read > 0) {
unsunder(line, read);
}
lineptr = parse_next_word(line);
xptr = parse_next_word(lineptr);
lineno = atoi(lineptr);
xno = atoi(xptr);
if (poshistory == NULL) {
poshistory = (poshiststruct *) nmalloc(sizeof(poshiststruct));
poshistory->filename = mallocstrcpy(NULL, line);
poshistory->lineno = lineno;
poshistory->xno = xno;
poshistory->next = NULL;
} else {
for (posptr = poshistory; posptr->next != NULL; posptr = posptr->next)
;
posptr->next = (poshiststruct *) nmalloc(sizeof(poshiststruct));
posptr->next->filename = mallocstrcpy(NULL, line);
posptr->next->lineno = lineno;
posptr->next->xno = xno;
posptr->next->next = NULL;
}
}
fclose(hist);
free(line);
}
free(nanohist);
}
}
#endif /* !NANO_TINY && ENABLE_NANORC */