;; date.asm: Copyright (C) 2001 by Brian Raiter, under the GNU General ;; Public License. No warranty. ;; ;; Usage: date [-u] [=UTIME] [+FORMAT] ;; ;; +FORMAT provides a format string to use to display the time. The ;; special sequences recognized are: ;; ;; %a %A %b %B %c %C %d %D %e %G %h %H %I %j %k %l %m %M %n %p %r ;; %s %S %t %T %u %U %V %w %W %x %X %y %Y %z %Z %% ;; ;; See date(1) and strftime(3) for the complete description of what ;; each of these sequences displays. ;; ;; The -u option indicates that the time should be displayed in UTC, ;; instead of the local time zone. ;; ;; =UTIME is a non-standard extension. It specifies a Unix time, ;; given as the number of seconds since the start of 1970 UTC, to ;; display instead of the current time. ;; ;; $Id: date.asm,v 1.5 2006/02/09 08:03:58 konst Exp $ %include "system.inc" CPU 586 ;; The collection of values associated with the calendric ;; representation of a time. STRUC tmfmt .ct: resd 1 ; C time (seconds since the Epoch) .sc: resd 1 ; seconds .mn: resd 1 ; minutes .hr: resd 1 ; hours .yr: resd 1 ; year .hm: resd 1 ; hour of the meridian (1-12) .mr: resd 1 ; meridian (0 for AM) .wd: resd 1 ; day of the week (Sunday=0, Saturday=6) .w1: resd 1 ; day of the week (Monday=1, Sunday=7) .dy: resd 1 ; day of the month .mo: resd 1 ; month (one-based) .ws: resd 1 ; week of the year (Sunday-based) .wm: resd 1 ; week of the year (Monday-based) .wi: resd 1 ; week of the year according to ISO 8601:1988 .yi: resd 1 ; year for the week according to ISO 8601:1988 .yd: resd 1 ; day of the year .ce: resd 1 ; century (zero-based) .cy: resd 1 ; year of the century .zo: resd 1 ; time zone offset .zi: resb 6 ; time zone identifier .tz: resb 10 ; time zone name ENDSTRUC CODESEG ;; mmapfile takes a filename and returns a read-only pointer to its ;; contents. ;; ;; input: ;; ebx = pointer to NUL-terminated filename ;; ;; output: ;; eax = pointer to file's contents ;; sign flag is set if an error occurred ;; ebx and ecx are altered mmapfile: ;; After opening the file, the fstat system call is used to obtain the ;; size of the file. buf is used to hold the returned stat structure. xor ecx, ecx sys_open or eax, eax js .return xchg eax, ebx lea ecx, [byte edi + buf - tmf] pusha sys_fstat ;; An mmap structure is filled out and handed off to the system call, ;; and the function returns. ; mov [byte ecx + 32], ebx ; lea ebx, [byte ecx + 16] ; xor eax, eax ; mov [ebx], eax ; mov [byte ebx + 20], eax ; inc eax ; mov [byte ebx + 8], eax ; mov [byte ebx + 12], eax ; sys_mmap sys_mmap 0,dword [ecx + 20],PROT_READ,MAP_SHARED,ebx,0 mov [esp + 4*7],eax popa or eax, eax .return: ret ;; The program proper begins here. START: ;; edi is initialized to point to the start of the .bss section. ebp ;; is pointed at the read-only data in the .text section, centered on ;; the list of format characters. It will retain this value throughout ;; the program. mov ebp, fmtchars mov edi, tmf ;; Call gettimeofday(), storing the current time in the .ct field. xor ecx, ecx mov ebx, edi sys_gettimeofday ;; The default format string is stashed in ecx, and the command-line ;; arguments are examined. lea ecx, [byte ebp + defaultfmt - fmtchars] pop esi pop esi ;; An argument beginning with a + provides a display format string. If ;; no such argument is present, the built-in formats is used. The ;; presence of a -u option indicates that the time should be displayed ;; for UTC instead of local time. An argument beginning with = provides ;; a C time to use instead of the current time. .argvloop: pop esi or esi, esi jz .argvloopend lodsb cmp al, '+' jnz .notfmt mov ecx, esi jmp short .argvloop .notfmt: cmp al, '=' jnz .nottime xor eax, eax cdq .atoiloop: lea edx, [edx*4 + edx] lea edx, [edx*2 + eax] lodsb sub al, '0' jc .atoidone cmp al, 10 jc .atoiloop .atoidone: mov [edi], edx jmp short .argvloop .nottime: cmp al, '-' jnz .argvloop .optloop: lodsb or al, al jz .argvloop cmp al, 'u' jnz .optloop mov dword [byte edi + tmfmt.tz], 'UTC' jmp short .optloop .argvloopend: mov [byte edi + fmt - tmf], ecx ;; The program stores a pointer to the default "C" locale, and then ;; copies over the first part of the localization file name. lea edx, [byte ebp + clocale - fmtchars] mov [byte edi + localedata - tmf], edx mov edx, edi add edi, byte buf - tmf lea esi, [byte ebp + lctimeprefix - fmtchars] push byte lctimeprefixlen pop ecx rep movsb ;; Then the list of environment variables is searched for a variable ;; named LANG. If no such variable is present, the default "C" locale ;; is retained. .searchenvloop: pop esi or esi, esi jz .useclocale lodsd cmp eax, 'LANG' jnz .searchenvloop lodsb cmp al, '=' jnz .searchenvloop ;; If one is present, its value is used to complete the name of the ;; localization file, which is then loaded into memory. .copylangloop: lodsb stosb or al, al jnz .copylangloop dec edi lea esi, [byte ebp + lctimesuffix - fmtchars] movsd movsd stosb mov edi, edx lea ebx, [byte edi + buf - tmf] call mmapfile js .useclocale mov [byte edi + localedata - tmf], eax .useclocale: mov edi, edx ;; If time zone information is already present, UTC is to be used and ;; the translation to local time is skipped. Otherwise, the file for ;; the local time zone is loaded. xor edx, edx cmp edx, [byte edi + tmfmt.tz] jnz skipzoning lea ebx, [byte ebp + tzfilename - fmtchars] call mmapfile js skipzoning ;; At the top of the localtime file is a sequence of integers ;; indicating the size of the various parts of the file. Following ;; this is a list of time changes for this zone. (See tzfile(5) for a ;; fuller description of the structure of this file.) The program runs ;; through the list backwards, finding the current subsequence of ;; linear time. mov ebx, [byte eax + 32] bswap ebx mov esi, [byte eax + 36] bswap esi lea esi, [esi*2 + esi] mov ecx, ebx add eax, byte 44 .tmchgloop: dec ecx jz .tmchgloopexit mov edx, [eax + ecx*4] bswap edx cmp edx, [edi] jg .tmchgloop .tmchgloopexit: ;; The index of the current subsequence gives us the offset into the ;; next array, which itself contains indexes into the array of the ;; current attributes of the time zone. These attributes give the ;; offset from GMT in seconds (which is stored in zo), a flag ;; indicating whether or not Daylight Savings is in effect, and a ;; pointer to the current time zone's name (which is stored in tz). lea eax, [eax + ebx*4] movzx ecx, byte [eax + ecx] add eax, ebx lea ecx, [ecx*2 + ecx] mov edx, [eax + ecx*2] bswap edx mov [byte edi + tmfmt.zo], edx movzx ecx, byte [byte eax + ecx*2 + 5] lea eax, [eax + esi*2] fild qword [eax + ecx] fistp qword [byte edi + tmfmt.tz] ;; The current offset from GMT in seconds is then changed into hours ;; and minutes. These are used to create a string of the form +HHMM, ;; stored in zi. skipzoning: mov al, '+' or edx, edx jns .eastward mov al, '-' neg edx .eastward: mov [byte edi + tmfmt.zi], al xchg eax, edx cdq lea ebx, [byte edx + 60] div ebx cdq div ebx aam xchg eax, edx aam shl edx, 16 lea eax, [edx + eax + '0000'] bswap eax mov [byte edi + tmfmt.zi + 1], eax ;; The current time is loaded into eax, the offset for the time zone ;; is applied. If this makes the current time negative, a year is ;; added to the time, and the start of the Epoch is moved back. The ;; day of the week of the start of the Epoch is stored in esi and on ;; the stack. mov ecx, 1969 push byte 4 pop esi mov eax, [edi] add eax, [byte edi + tmfmt.zo] jge .positivetime add eax, 365 * 24 * 60 * 60 dec ecx dec esi .positivetime: push esi ;; eax holds the number of seconds since the Epoch. This is divided by ;; 60 to get the current number of seconds, by 60 again to get the ;; current number of minutes, and then by 24 to get the current number ;; of hours. cdq lea ebx, [byte edx + 60] div ebx mov [byte edi + tmfmt.sc], edx cdq div ebx mov [byte edi + tmfmt.mn], edx mov bl, 24 cdq div ebx mov [byte edi + tmfmt.hr], edx ;; The hours are also tested to determine the current side of the ;; meridian, and the hours of the meridian. sub edx, byte 12 setae byte [byte edi + tmfmt.mr] ja .morning .midnight: add edx, byte 12 jz .midnight .morning: mov [byte edi + tmfmt.hm], edx ;; eax now holds the number of days since the Epoch. This is divided ;; by seven, after offsetting by the value in esi, to determine the ;; current day of the week. push eax add eax, esi mov bl, 7 cdq div ebx mov [byte edi + tmfmt.wd], edx xor eax, eax cmpxchg edx, ebx mov [byte edi + tmfmt.w1], edx mov eax, [esp] ;; A year's worth of days are successively subtracted from eax until ;; the current year is determined. The program takes advantage of the ;; fact that every 4th year is a leap year within the range of our ;; Epoch. mov bh, 1 .yrloop: mov bl, 110 inc ecx test cl, 3 jz .leap dec ebx .leap: sub eax, ebx jge .yrloop add eax, ebx mov [byte edi + tmfmt.yr], ecx ;; 1900 or 2000 is subtracted to determined the century and the year ;; of the century. mov ch, 20 sub cl, 208 jnc .twentieth dec ch add cl, 100 .twentieth: mov [byte edi + tmfmt.cy], cl mov [byte edi + tmfmt.ce], ch ;; eax now holds the day of the year, and this is saved in esi. ebx is ;; altered to hold a string of pairs of bits indicating the length of ;; each month over 28 days. Each month's worth of days are ;; successively subtracted from eax until the current month, and thus ;; the current day of the month, is determined. mov esi, eax add ebx, 11000000001110111011111011101100b - 365 xor ecx, ecx cdq .moloop: mov dl, 7 shld edx, ebx, 2 ror ebx, 2 inc ecx sub eax, edx jge .moloop add eax, edx inc eax add edi, byte tmfmt.dy stosd xchg eax, ecx stosd ;; The program retrieves from the stack the day of the year, the ;; number of days since the Epoch, and the day of the week at the ;; start of the Epoch, respectively. These are used to calculate the ;; day of the week of January 1st of the current year. pop eax pop ebx sub eax, esi add eax, ebx mov bl, 7 cdq div ebx mov ecx, edx ;; Using this, the program now determines the current week of the year ;; according to three different measurements. The first uses Sunday as ;; the start of the week, and a partial week at the beginning of the ;; year is considered to be week zero. The second is the same, except ;; that it uses Monday as the start of the week. xor eax, eax cmpxchg ecx, ebx lea eax, [esi + ecx] cdq div ebx stosd dec ecx jnz .mondaynot1st mov cl, bl .mondaynot1st: lea eax, [esi + ecx] cdq div ebx stosd ;; Finally, the ISO-8601 week number uses Monday as the start of the ;; week, and requires every week counted to be the full seven days. A ;; partial week at the end of the year of less than four days is ;; counted as week 1 of the following year; likewise, a partial week ;; at the start of the year of less than four days is counted as week ;; 52 or 53 of the previous year. In order to cover all possibilities, ;; the program must examine the current day of the week, and whether ;; the current year and/or the previous year was a leap year. Outside ;; of these special cases, the ISO-8601 week number will either be ;; equal to or one more than the value previously calculated. mov ebx, [byte edi + tmfmt.yr - tmfmt.wi] mov dl, 3 and dl, bl sub ecx, byte 4 adc al, 0 jnz .fullweek dec ebx mov al, 52 or ecx, ecx jz .add1stweek dec ecx jnz .wifound dec edx jnz .wifound .add1stweek: inc eax .fullweek: cmp al, 53 jnz .wifound cmp dl, 1 mov edx, esi sbb dl, 104 cmp dl, [byte edi + tmfmt.wd - tmfmt.wi] jle .wifound mov al, 1 inc ebx .wifound: stosd xchg eax, ebx stosd inc esi xchg eax, esi stosd ;; All representable values have now been calculated. edi is changed ;; to point to the output buffer. The program then proceeds to render ;; the format string into buf by calling ftime. lea ebx, [byte edi - tmfmt.ce] mov esi, [byte ebx + fmt - tmf] lea edi, [byte ebx + buf - tmf] push edi call ftime.loop ;; A newline is appended, the string is output to stdout, and the ;; program exits. mov al, 10 stosb pop ecx mov edx, edi sub edx, ecx sys_write STDOUT xor ebx, ebx neg eax js .noerror xchg eax, ebx .noerror: sys_exit ;; ftime formats the current time according to a format specification. ;; ;; input: ;; ebx = pointer to the tm structure ;; esi = pointer to the format string (relative to ebp, unless the ;; function is entered at ftime.loop) ;; edi = output buffer ;; ;; output: ;; edi = the position immediately following the output string ;; eax, ecx, edx, and esi are altered ftime: add esi, ebp jmp short .loop ;; Whenever a % character is encountered in the format string, the ;; program scans for the character immediately following in the ;; fmtchars array. If it is not found, the % is treated like any other ;; character and copied into outbuf verbatim. The program returns when ;; a NUL byte is found. .nofmt: mov byte [edi], '%' inc edi .literal: stosb .loop: lodsb or al, al jz return cmp al, '%' jnz .literal cmp byte [esi], 0 jz .literal lodsb push edi mov edi, ebp push byte fmtcharcount pop ecx repnz scasb pop edi jnz .nofmt ;; If the character is found, the value in ecx is used as an offset ;; into three arrays. The fmtfuns array indicates which function in ;; the funclist array to call in order to render the value. The ;; fmtargs array indicates which field in tmfmt to load into eax ;; before calling the function. Finally the fmtpars array provides a ;; value, stored in both esi and edx, that is specific to the function ;; called. push esi movzx eax, byte [byte ebp + fmtargs - fmtchars + ecx] mov eax, [ebx + eax] movzx esi, byte [byte ebp + fmtpars - fmtchars + ecx] mov edx, esi mov cl, [byte ebp + fmtfuns - fmtchars + ecx] ;; The chosen function is called and ftime continues on to the next ;; character in the format string. call [byte ebp + funclist - fmtchars + ecx] pop esi jmp short .loop ;; ftimelocale calls ftime for a format specification stored in the ;; LC_TIME file. ;; ;; input: ;; ebx = pointer to the tm structure ;; edx = offset of the index of the format in the LC_TIME file ;; edi = output buffer ;; ;; output: ;; edi = the position immediately following the output string ;; eax, ecx, edx, and esi are altered ftimelocale: mov esi, [byte ebx + localedata - tmf] add esi, [esi + edx] jmp short ftime.loop ;; itoa renders a number in decimal ASCII. ;; ;; input: ;; eax = the number to render ;; dl & 0F = the minimum width to display ;; dl & F0 = the character to pad with on the left itoa: ;; First, the number is rendered as decimal ASCII in reverse, in a ;; private buffer, using successive divisions by 10. lea esi, [byte ebx + itoabuf - tmf + 16] push esi push edx push byte 10 pop ecx .loop: cdq div ecx add dl, '0' dec esi mov [esi], dl or eax, eax jnz .loop ;; If the string created is shorter than the size given in the low ;; nybble in edx, extra characters are first written to edi to pad out ;; the field to the minimum required length. Then the number is copied ;; from the private buffer and the function returns. pop eax mov cl, 0x0F and cl, al add ecx, esi sub ecx, [esp] jle .nopadding and al, 0xF0 rep stosb .nopadding: pop ecx sub ecx, esi rep movsb return: ret ;; putlocalstr selects a string from a list stored in the LC_TIME file ;; and copies it to an output buffer. ;; ;; input: ;; eax = index of the string to copy ;; edx = offset of the list of indexes in the LC_TIME file putlocalestr: mov esi, [byte ebx + localedata - tmf] add edx, esi add esi, [edx + eax*4] .loop: lodsb stosb or al, al jnz .loop dec edi ret ;; putstr simply copies a string from the tmfmt structure to an output ;; buffer. putstr: add esi, ebx .loop: lodsb stosb or al, al jnz .loop dec edi ret ;; putchar copies a single character to the output buffer. putchar: xchg eax, edx stosb ret ;; read-only data ;; The byte array of indexes into funclist, given the function for ;; each format character. fmtfuns: db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; s S M H k d e m Y C db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; I l u w U W V G j y db 4, 4, 4, 4, 4, 4 ; a A h b B p db 8, 8, 8 ; c x X db 12, 12, 12 ; r D T db 16, 16 ; z Z db 20, 20, 20 ; t n % ;; The byte array of offsets into tmfmt, indicating which field to use ;; for each format. Formats after %p do not use a tmfmt field, and so ;; the values for those formats are omitted. fmtargs: db tmfmt.ct, tmfmt.sc, tmfmt.mn, tmfmt.hr, tmfmt.hr db tmfmt.dy, tmfmt.dy, tmfmt.mo, tmfmt.yr, tmfmt.ce db tmfmt.hm, tmfmt.hm, tmfmt.w1, tmfmt.wd, tmfmt.ws db tmfmt.wm, tmfmt.wi, tmfmt.yi, tmfmt.yd, tmfmt.cy db tmfmt.wd, tmfmt.wd, tmfmt.mo, tmfmt.mo, tmfmt.mo db tmfmt.mr ;; The default format displayed when none is supplied by the user. defaultfmt: db '%a %b %d %T %Z %Y' ;; The byte array of function-specific data for each format. This ;; value is loaded in to edx and esi before calling the function. The ;; values in the first two rows are for itoa, and provide a minimum ;; field width and a padding character (either space or '0'). The ;; values in the next two rows are for ftimelocale, and give an offset ;; into the words memory to an array of pointers to strings. The fifth ;; row, for ftimelocale, contains offsets relative to ebp to format ;; strings. The sixth row, for putlocalestr, contains offsets relative ;; to tmfmt to constant strings. The final row, for putchar, specifies ;; literal characters. fmtpars: db 0, 0x32, 0x32, 0x32, 0x22, 0x32, 0x22, 0x32, 0, 0 db 0x32, 0x22, 0, 0, 0x32, 0x32, 0x32, 0, 0x33, 0x32 db 0x08, 0x24, 0x3C, 0x3C, 0x6C, 0xA0 db 0xA8, 0xAC, 0xB0 db percentr - fmtchars db percentD - fmtchars, percentT - fmtchars db tmfmt.zi, tmfmt.tz db 9, 10 ;; The complete list of format characters, in reverse from the ;; corresponding values used in the three preceding byte arrays. fmtchars: db '%ntZzTDrXxcpBbhAayjGVWUwulICYmedkHMSs' fmtcharcount equ $ - fmtchars ;; The (incomplete) pathname of the localization file. ;;lctimeprefix: db '/usr/share/locale/' lctimeprefix: db '/usr/lib/locale/' lctimeprefixlen equ $ - lctimeprefix lctimesuffix equ $ - 1 db 'LC_TIME' ;; The format used to render the %D, %T, and %r formats. percentD: db '%m/%d/%y', 0 percentT: db '%H:%M:%S', 0 percentr: db '%I:%M:%S %p', 0 ;; The pathname of the time zone file. tzfilename: db '/etc/localtime', 0 ALIGN 4 ;; The list of different functions called to render a format. funclist: dd itoa, putlocalestr, ftimelocale, ftime, putstr, putchar ;; The "C" locale data. clocale equ $ - 8 %define O(p) (p) - clocale dd O(ca0), O(ca1), O(ca2), O(ca3), O(ca4), O(ca5), O(ca6) dd O(cA0), O(cA1), O(cA2), O(cA3), O(cA4), O(cA5), O(cA6) dd O(cb00), O(cb01), O(cb02), O(cb03), O(cb04), O(cb05) dd O(cb06), O(cb07), O(cb08), O(cb09), O(cb10), O(cb11) dd O(cB00), O(cB01), O(cB02), O(cB03), O(cb04), O(cB05) dd O(cB06), O(cB07), O(cB08), O(cB09), O(cB10), O(cB11) dd O(cp0), O(cp1), O(cpc), O(percentD), O(percentT) ca0: db 'Sun', 0 ca1: db 'Mon', 0 ca2: db 'Tue', 0 ca3: db 'Wed', 0 ca4: db 'Thu', 0 ca5: db 'Fri', 0 ca6: db 'Sat', 0 cA0: db 'Sunday', 0 cA1: db 'Monday', 0 cA2: db 'Tuesday', 0 cA3: db 'Wednesday', 0 cA4: db 'Thursday', 0 cA5: db 'Friday', 0 cA6: db 'Saturday', 0 cb00: db 'Jan', 0 cb01: db 'Feb', 0 cb02: db 'Mar', 0 cb03: db 'Apr', 0 cb04: db 'May', 0 cb05: db 'Jun', 0 cb06: db 'Jul', 0 cb07: db 'Aug', 0 cb08: db 'Sep', 0 cb09: db 'Oct', 0 cb10: db 'Nov', 0 cb11: db 'Dec', 0 cB00: db 'January', 0 cB01: db 'February', 0 cB02: db 'March', 0 cB03: db 'April', 0 cB05: db 'June', 0 cB06: db 'July', 0 cB07: db 'August', 0 cB08: db 'September', 0 cB09: db 'October', 0 cB10: db 'November', 0 cB11: db 'December', 0 cp0: db 'AM', 0 cp1: db 'PM', 0 cpc: db '%a %b %e %T %Y', 0 UDATASEG ALIGNB 16 ;; The tmfmt structure. tmf: resb tmfmt_size ;; The pointer to the top-level format string. fmt: resd 1 ;; The pointer to the localization file's contents. localedata: resd 1 ;; itoa's private temporary buffer. itoabuf: resb 16 ;; The output buffer that the formatting string is rendered into. buf: resb 8192 END