mirror of https://github.com/adamdruppe/arsd.git
Compare commits
No commits in common. "master" and "v12.1.0" have entirely different histories.
|
|
@ -98,7 +98,6 @@ import arsd.rtf;
|
||||||
// import arsd.screen; // D1 or 2.098
|
// import arsd.screen; // D1 or 2.098
|
||||||
import arsd.script;
|
import arsd.script;
|
||||||
import arsd.sha;
|
import arsd.sha;
|
||||||
import arsd.shell;
|
|
||||||
import arsd.simpleaudio;
|
import arsd.simpleaudio;
|
||||||
import arsd.simpledisplay;
|
import arsd.simpledisplay;
|
||||||
import arsd.sqlite;
|
import arsd.sqlite;
|
||||||
|
|
|
||||||
524
database.d
524
database.d
|
|
@ -109,8 +109,6 @@ interface Database {
|
||||||
import arsd.core;
|
import arsd.core;
|
||||||
Variant[] vargs;
|
Variant[] vargs;
|
||||||
string sql;
|
string sql;
|
||||||
|
|
||||||
// FIXME: use the new helper functions sqlFromInterpolatedArgs and variantsFromInterpolatedArgs
|
|
||||||
foreach(arg; args) {
|
foreach(arg; args) {
|
||||||
static if(is(typeof(arg) == InterpolatedLiteral!str, string str)) {
|
static if(is(typeof(arg) == InterpolatedLiteral!str, string str)) {
|
||||||
sql ~= str;
|
sql ~= str;
|
||||||
|
|
@ -125,19 +123,10 @@ interface Database {
|
||||||
}
|
}
|
||||||
return queryImpl(sql, vargs);
|
return queryImpl(sql, vargs);
|
||||||
}
|
}
|
||||||
// see test/dbis.d
|
|
||||||
|
|
||||||
/// turns a systime into a value understandable by the target database as a timestamp to be concated into a query. so it should be quoted and escaped etc as necessary
|
/// turns a systime into a value understandable by the target database as a timestamp to be concated into a query. so it should be quoted and escaped etc as necessary
|
||||||
string sysTimeToValue(SysTime);
|
string sysTimeToValue(SysTime);
|
||||||
|
|
||||||
/++
|
|
||||||
Return true if the connection appears to be alive
|
|
||||||
|
|
||||||
History:
|
|
||||||
Added October 30, 2025
|
|
||||||
+/
|
|
||||||
bool isAlive();
|
|
||||||
|
|
||||||
/// Prepared statement api
|
/// Prepared statement api
|
||||||
/*
|
/*
|
||||||
PreparedStatement prepareStatement(string sql, int numberOfArguments);
|
PreparedStatement prepareStatement(string sql, int numberOfArguments);
|
||||||
|
|
@ -360,519 +349,6 @@ interface ResultSet {
|
||||||
/* deprecated */ final ResultSet byAssoc() { return this; }
|
/* deprecated */ final ResultSet byAssoc() { return this; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
|
||||||
Converts a database result set to a html table, using [arsd.dom].
|
|
||||||
|
|
||||||
History:
|
|
||||||
Added October 29, 2025
|
|
||||||
+/
|
|
||||||
auto resultSetToHtmlTable()(ResultSet resultSet) {
|
|
||||||
import arsd.dom;
|
|
||||||
|
|
||||||
Table table = cast(Table) Element.make("table");
|
|
||||||
table.appendHeaderRow(resultSet.fieldNames);
|
|
||||||
foreach(row; resultSet) {
|
|
||||||
table.appendRow(row.toStringArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class ConnectionPoolBase : arsd.core.SynchronizableObject {
|
|
||||||
protected struct DatabaseListItem {
|
|
||||||
Database db;
|
|
||||||
DatabaseListItem* nextAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DatabaseListItem* firstAvailable;
|
|
||||||
|
|
||||||
// FIXME: add a connection count limit and some kind of wait mechanism for one to become available
|
|
||||||
|
|
||||||
final protected void makeAvailable(DatabaseListItem* what) {
|
|
||||||
synchronized(this) {
|
|
||||||
auto keep = this.firstAvailable;
|
|
||||||
what.nextAvailable = keep;
|
|
||||||
this.firstAvailable = what;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final protected DatabaseListItem* getNext() {
|
|
||||||
DatabaseListItem* toUse;
|
|
||||||
synchronized(this) {
|
|
||||||
if(this.firstAvailable !is null) {
|
|
||||||
toUse = this.firstAvailable;
|
|
||||||
this.firstAvailable = this.firstAvailable.nextAvailable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return toUse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
PooledConnection is an RAII holder for a database connection that is automatically recycled to the pool it came from (unless you [discard] it).
|
|
||||||
|
|
||||||
History:
|
|
||||||
Added October 29, 2025
|
|
||||||
+/
|
|
||||||
struct PooledConnection(ConnectionPoolType) {
|
|
||||||
private ConnectionPoolType.DatabaseListItem* dli;
|
|
||||||
private ConnectionPoolType pool;
|
|
||||||
private bool discarded;
|
|
||||||
private this(ConnectionPoolType.DatabaseListItem* dli, ConnectionPoolType pool) {
|
|
||||||
this.dli = dli;
|
|
||||||
this.pool = pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
@disable this(this);
|
|
||||||
|
|
||||||
/++
|
|
||||||
Indicates you want the connection discarded instead of returned to the pool when you're finished with it.
|
|
||||||
|
|
||||||
You should call this if you know the connection is dead.
|
|
||||||
+/
|
|
||||||
void discard() {
|
|
||||||
this.discarded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
~this() {
|
|
||||||
import core.memory;
|
|
||||||
if(GC.inFinalizer)
|
|
||||||
return;
|
|
||||||
if(!discarded && dli.db.isAlive) {
|
|
||||||
// FIXME: a connection must not be returned to the pool unless it is both alive and idle; any pending query work would screw up the next user
|
|
||||||
// it is the user's responsibility to live with other state though like prepared statements or whatever saved per-connection.
|
|
||||||
pool.makeAvailable(dli);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
|
|
||||||
+/
|
|
||||||
ConnectionPoolType.DriverType borrow() @system return {
|
|
||||||
return cast(ConnectionPoolType.DriverType) dli.db; // static_cast
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
+/
|
|
||||||
ResultSet rtQuery(T...)(T t) {
|
|
||||||
return dli.db.query(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
|
|
||||||
+/
|
|
||||||
template query(string file = __FILE__, size_t line = __LINE__, Args...) {
|
|
||||||
enum asSql = sqlFromInterpolatedArgs!(Args);
|
|
||||||
__gshared queryMetadata = new QueryMetadata!(asSql, file, line);
|
|
||||||
@(arsd.core.standalone) @system shared static this() {
|
|
||||||
ConnectionPoolType.registeredQueries_ ~= queryMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto query(arsd.core.InterpolationHeader ihead, Args args, arsd.core.InterpolationFooter ifoot) {
|
|
||||||
return new QueryResult!queryMetadata(dli.db.queryImpl(asSql, variantsFromInterpolatedArgs(args)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
|
|
||||||
+/
|
|
||||||
unittest {
|
|
||||||
import arsd.database;
|
|
||||||
|
|
||||||
shared dbPool = new shared ConnectionPool!(() => new MockDatabase())();
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
auto db = dbPool.get();
|
|
||||||
foreach(row; db.query(i"SELECT * FROM test")) {
|
|
||||||
if(row.id.isNull)
|
|
||||||
continue;
|
|
||||||
auto id = row.id.get!int;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
main(); // remove from docs
|
|
||||||
}
|
|
||||||
|
|
||||||
private Variant[] variantsFromInterpolatedArgs(Args...)(Args args) {
|
|
||||||
Variant[] ret;
|
|
||||||
|
|
||||||
import arsd.core;
|
|
||||||
|
|
||||||
foreach(arg; args) {
|
|
||||||
static if(is(typeof(arg) == InterpolationHeader))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == InterpolationFooter))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == InterpolatedLiteral!sql, string sql))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == InterpolatedExpression!code, string code))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == AdHocBuiltStruct!(tag, names, Values), string tag, string[] names, Values...)) {
|
|
||||||
static if(tag == "VALUES") {
|
|
||||||
foreach(value; arg.values) {
|
|
||||||
static if(is(value == sql_!code, string code)) {
|
|
||||||
// intentionally blank
|
|
||||||
} else {
|
|
||||||
ret ~= Variant(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else static assert(0);
|
|
||||||
}
|
|
||||||
// FIXME: iraw and sql!"" too and VALUES
|
|
||||||
else
|
|
||||||
ret ~= Variant(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string sqlFromInterpolatedArgs(Args...)() {
|
|
||||||
string ret;
|
|
||||||
|
|
||||||
import arsd.core;
|
|
||||||
|
|
||||||
foreach(arg; Args) {
|
|
||||||
static if(is(arg == InterpolationHeader))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(arg == InterpolationFooter))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(arg == InterpolatedLiteral!sql, string sql))
|
|
||||||
ret ~= sql;
|
|
||||||
else
|
|
||||||
static if(is(arg == InterpolatedExpression!code, string code))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(arg == AdHocBuiltStruct!(tag, names, values), string tag, string[] names, values...)) {
|
|
||||||
static if(tag == "VALUES") {
|
|
||||||
ret ~= "(";
|
|
||||||
foreach(idx, name; names) {
|
|
||||||
if(idx) ret ~= ", ";
|
|
||||||
ret ~= name;
|
|
||||||
}
|
|
||||||
ret ~= ") VALUES (";
|
|
||||||
foreach(idx, value; values) {
|
|
||||||
if(idx) ret ~= ", ";
|
|
||||||
|
|
||||||
static if(is(value == sql_!code, string code)) {
|
|
||||||
ret ~= code;
|
|
||||||
} else {
|
|
||||||
ret ~= "?";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret ~= ")";
|
|
||||||
}
|
|
||||||
else static assert(0);
|
|
||||||
}
|
|
||||||
// FIXME: iraw and sql_!"" too
|
|
||||||
else
|
|
||||||
ret ~= "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/+
|
|
||||||
+/
|
|
||||||
|
|
||||||
struct AssociatedDatabaseDatum(alias queryMetadata, string name, string file, size_t line) {
|
|
||||||
@(arsd.core.standalone) @system shared static this() {
|
|
||||||
queryMetadata.registerName(name, file, line);
|
|
||||||
}
|
|
||||||
|
|
||||||
template get(T, string file = __FILE__, size_t line = __LINE__) {
|
|
||||||
shared static this() {
|
|
||||||
// FIXME: empty string and null must be distinguishable in arsd.core
|
|
||||||
static if(is(T == string))
|
|
||||||
T t = "sample";
|
|
||||||
else
|
|
||||||
T t = T.init;
|
|
||||||
queryMetadata.registerType(name, LimitedVariant(t), T.stringof, file, line);
|
|
||||||
}
|
|
||||||
|
|
||||||
T get() {
|
|
||||||
import std.conv;
|
|
||||||
return datum.toString().to!T;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseDatum datum;
|
|
||||||
|
|
||||||
bool isNull() {
|
|
||||||
return datum.isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
string toString() {
|
|
||||||
if(isNull)
|
|
||||||
return null;
|
|
||||||
else
|
|
||||||
return datum.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
alias toString this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract class QueryResultBase {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueryResult(alias queryMetadata) : QueryResultBase {
|
|
||||||
private ResultSet resultSet;
|
|
||||||
|
|
||||||
this(ResultSet resultSet) {
|
|
||||||
this.resultSet = resultSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResultRow!queryMetadata front() {
|
|
||||||
return new QueryResultRow!queryMetadata(resultSet.front);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool empty() {
|
|
||||||
return resultSet.empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
void popFront() {
|
|
||||||
resultSet.popFront();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueryResultRow(alias queryMetadata) {
|
|
||||||
Row row;
|
|
||||||
this(Row row) {
|
|
||||||
this.row = row;
|
|
||||||
}
|
|
||||||
|
|
||||||
AssociatedDatabaseDatum!(queryMetadata, name, file, line) opDispatch(string name, string file = __FILE__, size_t line = __LINE__)() if(name != "__dtor") {
|
|
||||||
return typeof(return)(row[name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// i could support an opSlice. maybe opIndex tho it won't be CT bound checked w/o a ct!0 thing
|
|
||||||
// also want opApply which discards type check prolly and just gives you the datum.
|
|
||||||
|
|
||||||
int opApply(int delegate(string, DatabaseDatum) dg) {
|
|
||||||
string[] fn = row.resultSet.fieldNames();
|
|
||||||
foreach(idx, item; row.row)
|
|
||||||
mixin(yield("fn[idx], item"));
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ditto
|
|
||||||
int opApply(int delegate(DatabaseDatum) dg) {
|
|
||||||
foreach(item; row.row)
|
|
||||||
mixin(yield("item"));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ReferencedColumn {
|
|
||||||
string name;
|
|
||||||
LimitedVariant sampleData;
|
|
||||||
string assumedType;
|
|
||||||
string actualType;
|
|
||||||
string file;
|
|
||||||
size_t line;
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueryMetadataBase {
|
|
||||||
ReferencedColumn[] names;
|
|
||||||
void registerName(string name, string file, size_t line) {
|
|
||||||
names ~= ReferencedColumn(name, LimitedVariant.init, null, null, file, line);
|
|
||||||
}
|
|
||||||
void registerType(string name, LimitedVariant sample, string type, string file, size_t line) {
|
|
||||||
foreach(ref n; names)
|
|
||||||
if(n.name == name) {
|
|
||||||
if(n.assumedType.length && type.length) {
|
|
||||||
n.actualType = type;
|
|
||||||
}
|
|
||||||
n.assumedType = type;
|
|
||||||
n.sampleData = sample;
|
|
||||||
n.file = file;
|
|
||||||
n.line = line;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
names ~= ReferencedColumn(name, sample, type, type, file, line);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract string sql() const;
|
|
||||||
abstract string file() const;
|
|
||||||
abstract size_t line() const;
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueryMetadata(string q, string file_, size_t line_) : QueryMetadataBase {
|
|
||||||
override string sql() const { return q; }
|
|
||||||
override string file() const { return file_; }
|
|
||||||
override size_t line() const { return line_; }
|
|
||||||
}
|
|
||||||
|
|
||||||
version(unittest)
|
|
||||||
class MockDatabase : Database {
|
|
||||||
void startTransaction() {}
|
|
||||||
string sysTimeToValue(SysTime s) { return null; }
|
|
||||||
bool isAlive() { return true; }
|
|
||||||
|
|
||||||
ResultSet queryImpl(string sql, Variant[] args...) {
|
|
||||||
return new PredefinedResultSet(null, null);
|
|
||||||
}
|
|
||||||
string escape(string sqlData) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
string escapeBinaryString(const(ubyte)[] sqlData) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
Helpers for interpolated queries.
|
|
||||||
|
|
||||||
History:
|
|
||||||
Added October 31, 2025
|
|
||||||
|
|
||||||
See_Also:
|
|
||||||
[arsd.core.iraw]
|
|
||||||
+/
|
|
||||||
auto VALUES() {
|
|
||||||
import arsd.core;
|
|
||||||
return AdHocBuiltStruct!"VALUES"();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ditto
|
|
||||||
auto sql(string s)() {
|
|
||||||
return sql_!s();
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct sql_(string s) { }
|
|
||||||
|
|
||||||
/++
|
|
||||||
A ConnectionPool manages a set of shared connections to a database.
|
|
||||||
|
|
||||||
|
|
||||||
Create one like this:
|
|
||||||
|
|
||||||
---
|
|
||||||
// at top level
|
|
||||||
shared dbPool = new shared ConnectionPool!(() => new PostgreSql("dbname=me"))();
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
auto db = dbPool.get(); // in the function, get it and use it temporarily
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
History:
|
|
||||||
Added October 29, 2025
|
|
||||||
+/
|
|
||||||
class ConnectionPool(alias connectionFactory) : ConnectionPoolBase {
|
|
||||||
private alias unsharedThis = ConnectionPool!connectionFactory;
|
|
||||||
|
|
||||||
static if(is(typeof(connectionFactory) DriverType == return)) {
|
|
||||||
static if(!is(DriverType : Database))
|
|
||||||
static assert(0, "unusable connectionFactory - it needs to return an instance of Database");
|
|
||||||
} else {
|
|
||||||
static assert(0, "unusable connectionFactory - it needs to be a function");
|
|
||||||
}
|
|
||||||
|
|
||||||
private __gshared QueryMetadataBase[] registeredQueries_;
|
|
||||||
|
|
||||||
immutable(QueryMetadataBase[]) registeredQueries() shared {
|
|
||||||
return cast(immutable(QueryMetadataBase[])) registeredQueries_;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool checkQueries()(DriverType db) shared {
|
|
||||||
bool succeeded = true;
|
|
||||||
|
|
||||||
import arsd.postgres; // FIXME is this really postgres only? looks like sqlite has no similar function... maybe make a view then sqlite3_table_column_metadata ?
|
|
||||||
static assert(is(DriverType == PostgreSql), "Only implemented for postgres right now");
|
|
||||||
|
|
||||||
int count;
|
|
||||||
import arsd.core;
|
|
||||||
import arsd.conv;
|
|
||||||
foreach(q; registeredQueries) {
|
|
||||||
//writeln(q.file, ":", q.line, " ", q.sql);
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
string dbSpecificSql;
|
|
||||||
int placeholderNumber = 1;
|
|
||||||
size_t lastCopied = 0;
|
|
||||||
foreach(idx, ch; q.sql) {
|
|
||||||
if(ch == '?') {
|
|
||||||
dbSpecificSql ~= q.sql[lastCopied .. idx];
|
|
||||||
lastCopied = idx + 1;
|
|
||||||
dbSpecificSql ~= "$" ~ to!string(placeholderNumber);
|
|
||||||
placeholderNumber++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dbSpecificSql ~= q.sql[lastCopied .. $];
|
|
||||||
// FIXME: pipeline this
|
|
||||||
db.query("PREPARE thing_"~to!string(++count)~" AS " ~ dbSpecificSql);
|
|
||||||
} catch(Exception e) {
|
|
||||||
e.file = q.file;
|
|
||||||
e.line = q.line;
|
|
||||||
throw e;
|
|
||||||
// continue;
|
|
||||||
}
|
|
||||||
// this mysql function looks about right: https://dev.mysql.com/doc/c-api/8.0/en/mysql-stmt-result-metadata.html
|
|
||||||
// could maybe emulate by trying it in a rolled back transaction though.
|
|
||||||
auto desca = describePrepared(db,"thing_"~arsd.conv.to!string(count));
|
|
||||||
LimitedVariant[string] byName;
|
|
||||||
foreach(col; desca.result) {
|
|
||||||
byName[col.fieldName] = col.type.storage;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach(name; q.names) {
|
|
||||||
if(name.name !in byName)
|
|
||||||
throw ArsdException!"you reference unknown field"(name.name, name.file, name.line);
|
|
||||||
if(name.assumedType.length == 0)
|
|
||||||
continue;
|
|
||||||
if(byName[name.name].contains != name.sampleData.contains)
|
|
||||||
throw ArsdException!"type mismatch"(
|
|
||||||
name.name,
|
|
||||||
arsd.conv.to!string(byName[name.name].contains),
|
|
||||||
arsd.conv.to!string(name.sampleData.contains),
|
|
||||||
name.file,
|
|
||||||
name.line,
|
|
||||||
);
|
|
||||||
|
|
||||||
// i think this is redundant
|
|
||||||
if(name.assumedType.length && name.actualType.length && name.actualType != name.assumedType) {
|
|
||||||
throw ArsdException!"usage mismatch"(name.assumedType, name.actualType, name.file, name.line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
writeln(e.toString());
|
|
||||||
succeeded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!succeeded)
|
|
||||||
writeln("db check failed.");
|
|
||||||
|
|
||||||
return succeeded;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
|
|
||||||
+/
|
|
||||||
public PooledConnection!(unsharedThis) get() shared {
|
|
||||||
auto toUse = (cast(unsharedThis) this).getNext();
|
|
||||||
|
|
||||||
if(toUse is null)
|
|
||||||
toUse = new DatabaseListItem(connectionFactory());
|
|
||||||
|
|
||||||
return PooledConnection!(unsharedThis)(toUse, cast(unsharedThis) this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DatabaseException : Exception {
|
class DatabaseException : Exception {
|
||||||
this(string msg, string file = __FILE__, size_t line = __LINE__) {
|
this(string msg, string file = __FILE__, size_t line = __LINE__) {
|
||||||
super(msg, file, line);
|
super(msg, file, line);
|
||||||
|
|
|
||||||
43
dom.d
43
dom.d
|
|
@ -1,5 +1,3 @@
|
||||||
// FIXME: i want css nesting via the new standard now.
|
|
||||||
|
|
||||||
// FIXME: xml namespace support???
|
// FIXME: xml namespace support???
|
||||||
// FIXME: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
|
// FIXME: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
|
||||||
// FIXME: parentElement is parentNode that skips DocumentFragment etc but will be hard to work in with my compatibility...
|
// FIXME: parentElement is parentNode that skips DocumentFragment etc but will be hard to work in with my compatibility...
|
||||||
|
|
@ -2697,47 +2695,6 @@ class Element : DomParent {
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
|
||||||
Makes an element from an interpolated sequence.
|
|
||||||
|
|
||||||
FIXME: add a type interpolator thing that can be replaced
|
|
||||||
FIXME: syntax check at compile time?
|
|
||||||
FIXME: allow a DocumentFragment in some cases
|
|
||||||
+/
|
|
||||||
static Element make(Args...)(arsd.core.InterpolationHeader head, Args args, arsd.core.InterpolationFooter foot) {
|
|
||||||
string html;
|
|
||||||
|
|
||||||
import arsd.core;
|
|
||||||
foreach(arg; args) {
|
|
||||||
static if(is(typeof(arg) == InterpolationHeader))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == InterpolationFooter))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == InterpolatedLiteral!h, string h))
|
|
||||||
html ~= h;
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == InterpolatedExpression!code, string code))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) : iraw))
|
|
||||||
html ~= arg.s;
|
|
||||||
else
|
|
||||||
// FIXME: what if we are inside a <script> ? or an attribute etc
|
|
||||||
static if(is(typeof(arg) : Html))
|
|
||||||
html ~= arg.source;
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) : Element))
|
|
||||||
html ~= arg.toString();
|
|
||||||
else
|
|
||||||
html ~= htmlEntitiesEncode(toStringInternal(arg));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto root = Element.make("root");
|
|
||||||
root.innerHTML(html, true /* strict mode */);
|
|
||||||
return root.querySelector(" > *");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generally, you don't want to call this yourself - use Element.make or document.createElement instead.
|
/// Generally, you don't want to call this yourself - use Element.make or document.createElement instead.
|
||||||
this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) {
|
this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) {
|
||||||
|
|
|
||||||
28
dub.json
28
dub.json
|
|
@ -11,30 +11,10 @@
|
||||||
"description": "Shared components across other arsd modules",
|
"description": "Shared components across other arsd modules",
|
||||||
"targetType": "library",
|
"targetType": "library",
|
||||||
"libs-windows": ["user32", "ws2_32"],
|
"libs-windows": ["user32", "ws2_32"],
|
||||||
"dflags-dmd": [
|
"dflags-dmd": ["-mv=arsd.core=$PACKAGE_DIR/core.d"],
|
||||||
"-mv=arsd.core=$PACKAGE_DIR/core.d",
|
"dflags-ldc": ["--mv=arsd.core=$PACKAGE_DIR/core.d"],
|
||||||
"-mv=arsd.string=$PACKAGE_DIR/string.d",
|
"dflags-gdc": ["-fmodule-file=arsd.core=$PACKAGE_DIR/core.d"],
|
||||||
"-mv=arsd.uri=$PACKAGE_DIR/uri.d",
|
"sourceFiles": ["core.d"]
|
||||||
"-mv=arsd.conv=$PACKAGE_DIR/conv.d"
|
|
||||||
],
|
|
||||||
"dflags-ldc": [
|
|
||||||
"--mv=arsd.core=$PACKAGE_DIR/core.d",
|
|
||||||
"--mv=arsd.string=$PACKAGE_DIR/string.d",
|
|
||||||
"--mv=arsd.uri=$PACKAGE_DIR/uri.d",
|
|
||||||
"--mv=arsd.conv=$PACKAGE_DIR/conv.d"
|
|
||||||
],
|
|
||||||
"dflags-gdc": [
|
|
||||||
"-fmodule-file=arsd.core=$PACKAGE_DIR/core.d",
|
|
||||||
"-fmodule-file=arsd.string=$PACKAGE_DIR/string.d",
|
|
||||||
"-fmodule-file=arsd.uri=$PACKAGE_DIR/uri.d",
|
|
||||||
"-fmodule-file=arsd.conv=$PACKAGE_DIR/conv.d"
|
|
||||||
],
|
|
||||||
"sourceFiles": [
|
|
||||||
"core.d",
|
|
||||||
"string.d",
|
|
||||||
"uri.d",
|
|
||||||
"conv.d"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "simpledisplay",
|
"name": "simpledisplay",
|
||||||
|
|
|
||||||
14
minigui.d
14
minigui.d
|
|
@ -17879,26 +17879,15 @@ class FilePicker : Dialog {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
enum specialZoneSize = 1;
|
enum specialZoneSize = 1;
|
||||||
string originalString = whole;
|
|
||||||
bool fallback;
|
|
||||||
|
|
||||||
start_over:
|
|
||||||
|
|
||||||
char current = whole[0];
|
char current = whole[0];
|
||||||
if(!fallback && current >= '0' && current <= '9') {
|
if(current >= '0' && current <= '9') {
|
||||||
// if this overflows, it can mess up the sort, so it will fallback to string sort in that event
|
|
||||||
int accumulator;
|
int accumulator;
|
||||||
do {
|
do {
|
||||||
auto before = accumulator;
|
|
||||||
whole = whole[1 .. $];
|
whole = whole[1 .. $];
|
||||||
accumulator *= 10;
|
accumulator *= 10;
|
||||||
accumulator += current - '0';
|
accumulator += current - '0';
|
||||||
current = whole.length ? whole[0] : 0;
|
current = whole.length ? whole[0] : 0;
|
||||||
if(accumulator < before) {
|
|
||||||
fallback = true;
|
|
||||||
whole = originalString;
|
|
||||||
goto start_over;
|
|
||||||
}
|
|
||||||
} while (current >= '0' && current <= '9');
|
} while (current >= '0' && current <= '9');
|
||||||
|
|
||||||
return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols
|
return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols
|
||||||
|
|
@ -18288,7 +18277,6 @@ private FileType getFileType(string name) {
|
||||||
auto ret = stat((name ~ '\0').ptr, &buf);
|
auto ret = stat((name ~ '\0').ptr, &buf);
|
||||||
if(ret == -1)
|
if(ret == -1)
|
||||||
return FileType.error;
|
return FileType.error;
|
||||||
// FIXME: what about a symlink to a dir? S_IFLNK then readlink then i believe stat it again.
|
|
||||||
return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
|
return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
|
||||||
} else assert(0, "Not implemented");
|
} else assert(0, "Not implemented");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
4
mssql.d
4
mssql.d
|
|
@ -88,10 +88,6 @@ class MsSql : Database {
|
||||||
return null; // FIXME
|
return null; // FIXME
|
||||||
}
|
}
|
||||||
|
|
||||||
override bool isAlive() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SQLHENV env;
|
SQLHENV env;
|
||||||
SQLHDBC conn;
|
SQLHDBC conn;
|
||||||
|
|
|
||||||
3
mysql.d
3
mysql.d
|
|
@ -219,9 +219,6 @@ class MySql : Database {
|
||||||
query("START TRANSACTION");
|
query("START TRANSACTION");
|
||||||
}
|
}
|
||||||
|
|
||||||
override bool isAlive() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
string sysTimeToValue(SysTime s) {
|
string sysTimeToValue(SysTime s) {
|
||||||
return "cast('" ~ escape(s.toISOExtString()) ~ "' as datetime)";
|
return "cast('" ~ escape(s.toISOExtString()) ~ "' as datetime)";
|
||||||
|
|
|
||||||
3
oauth.d
3
oauth.d
|
|
@ -2,8 +2,7 @@
|
||||||
module arsd.oauth;
|
module arsd.oauth;
|
||||||
|
|
||||||
import arsd.curl;
|
import arsd.curl;
|
||||||
import arsd.uri;
|
import arsd.cgi; // for decodeVariables
|
||||||
import arsd.cgi : Cgi;
|
|
||||||
import std.array;
|
import std.array;
|
||||||
static import std.uri;
|
static import std.uri;
|
||||||
static import std.algorithm;
|
static import std.algorithm;
|
||||||
|
|
|
||||||
106
postgres.d
106
postgres.d
|
|
@ -60,12 +60,9 @@ class PostgreSql : Database {
|
||||||
conn = PQconnectdb(toStringz(connectionString));
|
conn = PQconnectdb(toStringz(connectionString));
|
||||||
if(conn is null)
|
if(conn is null)
|
||||||
throw new DatabaseException("Unable to allocate PG connection object");
|
throw new DatabaseException("Unable to allocate PG connection object");
|
||||||
if(PQstatus(conn) != CONNECTION_OK) {
|
if(PQstatus(conn) != CONNECTION_OK)
|
||||||
this.connectionOk = false;
|
|
||||||
throw new DatabaseException(error());
|
throw new DatabaseException(error());
|
||||||
}
|
|
||||||
query("SET NAMES 'utf8'"); // D does everything with utf8
|
query("SET NAMES 'utf8'"); // D does everything with utf8
|
||||||
this.connectionOk = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string connectionString;
|
string connectionString;
|
||||||
|
|
@ -78,11 +75,6 @@ class PostgreSql : Database {
|
||||||
return "'" ~ escape(s.toISOExtString()) ~ "'::timestamptz";
|
return "'" ~ escape(s.toISOExtString()) ~ "'::timestamptz";
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool connectionOk;
|
|
||||||
override bool isAlive() {
|
|
||||||
return connectionOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Prepared statement support
|
Prepared statement support
|
||||||
|
|
||||||
|
|
@ -140,10 +132,8 @@ class PostgreSql : Database {
|
||||||
conn = PQconnectdb(toStringz(connectionString));
|
conn = PQconnectdb(toStringz(connectionString));
|
||||||
if(conn is null)
|
if(conn is null)
|
||||||
throw new DatabaseException("Unable to allocate PG connection object");
|
throw new DatabaseException("Unable to allocate PG connection object");
|
||||||
if(PQstatus(conn) != CONNECTION_OK) {
|
if(PQstatus(conn) != CONNECTION_OK)
|
||||||
this.connectionOk = false;
|
|
||||||
throw new DatabaseException(error());
|
throw new DatabaseException(error());
|
||||||
}
|
|
||||||
goto retry;
|
goto retry;
|
||||||
}
|
}
|
||||||
throw new DatabaseException(error());
|
throw new DatabaseException(error());
|
||||||
|
|
@ -189,82 +179,6 @@ class PostgreSql : Database {
|
||||||
PGconn* conn;
|
PGconn* conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
/+
|
|
||||||
# when it changes from lowercase to upper case, call that a new word. or when it goes to/from anything else and underscore or dashes.
|
|
||||||
+/
|
|
||||||
|
|
||||||
struct PreparedStatementDescription {
|
|
||||||
PreparedStatementResult[] result;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PreparedStatementResult {
|
|
||||||
string fieldName;
|
|
||||||
DatabaseDatum type;
|
|
||||||
}
|
|
||||||
|
|
||||||
PreparedStatementDescription describePrepared(PostgreSql db, string name) {
|
|
||||||
auto res = PQdescribePrepared(db.conn, name.toStringz);
|
|
||||||
|
|
||||||
PreparedStatementResult[] ret;
|
|
||||||
|
|
||||||
// PQnparams PQparamtype for params
|
|
||||||
auto numFields = PQnfields(res);
|
|
||||||
foreach(num; 0 .. numFields) {
|
|
||||||
auto typeId = PQftype(res, num);
|
|
||||||
DatabaseDatum dd;
|
|
||||||
dd.platformSpecificTag = typeId;
|
|
||||||
dd.storage = sampleForOid(typeId);
|
|
||||||
ret ~= PreparedStatementResult(
|
|
||||||
copyCString(PQfname(res, num)),
|
|
||||||
dd,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PQclear(res);
|
|
||||||
|
|
||||||
return PreparedStatementDescription(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
import arsd.core : LimitedVariant, PackedDateTime, SimplifiedUtcTimestamp;
|
|
||||||
LimitedVariant sampleForOid(int platformSpecificTag) {
|
|
||||||
switch(platformSpecificTag) {
|
|
||||||
case BOOLOID:
|
|
||||||
return LimitedVariant(false);
|
|
||||||
case BYTEAOID:
|
|
||||||
return LimitedVariant(cast(const(ubyte)[]) null);
|
|
||||||
case TEXTOID:
|
|
||||||
case VARCHAROID:
|
|
||||||
return LimitedVariant("");
|
|
||||||
case INT4OID:
|
|
||||||
return LimitedVariant(0);
|
|
||||||
case INT8OID:
|
|
||||||
return LimitedVariant(0L);
|
|
||||||
case FLOAT4OID:
|
|
||||||
return LimitedVariant(0.0f);
|
|
||||||
case FLOAT8OID:
|
|
||||||
return LimitedVariant(0.0);
|
|
||||||
case TIMESTAMPOID:
|
|
||||||
case TIMESTAMPTZOID:
|
|
||||||
return LimitedVariant(SimplifiedUtcTimestamp(0));
|
|
||||||
case DATEOID:
|
|
||||||
PackedDateTime d;
|
|
||||||
d.hasDate = true;
|
|
||||||
return LimitedVariant(d); // might want a different type so contains shows the thing without checking hasDate and hasTime
|
|
||||||
case TIMETZOID: // possibly wrong... the tz isn't in my packed thing
|
|
||||||
case TIMEOID:
|
|
||||||
PackedDateTime d;
|
|
||||||
d.hasTime = true;
|
|
||||||
return LimitedVariant(d);
|
|
||||||
case INTERVALOID:
|
|
||||||
// months, days, and microseconds
|
|
||||||
|
|
||||||
case NUMERICOID: // aka decimal
|
|
||||||
default:
|
|
||||||
// when in doubt, assume it is just a string
|
|
||||||
return LimitedVariant("sample");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string toLowerFast(string s) {
|
private string toLowerFast(string s) {
|
||||||
import std.ascii : isUpper;
|
import std.ascii : isUpper;
|
||||||
foreach (c; s)
|
foreach (c; s)
|
||||||
|
|
@ -460,22 +374,7 @@ extern(C) {
|
||||||
int PQfformat(const PGresult *res, int column_number);
|
int PQfformat(const PGresult *res, int column_number);
|
||||||
|
|
||||||
alias Oid = int;
|
alias Oid = int;
|
||||||
enum BOOLOID = 16;
|
|
||||||
enum BYTEAOID = 17;
|
enum BYTEAOID = 17;
|
||||||
enum TEXTOID = 25;
|
|
||||||
enum INT4OID = 23; // integer
|
|
||||||
enum INT8OID = 20; // bigint
|
|
||||||
enum NUMERICOID = 1700;
|
|
||||||
enum FLOAT4OID = 700;
|
|
||||||
enum FLOAT8OID = 701;
|
|
||||||
enum VARCHAROID = 1043;
|
|
||||||
enum DATEOID = 1082;
|
|
||||||
enum TIMEOID = 1083;
|
|
||||||
enum TIMESTAMPOID = 1114;
|
|
||||||
enum TIMESTAMPTZOID = 1184;
|
|
||||||
enum INTERVALOID = 1186;
|
|
||||||
enum TIMETZOID = 1266;
|
|
||||||
|
|
||||||
Oid PQftype(const PGresult* res, int column_number);
|
Oid PQftype(const PGresult* res, int column_number);
|
||||||
|
|
||||||
char *PQescapeByteaConn(PGconn *conn,
|
char *PQescapeByteaConn(PGconn *conn,
|
||||||
|
|
@ -487,7 +386,6 @@ extern(C) {
|
||||||
|
|
||||||
char* PQcmdTuples(PGresult *res);
|
char* PQcmdTuples(PGresult *res);
|
||||||
|
|
||||||
PGresult *PQdescribePrepared(PGconn *conn, const char *stmtName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
839
shell.d
839
shell.d
|
|
@ -1,839 +0,0 @@
|
||||||
/++
|
|
||||||
Support functions to build a custom unix-style shell.
|
|
||||||
|
|
||||||
|
|
||||||
$(PITFALL
|
|
||||||
Do NOT use this to try to sanitize, escape, or otherwise parse what another shell would do with a string! Every shell is different and this implements my rules which may differ in subtle ways from any other common shell.
|
|
||||||
|
|
||||||
If you want to use this to understand a command, also use it to execute that command so you get what you expect.
|
|
||||||
)
|
|
||||||
|
|
||||||
History:
|
|
||||||
Added October 18, 2025
|
|
||||||
|
|
||||||
Bugs:
|
|
||||||
$(LIST
|
|
||||||
* < and > redirections are not implemented at all
|
|
||||||
* >> not implemented
|
|
||||||
* | on Windows is not implemented
|
|
||||||
* glob expansion is minimal - * works, but no ?, no {item,other}, no {start..end}
|
|
||||||
* ~ expansion is not implemented
|
|
||||||
* `substitution` and $(...) is not implemented
|
|
||||||
* variable expansion is not implemented. can do $IDENT and ${IDENT} i think
|
|
||||||
* built-ins don't exist - `set`, want `for` and then like `export` and a way to hook in basic utilities polyfills especially on Windows (ls, rm, grep, etc)
|
|
||||||
* built-ins should have a pipe they can read/write to and return an int. integrate with arsd.cli?
|
|
||||||
* no !history recall. or history command in general
|
|
||||||
* job control is rudimentary - no fg, bg, jobs, &, ctrl+z, etc.
|
|
||||||
* set -o ignoreeof
|
|
||||||
* the path search is hardcoded
|
|
||||||
* prompt could be cooler
|
|
||||||
PS1 = normal prompt
|
|
||||||
PS2 = continuation prompt
|
|
||||||
Bash shell executes the content of the PROMPT_COMMAND just before displaying the PS1 variable.
|
|
||||||
|
|
||||||
bash does it with `\u` and stuff but i kinda thiink using `$USER` and such might make more sense.
|
|
||||||
* it prints command return values when you might not want that
|
|
||||||
* LS_COLORS env var is not set
|
|
||||||
* && and || is not implemented
|
|
||||||
* the api is not very good
|
|
||||||
* ulimit? sourcing things too. aliases.
|
|
||||||
* see my bash rc for other little things. maybe i want a deeshrc
|
|
||||||
* permission denied when hitting tab on Windows
|
|
||||||
* tab complete of available commands not implemented - get it from path search.
|
|
||||||
* some vars dynamic like $_ being the most recent command, $? being its return value, etc
|
|
||||||
)
|
|
||||||
|
|
||||||
Questionable_ideas:
|
|
||||||
$(LIST
|
|
||||||
* separate stdout and stderr more by default, allow stderr pipes.
|
|
||||||
* custom completion scripts? prolly not bash compatible since the scripts would be more involved
|
|
||||||
* some kind of scriptable cmdlet? a full on script language with shell stuff embeddable?
|
|
||||||
see https://hush-shell.github.io/cmd/index.html for some ok ideas
|
|
||||||
* do something fun with job control. idk what tho really.
|
|
||||||
* can terminal emulators get notifications when the foreground process group changes? i don't think so but i could make a "poll again now" sequence since i control shell and possibly terminal emulator now.
|
|
||||||
* change DISPLAY and such when attaching remote sessions
|
|
||||||
)
|
|
||||||
+/
|
|
||||||
module arsd.shell;
|
|
||||||
|
|
||||||
import arsd.core;
|
|
||||||
|
|
||||||
/++
|
|
||||||
Holds some context needed for shell expansions.
|
|
||||||
+/
|
|
||||||
struct ShellContext {
|
|
||||||
string[string] vars;
|
|
||||||
string cwd;
|
|
||||||
string[string] userHomes;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum QuoteStyle {
|
|
||||||
none,
|
|
||||||
nonExpanding, // 'thing'
|
|
||||||
expanding, // "thing"
|
|
||||||
subcommand, // `thing`
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
|
|
||||||
+/
|
|
||||||
alias Globber = string[] delegate(string str, ShellContext context);
|
|
||||||
|
|
||||||
/++
|
|
||||||
Represents one component of a shell command line as a precursor to parsing.
|
|
||||||
+/
|
|
||||||
struct ShellLexeme {
|
|
||||||
string l;
|
|
||||||
QuoteStyle quoteStyle;
|
|
||||||
|
|
||||||
string toEscapedFormat() {
|
|
||||||
if(quoteStyle == QuoteStyle.nonExpanding) {
|
|
||||||
string ret;
|
|
||||||
ret.reserve(l.length);
|
|
||||||
foreach(ch; l) {
|
|
||||||
if(ch == '*')
|
|
||||||
ret ~= "\\*";
|
|
||||||
else
|
|
||||||
ret ~= ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
} else {
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/+
|
|
||||||
/++
|
|
||||||
The second thing should be have toSingleArg called on it
|
|
||||||
+/
|
|
||||||
EnvironmentPair toEnvironmentPair(ShellLexeme context) {
|
|
||||||
assert(quoteStyle == QuoteStyle.none);
|
|
||||||
|
|
||||||
size_t splitPoint = l.length;
|
|
||||||
foreach(size_t idx, char ch; l) {
|
|
||||||
if(ch == '=') {
|
|
||||||
splitPoint = idx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(splitPoint != l.length) {
|
|
||||||
return EnvironmentPair(l[0 .. splitPoint], ShellLexeme(l[splitPoint + 1 .. $]));
|
|
||||||
} else {
|
|
||||||
return EnvironmentPair(null, ShellLexeme.init);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
Expands variables but not globs while replacing quotes and such. Note it is NOT safe to pass an expanded single arg to another shell
|
|
||||||
+/
|
|
||||||
string toExpandedSingleArg(ShellContext context) {
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
Returns the value as an argv array, after shell expansion of variables, tildes, and globs
|
|
||||||
|
|
||||||
Does NOT attempt to execute `subcommands`.
|
|
||||||
+/
|
|
||||||
string[] toExpandedArgs(ShellContext context, Globber globber) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
+/
|
|
||||||
|
|
||||||
/++
|
|
||||||
This function in pure in all but formal annotation; it does not interact with the outside world.
|
|
||||||
+/
|
|
||||||
ShellLexeme[] lexShellCommandLine(string commandLine) {
|
|
||||||
ShellLexeme[] ret;
|
|
||||||
|
|
||||||
enum State {
|
|
||||||
consumingWhitespace,
|
|
||||||
readingWord,
|
|
||||||
readingSingleQuoted,
|
|
||||||
readingEscaped,
|
|
||||||
readingExpandingContextEscaped,
|
|
||||||
readingDoubleQuoted,
|
|
||||||
// FIXME: readingSubcommand for `thing`
|
|
||||||
readingComment,
|
|
||||||
}
|
|
||||||
|
|
||||||
State state = State.consumingWhitespace;
|
|
||||||
size_t first = commandLine.length;
|
|
||||||
|
|
||||||
void endWord() {
|
|
||||||
state = State.consumingWhitespace;
|
|
||||||
first = commandLine.length; // we'll rewind upon encountering the next word, if there is one
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach(size_t idx, char ch; commandLine) {
|
|
||||||
again:
|
|
||||||
final switch(state) {
|
|
||||||
case State.consumingWhitespace:
|
|
||||||
switch(ch) {
|
|
||||||
case ' ':
|
|
||||||
// the arg separators should all be collapsed to exactly one
|
|
||||||
if(ret.length && !(ret[$-1].quoteStyle == QuoteStyle.none && ret[$-1].l == " "))
|
|
||||||
ret ~= ShellLexeme(" ");
|
|
||||||
continue;
|
|
||||||
case '#':
|
|
||||||
state = State.readingComment;
|
|
||||||
continue;
|
|
||||||
default:
|
|
||||||
first = idx;
|
|
||||||
state = State.readingWord;
|
|
||||||
goto again;
|
|
||||||
}
|
|
||||||
case State.readingWord:
|
|
||||||
switch(ch) {
|
|
||||||
case '\'':
|
|
||||||
if(first != idx)
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
|
||||||
first = idx + 1;
|
|
||||||
state = State.readingSingleQuoted;
|
|
||||||
break;
|
|
||||||
case '\\':
|
|
||||||
// a \ch can be treated as just a single quoted single char...
|
|
||||||
if(first != idx)
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
|
||||||
first = idx + 1;
|
|
||||||
state = State.readingEscaped;
|
|
||||||
break;
|
|
||||||
case '"':
|
|
||||||
if(first != idx)
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
|
||||||
first = idx + 1;
|
|
||||||
state = State.readingDoubleQuoted;
|
|
||||||
break;
|
|
||||||
case ' ':
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
|
||||||
ret ~= ShellLexeme(" "); // an argument separator
|
|
||||||
endWord();
|
|
||||||
continue;
|
|
||||||
case '|', '<', '>', '&':
|
|
||||||
if(first != idx)
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
|
||||||
ret ~= ShellLexeme(commandLine[idx .. idx + 1]); // shell special symbol
|
|
||||||
endWord();
|
|
||||||
continue;
|
|
||||||
default:
|
|
||||||
// keep searching
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case State.readingComment:
|
|
||||||
if(ch == '\n') {
|
|
||||||
endWord();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case State.readingSingleQuoted:
|
|
||||||
switch(ch) {
|
|
||||||
case '\'':
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
|
|
||||||
endWord();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case State.readingDoubleQuoted:
|
|
||||||
switch(ch) {
|
|
||||||
case '"':
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.expanding);
|
|
||||||
endWord();
|
|
||||||
break;
|
|
||||||
case '\\':
|
|
||||||
state = State.readingExpandingContextEscaped;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case State.readingEscaped:
|
|
||||||
if(ch >= 0x80 && ch <= 0xBF) {
|
|
||||||
// continuation byte
|
|
||||||
continue;
|
|
||||||
} else if(first == idx) {
|
|
||||||
// first byte, keep searching for continuations
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
// same as if the user wrote the escaped character in single quotes
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
|
|
||||||
|
|
||||||
if(state == State.readingExpandingContextEscaped) {
|
|
||||||
state = State.readingDoubleQuoted;
|
|
||||||
first = idx;
|
|
||||||
} else {
|
|
||||||
endWord();
|
|
||||||
}
|
|
||||||
goto again;
|
|
||||||
}
|
|
||||||
case State.readingExpandingContextEscaped:
|
|
||||||
if(ch == '"') {
|
|
||||||
// the -1 trims out the \
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. idx - 1], QuoteStyle.expanding);
|
|
||||||
state = State.readingDoubleQuoted;
|
|
||||||
first = idx; // we need to INCLUDE the " itself
|
|
||||||
} else {
|
|
||||||
// this was actually nothing special, the backslash is kept in the double quotes
|
|
||||||
state = State.readingDoubleQuoted;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(first != commandLine.length) {
|
|
||||||
if(state != State.readingWord && state != State.readingComment)
|
|
||||||
throw new Exception("ran out of data in inappropriate state");
|
|
||||||
ret ~= ShellLexeme(commandLine[first .. $]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
ShellLexeme[] got;
|
|
||||||
|
|
||||||
got = lexShellCommandLine("FOO=bar");
|
|
||||||
assert(got.length == 1);
|
|
||||||
assert(got[0].l == "FOO=bar");
|
|
||||||
|
|
||||||
// comments can only happen at whitespace contexts, not at the end of a single word
|
|
||||||
got = lexShellCommandLine("FOO=bar#commentspam");
|
|
||||||
assert(got.length == 1);
|
|
||||||
assert(got[0].l == "FOO=bar#commentspam");
|
|
||||||
|
|
||||||
got = lexShellCommandLine("FOO=bar #commentspam");
|
|
||||||
assert(got.length == 2);
|
|
||||||
assert(got[0].l == "FOO=bar");
|
|
||||||
assert(got[1].l == " "); // arg separator still there even tho there is no arg cuz of the comment, but that's semantic
|
|
||||||
|
|
||||||
got = lexShellCommandLine("#commentspam");
|
|
||||||
assert(got.length == 0, got[0].l);
|
|
||||||
|
|
||||||
got = lexShellCommandLine("FOO=bar ./prog");
|
|
||||||
assert(got.length == 3);
|
|
||||||
assert(got[0].l == "FOO=bar");
|
|
||||||
assert(got[1].l == " "); // argument separator
|
|
||||||
assert(got[2].l == "./prog");
|
|
||||||
|
|
||||||
// all whitespace should be collapsed to a single argument separator
|
|
||||||
got = lexShellCommandLine("FOO=bar ./prog");
|
|
||||||
assert(got.length == 3);
|
|
||||||
assert(got[0].l == "FOO=bar");
|
|
||||||
assert(got[1].l == " "); // argument separator
|
|
||||||
assert(got[2].l == "./prog");
|
|
||||||
|
|
||||||
got = lexShellCommandLine("'foo'bar");
|
|
||||||
assert(got.length == 2);
|
|
||||||
assert(got[0].l == "foo");
|
|
||||||
assert(got[0].quoteStyle == QuoteStyle.nonExpanding);
|
|
||||||
assert(got[1].l == "bar");
|
|
||||||
assert(got[1].quoteStyle == QuoteStyle.none);
|
|
||||||
|
|
||||||
// escaped single char works as if you wrote it in single quotes
|
|
||||||
got = lexShellCommandLine("test\\'bar");
|
|
||||||
assert(got.length == 3);
|
|
||||||
assert(got[0].l == "test");
|
|
||||||
assert(got[1].l == "'");
|
|
||||||
assert(got[2].l == "bar");
|
|
||||||
|
|
||||||
// checking for utf-8 decode of escaped char
|
|
||||||
got = lexShellCommandLine("test\\\»bar");
|
|
||||||
assert(got.length == 3);
|
|
||||||
assert(got[0].l == "test");
|
|
||||||
assert(got[1].l == "\»");
|
|
||||||
assert(got[2].l == "bar");
|
|
||||||
|
|
||||||
got = lexShellCommandLine(`"ok"`);
|
|
||||||
assert(got.length == 1);
|
|
||||||
assert(got[0].l == "ok");
|
|
||||||
assert(got[0].quoteStyle == QuoteStyle.expanding);
|
|
||||||
|
|
||||||
got = lexShellCommandLine(`"ok\"after"`);
|
|
||||||
assert(got.length == 2);
|
|
||||||
assert(got[0].l == "ok");
|
|
||||||
assert(got[0].quoteStyle == QuoteStyle.expanding);
|
|
||||||
assert(got[1].l == "\"after");
|
|
||||||
assert(got[1].quoteStyle == QuoteStyle.expanding);
|
|
||||||
|
|
||||||
got = lexShellCommandLine(`FOO=bar ./thing 'my ard' second_arg "quoted\"thing"`);
|
|
||||||
assert(got.length == 10); // because quoted\"thing is two in this weird system
|
|
||||||
assert(got[0].l == "FOO=bar");
|
|
||||||
assert(got[1].l == " ");
|
|
||||||
assert(got[2].l == "./thing");
|
|
||||||
assert(got[3].l == " ");
|
|
||||||
assert(got[4].l == "my ard");
|
|
||||||
assert(got[5].l == " ");
|
|
||||||
assert(got[6].l == "second_arg");
|
|
||||||
assert(got[7].l == " ");
|
|
||||||
assert(got[8].l == "quoted");
|
|
||||||
assert(got[9].l == "\"thing");
|
|
||||||
|
|
||||||
got = lexShellCommandLine("a | b c");
|
|
||||||
assert(got.length == 7);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ShellIo {
|
|
||||||
enum Kind {
|
|
||||||
inherit,
|
|
||||||
fd,
|
|
||||||
filename,
|
|
||||||
pipedCommand,
|
|
||||||
memoryBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
Kind kind;
|
|
||||||
int fd;
|
|
||||||
string filename;
|
|
||||||
ShellCommand pipedCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShellCommand {
|
|
||||||
ShellIo stdin;
|
|
||||||
ShellIo stdout;
|
|
||||||
ShellIo stderr;
|
|
||||||
// yes i know in unix you can do other fds too. do i care?
|
|
||||||
|
|
||||||
private ExternalProcess externalProcess;
|
|
||||||
|
|
||||||
string exePath;
|
|
||||||
string cwd;
|
|
||||||
string[] argv;
|
|
||||||
EnvironmentPair[] environmentPairs;
|
|
||||||
|
|
||||||
/++
|
|
||||||
The return value may be null! Some things can be executed without external processes.
|
|
||||||
|
|
||||||
This function is absolutely NOT pure. It may modify your shell context, run external processes, and generally carry out operations outside the shell.
|
|
||||||
+/
|
|
||||||
ExternalProcess execute(ref ShellContext context) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
A shell component - which is likely an argument, but that is a semantic distinction we can't make until parsing - may be made up of several lexemes. Think `foo'bar'`. This will extract them from the given array up to and including the next unquoted space or newline char.
|
|
||||||
+/
|
|
||||||
ShellLexeme[] nextComponent(ref ShellLexeme[] lexemes) {
|
|
||||||
if(lexemes.length == 0)
|
|
||||||
return lexemes[$ .. $];
|
|
||||||
|
|
||||||
int pos;
|
|
||||||
while(
|
|
||||||
pos < lexemes.length &&
|
|
||||||
!(
|
|
||||||
// identify an arg or command separator
|
|
||||||
lexemes[pos].quoteStyle == QuoteStyle.none &&
|
|
||||||
(
|
|
||||||
lexemes[pos].l == " " ||
|
|
||||||
lexemes[pos].l == "\n"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
pos++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(pos == 0)
|
|
||||||
pos++; // include the termination condition as its own component
|
|
||||||
|
|
||||||
auto ret = lexemes[0 .. pos];
|
|
||||||
lexemes = lexemes[pos .. $];
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EnvironmentPair {
|
|
||||||
string environmentVariableName;
|
|
||||||
string assignedValue;
|
|
||||||
|
|
||||||
string toString() {
|
|
||||||
return environmentVariableName ~ "=" ~ assignedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string expandSingleArg(string escapedArg, ShellContext context) {
|
|
||||||
return escapedArg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
Parses a set of lexemes into set of command objects.
|
|
||||||
|
|
||||||
This function in pure in all but formal annotation; it does not interact with the outside world, except through the globber delegate you provide (which should not make any changes to the outside world!).
|
|
||||||
+/
|
|
||||||
ShellCommand[] parseShellCommand(ShellLexeme[] lexemes, ShellContext context, Globber globber) {
|
|
||||||
ShellCommand[] ret;
|
|
||||||
|
|
||||||
ShellCommand currentCommand;
|
|
||||||
ShellCommand firstCommand;
|
|
||||||
|
|
||||||
enum ParseState {
|
|
||||||
lookingForVarAssignment,
|
|
||||||
lookingForArg,
|
|
||||||
}
|
|
||||||
ParseState parseState = ParseState.lookingForVarAssignment;
|
|
||||||
|
|
||||||
while(lexemes.length) {
|
|
||||||
auto component = nextComponent(lexemes);
|
|
||||||
if(component.length) {
|
|
||||||
if(currentCommand is null)
|
|
||||||
currentCommand = new ShellCommand();
|
|
||||||
if(firstCommand is null)
|
|
||||||
firstCommand = currentCommand;
|
|
||||||
|
|
||||||
/+
|
|
||||||
Command syntax in bash is basically:
|
|
||||||
|
|
||||||
Zero or more `ENV=value` sets, separated by whitespace, followed by zero or more arg things.
|
|
||||||
OR
|
|
||||||
a shell builtin which does special things to the rest of the command, and may even require subsequent commands
|
|
||||||
|
|
||||||
Argv[0] can be a shell built in which reads the rest of argv separately. It may even require subsequent commands!
|
|
||||||
|
|
||||||
For some shell built in keywords, you should not actually do expansion:
|
|
||||||
$ for $i in one two; do ls $i; done
|
|
||||||
bash: `$i': not a valid identifier
|
|
||||||
|
|
||||||
So there must be some kind of intermediate representation of possible expansions.
|
|
||||||
|
|
||||||
|
|
||||||
BUT THIS IS MY SHELL I CAN DO WHAT I WANT!!!!!!!!!!!!
|
|
||||||
+/
|
|
||||||
|
|
||||||
bool thisWasEnvironmentPair = false;
|
|
||||||
EnvironmentPair environmentPair;
|
|
||||||
bool thisWasRedirection = false;
|
|
||||||
bool thisWasPipe = false;
|
|
||||||
string arg;
|
|
||||||
|
|
||||||
if(component.length == 0) {
|
|
||||||
// nothing left, should never happen
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if(component.length == 1) {
|
|
||||||
if(component[0].quoteStyle == QuoteStyle.none && component[0].l == " ") {
|
|
||||||
// just an arg separator
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach(lexeme; component) {
|
|
||||||
again:
|
|
||||||
final switch(parseState) {
|
|
||||||
case ParseState.lookingForVarAssignment:
|
|
||||||
if(thisWasEnvironmentPair) {
|
|
||||||
environmentPair.assignedValue ~= lexeme.toEscapedFormat();
|
|
||||||
} else {
|
|
||||||
// assume there is no var until we prove otherwise
|
|
||||||
parseState = ParseState.lookingForArg;
|
|
||||||
if(lexeme.quoteStyle == QuoteStyle.none) {
|
|
||||||
foreach(idx, ch; lexeme.l) {
|
|
||||||
if(ch == '=') {
|
|
||||||
// actually found one!
|
|
||||||
thisWasEnvironmentPair = true;
|
|
||||||
environmentPair.environmentVariableName = lexeme.l[0 .. idx];
|
|
||||||
environmentPair.assignedValue = lexeme.l[idx + 1 .. $];
|
|
||||||
parseState = ParseState.lookingForVarAssignment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(parseState == ParseState.lookingForArg)
|
|
||||||
goto case;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ParseState.lookingForArg:
|
|
||||||
if(lexeme.quoteStyle == QuoteStyle.none) {
|
|
||||||
if(lexeme.l == "<" || lexeme.l == ">")
|
|
||||||
thisWasRedirection = true;
|
|
||||||
if(lexeme.l == "|")
|
|
||||||
thisWasPipe = true;
|
|
||||||
}
|
|
||||||
arg ~= lexeme.toEscapedFormat();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(thisWasEnvironmentPair) {
|
|
||||||
environmentPair.assignedValue = expandSingleArg(environmentPair.assignedValue, context);
|
|
||||||
currentCommand.environmentPairs ~= environmentPair;
|
|
||||||
} else if(thisWasRedirection) {
|
|
||||||
// FIXME: read the fd off this arg
|
|
||||||
// FIXME: read the filename off the next arg, new parse state
|
|
||||||
assert(0);
|
|
||||||
} else if(thisWasPipe) {
|
|
||||||
// FIXME: read the fd? i kinda wanna support 2| and such
|
|
||||||
auto newCommand = new ShellCommand();
|
|
||||||
currentCommand.stdout.kind = ShellIo.Kind.pipedCommand;
|
|
||||||
currentCommand.stdout.pipedCommand = newCommand;
|
|
||||||
newCommand.stdin.kind = ShellIo.Kind.pipedCommand;
|
|
||||||
newCommand.stdin.pipedCommand = currentCommand;
|
|
||||||
|
|
||||||
currentCommand = newCommand;
|
|
||||||
} else {
|
|
||||||
currentCommand.argv ~= globber(arg, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(firstCommand)
|
|
||||||
ret ~= firstCommand;
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
string[] globber(string s, ShellContext context) {
|
|
||||||
return [s];
|
|
||||||
}
|
|
||||||
ShellContext context;
|
|
||||||
ShellCommand[] commands;
|
|
||||||
|
|
||||||
commands = parseShellCommand(lexShellCommandLine("foo bar"), context, &globber);
|
|
||||||
assert(commands.length == 1);
|
|
||||||
assert(commands[0].argv.length == 2);
|
|
||||||
assert(commands[0].argv[0] == "foo");
|
|
||||||
assert(commands[0].argv[1] == "bar");
|
|
||||||
|
|
||||||
commands = parseShellCommand(lexShellCommandLine("foo bar'baz'"), context, &globber);
|
|
||||||
assert(commands.length == 1);
|
|
||||||
assert(commands[0].argv.length == 2);
|
|
||||||
assert(commands[0].argv[0] == "foo");
|
|
||||||
assert(commands[0].argv[1] == "barbaz");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/+
|
|
||||||
interface OSInterface {
|
|
||||||
setEnv
|
|
||||||
getEnv
|
|
||||||
getAllEnv
|
|
||||||
|
|
||||||
runCommand
|
|
||||||
waitForCommand
|
|
||||||
}
|
|
||||||
+/
|
|
||||||
|
|
||||||
class Shell {
|
|
||||||
protected ShellContext context;
|
|
||||||
|
|
||||||
this() {
|
|
||||||
context.cwd = getCurrentWorkingDirectory().toString;
|
|
||||||
prompt = "[deesh]" ~ context.cwd ~ "$ ";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string prompt;
|
|
||||||
|
|
||||||
protected string[] glob(string s) {
|
|
||||||
string[] ret;
|
|
||||||
getFiles(context.cwd, (string name, bool isDirectory) {
|
|
||||||
if(name.length && name[0] == '.' && (s.length == 0 || s[0] != '.'))
|
|
||||||
return; // skip hidden unless specifically requested
|
|
||||||
if(name.matchesFilePattern(s))
|
|
||||||
ret ~= name;
|
|
||||||
});
|
|
||||||
if(ret.length == 0)
|
|
||||||
return [s];
|
|
||||||
else
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private final string[] globberForwarder(string s, ShellContext context) {
|
|
||||||
return glob(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
void dumpCommand(ShellCommand command) {
|
|
||||||
foreach(ep; command.environmentPairs)
|
|
||||||
writeln(ep.toString());
|
|
||||||
writeln(command.argv);
|
|
||||||
if(command.stdout.kind == ShellIo.Kind.pipedCommand) {
|
|
||||||
writeln(" | ");
|
|
||||||
dumpCommand(command.stdout.pipedCommand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FilePath searchPathForCommand(string arg0) {
|
|
||||||
if(arg0.indexOf("/") != -1)
|
|
||||||
return FilePath(arg0);
|
|
||||||
// could also be built-ins and cmdlets...
|
|
||||||
// and on Windows we should check .exe, .com, .bat, or ask the OS maybe
|
|
||||||
|
|
||||||
version(Posix) { // FIXME
|
|
||||||
immutable searchPaths = ["/usr/bin", "/bin", "/usr/local/bin", "/home/me/bin"]; // FIXME
|
|
||||||
foreach(path; searchPaths) {
|
|
||||||
auto t = FilePath(arg0).makeAbsolute(FilePath(path));
|
|
||||||
import core.sys.posix.sys.stat;
|
|
||||||
stat_t sbuf;
|
|
||||||
|
|
||||||
CharzBuffer buf = t.toString();
|
|
||||||
auto ret = stat(buf.ptr, &sbuf);
|
|
||||||
if(ret != -1)
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FilePath(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
version(Windows)
|
|
||||||
private ExternalProcess startCommand(ShellCommand command, int inheritedPipe, int pgid) {
|
|
||||||
string windowsCommandLine;
|
|
||||||
foreach(arg; command.argv) {
|
|
||||||
if(windowsCommandLine.length)
|
|
||||||
windowsCommandLine ~= " ";
|
|
||||||
windowsCommandLine ~= arg;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto fp = searchPathForCommand(command.argv[0]);
|
|
||||||
|
|
||||||
auto proc = new ExternalProcess(fp, windowsCommandLine);
|
|
||||||
command.externalProcess = proc;
|
|
||||||
proc.start;
|
|
||||||
return proc;
|
|
||||||
}
|
|
||||||
|
|
||||||
version(Posix)
|
|
||||||
private ExternalProcess startCommand(ShellCommand command, int inheritedPipe, int pgid) {
|
|
||||||
auto fp = searchPathForCommand(command.argv[0]);
|
|
||||||
if(fp.isNull()) {
|
|
||||||
throw new Exception("Command not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
import core.sys.posix.unistd;
|
|
||||||
|
|
||||||
int[2] pipes;
|
|
||||||
if(command.stdout.pipedCommand) {
|
|
||||||
auto ret = pipe(pipes);
|
|
||||||
|
|
||||||
setCloExec(pipes[0]);
|
|
||||||
setCloExec(pipes[1]);
|
|
||||||
|
|
||||||
import core.stdc.errno;
|
|
||||||
|
|
||||||
if(ret == -1)
|
|
||||||
throw new ErrnoApiException("stdin pipe", errno);
|
|
||||||
} else {
|
|
||||||
pipes[0] = inheritedPipe;
|
|
||||||
pipes[1] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto proc = new ExternalProcess(fp, command.argv);
|
|
||||||
if(command.stdout.pipedCommand) {
|
|
||||||
proc.beforeExec = () {
|
|
||||||
// reset ignored signals to default behavior
|
|
||||||
import core.sys.posix.signal;
|
|
||||||
signal (SIGINT, SIG_DFL);
|
|
||||||
signal (SIGQUIT, SIG_DFL);
|
|
||||||
signal (SIGTSTP, SIG_DFL);
|
|
||||||
signal (SIGTTIN, SIG_DFL);
|
|
||||||
signal (SIGTTOU, SIG_DFL);
|
|
||||||
signal (SIGCHLD, SIG_DFL);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
proc.pgid = pgid; // 0 here means to lead the group, all subsequent pipe programs should inherit the leader
|
|
||||||
// and inherit the standard handles
|
|
||||||
proc.overrideStdinFd = inheritedPipe;
|
|
||||||
proc.overrideStdoutFd = pipes[1];
|
|
||||||
proc.overrideStderrFd = 2;
|
|
||||||
command.externalProcess = proc;
|
|
||||||
proc.start;
|
|
||||||
|
|
||||||
if(command.stdout.pipedCommand) {
|
|
||||||
startCommand(command.stdout.pipedCommand, pipes[0], pgid ? pgid : proc.pid);
|
|
||||||
|
|
||||||
// we're done with them now
|
|
||||||
close(pipes[0]);
|
|
||||||
close(pipes[1]);
|
|
||||||
pipes[] = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return proc;
|
|
||||||
}
|
|
||||||
|
|
||||||
int waitForCommand(ShellCommand command) {
|
|
||||||
command.externalProcess.waitForCompletion();
|
|
||||||
writeln(command.externalProcess.status);
|
|
||||||
if(auto cmd = command.stdout.pipedCommand)
|
|
||||||
waitForCommand(cmd);
|
|
||||||
return command.externalProcess.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void execute(string commandLine) {
|
|
||||||
auto commands = parseShellCommand(lexShellCommandLine(commandLine), context, &globberForwarder);
|
|
||||||
foreach(command; commands)
|
|
||||||
try {
|
|
||||||
dumpCommand(command);
|
|
||||||
|
|
||||||
version(Posix) {
|
|
||||||
import core.sys.posix.unistd;
|
|
||||||
import core.sys.posix.signal;
|
|
||||||
|
|
||||||
auto proc = startCommand(command, 0, 0);
|
|
||||||
|
|
||||||
// put the child group in control of the tty
|
|
||||||
ErrnoEnforce!tcsetpgrp(1, proc.pid);
|
|
||||||
kill(-proc.pid, SIGCONT); // and if it beat us to the punch and is waiting, go ahead and wake it up (this is harmless if it is already running
|
|
||||||
waitForCommand(command);
|
|
||||||
// reassert control of the tty to the shell
|
|
||||||
ErrnoEnforce!tcsetpgrp(1, getpid());
|
|
||||||
}
|
|
||||||
|
|
||||||
version(Windows) {
|
|
||||||
auto proc = startCommand(command, 0, 0);
|
|
||||||
waitForCommand(command);
|
|
||||||
}
|
|
||||||
} catch(ArsdExceptionBase e) {
|
|
||||||
string more;
|
|
||||||
e.getAdditionalPrintableInformation((string name, in char[] value) {
|
|
||||||
more ~= ", ";
|
|
||||||
more ~= name ~ ": " ~ value;
|
|
||||||
});
|
|
||||||
writeln("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.message, more);
|
|
||||||
} catch(Exception e) {
|
|
||||||
writeln("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
Constructs an instance of [arsd.terminal.LineGetter] appropriate for use in a repl for this shell.
|
|
||||||
+/
|
|
||||||
auto constructLineGetter()() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/+
|
|
||||||
Parts of bash I like:
|
|
||||||
|
|
||||||
glob expansion
|
|
||||||
! command recall
|
|
||||||
redirection
|
|
||||||
f{1..3} expand to f1 f2 f3. can add ..incr btw
|
|
||||||
f{a,b,c} expand to fa fb fc
|
|
||||||
for i in *; do cmd; done
|
|
||||||
`command expansion`. also $( cmd ) is a thing
|
|
||||||
~ expansion
|
|
||||||
|
|
||||||
foo && bar
|
|
||||||
foo || bar
|
|
||||||
|
|
||||||
$(( maybe arithmetic but idk ))
|
|
||||||
|
|
||||||
ENV=whatever cmd.
|
|
||||||
$ENV ...?
|
|
||||||
|
|
||||||
tab complete!
|
|
||||||
|
|
||||||
PATH lookup. if requested.
|
|
||||||
|
|
||||||
Globbing could insert -- before if there's any - in there.
|
|
||||||
|
|
||||||
Or better yet all the commands must either start with ./
|
|
||||||
or be found internally. Internal can be expanded by definition
|
|
||||||
files that tell how to expand the real thing.
|
|
||||||
|
|
||||||
* flags
|
|
||||||
* arguments
|
|
||||||
* command line
|
|
||||||
* i/o
|
|
||||||
+/
|
|
||||||
|
|
@ -1164,19 +1164,17 @@ unittest {
|
||||||
|
|
||||||
import arsd.core;
|
import arsd.core;
|
||||||
|
|
||||||
version(ArsdNoCocoa) {
|
version(D_OpenD) {
|
||||||
} else {
|
|
||||||
version(D_OpenD) {
|
|
||||||
version(OSX)
|
version(OSX)
|
||||||
version=OSXCocoa;
|
version=OSXCocoa;
|
||||||
version(iOS)
|
version(iOS)
|
||||||
version=OSXCocoa;
|
version=OSXCocoa;
|
||||||
} else {
|
} else {
|
||||||
version(OSX) version(DigitalMars) version=OSXCocoa;
|
version(OSX) version(DigitalMars) version=OSXCocoa;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
version(Emscripten) {
|
version(Emscripten) {
|
||||||
version=allow_unimplemented_features;
|
version=allow_unimplemented_features;
|
||||||
version=without_opengl;
|
version=without_opengl;
|
||||||
|
|
@ -1418,7 +1416,7 @@ version(Emscripten) {
|
||||||
}
|
}
|
||||||
|
|
||||||
version(libnotify) {
|
version(libnotify) {
|
||||||
//pragma(lib, "dl");
|
pragma(lib, "dl");
|
||||||
import core.sys.posix.dlfcn;
|
import core.sys.posix.dlfcn;
|
||||||
|
|
||||||
void delegate()[int] libnotify_action_delegates;
|
void delegate()[int] libnotify_action_delegates;
|
||||||
|
|
@ -2912,7 +2910,6 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon {
|
||||||
else
|
else
|
||||||
XMapWindow(impl.display, impl.window);
|
XMapWindow(impl.display, impl.window);
|
||||||
} else version(OSXCocoa) {
|
} else version(OSXCocoa) {
|
||||||
if(impl.window)
|
|
||||||
impl.window.setIsVisible = !b;
|
impl.window.setIsVisible = !b;
|
||||||
if(!hidden)
|
if(!hidden)
|
||||||
impl.view.setNeedsDisplay(true);
|
impl.view.setNeedsDisplay(true);
|
||||||
|
|
@ -4417,10 +4414,6 @@ struct EventLoopImpl {
|
||||||
|
|
||||||
void delegate(int) signalHandler;
|
void delegate(int) signalHandler;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
version(Posix) {
|
|
||||||
static import unix = core.sys.posix.unistd;
|
|
||||||
}
|
|
||||||
|
|
||||||
version(X11) {
|
version(X11) {
|
||||||
int pulseFd = -1;
|
int pulseFd = -1;
|
||||||
|
|
@ -4630,7 +4623,7 @@ struct EventLoopImpl {
|
||||||
ref int customEventFDRead() { return SimpleWindow.customEventFDRead; }
|
ref int customEventFDRead() { return SimpleWindow.customEventFDRead; }
|
||||||
version(Posix)
|
version(Posix)
|
||||||
ref int customEventFDWrite() { return SimpleWindow.customEventFDWrite; }
|
ref int customEventFDWrite() { return SimpleWindow.customEventFDWrite; }
|
||||||
version(Posix)
|
version(linux)
|
||||||
ref int customSignalFD() { return SimpleWindow.customSignalFD; }
|
ref int customSignalFD() { return SimpleWindow.customSignalFD; }
|
||||||
version(Windows)
|
version(Windows)
|
||||||
ref auto customEventH() { return SimpleWindow.customEventH; }
|
ref auto customEventH() { return SimpleWindow.customEventH; }
|
||||||
|
|
@ -7362,7 +7355,7 @@ version(Windows) {
|
||||||
}
|
}
|
||||||
|
|
||||||
version (X11) {
|
version (X11) {
|
||||||
//pragma(lib, "dl"); // already done by the standard compiler build and specifying it again messes up zig cross compile
|
pragma(lib, "dl");
|
||||||
import core.sys.posix.dlfcn;
|
import core.sys.posix.dlfcn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -8423,8 +8416,6 @@ struct Pen {
|
||||||
/++
|
/++
|
||||||
Represents an in-memory image in the format that the GUI expects, but with its raw data available to your program.
|
Represents an in-memory image in the format that the GUI expects, but with its raw data available to your program.
|
||||||
|
|
||||||
You can load an image with an alpha channel, but you cannot draw that in the current implementation. If you want alpha blending when drawing, use [Sprite] instead.
|
|
||||||
|
|
||||||
|
|
||||||
On Windows, this means a device-independent bitmap. On X11, it is an XImage.
|
On Windows, this means a device-independent bitmap. On X11, it is an XImage.
|
||||||
|
|
||||||
|
|
@ -9961,14 +9952,7 @@ struct ScreenPainter {
|
||||||
impl.drawPixmap(s, upperLeft.x, upperLeft.y, imageUpperLeft.x, imageUpperLeft.y, sliceSize.width, sliceSize.height);
|
impl.drawPixmap(s, upperLeft.x, upperLeft.y, imageUpperLeft.x, imageUpperLeft.y, sliceSize.width, sliceSize.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
///
|
||||||
Draws an [Image] to the window.
|
|
||||||
|
|
||||||
$(WARNING
|
|
||||||
Even if the Image was loaded with `enableAlpha`, drawing may not work!
|
|
||||||
|
|
||||||
Use [Sprite.fromMemoryImage] and [drawPixmap] instead if you want alpha blending to work better.
|
|
||||||
+/
|
|
||||||
void drawImage(Point upperLeft, Image i, Point upperLeftOfImage = Point(0, 0), int w = 0, int h = 0) {
|
void drawImage(Point upperLeft, Image i, Point upperLeftOfImage = Point(0, 0), int w = 0, int h = 0) {
|
||||||
if(impl is null) return;
|
if(impl is null) return;
|
||||||
//if(isClipped(upperLeft, w, h)) return; // FIXME
|
//if(isClipped(upperLeft, w, h)) return; // FIXME
|
||||||
|
|
@ -9982,8 +9966,6 @@ struct ScreenPainter {
|
||||||
if(upperLeftOfImage.y < 0)
|
if(upperLeftOfImage.y < 0)
|
||||||
upperLeftOfImage.y = 0;
|
upperLeftOfImage.y = 0;
|
||||||
|
|
||||||
assert(i.enableAlpha == false, "Alpha blending is not implemented for Image drawing - use Sprite and drawPixmap instead");
|
|
||||||
|
|
||||||
impl.drawImage(upperLeft.x, upperLeft.y, i, upperLeftOfImage.x, upperLeftOfImage.y, w, h);
|
impl.drawImage(upperLeft.x, upperLeft.y, i, upperLeftOfImage.x, upperLeftOfImage.y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19471,7 +19453,7 @@ version(OSXCocoa) {
|
||||||
|
|
||||||
auto contentRect = NSRect(NSPoint(0, 0), NSSize(width, height));
|
auto contentRect = NSRect(NSPoint(0, 0), NSSize(width, height));
|
||||||
|
|
||||||
window = NSWindow.alloc.initWithContentRect(
|
auto window = NSWindow.alloc.initWithContentRect(
|
||||||
contentRect,
|
contentRect,
|
||||||
NSWindowStyleMask.resizable | NSWindowStyleMask.closable | NSWindowStyleMask.miniaturizable | NSWindowStyleMask.titled,
|
NSWindowStyleMask.resizable | NSWindowStyleMask.closable | NSWindowStyleMask.miniaturizable | NSWindowStyleMask.titled,
|
||||||
NSBackingStoreType.buffered,
|
NSBackingStoreType.buffered,
|
||||||
|
|
|
||||||
4
sqlite.d
4
sqlite.d
|
|
@ -124,10 +124,6 @@ class Sqlite : Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override bool isAlive() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
override void startTransaction() {
|
override void startTransaction() {
|
||||||
query("BEGIN TRANSACTION");
|
query("BEGIN TRANSACTION");
|
||||||
|
|
|
||||||
73
string.d
73
string.d
|
|
@ -53,76 +53,3 @@ deprecated("D calls this `stripRight` instead") alias trimRight = stripRight;
|
||||||
alias stringz = arsd.core.stringz;
|
alias stringz = arsd.core.stringz;
|
||||||
// CharzBuffer
|
// CharzBuffer
|
||||||
// WCharzBuffer
|
// WCharzBuffer
|
||||||
|
|
||||||
// ********* UTILITIES **************
|
|
||||||
|
|
||||||
string[] split(string s, string onWhat) {
|
|
||||||
assert(onWhat.length);
|
|
||||||
string[] ret;
|
|
||||||
more:
|
|
||||||
auto idx = s.indexOf(onWhat);
|
|
||||||
if(idx == -1) {
|
|
||||||
ret ~= s;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
ret ~= s[0 .. idx];
|
|
||||||
s = s[idx + onWhat.length .. $];
|
|
||||||
goto more;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
assert("foo.bar".split(".") == ["foo", "bar"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
ptrdiff_t lastIndexOf(string s, string what) {
|
|
||||||
assert(what.length);
|
|
||||||
if(s.length < what.length)
|
|
||||||
return -1;
|
|
||||||
ptrdiff_t checking = s.length - what.length;
|
|
||||||
while(checking >= 0) {
|
|
||||||
if(s[checking .. checking + what.length] == what)
|
|
||||||
return checking;
|
|
||||||
|
|
||||||
checking--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
assert("31234".lastIndexOf("3") == 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
string join(string[] str, string w) {
|
|
||||||
string ret;
|
|
||||||
foreach(i, s; str) {
|
|
||||||
if(i)
|
|
||||||
ret ~= w;
|
|
||||||
ret ~= s;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
assert(["a", "b"].join(" ") == "a b");
|
|
||||||
}
|
|
||||||
|
|
||||||
string replace(string str, string find, string repacement) {
|
|
||||||
assert(find.length);
|
|
||||||
|
|
||||||
string ret;
|
|
||||||
more:
|
|
||||||
auto idx = str.indexOf(find);
|
|
||||||
if(idx == -1) {
|
|
||||||
ret ~= str;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
ret ~= str[0 .. idx];
|
|
||||||
ret ~= repacement;
|
|
||||||
str = str[idx + find.length .. $];
|
|
||||||
goto more;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
assert("foobarfoo".replace("foo", "bar") == "barbarbar");
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9268,9 +9268,6 @@ version(TerminalDirectToEmulator) {
|
||||||
|
|
||||||
private class TerminalEmulatorInsideWidget : TerminalEmulator {
|
private class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||||
|
|
||||||
import arsd.core : EnableSynchronization;
|
|
||||||
mixin EnableSynchronization;
|
|
||||||
|
|
||||||
private ScrollbackBuffer sbb() { return scrollbackBuffer; }
|
private ScrollbackBuffer sbb() { return scrollbackBuffer; }
|
||||||
|
|
||||||
void resized(int w, int h) {
|
void resized(int w, int h) {
|
||||||
|
|
@ -9470,8 +9467,8 @@ version(TerminalDirectToEmulator) {
|
||||||
if(this.font.isNull) {
|
if(this.font.isNull) {
|
||||||
// carry on, it will try a default later
|
// carry on, it will try a default later
|
||||||
} else if(this.font.isMonospace) {
|
} else if(this.font.isMonospace) {
|
||||||
this.fontWidth = castFnumToCnum(font.averageWidth);
|
this.fontWidth = font.averageWidth;
|
||||||
this.fontHeight = castFnumToCnum(font.height);
|
this.fontHeight = font.height;
|
||||||
} else {
|
} else {
|
||||||
this.font.unload(); // can't really use a non-monospace font, so just going to unload it so the default font loads again
|
this.font.unload(); // can't really use a non-monospace font, so just going to unload it so the default font loads again
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,13 +111,6 @@ struct ScopeBuffer(T, size_t maxSize, bool allowGrowth = false) {
|
||||||
size_t length;
|
size_t length;
|
||||||
bool isNull = true;
|
bool isNull = true;
|
||||||
T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
|
T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
|
||||||
|
|
||||||
static if(is(T == char))
|
|
||||||
void appendIntAsString(int n) {
|
|
||||||
import std.conv;
|
|
||||||
this ~= to!string(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
void opOpAssign(string op : "~")(in T rhs) {
|
void opOpAssign(string op : "~")(in T rhs) {
|
||||||
if(buffer is null) buffer = bufferInternal[];
|
if(buffer is null) buffer = bufferInternal[];
|
||||||
isNull = false;
|
isNull = false;
|
||||||
|
|
@ -334,7 +327,6 @@ class TerminalEmulator {
|
||||||
if(termY >= screenHeight)
|
if(termY >= screenHeight)
|
||||||
termY = screenHeight - 1;
|
termY = screenHeight - 1;
|
||||||
|
|
||||||
/+
|
|
||||||
version(Windows) {
|
version(Windows) {
|
||||||
// I'm swapping these because my laptop doesn't have a middle button,
|
// I'm swapping these because my laptop doesn't have a middle button,
|
||||||
// and putty swaps them too by default so whatevs.
|
// and putty swaps them too by default so whatevs.
|
||||||
|
|
@ -343,7 +335,6 @@ class TerminalEmulator {
|
||||||
else if(button == MouseButton.middle)
|
else if(button == MouseButton.middle)
|
||||||
button = MouseButton.right;
|
button = MouseButton.right;
|
||||||
}
|
}
|
||||||
+/
|
|
||||||
|
|
||||||
int baseEventCode() {
|
int baseEventCode() {
|
||||||
int b;
|
int b;
|
||||||
|
|
@ -369,9 +360,6 @@ class TerminalEmulator {
|
||||||
if(alt) // sending alt as meta
|
if(alt) // sending alt as meta
|
||||||
b |= 8;
|
b |= 8;
|
||||||
|
|
||||||
if(!sgrMouseMode)
|
|
||||||
b |= 32; // it just always does this
|
|
||||||
|
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,9 +411,14 @@ class TerminalEmulator {
|
||||||
dragging = false;
|
dragging = false;
|
||||||
if(mouseButtonReleaseTracking) {
|
if(mouseButtonReleaseTracking) {
|
||||||
int b = baseEventCode;
|
int b = baseEventCode;
|
||||||
if(!sgrMouseMode)
|
|
||||||
b |= 3; // always send none / button released
|
b |= 3; // always send none / button released
|
||||||
sendMouseEvent(b, termX, termY, true);
|
ScopeBuffer!(char, 16) buffer;
|
||||||
|
buffer ~= "\033[M";
|
||||||
|
buffer ~= cast(char) (b | 32);
|
||||||
|
addMouseCoordinates(buffer, termX, termY);
|
||||||
|
//buffer ~= cast(char) (termX+1 + 32);
|
||||||
|
//buffer ~= cast(char) (termY+1 + 32);
|
||||||
|
sendToApplication(buffer[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,7 +428,13 @@ class TerminalEmulator {
|
||||||
lastDragX = termX;
|
lastDragX = termX;
|
||||||
if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
|
if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
|
||||||
int b = baseEventCode;
|
int b = baseEventCode;
|
||||||
sendMouseEvent(b + 32, termX, termY);
|
ScopeBuffer!(char, 16) buffer;
|
||||||
|
buffer ~= "\033[M";
|
||||||
|
buffer ~= cast(char) ((b | 32) + 32);
|
||||||
|
addMouseCoordinates(buffer, termX, termY);
|
||||||
|
//buffer ~= cast(char) (termX+1 + 32);
|
||||||
|
//buffer ~= cast(char) (termY+1 + 32);
|
||||||
|
sendToApplication(buffer[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(dragging) {
|
if(dragging) {
|
||||||
|
|
@ -512,9 +511,18 @@ class TerminalEmulator {
|
||||||
|
|
||||||
int b = baseEventCode;
|
int b = baseEventCode;
|
||||||
|
|
||||||
sendMouseEvent(b, termX, termY);
|
int x = termX;
|
||||||
|
int y = termY;
|
||||||
|
x++; y++; // applications expect it to be one-based
|
||||||
|
|
||||||
|
ScopeBuffer!(char, 16) buffer;
|
||||||
|
buffer ~= "\033[M";
|
||||||
|
buffer ~= cast(char) (b | 32);
|
||||||
|
addMouseCoordinates(buffer, termX, termY);
|
||||||
//buffer ~= cast(char) (x + 32);
|
//buffer ~= cast(char) (x + 32);
|
||||||
//buffer ~= cast(char) (y + 32);
|
//buffer ~= cast(char) (y + 32);
|
||||||
|
|
||||||
|
sendToApplication(buffer[]);
|
||||||
} else {
|
} else {
|
||||||
do_default_behavior:
|
do_default_behavior:
|
||||||
if(button == MouseButton.middle) {
|
if(button == MouseButton.middle) {
|
||||||
|
|
@ -615,22 +623,7 @@ class TerminalEmulator {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMouseEvent(int b, int x, int y, bool isRelease = false) {
|
private void addMouseCoordinates(ref ScopeBuffer!(char, 16) buffer, int x, int y) {
|
||||||
|
|
||||||
ScopeBuffer!(char, 16) buffer;
|
|
||||||
|
|
||||||
if(sgrMouseMode) {
|
|
||||||
buffer ~= "\033[<";
|
|
||||||
buffer.appendIntAsString(b);
|
|
||||||
buffer ~= ";";
|
|
||||||
buffer.appendIntAsString(x + 1);
|
|
||||||
buffer ~= ";";
|
|
||||||
buffer.appendIntAsString(y + 1);
|
|
||||||
buffer ~= isRelease ? "m" : "M";
|
|
||||||
} else {
|
|
||||||
buffer ~= "\033[M";
|
|
||||||
buffer ~= cast(char) b;
|
|
||||||
|
|
||||||
// 1-based stuff and 32 is the base value
|
// 1-based stuff and 32 is the base value
|
||||||
x += 1 + 32;
|
x += 1 + 32;
|
||||||
y += 1 + 32;
|
y += 1 + 32;
|
||||||
|
|
@ -650,9 +643,6 @@ class TerminalEmulator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToApplication(buffer[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void returnToNormalScreen() {
|
protected void returnToNormalScreen() {
|
||||||
alternateScreenActive = false;
|
alternateScreenActive = false;
|
||||||
|
|
||||||
|
|
@ -1993,7 +1983,6 @@ class TerminalEmulator {
|
||||||
bool mouseButtonTracking;
|
bool mouseButtonTracking;
|
||||||
private bool _mouseMotionTracking;
|
private bool _mouseMotionTracking;
|
||||||
bool utf8MouseMode;
|
bool utf8MouseMode;
|
||||||
bool sgrMouseMode;
|
|
||||||
bool mouseButtonReleaseTracking;
|
bool mouseButtonReleaseTracking;
|
||||||
bool mouseButtonMotionTracking;
|
bool mouseButtonMotionTracking;
|
||||||
bool selectiveMouseTracking;
|
bool selectiveMouseTracking;
|
||||||
|
|
@ -3234,10 +3223,7 @@ SGR (1006)
|
||||||
The highlight tracking responses are also modified to an SGR-
|
The highlight tracking responses are also modified to an SGR-
|
||||||
like format, using the same SGR-style scheme and button-encod-
|
like format, using the same SGR-style scheme and button-encod-
|
||||||
ings.
|
ings.
|
||||||
|
|
||||||
Note that M is used for motion; m is only release
|
|
||||||
*/
|
*/
|
||||||
sgrMouseMode = true;
|
|
||||||
break;
|
break;
|
||||||
case 1014:
|
case 1014:
|
||||||
// ARSD extension: it is 1002 but selective, only
|
// ARSD extension: it is 1002 but selective, only
|
||||||
|
|
@ -3314,7 +3300,6 @@ URXVT (1015)
|
||||||
break;
|
break;
|
||||||
case 1006:
|
case 1006:
|
||||||
// turn off sgr mouse
|
// turn off sgr mouse
|
||||||
sgrMouseMode = false;
|
|
||||||
break;
|
break;
|
||||||
case 1015:
|
case 1015:
|
||||||
// turn off urxvt mouse
|
// turn off urxvt mouse
|
||||||
|
|
|
||||||
|
|
@ -1503,10 +1503,6 @@ class TextLayouter {
|
||||||
|
|
||||||
auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus);
|
auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus);
|
||||||
|
|
||||||
// the y is added elsewhere already. i think.
|
|
||||||
bb.left += segment.upperLeft.x;
|
|
||||||
bb.right += segment.upperLeft.x;
|
|
||||||
|
|
||||||
bb.top += castFnumToCnum(glyphStyle.font.ascent);
|
bb.top += castFnumToCnum(glyphStyle.font.ascent);
|
||||||
bb.bottom -= castFnumToCnum(glyphStyle.font.descent);
|
bb.bottom -= castFnumToCnum(glyphStyle.font.descent);
|
||||||
|
|
||||||
|
|
|
||||||
628
uri.d
628
uri.d
|
|
@ -8,9 +8,6 @@ module arsd.uri;
|
||||||
|
|
||||||
import arsd.core;
|
import arsd.core;
|
||||||
|
|
||||||
import arsd.conv;
|
|
||||||
import arsd.string;
|
|
||||||
|
|
||||||
alias encodeUriComponent = arsd.core.encodeUriComponent;
|
alias encodeUriComponent = arsd.core.encodeUriComponent;
|
||||||
alias decodeUriComponent = arsd.core.decodeUriComponent;
|
alias decodeUriComponent = arsd.core.decodeUriComponent;
|
||||||
|
|
||||||
|
|
@ -21,628 +18,3 @@ alias decodeComponent = decodeUriComponent;
|
||||||
// FIXME: merge and pull Uri struct from http2 and cgi. maybe via core.
|
// FIXME: merge and pull Uri struct from http2 and cgi. maybe via core.
|
||||||
|
|
||||||
// might also put base64 in here....
|
// might also put base64 in here....
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/++
|
|
||||||
Represents a URI. It offers named access to the components and relative uri resolution, though as a user of the library, you'd mostly just construct it like `Uri("http://example.com/index.html")`.
|
|
||||||
|
|
||||||
History:
|
|
||||||
Moved from duplication in [arsd.cgi] and [arsd.http2] to arsd.uri on November 2, 2025.
|
|
||||||
+/
|
|
||||||
struct Uri {
|
|
||||||
UriString toUriString() {
|
|
||||||
return UriString(toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
alias toUriString this; // blargh idk a url really is a string, but should it be implicit?
|
|
||||||
|
|
||||||
// scheme://userinfo@host:port/path?query#fragment
|
|
||||||
|
|
||||||
string scheme; /// e.g. "http" in "http://example.com/"
|
|
||||||
string userinfo; /// the username (and possibly a password) in the uri
|
|
||||||
string host; /// the domain name. note it may be an ip address or have percent encoding too.
|
|
||||||
int port; /// port number, if given. Will be zero if a port was not explicitly given
|
|
||||||
string path; /// e.g. "/folder/file.html" in "http://example.com/folder/file.html"
|
|
||||||
string query; /// the stuff after the ? in a uri
|
|
||||||
string fragment; /// the stuff after the # in a uri.
|
|
||||||
|
|
||||||
// cgi.d specific.......
|
|
||||||
// idk if i want to keep these, since the functions they wrap are used many, many, many times in existing code, so this is either an unnecessary alias or a gratuitous break of compatibility
|
|
||||||
// the decode ones need to keep different names anyway because we can't overload on return values...
|
|
||||||
static string encode(string s) { return encodeUriComponent(s); }
|
|
||||||
static string encode(string[string] s) { return encodeVariables(s); }
|
|
||||||
static string encode(string[][string] s) { return encodeVariables(s); }
|
|
||||||
|
|
||||||
/++
|
|
||||||
Parses an existing uri string (which should be pre-validated) into this further detailed structure.
|
|
||||||
|
|
||||||
History:
|
|
||||||
Added November 2, 2025.
|
|
||||||
+/
|
|
||||||
this(UriString uriString) {
|
|
||||||
this(uriString.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
Transforms an interpolated expression sequence into a uri, encoding as appropriate as it reads.
|
|
||||||
|
|
||||||
History:
|
|
||||||
Added November 2, 2025.
|
|
||||||
+/
|
|
||||||
this(Args...)(InterpolationHeader header, Args args, InterpolationFooter footer) {
|
|
||||||
// will need to use iraw here for some cases. paths may partially encoded but still allow slashes, prolly needs a type.
|
|
||||||
// so like $(path(x)) or $(queryString(x)) or maybe isemi or something. or make user split it into a string[] then recombine here....
|
|
||||||
string thing;
|
|
||||||
foreach(arg; args) {
|
|
||||||
static if(is(typeof(arg) == InterpolationHeader))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == InterpolationFooter))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == InterpolatedLiteral!part, string part))
|
|
||||||
thing ~= part;
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == InterpolatedExpression!code, string code))
|
|
||||||
{}
|
|
||||||
else
|
|
||||||
static if(is(typeof(arg) == iraw))
|
|
||||||
thing ~= iraw.s;
|
|
||||||
else
|
|
||||||
thing ~= encodeUriComponent(to!string(arg));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
this(thing);
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
string bar = "12/";
|
|
||||||
string baz = "&omg";
|
|
||||||
auto uri = Uri(i"http://example.com/foo/$bar?thing=$baz");
|
|
||||||
|
|
||||||
assert(uri.toString() == "http://example.com/foo/12%2F?thing=%26omg");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Breaks down a uri string to its components
|
|
||||||
this(string uri) {
|
|
||||||
size_t lastGoodIndex;
|
|
||||||
foreach(char ch; uri) {
|
|
||||||
if(ch > 127) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
lastGoodIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
string replacement = uri[0 .. lastGoodIndex];
|
|
||||||
foreach(char ch; uri[lastGoodIndex .. $]) {
|
|
||||||
if(ch > 127) {
|
|
||||||
// need to percent-encode any non-ascii in it
|
|
||||||
char[3] buffer;
|
|
||||||
buffer[0] = '%';
|
|
||||||
|
|
||||||
auto first = ch / 16;
|
|
||||||
auto second = ch % 16;
|
|
||||||
first += (first >= 10) ? ('A'-10) : '0';
|
|
||||||
second += (second >= 10) ? ('A'-10) : '0';
|
|
||||||
|
|
||||||
buffer[1] = cast(char) first;
|
|
||||||
buffer[2] = cast(char) second;
|
|
||||||
|
|
||||||
replacement ~= buffer[];
|
|
||||||
} else {
|
|
||||||
replacement ~= ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reparse(replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `port` if set, otherwise if scheme is https 443, otherwise always 80
|
|
||||||
int effectivePort() const @property nothrow pure @safe @nogc {
|
|
||||||
return port != 0 ? port
|
|
||||||
: scheme == "https" ? 443 : 80;
|
|
||||||
}
|
|
||||||
|
|
||||||
package string unixSocketPath = null;
|
|
||||||
/// Indicates it should be accessed through a unix socket instead of regular tcp. Returns new version without modifying this object.
|
|
||||||
Uri viaUnixSocket(string path) const {
|
|
||||||
Uri copy = this;
|
|
||||||
copy.unixSocketPath = path;
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Goes through a unix socket in the abstract namespace (linux only). Returns new version without modifying this object.
|
|
||||||
version(linux)
|
|
||||||
Uri viaAbstractSocket(string path) const {
|
|
||||||
Uri copy = this;
|
|
||||||
copy.unixSocketPath = "\0" ~ path;
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
// these are like javascript's location.search and location.hash
|
|
||||||
string search() const {
|
|
||||||
return query.length ? ("?" ~ query) : "";
|
|
||||||
}
|
|
||||||
string hash() const {
|
|
||||||
return fragment.length ? ("#" ~ fragment) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void reparse(string uri) {
|
|
||||||
// from RFC 3986
|
|
||||||
// the ctRegex triples the compile time and makes ugly errors for no real benefit
|
|
||||||
// it was a nice experiment but just not worth it.
|
|
||||||
// enum ctr = ctRegex!r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?";
|
|
||||||
/*
|
|
||||||
Captures:
|
|
||||||
0 = whole url
|
|
||||||
1 = scheme, with :
|
|
||||||
2 = scheme, no :
|
|
||||||
3 = authority, with //
|
|
||||||
4 = authority, no //
|
|
||||||
5 = path
|
|
||||||
6 = query string, with ?
|
|
||||||
7 = query string, no ?
|
|
||||||
8 = anchor, with #
|
|
||||||
9 = anchor, no #
|
|
||||||
*/
|
|
||||||
// Yikes, even regular, non-CT regex is also unacceptably slow to compile. 1.9s on my computer!
|
|
||||||
// instead, I will DIY and cut that down to 0.6s on the same computer.
|
|
||||||
/*
|
|
||||||
|
|
||||||
Note that authority is
|
|
||||||
user:password@domain:port
|
|
||||||
where the user:password@ part is optional, and the :port is optional.
|
|
||||||
|
|
||||||
Regex translation:
|
|
||||||
|
|
||||||
Scheme cannot have :, /, ?, or # in it, and must have one or more chars and end in a :. It is optional, but must be first.
|
|
||||||
Authority must start with //, but cannot have any other /, ?, or # in it. It is optional.
|
|
||||||
Path cannot have any ? or # in it. It is optional.
|
|
||||||
Query must start with ? and must not have # in it. It is optional.
|
|
||||||
Anchor must start with # and can have anything else in it to end of string. It is optional.
|
|
||||||
*/
|
|
||||||
|
|
||||||
this = Uri.init; // reset all state
|
|
||||||
|
|
||||||
// empty uri = nothing special
|
|
||||||
if(uri.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t idx;
|
|
||||||
|
|
||||||
scheme_loop: foreach(char c; uri[idx .. $]) {
|
|
||||||
switch(c) {
|
|
||||||
case ':':
|
|
||||||
case '/':
|
|
||||||
case '?':
|
|
||||||
case '#':
|
|
||||||
break scheme_loop;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(idx == 0 && uri[idx] == ':') {
|
|
||||||
// this is actually a path! we skip way ahead
|
|
||||||
goto path_loop;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(idx == uri.length) {
|
|
||||||
// the whole thing is a path, apparently
|
|
||||||
path = uri;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(idx > 0 && uri[idx] == ':') {
|
|
||||||
scheme = uri[0 .. idx];
|
|
||||||
idx++;
|
|
||||||
} else {
|
|
||||||
// we need to rewind; it found a / but no :, so the whole thing is prolly a path...
|
|
||||||
idx = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(idx + 2 < uri.length && uri[idx .. idx + 2] == "//") {
|
|
||||||
// we have an authority....
|
|
||||||
idx += 2;
|
|
||||||
|
|
||||||
auto authority_start = idx;
|
|
||||||
authority_loop: foreach(char c; uri[idx .. $]) {
|
|
||||||
switch(c) {
|
|
||||||
case '/':
|
|
||||||
case '?':
|
|
||||||
case '#':
|
|
||||||
break authority_loop;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto authority = uri[authority_start .. idx];
|
|
||||||
|
|
||||||
auto idx2 = authority.indexOf("@");
|
|
||||||
if(idx2 != -1) {
|
|
||||||
userinfo = authority[0 .. idx2];
|
|
||||||
authority = authority[idx2 + 1 .. $];
|
|
||||||
}
|
|
||||||
|
|
||||||
if(authority.length && authority[0] == '[') {
|
|
||||||
// ipv6 address special casing
|
|
||||||
idx2 = authority.indexOf("]");
|
|
||||||
if(idx2 != -1) {
|
|
||||||
auto end = authority[idx2 + 1 .. $];
|
|
||||||
if(end.length && end[0] == ':')
|
|
||||||
idx2 = idx2 + 1;
|
|
||||||
else
|
|
||||||
idx2 = -1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
idx2 = authority.indexOf(":");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(idx2 == -1) {
|
|
||||||
port = 0; // 0 means not specified; we should use the default for the scheme
|
|
||||||
host = authority;
|
|
||||||
} else {
|
|
||||||
host = authority[0 .. idx2];
|
|
||||||
if(idx2 + 1 < authority.length)
|
|
||||||
port = to!int(authority[idx2 + 1 .. $]);
|
|
||||||
else
|
|
||||||
port = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path_loop:
|
|
||||||
auto path_start = idx;
|
|
||||||
|
|
||||||
foreach(char c; uri[idx .. $]) {
|
|
||||||
if(c == '?' || c == '#')
|
|
||||||
break;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
path = uri[path_start .. idx];
|
|
||||||
|
|
||||||
if(idx == uri.length)
|
|
||||||
return; // nothing more to examine...
|
|
||||||
|
|
||||||
if(uri[idx] == '?') {
|
|
||||||
idx++;
|
|
||||||
auto query_start = idx;
|
|
||||||
foreach(char c; uri[idx .. $]) {
|
|
||||||
if(c == '#')
|
|
||||||
break;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
query = uri[query_start .. idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
if(idx < uri.length && uri[idx] == '#') {
|
|
||||||
idx++;
|
|
||||||
fragment = uri[idx .. $];
|
|
||||||
}
|
|
||||||
|
|
||||||
// uriInvalidated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string rebuildUri() const {
|
|
||||||
string ret;
|
|
||||||
if(scheme.length)
|
|
||||||
ret ~= scheme ~ ":";
|
|
||||||
if(userinfo.length || host.length)
|
|
||||||
ret ~= "//";
|
|
||||||
if(userinfo.length)
|
|
||||||
ret ~= userinfo ~ "@";
|
|
||||||
if(host.length)
|
|
||||||
ret ~= host;
|
|
||||||
if(port)
|
|
||||||
ret ~= ":" ~ to!string(port);
|
|
||||||
|
|
||||||
ret ~= path;
|
|
||||||
|
|
||||||
if(query.length)
|
|
||||||
ret ~= "?" ~ query;
|
|
||||||
|
|
||||||
if(fragment.length)
|
|
||||||
ret ~= "#" ~ fragment;
|
|
||||||
|
|
||||||
// uri = ret;
|
|
||||||
// uriInvalidated = false;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts the broken down parts back into a complete string
|
|
||||||
string toString() const {
|
|
||||||
// if(uriInvalidated)
|
|
||||||
return rebuildUri();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new absolute Uri given a base. It treats this one as
|
|
||||||
/// relative where possible, but absolute if not. (If protocol, domain, or
|
|
||||||
/// other info is not set, the new one inherits it from the base.)
|
|
||||||
///
|
|
||||||
/// Browsers use a function like this to figure out links in html.
|
|
||||||
Uri basedOn(in Uri baseUrl) const {
|
|
||||||
Uri n = this; // copies
|
|
||||||
if(n.scheme == "data")
|
|
||||||
return n;
|
|
||||||
// n.uriInvalidated = true; // make sure we regenerate...
|
|
||||||
|
|
||||||
// userinfo is not inherited... is this wrong?
|
|
||||||
|
|
||||||
// if anything is given in the existing url, we don't use the base anymore.
|
|
||||||
if(n.scheme.length == 0) {
|
|
||||||
n.scheme = baseUrl.scheme;
|
|
||||||
if(n.host.length == 0) {
|
|
||||||
n.host = baseUrl.host;
|
|
||||||
if(n.port == 0) {
|
|
||||||
n.port = baseUrl.port;
|
|
||||||
if(n.path.length > 0 && n.path[0] != '/') {
|
|
||||||
auto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1];
|
|
||||||
if(b.length == 0)
|
|
||||||
b = "/";
|
|
||||||
n.path = b ~ n.path;
|
|
||||||
} else if(n.path.length == 0) {
|
|
||||||
n.path = baseUrl.path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
n.removeDots();
|
|
||||||
|
|
||||||
// if still basically talking to the same thing, we should inherit the unix path
|
|
||||||
// too since basically the unix path is saying for this service, always use this override.
|
|
||||||
if(n.host == baseUrl.host && n.scheme == baseUrl.scheme && n.port == baseUrl.port)
|
|
||||||
n.unixSocketPath = baseUrl.unixSocketPath;
|
|
||||||
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
/++
|
|
||||||
Resolves ../ and ./ parts of the path. Used in the implementation of [basedOn] and you could also use it to normalize things.
|
|
||||||
+/
|
|
||||||
void removeDots() {
|
|
||||||
auto parts = this.path.split("/");
|
|
||||||
string[] toKeep;
|
|
||||||
foreach(part; parts) {
|
|
||||||
if(part == ".") {
|
|
||||||
continue;
|
|
||||||
} else if(part == "..") {
|
|
||||||
//if(toKeep.length > 1)
|
|
||||||
toKeep = toKeep[0 .. $-1];
|
|
||||||
//else
|
|
||||||
//toKeep = [""];
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
//if(toKeep.length && toKeep[$-1].length == 0 && part.length == 0)
|
|
||||||
//continue; // skip a `//` situation
|
|
||||||
toKeep ~= part;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto path = toKeep.join("/");
|
|
||||||
if(path.length && path[0] != '/')
|
|
||||||
path = "/" ~ path;
|
|
||||||
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
auto uri = Uri("test.html");
|
|
||||||
assert(uri.path == "test.html");
|
|
||||||
uri = Uri("path/1/lol");
|
|
||||||
assert(uri.path == "path/1/lol");
|
|
||||||
uri = Uri("http://me@example.com");
|
|
||||||
assert(uri.scheme == "http");
|
|
||||||
assert(uri.userinfo == "me");
|
|
||||||
assert(uri.host == "example.com");
|
|
||||||
uri = Uri("http://example.com/#a");
|
|
||||||
assert(uri.scheme == "http");
|
|
||||||
assert(uri.host == "example.com");
|
|
||||||
assert(uri.fragment == "a");
|
|
||||||
uri = Uri("#foo");
|
|
||||||
assert(uri.fragment == "foo");
|
|
||||||
uri = Uri("?lol");
|
|
||||||
assert(uri.query == "lol");
|
|
||||||
uri = Uri("#foo?lol");
|
|
||||||
assert(uri.fragment == "foo?lol");
|
|
||||||
uri = Uri("?lol#foo");
|
|
||||||
assert(uri.fragment == "foo");
|
|
||||||
assert(uri.query == "lol");
|
|
||||||
|
|
||||||
uri = Uri("http://127.0.0.1/");
|
|
||||||
assert(uri.host == "127.0.0.1");
|
|
||||||
assert(uri.port == 0);
|
|
||||||
|
|
||||||
uri = Uri("http://127.0.0.1:123/");
|
|
||||||
assert(uri.host == "127.0.0.1");
|
|
||||||
assert(uri.port == 123);
|
|
||||||
|
|
||||||
uri = Uri("http://[ff:ff::0]/");
|
|
||||||
assert(uri.host == "[ff:ff::0]");
|
|
||||||
|
|
||||||
uri = Uri("http://[ff:ff::0]:123/");
|
|
||||||
assert(uri.host == "[ff:ff::0]");
|
|
||||||
assert(uri.port == 123);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover
|
|
||||||
// the possibilities.
|
|
||||||
unittest {
|
|
||||||
auto url = Uri("cool.html"); // checking relative links
|
|
||||||
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/cool.html");
|
|
||||||
assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html");
|
|
||||||
|
|
||||||
url = Uri("/something/cool.html"); // same server, different path
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/something/cool.html");
|
|
||||||
assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/something/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/something/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/something/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/something/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/something/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/something/cool.html");
|
|
||||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html");
|
|
||||||
|
|
||||||
url = Uri("?query=answer"); // same path. server, protocol, and port, just different query string and fragment
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/test.html?query=answer");
|
|
||||||
assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/test.html?query=answer");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/?query=answer");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/?query=answer");
|
|
||||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/test.html?query=answer");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/test.html?query=answer");
|
|
||||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/test.html?query=answer");
|
|
||||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer");
|
|
||||||
|
|
||||||
url = Uri("/test/bar");
|
|
||||||
assert(Uri("./").basedOn(url) == "/test/", Uri("./").basedOn(url));
|
|
||||||
assert(Uri("../").basedOn(url) == "/");
|
|
||||||
|
|
||||||
url = Uri("http://example.com/");
|
|
||||||
assert(Uri("../foo").basedOn(url) == "http://example.com/foo");
|
|
||||||
|
|
||||||
//auto uriBefore = url;
|
|
||||||
url = Uri("#anchor"); // everything should remain the same except the anchor
|
|
||||||
//uriBefore.anchor = "anchor");
|
|
||||||
//assert(url == uriBefore);
|
|
||||||
|
|
||||||
url = Uri("//example.com"); // same protocol, but different server. the path here should be blank.
|
|
||||||
|
|
||||||
url = Uri("//example.com/example.html"); // same protocol, but different server and path
|
|
||||||
|
|
||||||
url = Uri("http://example.com/test.html"); // completely absolute link should never be modified
|
|
||||||
|
|
||||||
url = Uri("http://example.com"); // completely absolute link should never be modified, even if it has no path
|
|
||||||
|
|
||||||
// FIXME: add something for port too
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Makes a data:// uri that can be used as links in most newer browsers (IE8+).
|
|
||||||
string makeDataUrl()(string mimeType, in void[] data) {
|
|
||||||
import std.base64; // FIXME then i can remove the () template
|
|
||||||
auto data64 = Base64.encode(cast(const(ubyte[])) data);
|
|
||||||
return "data:" ~ mimeType ~ ";base64," ~ cast(string)(data64);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// breaks down a url encoded string
|
|
||||||
string[][string] decodeVariables(string data, string separator = "&", string[]* namesInOrder = null, string[]* valuesInOrder = null) {
|
|
||||||
auto vars = data.split(separator);
|
|
||||||
string[][string] _get;
|
|
||||||
foreach(var; vars) {
|
|
||||||
auto equal = var.indexOf("=");
|
|
||||||
string name;
|
|
||||||
string value;
|
|
||||||
if(equal == -1) {
|
|
||||||
name = decodeUriComponent(var);
|
|
||||||
value = "";
|
|
||||||
} else {
|
|
||||||
//_get[decodeUriComponent(var[0..equal])] ~= decodeUriComponent(var[equal + 1 .. $].replace("+", " "));
|
|
||||||
// stupid + -> space conversion.
|
|
||||||
name = decodeUriComponent(var[0..equal].replace("+", " "));
|
|
||||||
value = decodeUriComponent(var[equal + 1 .. $].replace("+", " "));
|
|
||||||
}
|
|
||||||
|
|
||||||
_get[name] ~= value;
|
|
||||||
if(namesInOrder)
|
|
||||||
(*namesInOrder) ~= name;
|
|
||||||
if(valuesInOrder)
|
|
||||||
(*valuesInOrder) ~= value;
|
|
||||||
}
|
|
||||||
return _get;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// breaks down a url encoded string, but only returns the last value of any array
|
|
||||||
string[string] decodeVariablesSingle(string data) {
|
|
||||||
string[string] va;
|
|
||||||
auto varArray = decodeVariables(data);
|
|
||||||
foreach(k, v; varArray)
|
|
||||||
va[k] = v[$-1];
|
|
||||||
|
|
||||||
return va;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// url encodes the whole string
|
|
||||||
string encodeVariables(in string[string] data) {
|
|
||||||
string ret;
|
|
||||||
|
|
||||||
bool outputted = false;
|
|
||||||
foreach(k, v; data) {
|
|
||||||
if(outputted)
|
|
||||||
ret ~= "&";
|
|
||||||
else
|
|
||||||
outputted = true;
|
|
||||||
|
|
||||||
ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// url encodes a whole string
|
|
||||||
string encodeVariables(in string[][string] data) {
|
|
||||||
string ret;
|
|
||||||
|
|
||||||
bool outputted = false;
|
|
||||||
foreach(k, arr; data) {
|
|
||||||
foreach(v; arr) {
|
|
||||||
if(outputted)
|
|
||||||
ret ~= "&";
|
|
||||||
else
|
|
||||||
outputted = true;
|
|
||||||
ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encodes all but the explicitly unreserved characters per rfc 3986
|
|
||||||
/// Alphanumeric and -_.~ are the only ones left unencoded
|
|
||||||
/// name is borrowed from php
|
|
||||||
string rawurlencode(in char[] data) {
|
|
||||||
string ret;
|
|
||||||
ret.reserve(data.length * 2);
|
|
||||||
foreach(char c; data) {
|
|
||||||
if(
|
|
||||||
(c >= 'a' && c <= 'z') ||
|
|
||||||
(c >= 'A' && c <= 'Z') ||
|
|
||||||
(c >= '0' && c <= '9') ||
|
|
||||||
c == '-' || c == '_' || c == '.' || c == '~')
|
|
||||||
{
|
|
||||||
ret ~= c;
|
|
||||||
} else {
|
|
||||||
ret ~= '%';
|
|
||||||
// since we iterate on char, this should give us the octets of the full utf8 string
|
|
||||||
ret ~= toHexUpper(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
char[2] toHexUpper(ubyte num) {
|
|
||||||
char[2] ret = 0;
|
|
||||||
ret[0] = num / 16;
|
|
||||||
ret[1] = num % 16;
|
|
||||||
ret[0] += cast(char)(ret[0] >= 10 ? 'A' : '0');
|
|
||||||
ret[1] += cast(char)(ret[1] >= 10 ? 'A' : '0');
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
2
web.d
2
web.d
|
|
@ -11,8 +11,6 @@
|
||||||
+/
|
+/
|
||||||
module arsd.web;
|
module arsd.web;
|
||||||
|
|
||||||
import arsd.uri : decodeVariablesSingle, encodeVariables;
|
|
||||||
|
|
||||||
static if(__VERSION__ <= 2076) {
|
static if(__VERSION__ <= 2076) {
|
||||||
// compatibility shims with gdc
|
// compatibility shims with gdc
|
||||||
enum JSONType {
|
enum JSONType {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ module arsd.webtemplate;
|
||||||
|
|
||||||
import arsd.script;
|
import arsd.script;
|
||||||
import arsd.dom;
|
import arsd.dom;
|
||||||
import arsd.uri;
|
|
||||||
|
|
||||||
public import arsd.jsvar : var;
|
public import arsd.jsvar : var;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue