Compare commits

..

19 Commits

Author SHA1 Message Date
Adam D. Ruppe f494fcf9ca sgr mode 1006 2025-12-15 16:49:20 -05:00
Adam D. Ruppe 26acaaae99 the fruits of the browserjam thing i did in early 2025 2025-12-07 09:40:20 -05:00
Adam D. Ruppe 21e8be8dcc fix bug in scgi 2025-12-06 12:53:45 -05:00
Adam D. Ruppe de5c8055cf make mac allow X again via -version=ArsdNoCocoa 2025-12-03 14:22:04 -05:00
Adam D. Ruppe 6b3aba5374 fix caret on tab at start of line 2025-12-03 14:22:04 -05:00
Adam D. Ruppe 736477b128
Merge pull request #518 from analogjupiter/event-set
Call `event.setIfInitialized` if available
2025-11-22 08:25:17 -05:00
Mindy Batek 2ab06d1249 Call `event.setIfInitialized` if available 2025-11-22 04:33:21 +01:00
Adam D. Ruppe cea860a447 some bug fixes 2025-11-03 19:25:24 -05:00
Adam D. Ruppe 8da8d51a86 new connection pool and query check stuff 2025-11-03 19:24:52 -05:00
Adam D. Ruppe bfc0014ae2 oops opend fix 2025-11-03 19:24:15 -05:00
Adam D. Ruppe 779bcb0199 uri stuff available 2025-11-03 19:23:55 -05:00
Adam D. Ruppe e8cacb7277 using more useful functions 2025-11-03 19:22:09 -05:00
Adam D. Ruppe e73713c026 shared functionality in core 2025-11-03 19:21:28 -05:00
Adam D. Ruppe c94cc54f43 moved some functions from cgi.d 2025-11-03 19:21:02 -05:00
Adam D. Ruppe aa2e04e6ca basic interpolation 2025-11-03 19:18:25 -05:00
Adam D. Ruppe b651bc567e warn about broken alpha blend on Image 2025-10-21 09:03:51 -04:00
Adam D. Ruppe 8efd1ffa0e Cross compile woes: didn't assign window in osx, the pragma lib dl fails when using the zig cross compiler but is unnecessary anyway since standard build includes it (i think everywhere we need it, if not ill do a follow up commit) 2025-10-21 09:02:31 -04:00
Adam D. Ruppe b3d88cafa8 initial arsd.shell commit 2025-10-19 20:29:09 -04:00
Adam D. Ruppe f7c2cbc0d2 long.min is a weird number 2025-10-17 19:20:36 -04:00
23 changed files with 4379 additions and 2268 deletions

View File

@ -98,6 +98,7 @@ 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;

1128
cgi.d

File diff suppressed because it is too large Load Diff

1031
core.d

File diff suppressed because it is too large Load Diff

View File

@ -109,6 +109,8 @@ 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;
@ -123,10 +125,19 @@ 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);
@ -349,6 +360,519 @@ 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
View File

@ -1,3 +1,5 @@
// 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...
@ -2695,6 +2697,47 @@ 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) {

View File

@ -11,10 +11,30 @@
"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": ["-mv=arsd.core=$PACKAGE_DIR/core.d"], "dflags-dmd": [
"dflags-ldc": ["--mv=arsd.core=$PACKAGE_DIR/core.d"], "-mv=arsd.core=$PACKAGE_DIR/core.d",
"dflags-gdc": ["-fmodule-file=arsd.core=$PACKAGE_DIR/core.d"], "-mv=arsd.string=$PACKAGE_DIR/string.d",
"sourceFiles": ["core.d"] "-mv=arsd.uri=$PACKAGE_DIR/uri.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",

1041
http2.d

File diff suppressed because it is too large Load Diff

View File

@ -17879,15 +17879,26 @@ 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(current >= '0' && current <= '9') { if(!fallback && 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
@ -18277,6 +18288,7 @@ 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");
} }

1022
minigui_addons/html_viewer.d Normal file

File diff suppressed because it is too large Load Diff

View File

@ -88,6 +88,10 @@ class MsSql : Database {
return null; // FIXME return null; // FIXME
} }
override bool isAlive() {
return true;
}
private: private:
SQLHENV env; SQLHENV env;
SQLHDBC conn; SQLHDBC conn;

View File

@ -219,6 +219,9 @@ 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)";

View File

@ -2,7 +2,8 @@
module arsd.oauth; module arsd.oauth;
import arsd.curl; import arsd.curl;
import arsd.cgi; // for decodeVariables import arsd.uri;
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;

View File

@ -60,9 +60,12 @@ 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;
@ -75,6 +78,11 @@ 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
@ -132,8 +140,10 @@ 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());
@ -179,6 +189,82 @@ 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)
@ -374,7 +460,22 @@ 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,
@ -386,6 +487,7 @@ extern(C) {
char* PQcmdTuples(PGresult *res); char* PQcmdTuples(PGresult *res);
PGresult *PQdescribePrepared(PGconn *conn, const char *stmtName);
} }
/* /*

839
shell.d Normal file
View File

@ -0,0 +1,839 @@
/++
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\\\&raquo;bar");
assert(got.length == 3);
assert(got[0].l == "test");
assert(got[1].l == "\&raquo;");
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
+/

View File

@ -1164,17 +1164,19 @@ unittest {
import arsd.core; import arsd.core;
version(D_OpenD) { version(ArsdNoCocoa) {
version(OSX)
version=OSXCocoa;
version(iOS)
version=OSXCocoa;
} else { } else {
version(OSX) version(DigitalMars) version=OSXCocoa; version(D_OpenD) {
version(OSX)
version=OSXCocoa;
version(iOS)
version=OSXCocoa;
} else {
version(OSX) version(DigitalMars) version=OSXCocoa;
}
} }
version(Emscripten) { version(Emscripten) {
version=allow_unimplemented_features; version=allow_unimplemented_features;
version=without_opengl; version=without_opengl;
@ -1416,7 +1418,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;
@ -2910,7 +2912,8 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon {
else else
XMapWindow(impl.display, impl.window); XMapWindow(impl.display, impl.window);
} else version(OSXCocoa) { } else version(OSXCocoa) {
impl.window.setIsVisible = !b; if(impl.window)
impl.window.setIsVisible = !b;
if(!hidden) if(!hidden)
impl.view.setNeedsDisplay(true); impl.view.setNeedsDisplay(true);
} else version(Emscripten) { } else version(Emscripten) {
@ -4414,6 +4417,10 @@ 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;
@ -4623,7 +4630,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(linux) version(Posix)
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; }
@ -7355,7 +7362,7 @@ version(Windows) {
} }
version (X11) { version (X11) {
pragma(lib, "dl"); //pragma(lib, "dl"); // already done by the standard compiler build and specifying it again messes up zig cross compile
import core.sys.posix.dlfcn; import core.sys.posix.dlfcn;
} }
@ -8416,6 +8423,8 @@ 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.
@ -9952,7 +9961,14 @@ 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
@ -9966,6 +9982,8 @@ 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);
} }
@ -19453,7 +19471,7 @@ version(OSXCocoa) {
auto contentRect = NSRect(NSPoint(0, 0), NSSize(width, height)); auto contentRect = NSRect(NSPoint(0, 0), NSSize(width, height));
auto window = NSWindow.alloc.initWithContentRect( 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,

View File

@ -124,6 +124,10 @@ class Sqlite : Database {
} }
} }
override bool isAlive() {
return true;
}
/// ///
override void startTransaction() { override void startTransaction() {
query("BEGIN TRANSACTION"); query("BEGIN TRANSACTION");

View File

@ -53,3 +53,76 @@ 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");
}

View File

@ -9268,6 +9268,9 @@ 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) {
@ -9467,8 +9470,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 = font.averageWidth; this.fontWidth = castFnumToCnum(font.averageWidth);
this.fontHeight = font.height; this.fontHeight = castFnumToCnum(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
} }

View File

@ -111,6 +111,13 @@ 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;
@ -327,6 +334,7 @@ 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.
@ -335,6 +343,7 @@ 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;
@ -360,6 +369,9 @@ 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;
} }
@ -411,14 +423,9 @@ class TerminalEmulator {
dragging = false; dragging = false;
if(mouseButtonReleaseTracking) { if(mouseButtonReleaseTracking) {
int b = baseEventCode; int b = baseEventCode;
b |= 3; // always send none / button released if(!sgrMouseMode)
ScopeBuffer!(char, 16) buffer; b |= 3; // always send none / button released
buffer ~= "\033[M"; sendMouseEvent(b, termX, termY, true);
buffer ~= cast(char) (b | 32);
addMouseCoordinates(buffer, termX, termY);
//buffer ~= cast(char) (termX+1 + 32);
//buffer ~= cast(char) (termY+1 + 32);
sendToApplication(buffer[]);
} }
} }
@ -428,13 +435,7 @@ class TerminalEmulator {
lastDragX = termX; lastDragX = termX;
if(mouseMotionTracking || (mouseButtonMotionTracking && button)) { if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
int b = baseEventCode; int b = baseEventCode;
ScopeBuffer!(char, 16) buffer; sendMouseEvent(b + 32, termX, termY);
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) {
@ -511,18 +512,9 @@ class TerminalEmulator {
int b = baseEventCode; int b = baseEventCode;
int x = termX; sendMouseEvent(b, termX, termY);
int y = termY; //buffer ~= cast(char) (x + 32);
x++; y++; // applications expect it to be one-based //buffer ~= cast(char) (y + 32);
ScopeBuffer!(char, 16) buffer;
buffer ~= "\033[M";
buffer ~= cast(char) (b | 32);
addMouseCoordinates(buffer, termX, termY);
//buffer ~= cast(char) (x + 32);
//buffer ~= cast(char) (y + 32);
sendToApplication(buffer[]);
} else { } else {
do_default_behavior: do_default_behavior:
if(button == MouseButton.middle) { if(button == MouseButton.middle) {
@ -623,24 +615,42 @@ class TerminalEmulator {
return false; return false;
} }
private void addMouseCoordinates(ref ScopeBuffer!(char, 16) buffer, int x, int y) { private void sendMouseEvent(int b, int x, int y, bool isRelease = false) {
// 1-based stuff and 32 is the base value
x += 1 + 32;
y += 1 + 32;
if(utf8MouseMode) { ScopeBuffer!(char, 16) buffer;
import std.utf;
char[4] str;
foreach(char ch; str[0 .. encode(str, x)]) if(sgrMouseMode) {
buffer ~= ch; buffer ~= "\033[<";
buffer.appendIntAsString(b);
foreach(char ch; str[0 .. encode(str, y)]) buffer ~= ";";
buffer ~= ch; buffer.appendIntAsString(x + 1);
buffer ~= ";";
buffer.appendIntAsString(y + 1);
buffer ~= isRelease ? "m" : "M";
} else { } else {
buffer ~= cast(char) x; buffer ~= "\033[M";
buffer ~= cast(char) y; buffer ~= cast(char) b;
// 1-based stuff and 32 is the base value
x += 1 + 32;
y += 1 + 32;
if(utf8MouseMode) {
import std.utf;
char[4] str;
foreach(char ch; str[0 .. encode(str, x)])
buffer ~= ch;
foreach(char ch; str[0 .. encode(str, y)])
buffer ~= ch;
} else {
buffer ~= cast(char) x;
buffer ~= cast(char) y;
}
} }
sendToApplication(buffer[]);
} }
protected void returnToNormalScreen() { protected void returnToNormalScreen() {
@ -1983,6 +1993,7 @@ 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;
@ -3223,7 +3234,10 @@ 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
@ -3300,6 +3314,7 @@ 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

View File

@ -1503,6 +1503,10 @@ 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
View File

@ -8,6 +8,9 @@ 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;
@ -18,3 +21,628 @@ 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
View File

@ -11,6 +11,8 @@
+/ +/
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 {

View File

@ -51,6 +51,7 @@ 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;