php/net.php

<?php
require_once 'cloud.php';

Class Pipeline {
    /*----- cfgBase: build the base rowConfiguration (rwc)
        cfgBase converts the old base (the input) to a new base to work with later
        a configuration item is a list with 5 elements (see table below)
        cfgBase cleans up the items and replaces the old base with the new one, containing different maps (see table below)
        predefined rwc are in the new base with keys starting with a % - they will be lazily generated by the rowCfg function in the cloud

                                        item    cfg                     base new                        base old
        cpr     cloud property name     0       0 => cpr => item        0 => cpr => item                0 => list(item...)
        rwk     row Key                 1                               1 => rwk => list(item...)       1 => igMi
        meB     walk begin method       2
        meE     walk end method         3 
        rwPr    list of cpr for rwk     4       4 => list(all cpr)           

        rwEm    empty row: rwk => false         rwEm => rwEm
        rws     implode(' ', key rwk)           rws => rws
        nrPr    list(cpr) for no rwk                                    nrPr => list(cpr) for rwk=''
        nrPI    cpr => item if rwk=''                                   nrPI => cpr => item if rwk=''
        igMi    ignore missing rwk                                      igMi => rwk => 1                                                           
     -----*/
    public static function cfgBase(&$sCfg) { # cleanup the base config $src and generatebase 
        list ($src, $ignMiss) = $sCfg;
        $cpr = $rwk = $wNR = $fNR = [];
        foreach ($src as &$s) {
            for ($bx = 0; $bx < 4; $bx++)
                $s[$bx] = trim($s[$bx] ?? '');
            $s[4] = isset($s[4]) ? ($s[4] ? preg_split('/\s+/', $s[4], flags: PREG_SPLIT_NO_EMPTY) : []) : ($s[0] ? [$s[0]] : []);
            if ($s[0])
                isset($cpr[$s[0]]) ? err("duplicate cpr $s[0] in", $src) : $cpr[$s[0]] = $s;
            if ($s[1]) {
                $rwk[$s[1]][] = $s;
            } else {
                $wNR[$s[0]] = $s;
                array_push($fNR, ...$s[4]);
            }
        }
        $sCfg = [$cpr, $rwk, 'nrPI' => $wNR, 'nrPr' => $fNR, "igMi" => $ignMiss];
        $sCfg['%allAll']  = $k = array_keys($rwk);
        $sCfg['%all']  = array_filter($k, fn ($e) => ! str_starts_with($e, 'version'));
        foreach(Cloud::ROWKYS as $nm => $flds)
            $sCfg[$nm] = ! isset($sCfg[$nm]) ? $flds : err("rcw key $nm already defined:", array_keys($sCfg));
    }

    /*----- add a cfg to $c with name $cfgN and keys $kys 
            indexes: walk => by wlk ix, row => $rwkys empty => empty row , flds => fields 
    -----*/
    public static function rowCfg($c, $rwKys) { 
        dbg1(__METHOD__, $c, "rwKys", $rwKys); 
        is_string($rwKys) and $rwKys = preg_split('/\s+/', $rwKys, flags: PREG_SPLIT_NO_EMPTY);
        $wlk = $c['nrPI']; #walk indexes without row key
        $empty = [];
        $flds = $c['nrPr']; #fields without row key
        foreach($rwKys as $k) {
            if ( ! $aLi = $c[1][$k] ?? null) {
                if (isset($c['igMi'][$k]))
                    continue;
                else
                    err("unknow rowKey $k");
            }
            $empty[$k] = false; # not null because isset($empty[$k]) should yield true !
            foreach($aLi as $a) {
                if ($a[0]) {
                    if (isset($wlk[$a[0]]))
                        err("cfgAdd $a[0] already set in ele, keys", $wlk);
                    $wlk[$a[0]] = $a;
                }
                array_push($flds, ...$a[4]);
            }
        }
        return [0 => $wlk, 'rws' => implode(' ', $rwKys),  'rwEm' => $empty,  4 => $flds];
    }

    public $doBits  # a bit mask, may be different for pipelines with the identical ppl
        , $ppc     # this is an object, shared by all pipe of it, so changes are visible for all pipelines with this ppl
        , $outFD;
    public function __construct($clOrPi, $doBi=Net::PHST, $rwc=null) {
        if ($clOrPi instanceof Pipeline) {
            $rwc and err("rwc not null with pipeline", $clOrPi);
            $this->ppc = $clOrPi->ppc;
        } else {
            $rwc or err("rwc null with cloud", $clOrPi);
            $this->ppc = (object) ['cloud' => $clOrPi, 'rwcW' => $rwc, 'rwcR' => $rwc, 'rowWrite2' => null, 'out' => null, 'outFD' => null];
        }
        $this->doBits = is_int($doBi) ? $doBi : (ctype_digit($doBi) ? (int) $doBi : Net::s2bits($doBi));
    }

    public function setBits($doBi) { $this->doBits = $doBi; return $this; }

    public function makeWalk($trg) {
       if (str_starts_with($trg, 'meta')) {
            $this->ppc->metaDo = 1;
            $trg = substr($trg, 4);
        }
        $this->ppc->rowWriter = new $trg;
        return $this;
    }

    public function outBegin() {
        return (($p = $this->ppc)->outFD) ? err("outFD already exists") : $p->outFD = fopen(is_string($p->out) ? $p->out : 'php://temp', 'w+');
    }
 
    public function outEnd() { 
        if (! $fd = ($p = $this->ppc)->outFD)
            err("no outFD");
        $p->outFD = null;
        return is_string($p->out) ? fclose($fd) : $fd;
    }

    public function request($uri, $ctx, ...$aa) {
        dbg1("request $uri", $this);
        $p = $this->ppc; 
        $doBits = $this->doBits;
        foreach ($aa as $k => $v)
            $p->$k = $v;
        $res = 0;
        if ($doBits & Net::W1) {
            $res = $p->req->request($uri, $ctx);
            dbg(5, "req got", $res);
            if (isset($p->httpChk) and $p->httpChk !== $p->req->rspH[0])
                throw new NetEx("unexpected status {$p->req->rspH[0]} expected $p->httpChk", rspBody: $res, uri: $uri, rspH: $p->req->rspH);
            if (isset($p->json)) {
                $res =  json_decode($o=$res, $p->json);
                if (('No error' !== $l = json_last_error_msg()) or ! $res) 
                    throw new NetEx("json err $l", res: $res, json: $l, input: $o);
            }
        }
        if ( ! isset($p->walk)) # config/begin/end is only necessary with walk !
            return $res;

        if ($doBits & Net::CONFIG) {
            $p->rowWrite2 and $p->rowWrite2->config($this, $p->out);  #$p->walk->beginpPath = $pPath ?? $p->cloud->pPath ?? null;   # todo: am richtigen Ort?
            $p->rowWriter->config($this, $p->rowWrite2 ?? $p->out);
            $p->walk->config($this, $p->rowWriter);
         }
        if ($doBits & Net::BEGIN) {
            $doBits & Net::OUT and $p->out and $this->outBegin();
            if (isset($p->metaDo))
                ($p->rowWrite2 ?? $p->rowWriter)->single('meta', $p->meta);
            if ($doBits & Net::RWR) {
                $p->rowWrite2 and $p->rowWrite2->begin();
                $p->rowWriter->begin();
            }
        }
        $doBits & Net::W1 and $p->walk->walk($res);

        if ($doBits & Net::END) {
            if ($doBits & Net::RWR) {
                $res = $p->rowWriter->end();
                $p->rowWrite2 and $res = $p->rowWrite2->end();
            }
            return ($doBits & Net::OUT and $p->out) ? $this->outEnd() : $res;
        }
    }
} # end Class Pipeline


function netEx($m, ...$a) {
    $a['NetExCall'] =  1 + ($a['NetExCall'] ?? 0);
    if (($h = $a['rspH'] ?? false) and $h[0] !== 'HTTP/1.1 200 OK')
        throw new (($ct = NetEx::STA2CT[$h[0]] ?? ['NetEx', $h[0]])[0])("$m $ct[1]", ...$a);
    elseif ($e = error_get_last())
        throw strpos($f = $e['message'], 'Failed to open stream: No such fil') !== false ? new NetNotFound("$m No such file or directory", ...$a) : new NetEx("$m $f", ...$a);
    else 
        throw new NetEx($m, ...$a);
}

class NetEx extends Exception {
    public const STA2CT =
           [ 'HTTP/1.1 403 Forbidden'           => ['NetDuplicate', 'already exists but no directory']
           , 'HTTP/1.1 404 Not Found'           => ['NetNotFound', '']
           , 'HTTP/1.1 405 Method Not Allowed'  => ['NetDuplicate', 'already exists']
           , 'HTTP/1.1 409 Conflict'            => ['NetNotFound', 'parent missing']
           ];
    /*
        check errors and put them in the array $a
        if $all check all errors and return error message
        if ! $all check only errors necessary to determine exception class and return its name
    */
    public static $infoLv = 9;

    public static function errCheck($a) {
        if (isset($a['rspH']) and ($e = $a['rspH'][0]) !== 'HTTP/1.1 200 OK') 
            $n['rspE'] = $a['rspE'] ?? $e; 
        if ($e = error_get_last()) {
            $n['errLast'] = $a['errLast'] ?? "$e[message] at $e[file]:$e[line]";  
            error_clear_last();
        }
        if ($e = libxml_get_last_error()) {
            $n['xml'] = $a['xml'] ?? $e->message; 
            libxml_clear_errors();
        }
        if (($e = json_last_error_msg()) !== 'No error') {
            $n['json'] = $a['json'] ?? $e; 
            json_decode('{}'); # decoding a correct json clears the error - I do not know another method ...
        }
        return $n ?? null;
    }

    public static function errClear($silent=false) {
        $a = self::errCheck([]);
        if (! $silent and $a) 
            out('clearing',  count($a), 'errors', $a);
    }

    public function __construct(...$a) {
        $a[0] ??= ($t = $this->getTrace()[$a['NetExCall'] ?? 0] ?? false) ? (($t['class'] ? "$t[class]$t[type]" : '') . "$t[function]()") : '?';
        Exception::__construct($a[0]);
        $this->det = $a;
    }

    public $det;
    public function __toString() {
        global $dbg;
        if (! $c = self::errCheck($this->det)) {
            $f = "errChck none";
        } else {
            $this->add(...$c);
            $f = reset($c);
        }
        if (strlen($m = trim($this->det[0])) > 55)
            $m = substr($m, 0, 50) . ' ...';
        elseif (strlen($m) < 40 and strpos($m, $g = substr($f, 0, 50-strlen($m))) === false)
            $m .= " $g" . ($f === $g ? '' : ' ...');
        $r = get_class($this) . " $m";
        if ($lv = self::$infoLv ?? $dbg) {
            foreach ($this->det as $k => $v) {
                if (($k === 0 and strlen($v) < 50) or ($k === 'rspH' and $lv < 2)  or $k === 'NetExCall')
                    continue;
                $w = trim(a2str($v));
                $r .= "\n$k => " . (($lv === 1 and strlen($w) > 125) ? substr($w, 0, 120) . "..." : $w);
            }
        }
        $tx = ($this->det['NetExCall'] ?? 0) -1;
        $r .= "\n" . (($t = $this->getTrace()[$tx+1] ?? false) ? ("by " . ($t['class'] ? "$t[class]$t[type]" : '') . "$t[function]() ") : '')
                   ."in " . ($tx < 0 ? $this->getFile() . ':' . $this->getLine() 
                           : ($t = $this->getTrace()[$tx])['file'] . ":$t[line] $t[function]()");
        if ($lv)
            $r .= "\nstack trace\n" . $this->getTraceAsString();
        return $r; # (self::$html ?? OutHtml) ? str_replace("\n", "\n<br>", $r) : $r;
    }
    
    /*----- add the given key/values at the beginning of det -----*/
    public function add(...$a) { 
        array_key_exists(0, $a) or array_unshift($a, $this->det[0] ?? '?');  
        foreach($this->det as $k => $v)
            ! isset($a[$k]) ? $a[$k] = $v : (is_string($a[$k]) and is_string($v) and strpos($a[$k], $v) === false and (strpos($v, $a[$k]) === false ? $a[$k] .= "; $v" : $a[$k] = $v));
        $this->det = $a;
        return $this;
    } 
} # end class NetEx

class NetNotFound extends NetEx { }
class NetDuplicate extends NetEx { }

Class Net {
    public const
          BEGIN =  1    #--- 4 phases in the bitmask $doBits
        , W1    =  2
        , END   =  4
        , CONFIG=  8
        , PHASE = Net::BEGIN | Net::W1 | Net::END | Net::CONFIG
        , RWR = 16    #--- 2 steps (after the first two steps request and walk - which are governed by pipeline target) in the bitmask $doBits
        , OUT = 32
        , STEP  = Net::RWR | Net::OUT
        , PHST  = Net::PHASE | Net::STEP
                        #--- operational options  in the bitmask $doBits
        , RECUR = 1<<8  # walk/recurse thru the ff tree
        , WRIPA = 1<<9  # ff write parent/start directory
        , CREPA = 1<<10 # create parent directories if necessary (recursive)
        , KRF   = 1<<11 # Keep Revision (Version) Forever
        , OVERWR= 1<<12 # overwrite existing for copy etc.
    ;

    public static function s2bits($s) { # return the http context array for a mime multipart request
        static $cnsts;
        $cnsts ??= (new ReflectionClass(__CLASS__))->getConstants();
        $r = 0;
        foreach(preg_split('/\s+/', $s, flags: PREG_SPLIT_NO_EMPTY) as $t)
            $r |= $cnsts[$t] ?? err("not a Net constang $t");
        return $r;
    }

    public static function mimeMultipart(...$parts) { # return the http context array for a mime multipart request
        $cnt = '';
        $bndry = 'seParaTor[}{)(]';
        for($px=0; $px < count($parts); $px+=2) { 
            $c1 = $parts[$px+1] ?? '';
            $cnt .= "\n--$bndry\nContent-Type: {$parts[$px]}\nContent-Length: " . strlen($c1) ."\n\n$c1";
        }
        $cnt .= "\n--$bndry--\n";
        return ['header' => "Content-Type: Multipart/Related; boundary=$bndry\nContent-Length: " . strlen($cnt)
               ,'content' => $cnt]; 
    }
    public $rspH;
} #end Class Net

Class Net2Str extends Net {
    public function request($uri, $ctx) {
        NetEx::errClear();
        dbg1(__METHOD__, "uri $uri, ctx", $ctx);
        $r = @ file_get_contents($uri, 0, $ctx);
        $h = $this->rspH = $http_response_header;
        dbg1(__METHOD__, "got", is_string($r) ?  "len " . strlen($r) . ': ' . xmlPP($r) : $r, "rspH", $h);
        return is_string($r) ? $r : throw new (($ct = NetEx::STA2CT[$h[0]] ?? ['NetEx', 'error response'])[0])($ct[1], uri: $uri, rspH: $h);
    }
}

Class Net2XMLReader extends Net {
    public $xr;
    public function request($uri, $ctx) {
        NetEx::errClear();
        # libxml_use_internal_errors(true); done in errClear
        $ctx and libxml_set_streams_context($ctx);
        $this->xr ??= new XMLReader;
        dbg(3, "xmlReader request to uri $uri");
        $r = @$this->xr->open($uri);
        $h = $this->rspH = $http_response_header;
        dbg1(__METHOD__, "got", $http_response_header);
        return $r ? $this->xr : throw new (($ct = NetEx::STA2CT[$h[0]] ?? ['NetEx', 'error response'])[0])("XMLReader open $ct[1]", uri: $uri, rspH: $h);
    }
}

Class Req2SimpleXML {
    public function webRequest($uri, $ctx, $cld) {
        libxml_set_streams_context($ctx);
        $xml = simplexml_load_file($uri,options: LIBXML_PEDANTIC);
        var_dump($xml);
        print_r($xml);
        for ($xml->rewind(); $xml->valid(); $xml->next()) {
            foreach($xml->getChildren() as $name => $data) {
            echo "The $name is '$data' from the class " . get_class($data) . "\n";
            }
        }
        foreach($xml->children() as $c) out("child $c", $c->getName());
        out('getName', $xml->getName());
        out('children', $xml->getChildren());
        out('rsp getName', $xml->response[0]->getName());
        out('rsp children', $xml->response->children());
        return $xml;
        $r = file_get_contents($uri, 0, $ctx);
        // echo "ff response\n" . print_r($http_response_header, true). "\n";
        if ($r == '' or $http_response_header[0] != 'HTTP/1.1 207 Multi-Status')
            netEx("$uri bad response", rspH: $http_response_header);
        /* echo "\nff result\n" . preg_replace_callback('=(<d:getlastmodified>)([^<>]*)(</d:getlastmodified>)='
            , fn ($m) => $m[1] . $m[2] . ' => ' . toLocalTst($m[2]) . $m[3]
            , xmlpp($r)) . "\n"; */
        return $r;
    }
}

class RowWriter {
    public $writeC = 'writeCconstruct', $id, $rwEm;
    public function config($ppl, $next) { $this->id = $ppl->ppc->meta['fun'] ?? '?'; $this->rwEm = $ppl->ppc->rwcR['rwEm']; }
    public function single ($id, $row, $doBits = Net::PHASE) { # a sequence of a single row
        $idO = $this->id;
        $rwEmO = $this->rwEm;
        $this->id = $id;
        $this->rwEm = $row;
        $doBits & Net::BEGIN and $this->begin();
        $doBits & Net::W1 and $this->write($row);
        $doBits & Net::END and $r = $this->end();
        $this->id = $idO;
        $this->rwEm = $rwEmO;
        return $r ?? null;
    }
} # end class RowWriter
    
class RowOut extends RowWriter {
    public function begin () { # write header info for the following rows
        $this->writeC = 0;
        out("RowWriter $$this->id begin", array_keys($this->rwEm));
    } 
    public function write($row) { # write one row
        out("row $this->writeC", $row);
        $this->writeC++;
    } 
    public function end() { # end of the row sequence
        out("RowWriter $this->id end, $this->writeC rows");
    } 
    public function single($id, $row, $doBits=Net::PHASE) { # a sequence of a single row ????
        ($doBits & Net::W1) ? out("RowWriter $this->id single", $row) : parent::single($id, $row, $doBits);
    } 
} # end class RowOut

class RowTb  extends RowWriter {
    public function begin () { # write header info for the following rows
        outH("tbh $this->id", __CLASS__);
        $this->writeC = 0;
        outTb();
        if ($this->rwEm)
            outTRH(...array_keys($this->rwEm));
    } 

    public function write($row) { # write one row
        outTRD(...array_values($row));
        $this->writeC++;
    }
 
    public function end() { # end of the row sequence
        outTbEnd();
        out("tbh $this->id end, $this->writeC rows", __CLASS__);
    } 
} # end class RowTb

class Row1  extends RowWriter {
    public $result;
    public function begin () { $this->result = 0; $this->writeC = 0;}

    public function write($row) { # write one row
        if ($this->result)
            throw new NetDuplicate(__METHOD__ . " only one row allowed", newRow: $row, oldResult: $this->result); 
        $this->result = $row;
        $this->writeC++;
    }
 
    public function end() { return $this->result ? $this->result : throw new NetNotFound; } #Ex(NetEx::NOTFOUND, __METHOD__) ; } 

    public function single ($id, $row, $doBits=Net::PHASE) { return ($doBits & Net::W1) ? $this->result = $row : parent::single($id, $row, $doBits); } 
} # end class Row1

class Rows extends RowWriter {
    public $result;
    public function begin () { $this->result = []; $this->writeC = 0; } 

    public function write($row) { $this->result[] = $row; $this->writeC++; }
 
    public function end() { return $this->result; } 

    public function single ($id, $row, $doBits=Net::PHASE) { return ($doBits & Net::W1) ? $this->result = [$row] : parent::single($id, $row, $doBits); } 
}# end class Rows

class RowCsv extends RowWriter {
    private $outFD, $ppl;
    public function config($ppl, $next) { parent::config($ppl, $next); $this->ppl = $ppl; $ppl->ppc->out ??= true; }
    public function begin () {
        $this->outFD = $this->ppl->ppc->outFD;
        if ($this->rwEm) {
           fwrite($this->outFD, "///tbh $this->id\n");
           fputcsv($this->outFD, array_keys($this->rwEm));
        } else {
           fwrite($this->outFD, "///tb $this->id\n");
        }
        $this->writeC = 0;
    }
    public function write($row) { $this->writeC++; fputcsv($this->outFD, array_values($row));}
 
    public function end() { fwrite($this->outFD, "///tbEnd $this->id - $this->writeC rows\n"); } 

}# end class RowCsv

class RowWorker extends RowWriter {
    public $writerNext;
    public function __construct(...$aa) {
        foreach ($aa as $k => $v)
            $this->$k = $v;
    }

    public function config($ppl, $next) { parent::config($ppl, $next); $this->writerNext = $next; }
} # end class RowWorker

class WalkTree /* implements Stringable */ {
    public const SIBLING = 99;

    /*  build base config: trim $src, 
        indexes: 0 => by walk ix, 1 => by row key 2 => by wlkIx if rowkey='', 3 => default cloud fields, 4 => ignoreMiss, 5 src */
 
    public $e2m, $allB, $allE, $row, $rowWriter, $rowH, $rwEm, $nst, $pPath, $out, $ppl, $typeMap;

    public function __construct($allB = null, $allE = null) {
        $this->allB = $allB;
        $this->allE = $allE;
        $this->row = 0;
    }

    public function config($ppl, $next) {
        $this->ppl = $ppl;
        $this->e2m = $ppl->ppc->rwcW[0];
        $this->rwEm = $ppl->ppc->rwcW['rwEm'];
        $this->rowWriter = $next;
    }

    public function aText($mm, $tx) { $this->row[$mm[1]] = $tx; } # assign text unchanged into row

    public function aTst($mm, $tx) { $this->row[$mm[1]] = toLocalTst($tx); } # assign string timestamp converted to local timestamp

    public function aUTi($mm, $tx) { $this->row[$mm[1]] = 0 == $tx ? '' : Date(DTLFMT, $tx); } # assign unix timestamp converted to local timestamp

    public function aDir($mm) { $this->row[$mm[1]] = CLOUD::DIR; } # assign directory constant

    public function aType($mm, $tx) { $this->row[$mm[1]] = $this->typeMap[$tx] ?? $tx; } # assign the mapped mimeType

    public function aPpl($mm, $tx) { # assing text to ppl
        $k = $mm[0];
        if (! is_object($this->ppl ?? null))
            err("aPpl $k ppl missing or bad", $this->ppl ?? '') ;
        elseif ($this->ppl->ppc->$k ?? false)
            err("aPpl $k already set: {$this->ppl->ppc->$k}, new:", $tx) ;
        else
            $this->ppl->ppc->$k = $tx;
        return self::SIBLING;
    }

}  # end class WalkTree 

class WalkXML extends WalkTree {
    private $xr, $propC, $propS, $old;
    public function __toString() {
        $xr = $this->xr;
        return '{' . __CLASS__ . "->xr $xr->nodeType: $xr->name}";
    }

    public function config($ppl, $next) {
        parent::config($ppl, $next);
        dbg1(" ---config 1", $this->rwEm, isset($this->rwEm['name']), isset($this->rwEm['path']), 'seq', );
        $this->old = ($ppl->ppc->cloud->flags ?? false) && isset($this->rwEm['name']) && isset($this->rwEm['path']);
        dbg1(" ---config 2 $this->old", $this->rwEm, isset($this->rwEm['name']), isset($this->rwEm['path']));
    }

    function walk($xr) { # walk the Tree by the XMLReader $xr
        $this->nst = '';
        ($this->xr = $xr)->read() or $this->rChk();
        $this->walkRec();
        $xr->nodeType === XMLReader::NONE or netEx("not at end after walk $this");
    }

    public function rChk () {
        static $fCnt = 0;
        ++$fCnt;
        # out("rChk $fCnt $this, last", libxml_get_last_error(), ", errors", libxml_get_errors(), ', error_get_last', error_get_last());
        if ($this->xr->nodeType !== XMLReader::NONE or libxml_get_last_error() or error_get_last())
            netEx("rChk $this");
        ($fCnt > 30) and netEx("too many XML errs");
        return false;
    }

    function walkRec() {
        $nm = ($xr = $this->xr)->name;
        $pn = $this->nst = "$this->nst/$nm";
        $mm = $this->e2m[$nm] ?? null;
        $r = $this->allB ? $this->{$this->allB}($mm) : (($mm[2] ?? false) ? $this->{$mm[2]}($mm) : null);
        if ($xr->isEmptyElement) 
            return $xr->read() or $this->rChk();
        elseif ($r === self::SIBLING)
            return $xr->next();
        $txt = '';
        $xr->read() or $this->rChk();
        while (XMLReader::END_ELEMENT !== $ty = $xr->nodeType) {
            # out("     ty $this->nodeType name $this->name");
            if ($ty === XMLReader::TEXT) {
                $txt .= $xr->value;
                $xr->read() or $this->rChk();
            } elseif ($ty === XMLReader::ELEMENT) {
                $this->walkRec();
                $this->nst = $pn;
            } else {
                netEx("unsported xml node", $this);
            }
        }
        $this->allE ? $this->{$this->allE}($mm, $txt) : (($mm[3] ?? false) ? $this->{$mm[3]}($mm, $txt) : null);
        if ($nm === $xr->name)
            return $xr->read() or $this->rChk();
        netEx("end $pn expected but $this");
    }

    public function outB($mm) {
        $xr = $this->xr;
        out(($xr->isEmptyElement ? 'emp' : 'beg') ." $this->nst mm", $mm);
        for($i=0; $xr->moveToNextAttribute();$i++)
            out("  att $i: $xr->name => $xr->value");
        if ($mm[2] ?? false) {
            if (($r = $this->{$mm[2]}($mm)) === self::SIBLING and ! $xr->isEmptyElement)
                out("nxt $this->nst skipping to next node");
            return $r;
        }
    }

    public function outE($mm, $txt) {
        out("end $this->nst, txt=$txt");
        if ($mm[3] ?? false)
            return $this->{$mm[3]}($mm, $txt);
    }
    
    public function responseB($mm) {
        $this->row = $this->rwEm;
        $this->propC = $this->propS = -1;
    }

   public function responseE($mm) {
        ($this->propC === $this->propS) or netEx(__METHOD__ . "#prop $this->propC !==  #status $this->propS");
        if ($this->old and ! $this->row['name'] and $this->row['path'])
            $this->row['name'] = basename($this->row['path']);
        $this->rowWriter->write($this->row);
        $this->row = 0;
        unset($this->propC);
    }

    public function hrefE($mm, $tx) {
        ('' === $pa = str_starts_with($pa = rawurldecode($tx), $this->ppl->ppc->pPath) ? substr($pa, strlen($this->ppl->ppc->pPath)) : "???$pa") and $pa = Cloud::ROOT;
        $this->row[$mm[1]] = $pa;
        # $this->path = $pa;
    }
           
    public function propB($mm) {
        ($this->propC === $this->propS) or netEx(__METHOD__ . " #prop $this->propC !==  #status $this->propS");
        $c = ++$this->propC;
        return $c ? self::SIBLING : null;
    }

    public function statusE($mm, $tx) {
        ($this->propC !== $this->propS-1) or netEx(__METHOD__ . "#prop $this->propC !== -1 + #status $this->propS");
        $c = ++$this->propS;
        $c === 0 and $tx === 'HTTP/1.1 200 OK' or $c === 1 and $tx === 'HTTP/1.1 404 Not Found' or netEx("statusE #$c bad status $tx");
     }
}  # end class WalkXML

class WalkJson extends WalkTree {

    function walk($js) { # walk the Tree of the json $js
        $this->nst = '';
        #$this->filesCnt = false;
        $this->walkRec($js);
    }

    function walkRec($js) {
        $pa = $this->nst;
        if (! is_array($js)) {
            netEx("walkRec not an array at $pa, js", $js);
        } elseif (array_is_list($js)) {
           foreach ($js as $w => $c) {
                $this->nst = "$pa/$w";
                $this->walkRec($c);
            }
        } else {
            if (! isSet($js['files']))
                $this->row = $this->rwEm;
           foreach ($js as $w => $c) {
                $mm = $this->e2m[$w] ?? null;
                $this->nst = $pc = "$pa/$w";
                $r = isset($this->allB) ? $this->{$this->allB}($mm, $c) : (($mm[2] ?? false) ? $this->{$mm[2]}($mm, $c) : 0);
                if ($r !== self::SIBLING and is_array($c)) {
                    $this->nst = $pc;
                    $this->walkRec($c);
                    $this->nst = $pc;
                    $r = isset($this->allE) ? $this->{$this->allE}($mm, $c) : (($mm[3] ?? false) ? $this->{$mm[3]}($mm, $c) : 0);
                }
            }
            
            if (! isSet($js['files'])) {
                $this->row and $this->rowWriter->write($this->row);
                $this->row = 0;
            }
        }
    }

    public function outB($mm, $js) {
        is_array($js) ? out("beg $this->nst", $mm) : out("b-e $this->nst", $js, $mm);
        if ($mm[2] ?? false) 
            return $this->{$mm[2]}($mm, $js);
    }

    public function outE($mm, $js) {
        if ($mm[3] ?? false) 
            return $this->{$mm[3]}($mm, $js);
        out("end $this->nst", $js);
    }
    public function parentB($mm, $js) {
        $this->row[$mm[1]] = $js[0];
        return self::SIBLING;
    }

    public function ownerB($mm, $js) {
        $this->row[$mm[1]] = $js[0]['emailAddress'];
        return self::SIBLING;
    }

} # end class WalkJson