scsh-0.5/scsh/time1.c

425 lines
14 KiB
C

/* Posix time support for scsh.
** Copyright (c) 1994 by Olin Shivers.
*/
/* WARNING: THIS FILE HAS CODE THAT DEPENDS ON 32-BIT ARCHITECTURES.
** This code is so marked.
**
** The source code is also conditionalised by three #ifdef feature macros:
** HAVE_TZNAME
** The char *tzname[2] global variable is POSIX. Everyone provides
** it...except some "classic" versions of SunOS that we still care about
** running (People in LCS/AI refuse to switch to Solaris). So, we kluge
** around not having it.
**
** HAVE_GMTOFF
** Some systems (NetBSD, NeXTSTEP, Solaris) have a non-standard field in the
** tm struct, the tm_gmtoff field. localtime() sets it to the offset from
** UTC for the current time. If you have this field, it is trivial to
** compute the the UTC time zone offset. If you have a strict POSIX system,
** and don't have it, then the offset can be computed with a slower
** technique.
**
** NeXT
** The presence of this feature macro means that, basically, you are
** screwed, and should go download yourself a real Unix system off the
** Net. For free.
**
** More specifically, it means that (1) the presence of the strftime()
** function will cause the whole system build to die at link time,
** when compiled with the -posix flag. (NeXT bug #59098) There is no fix
** for this as of November 1994. Thanks, guys.
**
** We handle this problem by abandoning ship. When compiled under NeXT,
** your time zone is always computed to be the empty string.
**
** The other problem is that (2) NeXT's mktime() procedure pays attention
** to the gmt_offset field of the tm struct you give it, instead of
** the $TZ environment variable. So there is no way to convert a date
** to a time without knowing in advance what the UTC offset is in seconds.
** This screws up scsh's DATE->TIME procedure.
*/
#include <time.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include "sysdep.h"
#include "cstuff.h"
#include "time1.h" /* Make sure the .h interface agrees with the code. */
extern char **environ;
/* To work in the UTC time zone, do "environ = utc_env;". */
static char *utc_env[] = {"TZ=UCT0", 0};
#ifdef HAVE_TZNAME
#ifndef __CYGWIN__
extern char *tzname[]; /* Why isn't this defined in time.h? */
#endif
#endif
/* These two functions allow you to temporarily override
** the current time zone with one of your choice. make_newenv()
** takes a time zone string as an argument, and constructs a Unix environ
** vector with a single entry: "TZ=<zone>". You pass the new environ vector
** as an argument. It installs the new environment, and returns the old
** one. You can later pass the old environment back to revert_env()
** to reinstall the old environment and free up malloc'd storage.
**
** On error, make_newenv returns NULL.
*/
static char **make_newenv(scheme_value zone, char *newenv[2])
{
int zonelen = STRING_LENGTH(zone);
char **oldenv = environ,
*tz = Malloc(char, 4+zonelen);
if( !tz ) return NULL;
strcpy(tz, "TZ=");
strncpy(tz+3, &STRING_REF(zone,0), zonelen);
tz[zonelen+3] = '\0';
newenv[0] = tz;
newenv[1] = NULL;
environ = newenv; /* Install it. */
return oldenv;
}
static void revert_env(char **old_env)
{
char *tz = *environ;
environ = old_env;
Free(tz);
}
/*****************************************************************************/
/* Sux because it's dependent on 32-bitness. */
#define hi8(i) (((i)>>24) & 0xff)
#define lo24(i) ((i) & 0xffffff)
#define comp8_24(hi, lo) (((hi)<<24) + (lo))
scheme_value scheme_time(int *hi_secs, int *lo_secs)
{
time_t t;
errno = 0;
t = time(NULL);
if( t == -1 && errno ) return ENTER_FIXNUM(errno);
*hi_secs = hi8(t);
*lo_secs = lo24(t);
return SCHFALSE;
}
/* Zone:
** #f Local time
** int Offset from GMT in seconds.
** string Time zone understood by OS.
*/
scheme_value time2date(int hi_secs, int lo_secs, scheme_value zone,
int *sec, int *min, int *hour,
int *mday, int *month, int *year,
const char **tz_name, int *tz_secs,
int *summer,
int *wday, int *yday)
{
time_t t = comp8_24(hi_secs, lo_secs);
struct tm d;
if( FIXNUMP(zone) ) { /* Offset from GMT in secs. */
int offset = EXTRACT_FIXNUM(zone);
t += EXTRACT_FIXNUM(zone);
d = *gmtime(&t);
*tz_name = NULL;
*tz_secs = offset;
}
else {
char *newenv[2], **oldenv = NULL;
if( STRINGP(zone) ) { /* Time zone */
oldenv = make_newenv(zone, newenv); /* Install new TZ. */
if( !oldenv ) return ENTER_FIXNUM(errno); /* Error installing. */
d = *localtime(&t); /* Do it. */
}
else /* Local time */
d = *localtime(&t);
/* This little chunk of code copies the calculated time zone into
** a malloc'd buffer and assigns it to *tz_name. It's a little
** complicated because we have to clean up after detecting an
** error w/o walking on errno.
**
** The time zone has to be stashed into a malloc'd buffer because
** when revert_env resets to the original time zone, it will
** overwrite the static buffer tzname. We have to copy it out before
** that happens.
*/
{ int error = 0;
#ifndef HAVE_TZNAME
char *zone = d.tm_zone; /* Hack it for SunOS. */
#else
char *zone = tzname[d.tm_isdst];
#endif
char *newzone = Malloc(char, 1+strlen(zone));
*tz_name = newzone;
if( newzone ) strcpy(newzone, zone);
else error = errno;
if( oldenv ) revert_env(oldenv); /* Revert TZ & env. */
if( !newzone ) return ENTER_FIXNUM(error);
}
/* Calculate the time-zone offset in seconds from UTC. */
#ifdef HAVE_GMTOFF
*tz_secs = d.tm_gmtoff;
#else
{ struct tm dcopy = d;
char **oldenv = environ; /* Set TZ to UTC */
environ=utc_env; /* time temporarily. */
tzset(); /* NetBSD, SunOS POSIX-noncompliance requires this. */
dcopy.tm_isdst = 0;
*tz_secs = mktime(&dcopy) - t; /* mktime() may mung dcopy. */
environ=oldenv;
}
#endif
}
*sec = d.tm_sec; *min = d.tm_min; *hour = d.tm_hour;
*mday = d.tm_mday; *month = d.tm_mon; *year = d.tm_year;
*wday = d.tm_wday; *yday = d.tm_yday; *summer = d.tm_isdst;
return SCHFALSE;
}
/* Oops
** There's a fundamental problem with the Posix mktime() function used below
** -- it's error return value (-1) is also a valid return value, for date
** 11:59:00 UTC, 12/31/1969
**
** 1. We choose to err on the paranoid side. If mktime() returns -1, it is
** considered an error.
** 2. If we return an error, we try to return a useful errno value, if we can.
**
** Who designed this interface?
*/
scheme_value date2time(int sec, int min, int hour,
int mday, int month, int year,
scheme_value tz_name, scheme_value tz_secs,
int summer,
int *hi_secs, int *lo_secs)
{
time_t t;
struct tm d;
d.tm_sec = sec; d.tm_min = min; d.tm_hour = hour;
d.tm_mday = mday; d.tm_mon = month; d.tm_year = year;
d.tm_wday = 0; d.tm_yday = 0; d.tm_isdst = summer;
if( FIXNUMP(tz_secs) ) { /* Offset from GMT in seconds. */
char **oldenv = environ; /* Set TZ to UTC */
environ = utc_env; /* time temporarily. */
tzset(); /* NetBSD, SunOS POSIX-noncompliance requires this. */
d.tm_isdst = 0; /* FreeBSD, at least, needs this or it sulks. */
errno = 0;
t = mktime(&d);
/* t == -1 => you probably have an error. */
if( t == -1 ) return ENTER_FIXNUM(errno ? errno : -1);
t -= EXTRACT_FIXNUM(tz_secs);
environ = oldenv;
}
else if( STRINGP(tz_name) ) { /* Time zone */
char *newenv[2];
char **oldenv = make_newenv(tz_name, newenv);
if( !oldenv ) return ENTER_FIXNUM(errno);
tzset(); /* NetBSD, SunOS POSIX-noncompliance requires this. */
errno = 0;
t = mktime(&d);
if( t == -1 ) return ENTER_FIXNUM(errno ? errno : -1);
revert_env(oldenv);
}
else { /* Local time */
tzset(); /* NetBSD, SunOS POSIX-noncompliance requires this. */
errno = 0;
t = mktime(&d);
if( t == -1) return ENTER_FIXNUM(errno ? errno : -1);
}
*hi_secs = hi8(t);
*lo_secs = lo24(t);
return SCHFALSE;
}
/* WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
**
** This code doesn't work under NeXTSTEP. I have cleverly #included the
** critical call to strftime() out for NeXT. This is because the compiler
** blows up on Posix compiles involving strftime(). Go figure.
*/
/* It's disgusting how long and tortuous this function is, just
** to interface to the strftime() function. -Olin
**
** There's a weird screw case this code is careful to handle. Exhibiting
** classic Unix design (we use the term loosely), strftime()'s error
** return (0) is also a legal return value for some boundary cases.
** For example, if the format string is empty, or it is "%Z" and
** the time-zone is not available, then the result string is 0 chars long.
** We distinguish this case by suffixing an "x" to the format string,
** and flushing the last char in the formatted result.
**
** Don't consider *prefixing* an "x" instead, because then you'd
** probably pass back &result[1] to skip the x, and that would lose --
** the guy we are handing the string to will later pass it to free(),
** so we can't pass back a pointer to anything other than the very front
** of the block.
**
** Professional programmers sacrifice their pride that others may live.
** Why me? Why Unix?
*/
scheme_value format_date(const char *fmt, int sec, int min, int hour,
int mday, int month, int year,
scheme_value tz, int summer,
int week_day, int year_day,
const char **ans)
{
struct tm d;
int fmt_len = strlen(fmt);
char *fmt2 = Malloc(char, 2+2*fmt_len); /* 1 extra for prefixed "x" char.*/
int target_len = 1; /* 1 for the prefixed "x" char. Ugh. */
int zone = 0; /* Are we using the time-zone? */
char *q, *target;
const char *p;
char *newenv[2], **oldenv = NULL;
int result_len;
*ans = NULL; /* In case we error out. */
if( !fmt2 ) return ENTER_FIXNUM(errno);
d.tm_sec = sec; d.tm_min = min; d.tm_hour = hour;
d.tm_mday = mday; d.tm_mon = month; d.tm_year = year;
d.tm_wday = week_day; d.tm_yday = year_day; d.tm_isdst = summer;
/* Copy fmt -> fmt2, converting ~ escape codes to % escape codes.
** Set zone=1 if fmt has a ~Z.
** Build up an estimate of how large the target buffer needs to be.
** The length calculation is not required to be accurate.
*/
for(q=fmt2, p=fmt; *p; p++) {
if( *p != '~' ) {
target_len++;
*q++ = *p;
if( *p == '%' ) *q++ = '%'; /* Percents get doubled. */
}
else {
char c = *++p;
if( ! c ) {
Free(fmt2);
return SCHTRUE; /* % has to be followed by something. */
}
else if( c == '~' ) {
*q++ = '~';
target_len++;
}
else {
*q++ = '%';
*q++ = c;
switch (c) {
case 'a': target_len += 3; break;
case 'A': target_len += 9; break;
case 'b': target_len += 3; break;
case 'B': target_len += 9; break;
case 'c': target_len += 10; break; /* wtf */
case 'd': target_len += 2; break;
case 'H': target_len += 2; break;
case 'I': target_len += 2; break;
case 'j': target_len += 3; break;
case 'm': target_len += 2; break;
case 'M': target_len += 2; break;
case 'p': target_len += 2; break;
case 'S': target_len += 2; break;
case 'U': target_len += 2; break;
case 'w': target_len += 1; break;
case 'W': target_len += 2; break;
case 'x': target_len += 10; break; /* wtf */
case 'X': target_len += 10; break; /* wtf */
case 'y': target_len += 2; break;
case 'Y': target_len += 4; break;
case 'Z': target_len += 6; zone++; break;
default:
target_len += 5; break; /* wtf */
}
}
}
}
*q++ = 'x'; *q = '\0'; /* Append the guard "x" suffix and nul-terminate. */
/* Fix up the time-zone if it is being used and the user passed one in. */
if( zone && STRINGP(tz) ) {
oldenv = make_newenv(tz, newenv);
if( !oldenv ) {
int err = errno;
Free(fmt);
return ENTER_FIXNUM(err);
}
}
/* Call strftime with increasingly larger buffers until the result fits. */
target = Malloc(char, target_len);
if( !target ) goto lose; /* Alloc lost. */
#ifndef NeXT
while( !(result_len=strftime(target, target_len, fmt2, &d)) ) {
target_len *= 2;
target = Realloc(char, target, target_len);
if( !target ) goto lose;
}
target[result_len-1] = '\0'; /* Flush the trailing "x". */
#endif
*ans = target;
Free(fmt2);
if( oldenv ) revert_env(oldenv);
return SCHFALSE;
lose:
/* We lost trying to allocate space for the strftime() target buffer. */
{int err = errno;
if( oldenv ) revert_env(oldenv); /* Clean up */
Free(fmt2);
return ENTER_FIXNUM(err);
}
}
#if 0
/* This is a kludge one can use should the tzname variable
** not be present on the system. Only SunOS is broken this way,
** and it has a non-standard alternative we can use for this application.
** So this code is commented out.
**
** tzname_loser(int dst) returns a string containing the current time zone
** for loser OS's. The string is statically allocated. If the time zone
** is longer than some hidden, arbitrary length, the function simply
** returns the empty string. It is a workaround for tzname[dp->tm_isdst].
**
*/
char *tzname_loser(struct tm *dp)
{
static char buf[1024];
return strftime(buf, 1024, "x%Z", dp) ? buf+1 : "";
}
#endif
/* clear errno before mktime() and time(), if -1 ret, return errno.
** This is defined to work under HP-UX at least;
** other man pages are silent.
** gettimeofday() returns -1/errno
** localtime() & gmtime() don't error.
*/