Во-первых, после того, как на прошлой неделе я перевёл документацию по jParser (после ознакомления
Во-вторых, ≈полгода назад Итак, пример будет двойным. Предлагаю вашему вниманию анализ заголовков писем фидонетовской эхопочты, хранимой в формате JAM. Этот формат популярен в Фидонете со времён далёких и незапамятных
Документацию по JAM можно без труда найти на разных фидошных BBS, но BBS свойственно со временем закрываться или менять адреса, так что для надёжности сошлюсь на собственное письмо пятилетней давности, в котором я эту документацию цитировал дословно и целиком. (Чешская BBS, которая тогда послужила мне источником, сейчас ужé закрылася. Призрачно всё в этом мире бушующем.)
Как там видно, заголовки писем фидонетовской эхопочты хранятся внутри
Гораздо менее видно другое важное обстоятельство: внутри
Код модуля для Node.js, читающего заголовки эхопочты
var fs = require('fs'); var jParser = require('jParser'); var ulong = 'uint32'; var ushort = 'uint16'; var JAM = function(echotag){ if (!(this instanceof JAM)) return new JAM(echotag); this.echotag = echotag; // Buffers: this.JHR = null; /* this.JDT = null; this.JDX = null; this.JLR = null; */ } JAM.prototype.readJHR = function(callback){ // (err) if (this.JHR !== null) callback(null); fs.readFile(this.echotag+'.JHR', function (err, data) { if (err) callback(err); this.JHR = data; callback(null); }); } JAM.prototype.ReadHeaders = function(callback){ // err, struct this.readJHR(function(err){ if (err) callback(err); var thisJAM = this; var parser = new jParser(this.JHR, { 'reserved1000uchar': function(){ this.skip(1000); return true; }, 'JAM0' : ['string', 4], 'FixedHeaderInfoStruct': { 'Signature': 'JAM0', 'datecreated': ulong, 'modcounter': ulong, 'activemsgs': ulong, 'passwordcrc': ulong, 'basemsgnum': ulong, 'RESERVED': 'reserved1000uchar', }, 'SubField': { 'LoID': ushort, 'HiID': ushort, 'datlen': ulong, 'Buffer': ['string', function(){ return this.current.datlen }] /* 'type': function(){ switch( this.current.LoID ){ case 0: return 'OADDRESS'; break; case 1: return 'DADDRESS'; break; case 2: return 'SENDERNAME'; break; case 3: return 'RECEIVERNAME'; break; case 4: return 'MSGID'; break; case 5: return 'REPLYID'; break; case 6: return 'SUBJECT'; break; case 7: return 'PID'; break; case 8: return 'TRACE'; break; case 9: return 'ENCLOSEDFILE'; break; case 10: return 'ENCLOSEDFILEWALIAS'; break; case 11: return 'ENCLOSEDFREQ'; break; case 12: return 'ENCLOSEDFILEWCARD'; break; case 13: return 'ENCLOSEDINDIRECTFILE'; break; case 1000: return 'EMBINDAT'; break; case 2000: return 'FTSKLUDGE'; break; case 2001: return 'SEENBY2D'; break; case 2002: return 'PATH2D'; break; case 2003: return 'FLAGS'; break; case 2004: return 'TZUTCINFO'; break; default: return 'UNKNOWN'; break; } } */ }, 'MessageHeader': { 'Signature': 'JAM0', 'Revision': ushort, 'ReservedWord': ushort, 'SubfieldLen': ulong, 'TimesRead': ulong, 'MSGIDcrc': ulong, 'REPLYcrc': ulong, 'ReplyTo': ulong, 'Reply1st': ulong, 'Replynext': ulong, 'DateWritten': ulong, 'DateReceived': ulong, 'DateProcessed': ulong, 'MessageNumber': ulong, 'Attribute': ulong, 'Attribute2': ulong, 'Offset': ulong, 'TxtLen': ulong, 'PasswordCRC': ulong, 'Cost': ulong, 'Subfields': ['string', function(){ return this.current.SubfieldLen; } ], /* 'Subfields': function(){ var final = this.tell() + this.current.SubfieldLen; var sfArray = []; while (this.tell() < final) { sfArray.push( this.parse('SubField') ); } return sfArray; }, */ 'AfterSubfields': function(){ var initial = this.tell(); var bytesLeft = thisJAM.JHR.length - initial - 4; var seekJump = 0; var sigFound = false; var raw = this; if (bytesLeft <= 0) return 0; do { this.seek(initial + seekJump, function(){ var moveSIG = raw.parse('JAM0'); if (moveSIG === 'JAM\0') { sigFound = true; /* if (seekJump > 0){ console.log( 'initial = ' + initial + ', seekJump = ' + seekJump + ', moveSIG = ' + moveSIG ); } */ } }); seekJump++; } while (!sigFound && (seekJump < bytesLeft) ); this.skip(seekJump-1); return seekJump-1; } }, 'JHR': { 'FixedHeader': 'FixedHeaderInfoStruct', 'MessageHeaders': function(){ var mhArray = []; while (this.tell() < thisJAM.JHR.length - 69) { mhArray.push( this.parse('MessageHeader') ); } return mhArray; } } }); callback(null, parser.parse('JHR')); }); } module.exports = JAM;
В этом наброске используется кэширование сырых данных
Типы данных из документации JAM (например,
В описании структуры SubField вы обнаружите закомментированное поле
Поле Subfields внутри структуры MessageHeader определено двумя способами. Первый (быстрый) считывает это поле как строку байтов размером SubfieldLen. Второй (закомментированный) полностью обрабатывает это поле, вычленяя подполя посредством jParser — если приложению, использующему модуль, в любом случае понадобятся метаданные из переменной части заголовка фидопочты, то чего же откладывать их анализ в долгий ящик.
Поле AfterSubfields содержит простой поиск строки из трёх
Число 69 в описании поля MessageHeaders в структуре JHR является «волшебным»; его цель в том, чтобы анализ не подбирался слишком близко к концу файла, где также можно ожидать мусорные данные.
Скорость анализа я проверил при помощи вот какого тестового скрипта:
var JAM = require('../'); var util = require('util'); console.log( new Date().toLocaleString() ); var blog = JAM('blog-MtW'); blog.ReadHeaders(function(err,data){ if (err) throw err; //console.log( util.inspect(data, false, Infinity, false) ); console.log( new Date().toLocaleString() ); });
Скрипт лежит в подкаталоге test, поэтому в первой строке использует обращение к родительскому каталогу, где текст основного модуля лежит в файле
Тестовые данные в файле
Прогон теста на одноядерном Pentium IV (2,2 ГГц) показывает, что заголовки обрабатываются
Это много для одной эхоконференции, потому что на узле Фидонета таких эхоконференций легко может быть больше сотни, и общее время анализа заголовков эхопочты получится многоминутным.
Остаётся разве что цинично заподозрить, что GoldED в начале работы считывает для быстроты не весь файл, а только одну заголовочную структуру FixedHeaderInfoStruct (данных из неё хватило бы для вывода числа сообщений в эхоконференциях, а больше GoldED в начале работы ничего
Код своего модуля (читальника заголовков JAM) я выложил на Гитхабе под свободной лицензией MIT.

