mirror of
https://github.com/textmate/textmate.git
synced 2026-04-28 03:00:34 -04:00
511 lines
14 KiB
C++
511 lines
14 KiB
C++
#include "open.h"
|
|
#include "constants.h"
|
|
#include "path_info.h"
|
|
#include "encoding.h"
|
|
#include "filter.h"
|
|
#include "type.h"
|
|
#include <authorization/constants.h>
|
|
#include <authorization/server.h>
|
|
#include <cf/cf.h>
|
|
#include <command/parser.h>
|
|
#include <command/runner.h>
|
|
#include <settings/settings.h>
|
|
#include <text/trim.h>
|
|
#include <text/utf8.h>
|
|
#include <text/newlines.h>
|
|
#include <oak/server.h>
|
|
#include <oak/debug.h>
|
|
|
|
OAK_DEBUG_VAR(File_Charset);
|
|
|
|
/*
|
|
TODO Assign UUID to open request and keep with content
|
|
TODO Harmonize line endings should do actual conversions (to make it reversable / not drop a single \r in a \n file)
|
|
TODO Allow non-installed bundles to be used as import filters
|
|
*/
|
|
|
|
// ====================
|
|
// = Context Datatype =
|
|
// ====================
|
|
|
|
namespace
|
|
{
|
|
struct open_file_context_t : file::open_context_t
|
|
{
|
|
open_file_context_t (std::string const& path, io::bytes_ptr existingContent, osx::authorization_t auth, file::open_callback_ptr callback, std::string const& virtualPath) : _state(kStateIdle), _next_state(kStateStart), _estimate_encoding_state(kEstimateEncodingStateBOM), _callback(callback), _path(path), _virtual_path(virtualPath), _authorization(auth), _content(existingContent), _file_type(kFileTypePlainText), _path_attributes(NULL_STR), _error(NULL_STR)
|
|
{
|
|
}
|
|
|
|
~open_file_context_t ()
|
|
{
|
|
if(_state != kStateDone)
|
|
_callback->show_error(_path, _error, _filter);
|
|
}
|
|
|
|
void set_authorization (osx::authorization_t auth) { _authorization = auth; proceed(); }
|
|
void set_content (io::bytes_ptr content) { _content = content; proceed(); }
|
|
void set_content (io::bytes_ptr content, std::map<std::string, std::string> const& attr) { _attributes = attr; _content = content; proceed(); }
|
|
void set_charset (std::string const& charset) { _encoding.set_charset(charset); proceed(); }
|
|
void set_line_feeds (std::string const& newlines) { _encoding.set_newlines(newlines); proceed(); }
|
|
void set_file_type (std::string const& fileType) { _file_type = fileType; proceed(); }
|
|
|
|
void filter_error (bundle_command_t const& command, int rc, std::string const& out, std::string const& err)
|
|
{
|
|
_error = text::trim(err + out).empty() ? text::format("Command returned status code %d.", rc) : err + out;
|
|
_filter = command.uuid;
|
|
}
|
|
|
|
void proceed ()
|
|
{
|
|
_state = _next_state;
|
|
event_loop();
|
|
}
|
|
|
|
private:
|
|
void event_loop ();
|
|
|
|
enum state_t {
|
|
kStateIdle,
|
|
kStateStart,
|
|
kStateObtainAuthorization,
|
|
kStateLoadContent,
|
|
kStateExecuteBinaryImportFilter,
|
|
kStateEstimateEncoding,
|
|
kStateSelectEncoding,
|
|
kStateDecodeContent,
|
|
kStateEstimateLineFeeds,
|
|
kStateHarmonizeLineFeeds,
|
|
kStateExecuteTextImportFilter,
|
|
kStateEstimateFileType,
|
|
kStateEstimateTabSettings,
|
|
kStateShowContent,
|
|
kStateDone
|
|
};
|
|
|
|
enum estimate_encoding_state_t {
|
|
kEstimateEncodingStateBOM,
|
|
kEstimateEncodingStateExtendedAttribute,
|
|
kEstimateEncodingStateASCII,
|
|
kEstimateEncodingStateUTF8,
|
|
kEstimateEncodingStatePathSettings,
|
|
kEstimateEncodingStateAskUser
|
|
};
|
|
|
|
state_t _state;
|
|
state_t _next_state;
|
|
int _estimate_encoding_state;
|
|
|
|
file::open_callback_ptr _callback;
|
|
|
|
std::string _path;
|
|
std::string _virtual_path;
|
|
osx::authorization_t _authorization;
|
|
|
|
io::bytes_ptr _content;
|
|
std::map<std::string, std::string> _attributes;
|
|
encoding::type _encoding;
|
|
std::string _file_type; // root grammar scope
|
|
std::string _path_attributes;
|
|
std::string _error;
|
|
oak::uuid_t _filter; // this filter failed
|
|
|
|
std::vector<oak::uuid_t> _binary_import_filters;
|
|
std::vector<oak::uuid_t> _text_import_filters;
|
|
};
|
|
|
|
typedef std::shared_ptr<open_file_context_t> file_context_ptr;
|
|
}
|
|
|
|
// =================
|
|
// = Threaded Read =
|
|
// =================
|
|
|
|
namespace file
|
|
{
|
|
struct read_t
|
|
{
|
|
struct request_t { std::string path; osx::authorization_t authorization; };
|
|
struct result_t { io::bytes_ptr bytes; std::map<std::string, std::string> attributes; int error_code; };
|
|
|
|
WATCH_LEAKS(read_t);
|
|
|
|
read_t (std::string const& path, osx::authorization_t auth, file_context_ptr context);
|
|
virtual ~read_t ();
|
|
|
|
static read_t::result_t handle_request (read_t::request_t const& request);
|
|
void handle_reply (read_t::result_t const& result);
|
|
|
|
private:
|
|
size_t _client_key;
|
|
file_context_ptr _context;
|
|
};
|
|
|
|
static oak::server_t<read_t>& read_server ()
|
|
{
|
|
static oak::server_t<read_t> server;
|
|
return server;
|
|
}
|
|
|
|
read_t::read_t (std::string const& path, osx::authorization_t auth, file_context_ptr context) : _context(context)
|
|
{
|
|
_client_key = read_server().register_client(this);
|
|
read_server().send_request(_client_key, (request_t){ path, auth });
|
|
}
|
|
|
|
read_t::~read_t ()
|
|
{
|
|
read_server().unregister_client(_client_key);
|
|
}
|
|
|
|
read_t::result_t read_t::handle_request (read_t::request_t const& request)
|
|
{
|
|
result_t result;
|
|
result.error_code = 0;
|
|
|
|
int fd = ::open(request.path.c_str(), O_RDONLY|O_CLOEXEC);
|
|
if(fd != -1)
|
|
{
|
|
struct stat sbuf;
|
|
if(fstat(fd, &sbuf) != -1)
|
|
{
|
|
fcntl(fd, F_NOCACHE, 1);
|
|
result.bytes = std::make_shared<io::bytes_t>(sbuf.st_size);
|
|
if(read(fd, result.bytes->get(), result.bytes->size()) != sbuf.st_size)
|
|
result.bytes.reset();
|
|
}
|
|
else
|
|
{
|
|
result.error_code = errno;
|
|
}
|
|
close(fd);
|
|
|
|
result.attributes = path::attributes(request.path);
|
|
}
|
|
else if(errno == EACCES)
|
|
{
|
|
if(connection_t conn = connect_to_auth_server(request.authorization))
|
|
{
|
|
conn << "read" << request.path;
|
|
std::string content;
|
|
conn >> content >> result.attributes;
|
|
result.bytes = std::make_shared<io::bytes_t>(content);
|
|
}
|
|
else
|
|
{
|
|
result.error_code = EACCES;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.error_code = errno;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void read_t::handle_reply (read_t::result_t const& result)
|
|
{
|
|
if(!result.bytes && result.error_code == ENOENT)
|
|
_context->set_content(std::make_shared<io::bytes_t>(""), result.attributes);
|
|
else _context->set_content(result.bytes, result.attributes);
|
|
delete this;
|
|
}
|
|
|
|
} /* file */
|
|
|
|
// ====================
|
|
// = Encoding Support =
|
|
// ====================
|
|
|
|
static bool not_ascii (char ch)
|
|
{
|
|
return !(0x20 <= ch && ch < 0x80 || ch && strchr("\t\n\f\r\e", ch));
|
|
}
|
|
|
|
static io::bytes_ptr remove_bom (io::bytes_ptr content)
|
|
{
|
|
if(content)
|
|
{
|
|
ASSERT_GE(content->size(), 3); ASSERT_EQ(std::string(content->get(), content->get() + 3), "\uFEFF");
|
|
memmove(content->get(), content->get()+3, content->size()-3);
|
|
content->resize(content->size()-3);
|
|
}
|
|
return content;
|
|
}
|
|
|
|
static io::bytes_ptr convert (io::bytes_ptr content, std::string const& from, std::string const& to, bool bom = false)
|
|
{
|
|
if(from == kCharsetUnknown)
|
|
return io::bytes_ptr();
|
|
|
|
content = encoding::convert(content, from, to);
|
|
return bom ? remove_bom(content) : content;
|
|
}
|
|
|
|
// ===================================
|
|
// = Default Callback Implementation =
|
|
// ===================================
|
|
|
|
namespace file
|
|
{
|
|
void open_callback_t::obtain_authorization (std::string const& path, osx::authorization_t auth, open_context_ptr context)
|
|
{
|
|
if(auth.obtain_right(kAuthRightName))
|
|
context->set_authorization(auth);
|
|
}
|
|
|
|
void open_callback_t::select_charset (std::string const& path, io::bytes_ptr content, open_context_ptr context)
|
|
{
|
|
}
|
|
|
|
void open_callback_t::select_line_feeds (std::string const& path, io::bytes_ptr content, open_context_ptr context)
|
|
{
|
|
context->set_line_feeds(kLF);
|
|
}
|
|
|
|
void open_callback_t::select_file_type (std::string const& path, io::bytes_ptr content, open_context_ptr context)
|
|
{
|
|
context->set_file_type(kFileTypePlainText);
|
|
}
|
|
|
|
} /* file */
|
|
|
|
// ====================
|
|
// = Context Datatype =
|
|
// ====================
|
|
|
|
namespace
|
|
{
|
|
void open_file_context_t::event_loop ()
|
|
{
|
|
_next_state = kStateIdle;
|
|
while(_state != kStateIdle && _state != kStateDone)
|
|
{
|
|
switch(_state)
|
|
{
|
|
case kStateStart:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateObtainAuthorization;
|
|
|
|
_path_attributes = file::path_attributes(_path);
|
|
if(_content)
|
|
_next_state = kStateExecuteBinaryImportFilter;
|
|
|
|
proceed();
|
|
}
|
|
break;
|
|
|
|
case kStateObtainAuthorization:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateLoadContent;
|
|
|
|
if(_path != NULL_STR && access(_path.c_str(), R_OK) == -1 && errno == EACCES)
|
|
_callback->obtain_authorization(_path, _authorization, shared_from_this());
|
|
else proceed();
|
|
}
|
|
break;
|
|
|
|
case kStateLoadContent:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateExecuteBinaryImportFilter;
|
|
|
|
new file::read_t(_path, _authorization, std::static_pointer_cast<open_file_context_t>(shared_from_this()));
|
|
}
|
|
break;
|
|
|
|
case kStateExecuteBinaryImportFilter:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateEstimateEncoding;
|
|
|
|
if(!_content)
|
|
break;
|
|
|
|
std::vector<bundles::item_ptr> filters;
|
|
citerate(item, filter::find(_path, _content, _path_attributes, filter::kBundleEventBinaryImport))
|
|
{
|
|
if(!oak::contains(_binary_import_filters.begin(), _binary_import_filters.end(), (*item)->uuid()))
|
|
{
|
|
filters.push_back(*item);
|
|
_binary_import_filters.push_back((*item)->uuid());
|
|
break; // FIXME see next FIXME
|
|
}
|
|
}
|
|
|
|
if(filters.empty())
|
|
{
|
|
proceed();
|
|
}
|
|
else // FIXME we need to show dialog incase of multiple import hooks
|
|
{
|
|
_next_state = kStateExecuteBinaryImportFilter;
|
|
filter::run(filters.back(), _path, _content, std::static_pointer_cast<open_file_context_t>(shared_from_this()));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case kStateEstimateEncoding:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateDecodeContent;
|
|
|
|
char const* first = _content->begin();
|
|
char const* last = _content->end();
|
|
switch(_estimate_encoding_state++)
|
|
{
|
|
case kEstimateEncodingStateBOM:
|
|
{
|
|
std::string const charset = encoding::charset_from_bom(first, last);
|
|
if(charset != kCharsetNoEncoding)
|
|
{
|
|
_encoding.set_charset(charset);
|
|
_encoding.set_byte_order_mark(true);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case kEstimateEncodingStateExtendedAttribute:
|
|
{
|
|
std::string const charset = path::get_attr(_path, "com.apple.TextEncoding");
|
|
_encoding.set_charset(charset != NULL_STR ? charset.substr(0, charset.find(';')) : kCharsetUnknown);
|
|
}
|
|
break;
|
|
|
|
case kEstimateEncodingStateASCII:
|
|
_encoding.set_charset(std::find_if(first, last, ¬_ascii) == last ? kCharsetNoEncoding : kCharsetUnknown);
|
|
break;
|
|
|
|
case kEstimateEncodingStateUTF8:
|
|
_encoding.set_charset(utf8::is_valid(first, last) ? kCharsetUTF8 : kCharsetUnknown);
|
|
break;
|
|
|
|
case kEstimateEncodingStatePathSettings:
|
|
_encoding.set_charset(settings_for_path(_path, "attr.file.unknown-encoding " + _path_attributes).get(kSettingsEncodingKey, kCharsetUnknown));
|
|
break;
|
|
|
|
case kEstimateEncodingStateAskUser:
|
|
_next_state = kStateSelectEncoding;
|
|
_estimate_encoding_state = kEstimateEncodingStateAskUser;
|
|
break;
|
|
}
|
|
proceed();
|
|
}
|
|
break;
|
|
|
|
case kStateSelectEncoding:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateDecodeContent;
|
|
|
|
_callback->select_charset(_path, _content, shared_from_this());
|
|
}
|
|
break;
|
|
|
|
case kStateDecodeContent:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateEstimateLineFeeds;
|
|
|
|
if(io::bytes_ptr decodedContent = convert(_content, _encoding.charset() == kCharsetNoEncoding ? kCharsetASCII : _encoding.charset(), kCharsetUTF8, _encoding.byte_order_mark()))
|
|
_content = decodedContent;
|
|
else _next_state = kStateEstimateEncoding;
|
|
|
|
proceed();
|
|
}
|
|
break;
|
|
|
|
case kStateEstimateLineFeeds:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateHarmonizeLineFeeds;
|
|
|
|
_encoding.set_newlines(text::estimate_line_endings(_content->begin(), _content->end()));
|
|
if(_encoding.newlines() != kMIX)
|
|
proceed();
|
|
else _callback->select_line_feeds(_path, _content, shared_from_this());
|
|
}
|
|
break;
|
|
|
|
case kStateHarmonizeLineFeeds:
|
|
{
|
|
if(_encoding.newlines() != kLF)
|
|
{
|
|
char* newEnd = text::convert_line_endings(_content->begin(), _content->end(), _encoding.newlines());
|
|
_content->resize(newEnd - _content->begin());
|
|
}
|
|
_state = kStateExecuteTextImportFilter;
|
|
}
|
|
break;
|
|
|
|
case kStateExecuteTextImportFilter:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateEstimateFileType;
|
|
|
|
std::vector<bundles::item_ptr> filters;
|
|
citerate(item, filter::find(_path, _content, _path_attributes, filter::kBundleEventTextImport))
|
|
{
|
|
if(!oak::contains(_text_import_filters.begin(), _text_import_filters.end(), (*item)->uuid()))
|
|
{
|
|
filters.push_back(*item);
|
|
_text_import_filters.push_back((*item)->uuid());
|
|
break; // FIXME see next FIXME
|
|
}
|
|
}
|
|
|
|
if(filters.empty())
|
|
{
|
|
proceed();
|
|
}
|
|
else // FIXME we need to show dialog incase of multiple import hooks
|
|
{
|
|
_next_state = kStateExecuteTextImportFilter;
|
|
filter::run(filters.back(), _path, _content, std::static_pointer_cast<open_file_context_t>(shared_from_this()));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case kStateEstimateFileType:
|
|
{
|
|
_state = kStateIdle;
|
|
_next_state = kStateEstimateTabSettings;
|
|
|
|
_file_type = file::type(_path, _content, _virtual_path);
|
|
if(_file_type != NULL_STR)
|
|
proceed();
|
|
else _callback->select_file_type(_virtual_path != NULL_STR ? _virtual_path : _path, _content, shared_from_this());
|
|
}
|
|
break;
|
|
|
|
case kStateEstimateTabSettings:
|
|
{
|
|
_state = kStateShowContent;
|
|
// TODO run some heuristic
|
|
}
|
|
break;
|
|
|
|
case kStateShowContent:
|
|
{
|
|
_state = kStateDone;
|
|
_callback->show_content(_path, _content, _attributes, _file_type, _encoding, _binary_import_filters, _text_import_filters);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ====================
|
|
|
|
namespace file
|
|
{
|
|
void open (std::string const& path, osx::authorization_t auth, open_callback_ptr cb, io::bytes_ptr existingContent, std::string const& virtualPath)
|
|
{
|
|
auto context = std::make_shared<open_file_context_t>(path, existingContent, auth, cb, virtualPath);
|
|
context->proceed();
|
|
}
|
|
|
|
} /* file */
|