/* 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 #include #include #include #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=". 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. */