/*
* Copyright (C) 2012-2015 Robin Haberkorn
*
* 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 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, see .
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "sciteco.h"
#include "interface.h"
#include "undo.h"
#include "error.h"
#include "ioview.h"
#ifdef HAVE_WINDOWS_H
/* here it shouldn't cause conflicts with other headers */
#include
/*
* MinGW headers define an `interface` macro to work around
* Objective C issues
*/
#undef interface
#endif
namespace SciTECO {
#ifdef G_OS_WIN32
typedef DWORD FileAttributes;
/* INVALID_FILE_ATTRIBUTES already defined */
static inline FileAttributes
get_file_attributes(const gchar *filename)
{
return GetFileAttributes((LPCTSTR)filename);
}
static inline void
set_file_attributes(const gchar *filename, FileAttributes attrs)
{
SetFileAttributes((LPCTSTR)filename, attrs);
}
#else
typedef int FileAttributes;
#define INVALID_FILE_ATTRIBUTES (-1)
static inline FileAttributes
get_file_attributes(const gchar *filename)
{
struct stat buf;
return g_stat(filename, &buf) ? INVALID_FILE_ATTRIBUTES : buf.st_mode;
}
static inline void
set_file_attributes(const gchar *filename, FileAttributes attrs)
{
g_chmod(filename, attrs);
}
#endif /* !G_OS_WIN32 */
/**
* A wrapper around g_io_channel_read_chars() that also
* performs automatic EOL translation (if enabled) in a
* more or less efficient manner.
* Unlike g_io_channel_read_chars(), this returns an
* offset and length into the buffer with normalized
* EOL character.
* The function must therefore be called iteratively on
* on the same buffer while it returns G_IO_STATUS_NORMAL.
*
* @param channel The GIOChannel to read from.
' @param buffer Used to store blocks.
* @param buffer_len Size of buffer.
* @param read_len Total number of bytes read into buffer.
* Must be provided over the lifetime of buffer
* and initialized with 0.
* @param offset If a block could be read (G_IO_STATUS_NORMAL),
* this will be set to indicate its beginning in
* buffer. Should be initialized to 0.
* @param block_len Will be set to the block length.
* Should be initialized to 0.
* @param state Opaque state that must persist for the lifetime
* of the channel. Must be initialized with 0.
* @param eol_style Will be set to the EOL style guessed from
* the data in channel (if the data allows it).
* Should be initialized with -1 (unknown).
* @param eol_style_consistent Will be set to TRUE if
* inconsistent EOL styles are detected.
* @param error If not NULL and an error occurred, it is set to
* the error. It should be initialized to -1.
* @return A GIOStatus as returned by g_io_channel_read_chars()
*/
GIOStatus
IOView::channel_read_with_eol(GIOChannel *channel,
gchar *buffer, gsize buffer_len,
gsize &read_len,
guint &offset, gsize &block_len,
gint &state, gint &eol_style,
gboolean &eol_style_inconsistent,
GError **error)
{
GIOStatus status;
if (state < 0) {
/* a CRLF was last translated */
block_len++;
state = '\n';
}
offset += block_len;
if (offset == read_len) {
offset = 0;
status = g_io_channel_read_chars(channel, buffer, buffer_len,
&read_len, error);
if (status == G_IO_STATUS_EOF && state == '\r') {
/*
* Very last character read is CR.
* If this is the only EOL so far, the
* EOL style is MAC.
* This is also executed if auto-eol is disabled
* but it doesn't hurt.
*/
if (eol_style < 0)
eol_style = SC_EOL_CR;
else if (eol_style != SC_EOL_CR)
eol_style_inconsistent = TRUE;
}
if (status != G_IO_STATUS_NORMAL)
return status;
if (!(Flags::ed & Flags::ED_AUTOEOL)) {
/*
* No EOL translation - always return entire
* buffer
*/
block_len = read_len;
return G_IO_STATUS_NORMAL;
}
}
/*
* Return data with automatic EOL translation.
* Every EOL sequence is normalized to LF and
* the first sequence determines the documents
* EOL style.
* This loop is executed for every byte of the
* file/stream, so it was important to optimize
* it. Specifically, the number of returns
* is minimized by keeping a pointer to
* the beginning of a block of data in the buffer
* which already has LFs (offset).
* Mac EOLs can be converted to UNIX EOLs directly
* in the buffer.
* So if their EOLs are consistent, the function
* will return one block for the entire buffer.
* When reading a file with DOS EOLs, there will
* be one call per line which is significantly slower.
*/
for (guint i = offset; i < read_len; i++) {
switch (buffer[i]) {
case '\n':
if (state == '\r') {
if (eol_style < 0)
eol_style = SC_EOL_CRLF;
else if (eol_style != SC_EOL_CRLF)
eol_style_inconsistent = TRUE;
/*
* Return block. CR has already
* been made LF in `buffer`.
*/
block_len = i-offset;
/* next call will skip the CR */
state = -1;
return G_IO_STATUS_NORMAL;
}
if (eol_style < 0)
eol_style = SC_EOL_LF;
else if (eol_style != SC_EOL_LF)
eol_style_inconsistent = TRUE;
/*
* No conversion necessary and no need to
* return block yet.
*/
state = '\n';
break;
case '\r':
if (state == '\r') {
if (eol_style < 0)
eol_style = SC_EOL_CR;
else if (eol_style != SC_EOL_CR)
eol_style_inconsistent = TRUE;
}
/*
* Convert CR to LF in `buffer`.
* This way more than one line using
* Mac EOLs can be returned at once.
*/
buffer[i] = '\n';
state = '\r';
break;
default:
if (state == '\r') {
if (eol_style < 0)
eol_style = SC_EOL_CR;
else if (eol_style != SC_EOL_CR)
eol_style_inconsistent = TRUE;
}
state = buffer[i];
break;
}
}
/*
* Return remaining block.
* With UNIX/MAC EOLs, this will usually be the
* entire `buffer`
*/
block_len = read_len-offset;
return G_IO_STATUS_NORMAL;
}
/**
* Loads the view's document by reading all data from
* a GIOChannel.
* The EOL style is guessed from the channel's data
* (if AUTOEOL is enabled).
* This assumes that the channel is blocking.
* Also it tries to guess the size of the file behind
* channel in order to preallocate memory in Scintilla.
*
* @param channel Channel to read from.
* @param error Glib error or NULL.
* @returns A GIOStatus as returned by g_io_channel_read_chars()
*/
GIOStatus
IOView::load(GIOChannel *channel, GError **error)
{
GIOStatus status;
GStatBuf stat_buf;
gchar buffer[1024];
gsize read_len = 0;
guint offset = 0;
gsize block_len = 0;
gint state = 0; /* opaque state */
gint eol_style = -1; /* yet unknown */
gboolean eol_style_inconsistent = FALSE;
ssm(SCI_BEGINUNDOACTION);
ssm(SCI_CLEARALL);
/*
* Preallocate memory based on the file size.
* May waste a few bytes if file contains DOS EOLs
* and EOL translation is enabled, but is faster.
* NOTE: g_io_channel_unix_get_fd() should report the correct fd
* on Windows, too.
*/
stat_buf.st_size = 0;
if (!fstat(g_io_channel_unix_get_fd(channel), &stat_buf) &&
stat_buf.st_size > 0)
ssm(SCI_ALLOCATE, stat_buf.st_size);
for (;;) {
status = channel_read_with_eol(
channel, buffer, sizeof(buffer),
read_len, offset, block_len, state,
eol_style, eol_style_inconsistent,
error
);
if (status != G_IO_STATUS_NORMAL)
break;
ssm(SCI_APPENDTEXT, block_len, (sptr_t)(buffer+offset));
}
/*
* EOL-style guessed.
* Save it as the buffer's EOL mode, so save()
* can restore the original EOL-style.
* If auto-EOL-translation is disabled, this cannot
* have been guessed and the buffer's EOL mode should
* have a platform default.
* If it is enabled but the stream does not contain any
* EOL characters, the platform default is still assumed.
*/
if (eol_style >= 0)
ssm(SCI_SETEOLMODE, eol_style);
if (eol_style_inconsistent)
interface.msg(InterfaceCurrent::MSG_WARNING,
"Inconsistent EOL styles normalized");
ssm(SCI_ENDUNDOACTION);
return status;
}
/**
* Load view's document from file.
*/
void
IOView::load(const gchar *filename)
{
GError *error = NULL;
GIOChannel *channel;
GIOStatus status;
channel = g_io_channel_new_file(filename, "r", &error);
if (!channel) {
Error err("Error opening file \"%s\" for reading: %s",
filename, error->message);
g_error_free(error);
throw err;
}
/*
* The file loading algorithm does not need buffered
* streams, so disabling buffering should increase
* performance (slightly).
*/
g_io_channel_set_encoding(channel, NULL, NULL);
g_io_channel_set_buffered(channel, FALSE);
status = load(channel, &error);
/* also closes file: */
g_io_channel_unref(channel);
if (status == G_IO_STATUS_ERROR) {
Error err("Error reading file \"%s\": %s",
filename, error->message);
g_error_free(error);
throw err;
}
}
#if 0
/*
* TODO: on UNIX it may be better to open() the current file, unlink() it
* and keep the file descriptor in the UndoToken.
* When the operation is undone, the file descriptor's contents are written to
* the file (which should be efficient enough because it is written to the same
* filesystem). This way we could avoid messing around with save point files.
*/
#else
static gint savepoint_id = 0;
class UndoTokenRestoreSavePoint : public UndoToken {
gchar *savepoint;
gchar *filename;
#ifdef G_OS_WIN32
FileAttributes orig_attrs;
#endif
public:
UndoTokenRestoreSavePoint(gchar *_savepoint, const gchar *_filename)
: savepoint(_savepoint), filename(g_strdup(_filename))
{
#ifdef G_OS_WIN32
orig_attrs = get_file_attributes(filename);
if (orig_attrs != INVALID_FILE_ATTRIBUTES)
set_file_attributes(savepoint,
orig_attrs | FILE_ATTRIBUTE_HIDDEN);
#endif
}
~UndoTokenRestoreSavePoint()
{
if (savepoint) {
g_unlink(savepoint);
g_free(savepoint);
}
g_free(filename);
savepoint_id--;
}
void
run(void)
{
if (!g_rename(savepoint, filename)) {
g_free(savepoint);
savepoint = NULL;
#ifdef G_OS_WIN32
if (orig_attrs != INVALID_FILE_ATTRIBUTES)
set_file_attributes(filename,
orig_attrs);
#endif
} else {
interface.msg(InterfaceCurrent::MSG_WARNING,
"Unable to restore save point file \"%s\"",
savepoint);
}
}
gsize
get_size(void) const
{
gsize ret = sizeof(*this) + strlen(filename) + 1;
if (savepoint)
ret += strlen(savepoint) + 1;
return ret;
}
};
static void
make_savepoint(const gchar *filename)
{
gchar *dirname, *basename, *savepoint;
gchar savepoint_basename[FILENAME_MAX];
basename = g_path_get_basename(filename);
g_snprintf(savepoint_basename, sizeof(savepoint_basename),
".teco-%d-%s~", savepoint_id, basename);
g_free(basename);
dirname = g_path_get_dirname(filename);
savepoint = g_build_filename(dirname, savepoint_basename, NIL);
g_free(dirname);
if (g_rename(filename, savepoint)) {
interface.msg(InterfaceCurrent::MSG_WARNING,
"Unable to create save point file \"%s\"",
savepoint);
g_free(savepoint);
return;
}
savepoint_id++;
/* NOTE: passes ownership of savepoint string to undo token */
undo.push(new UndoTokenRestoreSavePoint(savepoint, filename));
}
#endif /* !G_OS_UNIX */
GIOStatus
IOView::save(GIOChannel *channel, guint position, gsize len,
gsize *bytes_written, gint &state, GError **error)
{
const gchar *buffer;
const gchar *eol_seq;
gchar last_c;
guint i = 0;
guint block_start;
gsize block_written;
GIOStatus status;
enum {
SAVE_STATE_START = 0,
SAVE_STATE_WRITE_LF
};
buffer = (const gchar *)ssm(SCI_GETRANGEPOINTER,
position, (sptr_t)len);
if (!(Flags::ed & Flags::ED_AUTOEOL))
/*
* Write without EOL-translation:
* `state` is not required
*/
return g_io_channel_write_chars(channel, buffer, len,
bytes_written, error);
/*
* Write to stream with EOL-translation.
* The document's EOL mode tells us what was guessed
* when its content was read in (presumably from a file)
* but might have been changed manually by the user.
* NOTE: This code assumes that the output stream is
* buffered, since otherwise it would be slower
* (has been benchmarked).
* NOTE: The loop is executed for every character
* in `buffer` and has been optimized for minimal
* function (i.e. GIOChannel) calls.
*/
*bytes_written = 0;
if (state == SAVE_STATE_WRITE_LF) {
/* complete writing a CRLF sequence */
status = g_io_channel_write_chars(channel, "\n", 1, NULL, error);
if (status != G_IO_STATUS_NORMAL)
return status;
state = SAVE_STATE_START;
(*bytes_written)++;
i++;
}
eol_seq = get_eol_seq(ssm(SCI_GETEOLMODE));
last_c = ssm(SCI_GETCHARAT, position-1);
block_start = i;
while (i < len) {
switch (buffer[i]) {
case '\n':
if (last_c == '\r') {
/* EOL sequence already written */
(*bytes_written)++;
block_start = i+1;
break;
}
/* fall through */
case '\r':
status = g_io_channel_write_chars(channel, buffer+block_start,
i-block_start, &block_written, error);
*bytes_written += block_written;
if (status != G_IO_STATUS_NORMAL ||
block_written < i-block_start)
return status;
status = g_io_channel_write_chars(channel, eol_seq,
-1, &block_written, error);
if (status != G_IO_STATUS_NORMAL)
return status;
if (eol_seq[block_written]) {
/* incomplete EOL seq - we have written CR of CRLF */
state = SAVE_STATE_WRITE_LF;
return G_IO_STATUS_NORMAL;
}
(*bytes_written)++;
block_start = i+1;
break;
}
last_c = buffer[i++];
}
/*
* Write out remaining block (i.e. line)
*/
status = g_io_channel_write_chars(channel, buffer+block_start,
len-block_start, &block_written, error);
*bytes_written += block_written;
return status;
}
gboolean
IOView::save(GIOChannel *channel, GError **error)
{
sptr_t gap;
gsize size;
gsize bytes_written;
gint state = 0;
/* write part of buffer before gap */
gap = ssm(SCI_GETGAPPOSITION);
if (gap > 0) {
if (save(channel, 0, gap, &bytes_written, state, error) == G_IO_STATUS_ERROR)
return FALSE;
g_assert(bytes_written == (gsize)gap);
}
/* write part of buffer after gap */
size = ssm(SCI_GETLENGTH) - gap;
if (size > 0) {
if (save(channel, gap, size, &bytes_written, state, error) == G_IO_STATUS_ERROR)
return FALSE;
g_assert(bytes_written == size);
}
return TRUE;
}
void
IOView::save(const gchar *filename)
{
GError *error = NULL;
GIOChannel *channel;
#ifdef G_OS_UNIX
GStatBuf file_stat;
file_stat.st_uid = -1;
file_stat.st_gid = -1;
#endif
FileAttributes attributes = INVALID_FILE_ATTRIBUTES;
if (undo.enabled) {
if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
#ifdef G_OS_UNIX
g_stat(filename, &file_stat);
#endif
attributes = get_file_attributes(filename);
make_savepoint(filename);
} else {
undo.push(new UndoTokenRemoveFile(filename));
}
}
/* leaves access mode intact if file still exists */
channel = g_io_channel_new_file(filename, "w", &error);
if (!channel)
throw GlibError(error);
/*
* save(GIOChannel *, const gchar *) expects a buffered
* and blocking channel
*/
g_io_channel_set_encoding(channel, NULL, NULL);
g_io_channel_set_buffered(channel, TRUE);
if (!save(channel, &error)) {
Error err("Error writing file \"%s\": %s", filename, error->message);
g_error_free(error);
g_io_channel_unref(channel);
throw err;
}
/* if file existed but has been renamed, restore attributes */
if (attributes != INVALID_FILE_ATTRIBUTES)
set_file_attributes(filename, attributes);
#ifdef G_OS_UNIX
/*
* only a good try to inherit owner since process user must have
* CHOWN capability traditionally reserved to root only.
* FIXME: We should probably fall back to another save point
* strategy.
*/
if (fchown(g_io_channel_unix_get_fd(channel),
file_stat.st_uid, file_stat.st_gid))
interface.msg(InterfaceCurrent::MSG_WARNING,
"Unable to preserve owner of \"%s\": %s",
filename, g_strerror(errno));
#endif
/* also closes file */
g_io_channel_unref(channel);
}
/*
* Auxiliary functions
*/
#ifdef G_OS_UNIX
gchar *
get_absolute_path(const gchar *path)
{
gchar buf[PATH_MAX];
gchar *resolved;
if (!path)
return NULL;
if (!realpath(path, buf)) {
if (g_path_is_absolute(path)) {
resolved = g_strdup(path);
} else {
gchar *cwd = g_get_current_dir();
resolved = g_build_filename(cwd, path, NIL);
g_free(cwd);
}
} else {
resolved = g_strdup(buf);
}
return resolved;
}
bool
file_is_visible(const gchar *path)
{
gchar *basename = g_path_get_basename(path);
bool ret = *basename != '.';
g_free(basename);
return ret;
}
#elif defined(G_OS_WIN32)
gchar *
get_absolute_path(const gchar *path)
{
TCHAR buf[MAX_PATH];
gchar *resolved = NULL;
if (path && GetFullPathName(path, sizeof(buf), buf, NULL))
resolved = g_strdup(buf);
return resolved;
}
bool
file_is_visible(const gchar *path)
{
return !(get_file_attributes(path) & FILE_ATTRIBUTE_HIDDEN);
}
#else
/*
* FIXME: I doubt that works on any platform...
*/
gchar *
get_absolute_path(const gchar *path)
{
return path ? g_file_read_link(path, NULL) : NULL;
}
/*
* There's no platform-independant way to determine if a file
* is visible/hidden, so we just assume that all files are
* visible.
*/
bool
file_is_visible(const gchar *path)
{
return true;
}
#endif /* !G_OS_UNIX && !G_OS_WIN32 */
} /* namespace SciTECO */