// PEG.js filter rules - see https://pegjs.org/
{
function or(first, second) {
    // Add explicit function names to ease debugging.
    function orFilter() {
        return first.apply(this, arguments) || second.apply(this, arguments);
    }
    orFilter.desc = first.desc + " or " + second.desc;
    return orFilter;
}

function and(first, second) {
    function andFilter() {
        return first.apply(this, arguments) && second.apply(this, arguments);
    }
    andFilter.desc = first.desc + " and " + second.desc;
    return andFilter;
}

function not(expr) {
    function notFilter() {
        return !expr.apply(this, arguments);
    }
    notFilter.desc = "not " + expr.desc;
    return notFilter;
}

function binding(expr) {
    function bindingFilter() {
        return expr.apply(this, arguments);
    }
    bindingFilter.desc = "(" + expr.desc + ")";
    return bindingFilter;
}

// ~all
function allFilter(flow) {
    return true;
}
allFilter.desc = "all flows";

// ~a
var ASSET_TYPES = [
    new RegExp("text/javascript"),
    new RegExp("application/x-javascript"),
    new RegExp("application/javascript"),
    new RegExp("text/css"),
    new RegExp("image/.*"),
    new RegExp("font/.*"),
    new RegExp("application/font.*"),
];
function assetFilter(flow) {
    if (flow.response) {
        var ct = flowutils.ResponseUtils.getContentType(flow.response);
        var i = ASSET_TYPES.length;
        while (i--) {
            if (ASSET_TYPES[i].test(ct)) {
                return true;
            }
        }
    }
    return false;
}
assetFilter.desc = "is asset";

// ~b
function body(regex){
    regex = new RegExp(regex, "i");
    function bodyFilter(flow){
        return true;
    }
    bodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
    return bodyFilter;
}

// ~bq
function requestBody(regex){
    regex = new RegExp(regex, "i");
    function requestBodyFilter(flow){
        return true;
    }
    requestBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
    return requestBodyFilter;
}

// ~bs
function responseBody(regex){
    regex = new RegExp(regex, "i");
    function responseBodyFilter(flow){
        return true;
    }
    responseBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
    return responseBodyFilter;
}

// ~c
function responseCode(code){
    function responseCodeFilter(flow){
        return flow.response && flow.response.status_code === code;
    }
    responseCodeFilter.desc = "resp. code is " + code;
    return responseCodeFilter;
}

// ~comment
function comment(regex){
    regex = new RegExp(regex, "i");
    function commentFilter(flow){
        return regex.test(flow.comment)
    }
    commentFilter.desc = "comment matches " + regex;
    return commentFilter;
}

// ~d
function domain(regex){
    regex = new RegExp(regex, "i");
    function domainFilter(flow){
        return flow.request && (regex.test(flow.request.host) || regex.test(flow.request.pretty_host));
    }
    domainFilter.desc = "domain matches " + regex;
    return domainFilter;
}

// ~dns
function dnsFilter(flow){
    return flow.type === "dns";
}
dnsFilter.desc = "is a DNS Flow";

// ~dst
function destination(regex){
    regex = new RegExp(regex, "i");
    function destinationFilter(flow){
    return (!!flow.server_conn.address)
           &&
           regex.test(flow.server_conn.address[0] + ":" + flow.server_conn.address[1]);
    }
    destinationFilter.desc = "destination address matches " + regex;
    return destinationFilter;
}

// ~e
function errorFilter(flow){
    return !!flow.error;
}
errorFilter.desc = "has error";

// ~h
function header(regex){
    regex = new RegExp(regex, "i");
    function headerFilter(flow){
        return (
            (flow.request && flowutils.RequestUtils.match_header(flow.request, regex))
            ||
            (flow.response && flowutils.ResponseUtils.match_header(flow.response, regex))
        );
    }
    headerFilter.desc = "header matches " + regex;
    return headerFilter;
}

// ~hq
function requestHeader(regex){
    regex = new RegExp(regex, "i");
    function requestHeaderFilter(flow){
        return (flow.request && flowutils.RequestUtils.match_header(flow.request, regex));
    }
    requestHeaderFilter.desc = "req. header matches " + regex;
    return requestHeaderFilter;
}

// ~hs
function responseHeader(regex){
    regex = new RegExp(regex, "i");
    function responseHeaderFilter(flow){
        return (flow.response && flowutils.ResponseUtils.match_header(flow.response, regex));
    }
    responseHeaderFilter.desc = "resp. header matches " + regex;
    return responseHeaderFilter;
}

// ~http
function httpFilter(flow){
    return flow.type === "http";
}
httpFilter.desc = "is an HTTP Flow";

// ~marked
function markedFilter(flow){
    return flow.marked;
}
markedFilter.desc = "is marked";

// ~marker
function marker(regex){
    regex = new RegExp(regex, "i");
    function markerFilter(flow){
        return regex.test(flow.marked)
    }
    markerFilter.desc = "marker matches " + regex;
    return markerFilter;
}

// ~m
function method(regex){
    regex = new RegExp(regex, "i");
    function methodFilter(flow){
        return flow.request && regex.test(flow.request.method);
    }
    methodFilter.desc = "method matches " + regex;
    return methodFilter;
}

// ~q
function noResponseFilter(flow){
    return flow.request && !flow.response;
}
noResponseFilter.desc = "has no response";

// ~replayq
function clientReplayFilter(flow){
    return flow.is_replay === "request";
}
clientReplayFilter.desc = "request has been replayed";

// ~replays
function serverReplayFilter(flow){
    return flow.is_replay === "response";
}
serverReplayFilter.desc = "response has been replayed";

// ~replay
function replayFilter(flow){
    return !!flow.is_replay;
}
replayFilter.desc = "flow has been replayed";

// ~src
function source(regex){
    regex = new RegExp(regex, "i");
    function sourceFilter(flow){
        return (!!flow.client_conn.peername)
               &&
               regex.test(flow.client_conn.peername[0] + ":" + flow.client_conn.peername[1]);
    }
    sourceFilter.desc = "source address matches " + regex;
    return sourceFilter;
}

// ~s
function responseFilter(flow){
    return !!flow.response;
}
responseFilter.desc = "has response";

// ~tcp
function tcpFilter(flow){
    return flow.type === "tcp";
}
tcpFilter.desc = "is a TCP Flow";

// ~udp
function udpFilter(flow){
    return flow.type === "udp";
}
udpFilter.desc = "is a UDP Flow";

// ~tq
function requestContentType(regex){
    regex = new RegExp(regex, "i");
    function requestContentTypeFilter(flow){
        return flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request));
    }
    requestContentTypeFilter.desc = "req. content type matches " + regex;
    return requestContentTypeFilter;
}

// ~ts
function responseContentType(regex){
    regex = new RegExp(regex, "i");
    function responseContentTypeFilter(flow){
        return flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response));
    }
    responseContentTypeFilter.desc = "resp. content type matches " + regex;
    return responseContentTypeFilter;
}

// ~t
function contentType(regex){
    regex = new RegExp(regex, "i");
    function contentTypeFilter(flow){
        return (
            (flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request)))
            ||
            (flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response)))
        );
    }
    contentTypeFilter.desc = "content type matches " + regex;
    return contentTypeFilter;
}

// ~u
function url(regex){
    regex = new RegExp(regex, "i");
    function urlFilter(flow){
        if (flow.type === "dns") {
            const question = flow.request?.questions[0]
            return question && regex.test(question.name)
        }
        return flow.request && regex.test(flowutils.RequestUtils.pretty_url(flow.request));
    }
    urlFilter.desc = "url matches " + regex;
    return urlFilter;
}

// ~websocket
function websocketFilter(flow){
    return !!flow.websocket;
}
websocketFilter.desc = "is a Websocket Flow";
}

start "filter expression"
  = __ orExpr:OrExpr __ { return orExpr; }

ws "whitespace" = [ \t\n\r]
cc "control character" = [|&!()~"]
__ "optional whitespace" = ws*

OrExpr
  = first:AndExpr __ "|" __ second:OrExpr
    { return or(first, second); }
  / AndExpr

AndExpr
  = first:NotExpr __ "&" __ second:AndExpr
    { return and(first, second); }
  / first:NotExpr ws+ second:AndExpr
    { return and(first, second); }
  / NotExpr

NotExpr
  = "!" __ expr:NotExpr
    { return not(expr); }
  / BindingExpr

BindingExpr
  = "(" __ expr:OrExpr __ ")"
    { return binding(expr); }
  / Expr

/* All the filters are generally arranged in the order as they are described
   on https://docs.mitmproxy.org/dev/concepts-filters/, with the exception of
    single-letter filters, which are moved to the bottom so that they evaluate properly */

Expr
  = "~all" { return allFilter; }
  / "~a" { return assetFilter; }
  / "~b" ws+ s:StringLiteral { return body(s); }
  / "~bq" ws+ s:StringLiteral { return requestBody(s); }
  / "~bs" ws+ s:StringLiteral { return responseBody(s); }
  / "~c" ws+ s:IntegerLiteral { return responseCode(s); }
  / "~comment" ws+ s:StringLiteral { return comment(s); }
  / "~d" ws+ s:StringLiteral { return domain(s); }
  / "~dns" { return dnsFilter; }
  / "~dst" ws+ s:StringLiteral { return destination(s); }
  / "~e" { return errorFilter; }
  / "~h" ws+ s:StringLiteral { return header(s); }
  / "~hq" ws+ s:StringLiteral { return requestHeader(s); }
  / "~hs" ws+ s:StringLiteral { return responseHeader(s); }
  / "~http" { return httpFilter; }
  / "~marked" { return markedFilter; }
  / "~marker" ws+ s:StringLiteral { return marker(s); }
  / "~m" ws+ s:StringLiteral { return method(s); }
  / "~q" { return noResponseFilter; }
  / "~replayq" { return clientReplayFilter; }
  / "~replays" { return serverReplayFilter; }
  / "~replay" { return replayFilter; }
  / "~src" ws+ s:StringLiteral { return source(s); }
  / "~s" { return responseFilter; }
  / "~tcp" { return tcpFilter; }
  / "~udp" { return udpFilter; }
  / "~tq" ws+ s:StringLiteral { return requestContentType(s); }
  / "~ts" ws+ s:StringLiteral { return responseContentType(s); }
  / "~t" ws+ s:StringLiteral { return contentType(s); }
  / "~u" ws+ s:StringLiteral { return url(s); }
  / "~websocket" { return websocketFilter; }
  / s:StringLiteral { return url(s); }

IntegerLiteral "integer"
  = ['"]? digits:[0-9]+ ['"]? { return parseInt(digits.join(""), 10); }

StringLiteral "string"
  = '"' chars:DoubleStringChar* '"' { return chars.join(""); }
  / "'" chars:SingleStringChar* "'" { return chars.join(""); }
  / !cc chars:UnquotedStringChar+ { return chars.join(""); }

DoubleStringChar
  = !["\\] char:. { return char; }
  / "\\" char:EscapeSequence { return char; }

SingleStringChar
  = !['\\] char:. { return char; }
  / "\\" char:EscapeSequence { return char; }

UnquotedStringChar
  = !ws char:. { return char; }

EscapeSequence
  = ['"\\]
  / "n" { return "\n"; }
  / "r" { return "\r"; }
  / "t" { return "\t"; }
