TRUE, 'areadesc' => FALSE, 'ORIGIN' => TRUE, 'FROM' => TRUE, 'to' => FALSE, 'FILE' => TRUE, // 8.3 DOS format 'lfile' => FALSE, // alias fullname 'fullname' => FALSE, 'size' => FALSE, 'date' => FALSE, // File creation date 'desc' => FALSE, // One line description of file 'ldesc' => FALSE, // Can have multiple 'created' => FALSE, 'magic' => FALSE, 'replaces' => FALSE, // ? and * are wildcards, as per DOS 'CRC' => TRUE, // crc-32 'PATH' => TRUE, // can have multiple: [FTN] [unix timestamp] [datetime human readable] [signature] 'SEENBY' => TRUE, 'pw' => FALSE, // Password ]; private File $fo; private Filearea $area; private Collection $values; private Address $origin; // Should be first address in Path private Address $from; // Should be last address in Path private Address $to; // Should be me public function __construct() { $this->fo = new File; $this->fo->kludges = collect(); $this->fo->set_path = collect(); $this->fo->set_seenby = collect(); $this->fo->rogue_seenby = collect(); $this->values = collect(); } public function __get(string $key): mixed { switch ($key) { case 'fo': return $this->{$key}; default: return parent::__get($key); } } /** * Generate a TIC file for an address * * @param Address $ao * @param File $fo * @return string */ public static function generate(Address $ao,File $fo): string { $sysaddress = Setup::findOrFail(config('app.id'))->system->match($ao->zone)->first(); $result = collect(); // Origin is the first address in our path $result->put('ORIGIN',$fo->path->first()->ftn3d); $result->put('FROM',$sysaddress->ftn3d); $result->put('TO',$ao->ftn3d); $result->put('FILE',$fo->name); $result->put('SIZE',$fo->size); if ($fo->description) $result->put('DESC',$fo->description); if ($fo->replaces) $result->put('REPLACES',$fo->replaces); $result->put('AREA',$fo->filearea->name); $result->put('AREADESC',$fo->filearea->description); if ($x=$ao->session('ticpass')) $result->put('PW',$x); $result->put('CRC',sprintf("%X",$fo->crc)); $out = ''; foreach ($result as $key=>$value) $out .= sprintf("%s %s\r\n",$key,$value); foreach ($fo->path as $o) $out .= sprintf("PATH %s %s %s\r\n",$o->ftn3d,$o->pivot->datetime,$o->pivot->extra); // Add ourself to the path: $out .= sprintf("PATH %s %s\r\n",$sysaddress->ftn3d,Carbon::now()); foreach ($fo->seenby as $o) $out .= sprintf("SEENBY %s\r\n",$o->ftn3d); $out .= sprintf("SEENBY %s\r\n",$sysaddress->ftn3d); return $out; } /** * Does this TIC file bring us a nodelist * * @return bool */ public function isNodelist(): bool { return (($this->fo->nodelist_filearea_id === $this->fo->filearea->domain->filearea_id) && (preg_match(str_replace(['.','?'],['\.','.'],'#^'.$this->fo->filearea->domain->nodelist_filename.'$#i'),$this->fo->name))); } /** * Load a TIC file from an existing filename * * @param string $sp * @param string $filename Relative to filesystem * @return void * @throws FileNotFoundException */ public function load(string $filename): void { Log::info(sprintf('%s:+ Processing TIC file [%s]',self::LOGKEY,$filename)); $fs = Storage::disk(config('fido.local_disk')); if (str_contains($filename,'-')) { list($hex,$name) = explode('-',$filename); $hex = basename($hex); } else { $hex = ''; } if (! $fs->exists($filename)) throw new FileNotFoundException(sprintf('File [%s] doesnt exist',$filename)); if (! is_readable($fs->path($filename))) throw new UnableToReadFile(sprintf('File [%s] is not readable',realpath($filename))); $f = $fs->readStream($filename); if (! $f) { Log::error(sprintf('%s:! Unable to open file [%s] for reading',self::LOGKEY,$filename)); return; } $ldesc = ''; while (! feof($f)) { $line = chop(fgets($f)); $matches = []; if (! $line) continue; preg_match('/([a-zA-Z]+)\ ?(.*)?/',$line,$matches); if (in_array(strtolower(Arr::get($matches,1,'-')),$this->_kludge)) { switch ($k=strtolower($matches[1])) { case 'area': $this->{$k} = Filearea::singleOrNew(['name'=>strtoupper($matches[2])]); break; case 'origin': case 'from': case 'to': $this->{$k} = Address::findFTN($matches[2]); if (! $this->{$k}) Log::alert(sprintf('%s:! Unable to find an FTN for [%s] for the (%s)',self::LOGKEY,$matches[2],$k)); break; case 'file': $this->fo->name = $matches[2]; $this->fo->prefix = $hex; if (! $fs->exists($this->fo->recvd_rel_name)) { // @todo Fail this, so that it is rescheduled to try again in 1-24hrs. throw new FileNotFoundException(sprintf('File not found? [%s]',$fs->path($this->fo->recvd_rel_name))); } break; case 'areadesc': $areadesc = $matches[2]; break; case 'created': // ignored break; case 'pw': $pw = $matches[2]; break; case 'lfile': $this->fo->lname = $matches[2]; break; case 'desc': case 'magic': case 'replaces': case 'size': $this->fo->{$k} = $matches[2]; break; case 'fullname': $this->fo->lfile = $matches[2]; break; case 'date': $this->fo->datetime = Carbon::createFromTimestamp($matches[2]); break; case 'ldesc': $ldesc .= ($ldesc ? "\r" : '').$matches[2]; break; case 'crc': $this->fo->{$k} = hexdec($matches[2]); break; case 'path': $this->fo->set_path->push($matches[2]); break; case 'seenby': $this->fo->set_seenby->push($matches[2]); break; } } else { $this->fo->kludges->push($line); } } if ($ldesc) $this->fo->ldesc = $ldesc; fclose($f); // @todo Add notifictions back to the system if ($this->fo->replaces && (! preg_match('/^'.$this->fo->replaces.'$/',$this->fo->name))) { Log::alert(sprintf('%s:! Regex [%s] doesnt match file name [%s]',self::LOGKEY,$this->fo->replaces,$this->fo->name)); $this->fo->replaces = NULL; } // Validate Size if ($this->fo->size !== ($y=$fs->size($this->fo->recvd_rel_name))) throw new \Exception(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->fo->size,$this->fo->recvd_rel_name,$y)); // Validate CRC if (sprintf('%08x',$this->fo->crc) !== ($y=$fs->checksum($this->fo->recvd_rel_name,['checksum_algo'=>'crc32b']))) throw new \Exception(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$this->fo->crc,$this->fo->recvd_rel_name,$y)); // Validate Password if ($pw !== ($y=$this->from->session('ticpass'))) throw new \Exception(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$pw,$this->from->ftn,$y)); // Validate Sender is linked (and permitted to send) if ($this->from->fileareas->search(function($item) { return $item->id === $this->area->id; }) === FALSE) throw new \Exception(sprintf('Node [%s] is not subscribed to [%s]',$this->from->ftn,$this->area->name)); // If the filearea is to be autocreated, create it if (! $this->area->exists) { $this->area->description = $areadesc; $this->area->active = TRUE; $this->area->show = FALSE; $this->area->notes = 'Autocreated'; $this->area->domain_id = $this->from->zone->domain_id; $this->area->save(); } $this->fo->filearea_id = $this->area->id; $this->fo->fftn_id = $this->origin->id; // If the file create time is blank, we'll take the files if (! $this->fo->datetime) $this->fo->datetime = Carbon::createFromTimestamp($fs->lastModified($this->fo->recvd_rel_name)); $this->fo->save(); } }