1 /*
   2  * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 #include <stdlib.h>
  27 #include <stdio.h>
  28 #include <strings.h>
  29 #include <time.h>
  30 #include <limits.h>
  31 #include <errno.h>
  32 #include <stddef.h>
  33 #include <sys/stat.h>
  34 #include <sys/types.h>
  35 #include <string.h>
  36 #include <dirent.h>
  37 #include <unistd.h>
  38 #ifdef __solaris__
  39 #include <libscf.h>
  40 #endif
  41 
  42 #include "jvm.h"
  43 
  44 #define SKIP_SPACE(p)   while (*p == ' ' || *p == '\t') p++;
  45 
  46 #if !defined(__solaris__) || defined(__sparcv9) || defined(amd64)
  47 #define fileopen        fopen
  48 #define filegets        fgets
  49 #define fileclose       fclose
  50 #endif
  51 
  52 #if defined(__linux__) || defined(_ALLBSD_SOURCE)
  53 
  54 
  55 static const char *ETC_TIMEZONE_FILE = "/etc/timezone";
  56 static const char *ZONEINFO_DIR = "/usr/share/zoneinfo";
  57 static const char *DEFAULT_ZONEINFO_FILE = "/etc/localtime";
  58 #else
  59 static const char *SYS_INIT_FILE = "/etc/default/init";
  60 static const char *ZONEINFO_DIR = "/usr/share/lib/zoneinfo";
  61 static const char *DEFAULT_ZONEINFO_FILE = "/usr/share/lib/zoneinfo/localtime";
  62 #endif /*__linux__*/
  63 
  64 /*
  65  * Returns a pointer to the zone ID portion of the given zoneinfo file
  66  * name, or NULL if the given string doesn't contain "zoneinfo/".
  67  */
  68 static char *
  69 getZoneName(char *str)
  70 {
  71     static const char *zidir = "zoneinfo/";
  72 
  73     char *pos = strstr((const char *)str, zidir);
  74     if (pos == NULL) {
  75         return NULL;
  76     }
  77     return pos + strlen(zidir);
  78 }
  79 
  80 /*
  81  * Returns a path name created from the given 'dir' and 'name' under
  82  * UNIX. This function allocates memory for the pathname calling
  83  * malloc(). NULL is returned if malloc() fails.
  84  */
  85 static char *
  86 getPathName(const char *dir, const char *name) {
  87     char *path;
  88 
  89     path = (char *) malloc(strlen(dir) + strlen(name) + 2);
  90     if (path == NULL) {
  91         return NULL;
  92     }
  93     return strcat(strcat(strcpy(path, dir), "/"), name);
  94 }
  95 
  96 /*
  97  * Scans the specified directory and its subdirectories to find a
  98  * zoneinfo file which has the same content as /etc/localtime on Linux
  99  * or /usr/share/lib/zoneinfo/localtime on Solaris given in 'buf'.
 100  * If file is symbolic link, then the contents it points to are in buf.
 101  * Returns a zone ID if found, otherwise, NULL is returned.
 102  */
 103 static char *
 104 findZoneinfoFile(char *buf, size_t size, const char *dir)
 105 {
 106     DIR *dirp = NULL;
 107     struct stat statbuf;
 108     struct dirent *dp = NULL;
 109     struct dirent *entry = NULL;
 110     char *pathname = NULL;
 111     int fd = -1;
 112     char *dbuf = NULL;
 113     char *tz = NULL;
 114 
 115     dirp = opendir(dir);
 116     if (dirp == NULL) {
 117         return NULL;
 118     }
 119 
 120     entry = (struct dirent *) malloc((size_t) pathconf(dir, _PC_NAME_MAX));
 121     if (entry == NULL) {
 122         (void) closedir(dirp);
 123         return NULL;
 124     }
 125 
 126 #if defined(_AIX) || defined(__linux__) || defined(MACOSX) || (defined(__solaris__) \
 127     && (defined(_POSIX_PTHREAD_SEMANTICS) || defined(_LP64)))
 128     while (readdir_r(dirp, entry, &dp) == 0 && dp != NULL) {
 129 #else
 130     while ((dp = readdir_r(dirp, entry)) != NULL) {
 131 #endif
 132 
 133         /*
 134          * Skip '.' and '..' (and possibly other .* files)
 135          */
 136         if (dp->d_name[0] == '.') {
 137             continue;
 138         }
 139 
 140         /*
 141          * Skip "ROC", "posixrules", and "localtime".
 142          */
 143         if ((strcmp(dp->d_name, "ROC") == 0)
 144             || (strcmp(dp->d_name, "posixrules") == 0)
 145 #ifdef __solaris__
 146             /*
 147              * Skip the "src" and "tab" directories on Solaris.
 148              */
 149             || (strcmp(dp->d_name, "src") == 0)
 150             || (strcmp(dp->d_name, "tab") == 0)
 151 #endif
 152             || (strcmp(dp->d_name, "localtime") == 0)) {
 153             continue;
 154         }
 155 
 156         pathname = getPathName(dir, dp->d_name);
 157         if (pathname == NULL) {
 158             break;
 159         }
 160         if (stat(pathname, &statbuf) == -1) {
 161             break;
 162         }
 163 
 164         if (S_ISDIR(statbuf.st_mode)) {
 165             tz = findZoneinfoFile(buf, size, pathname);
 166             if (tz != NULL) {
 167                 break;
 168             }
 169         } else if (S_ISREG(statbuf.st_mode) && (size_t)statbuf.st_size == size) {
 170             dbuf = (char *) malloc(size);
 171             if (dbuf == NULL) {
 172                 break;
 173             }
 174             if ((fd = open(pathname, O_RDONLY)) == -1) {
 175                 break;
 176             }
 177             if (read(fd, dbuf, size) != (ssize_t) size) {
 178                 break;
 179             }
 180             if (memcmp(buf, dbuf, size) == 0) {
 181                 tz = getZoneName(pathname);
 182                 if (tz != NULL) {
 183                     tz = strdup(tz);
 184                 }
 185                 break;
 186             }
 187             free((void *) dbuf);
 188             dbuf = NULL;
 189             (void) close(fd);
 190             fd = -1;
 191         }
 192         free((void *) pathname);
 193         pathname = NULL;
 194     }
 195 
 196     if (entry != NULL) {
 197         free((void *) entry);
 198     }
 199     if (dirp != NULL) {
 200         (void) closedir(dirp);
 201     }
 202     if (pathname != NULL) {
 203         free((void *) pathname);
 204     }
 205     if (fd != -1) {
 206         (void) close(fd);
 207     }
 208     if (dbuf != NULL) {
 209         free((void *) dbuf);
 210     }
 211     return tz;
 212 }
 213 
 214 #if defined(__linux__) || defined(MACOSX)
 215 
 216 /*
 217  * Performs Linux specific mapping and returns a zone ID
 218  * if found. Otherwise, NULL is returned.
 219  */
 220 static char *
 221 getPlatformTimeZoneID()
 222 {
 223     struct stat statbuf;
 224     char *tz = NULL;
 225     FILE *fp;
 226     int fd;
 227     char *buf;
 228     size_t size;
 229 
 230 #ifdef __linux__
 231     /*
 232      * Try reading the /etc/timezone file for Debian distros. There's
 233      * no spec of the file format available. This parsing assumes that
 234      * there's one line of an Olson tzid followed by a '\n', no
 235      * leading or trailing spaces, no comments.
 236      */
 237     if ((fp = fopen(ETC_TIMEZONE_FILE, "r")) != NULL) {
 238         char line[256];
 239 
 240         if (fgets(line, sizeof(line), fp) != NULL) {
 241             char *p = strchr(line, '\n');
 242             if (p != NULL) {
 243                 *p = '\0';
 244             }
 245             if (strlen(line) > 0) {
 246                 tz = strdup(line);
 247             }
 248         }
 249         (void) fclose(fp);
 250         if (tz != NULL) {
 251             return tz;
 252         }
 253     }
 254 #endif /* __linux__ */
 255 
 256     /*
 257      * Next, try /etc/localtime to find the zone ID.
 258      */
 259     if (lstat(DEFAULT_ZONEINFO_FILE, &statbuf) == -1) {
 260         return NULL;
 261     }
 262 
 263     /*
 264      * If it's a symlink, get the link name and its zone ID part. (The
 265      * older versions of timeconfig created a symlink as described in
 266      * the Red Hat man page. It was changed in 1999 to create a copy
 267      * of a zoneinfo file. It's no longer possible to get the zone ID
 268      * from /etc/localtime.)
 269      */
 270     if (S_ISLNK(statbuf.st_mode)) {
 271         char linkbuf[PATH_MAX+1];
 272         int len;
 273 
 274         if ((len = readlink(DEFAULT_ZONEINFO_FILE, linkbuf, sizeof(linkbuf)-1)) == -1) {
 275             jio_fprintf(stderr, (const char *) "can't get a symlink of %s\n",
 276                         DEFAULT_ZONEINFO_FILE);
 277             return NULL;
 278         }
 279         linkbuf[len] = '\0';
 280         tz = getZoneName(linkbuf);
 281         if (tz != NULL) {
 282             tz = strdup(tz);
 283             return tz;
 284         }
 285     }
 286 
 287     /*
 288      * If it's a regular file, we need to find out the same zoneinfo file
 289      * that has been copied as /etc/localtime.
 290      * If initial symbolic link resolution failed, we should treat target
 291      * file as a regular file.
 292      */
 293     if ((fd = open(DEFAULT_ZONEINFO_FILE, O_RDONLY)) == -1) {
 294         return NULL;
 295     }
 296     if (fstat(fd, &statbuf) == -1) {
 297         (void) close(fd);
 298         return NULL;
 299     }
 300     size = (size_t) statbuf.st_size;
 301     buf = (char *) malloc(size);
 302     if (buf == NULL) {
 303         (void) close(fd);
 304         return NULL;
 305     }
 306 
 307     if (read(fd, buf, size) != (ssize_t) size) {
 308         (void) close(fd);
 309         free((void *) buf);
 310         return NULL;
 311     }
 312     (void) close(fd);
 313 
 314     tz = findZoneinfoFile(buf, size, ZONEINFO_DIR);
 315     free((void *) buf);
 316     return tz;
 317 }
 318 #else
 319 #ifdef __solaris__
 320 #if !defined(__sparcv9) && !defined(amd64)
 321 
 322 /*
 323  * Those file* functions mimic the UNIX stream io functions. This is
 324  * because of the limitation of the number of open files on Solaris
 325  * (32-bit mode only) due to the System V ABI.
 326  */
 327 
 328 #define BUFFER_SIZE     4096
 329 
 330 static struct iobuffer {
 331     int     magic;      /* -1 to distinguish from the real FILE */
 332     int     fd;         /* file descriptor */
 333     char    *buffer;    /* pointer to buffer */
 334     char    *ptr;       /* current read pointer */
 335     char    *endptr;    /* end pointer */
 336 };
 337 
 338 static int
 339 fileclose(FILE *stream)
 340 {
 341     struct iobuffer *iop = (struct iobuffer *) stream;
 342 
 343     if (iop->magic != -1) {
 344         return fclose(stream);
 345     }
 346 
 347     if (iop == NULL) {
 348         return 0;
 349     }
 350     close(iop->fd);
 351     free((void *)iop->buffer);
 352     free((void *)iop);
 353     return 0;
 354 }
 355 
 356 static FILE *
 357 fileopen(const char *fname, const char *fmode)
 358 {
 359     FILE *fp;
 360     int fd;
 361     struct iobuffer *iop;
 362 
 363     if ((fp = fopen(fname, fmode)) != NULL) {
 364         return fp;
 365     }
 366 
 367     /*
 368      * It assumes read open.
 369      */
 370     if ((fd = open(fname, O_RDONLY)) == -1) {
 371         return NULL;
 372     }
 373 
 374     /*
 375      * Allocate struct iobuffer and its buffer
 376      */
 377     iop = malloc(sizeof(struct iobuffer));
 378     if (iop == NULL) {
 379         (void) close(fd);
 380         errno = ENOMEM;
 381         return NULL;
 382     }
 383     iop->magic = -1;
 384     iop->fd = fd;
 385     iop->buffer = malloc(BUFFER_SIZE);
 386     if (iop->buffer == NULL) {
 387         (void) close(fd);
 388         free((void *) iop);
 389         errno = ENOMEM;
 390         return NULL;
 391     }
 392     iop->ptr = iop->buffer;
 393     iop->endptr = iop->buffer;
 394     return (FILE *)iop;
 395 }
 396 
 397 /*
 398  * This implementation assumes that n is large enough and the line
 399  * separator is '\n'.
 400  */
 401 static char *
 402 filegets(char *s, int n, FILE *stream)
 403 {
 404     struct iobuffer *iop = (struct iobuffer *) stream;
 405     char *p;
 406 
 407     if (iop->magic != -1) {
 408         return fgets(s, n, stream);
 409     }
 410 
 411     p = s;
 412     for (;;) {
 413         char c;
 414 
 415         if (iop->ptr == iop->endptr) {
 416             ssize_t len;
 417 
 418             if ((len = read(iop->fd, (void *)iop->buffer, BUFFER_SIZE)) == -1) {
 419                 return NULL;
 420             }
 421             if (len == 0) {
 422                 *p = 0;
 423                 if (s == p) {
 424                     return NULL;
 425                 }
 426                 return s;
 427             }
 428             iop->ptr = iop->buffer;
 429             iop->endptr = iop->buffer + len;
 430         }
 431         c = *iop->ptr++;
 432         *p++ = c;
 433         if ((p - s) == (n - 1)) {
 434             *p = 0;
 435             return s;
 436         }
 437         if (c == '\n') {
 438             *p = 0;
 439             return s;
 440         }
 441     }
 442     /*NOTREACHED*/
 443 }
 444 #endif /* not __sparcv9 */
 445 
 446 
 447 /*
 448  * Performs Solaris dependent mapping. Returns a zone ID if
 449  * found. Otherwise, NULL is returned.  Solaris libc looks up
 450  * "/etc/default/init" to get the default TZ value if TZ is not defined
 451  * as an environment variable.
 452  */
 453 static char *
 454 getPlatformTimeZoneID()
 455 {
 456     char *tz = NULL;
 457     FILE *fp;
 458 
 459     /*
 460      * Try the TZ entry in /etc/default/init.
 461      */
 462     if ((fp = fileopen(SYS_INIT_FILE, "r")) != NULL) {
 463         char line[256];
 464         char quote = '\0';
 465 
 466         while (filegets(line, sizeof(line), fp) != NULL) {
 467             char *p = line;
 468             char *s;
 469             char c;
 470 
 471             /* quick check for comment lines */
 472             if (*p == '#') {
 473                 continue;
 474             }
 475             if (strncmp(p, "TZ=", 3) == 0) {
 476                 p += 3;
 477                 SKIP_SPACE(p);
 478                 c = *p;
 479                 if (c == '"' || c == '\'') {
 480                     quote = c;
 481                     p++;
 482                 }
 483 
 484                 /*
 485                  * PSARC/2001/383: quoted string support
 486                  */
 487                 for (s = p; (c = *s) != '\0' && c != '\n'; s++) {
 488                     /* No '\\' is supported here. */
 489                     if (c == quote) {
 490                         quote = '\0';
 491                         break;
 492                     }
 493                     if (c == ' ' && quote == '\0') {
 494                         break;
 495                     }
 496                 }
 497                 if (quote != '\0') {
 498                     jio_fprintf(stderr, "ZoneInfo: unterminated time zone name in /etc/TIMEZONE\n");
 499                 }
 500                 *s = '\0';
 501                 tz = strdup(p);
 502                 break;
 503             }
 504         }
 505         (void) fileclose(fp);
 506     }
 507     return tz;
 508 }
 509 
 510 #define TIMEZONE_FMRI   "svc:/system/timezone:default"
 511 #define TIMEZONE_PG     "timezone"
 512 #define LOCALTIME_PROP  "localtime"
 513 
 514 static void
 515 cleanupScf(scf_handle_t *h,
 516            scf_snapshot_t *snap,
 517            scf_instance_t *inst,
 518            scf_propertygroup_t *pg,
 519            scf_property_t *prop,
 520            scf_value_t *val,
 521            char *buf) {
 522     if (buf != NULL) {
 523         free(buf);
 524     }
 525     if (snap != NULL) {
 526         scf_snapshot_destroy(snap);
 527     }
 528     if (val != NULL) {
 529         scf_value_destroy(val);
 530     }
 531     if (prop != NULL) {
 532         scf_property_destroy(prop);
 533     }
 534     if (pg != NULL) {
 535         scf_pg_destroy(pg);
 536     }
 537     if (inst != NULL) {
 538         scf_instance_destroy(inst);
 539     }
 540     if (h != NULL) {
 541         scf_handle_destroy(h);
 542     }
 543 }
 544 
 545 /*
 546  * Retruns a zone ID of Solaris when the TZ value is "localtime".
 547  * First, it tries scf. If scf fails, it looks for the same file as
 548  * /usr/share/lib/zoneinfo/localtime under /usr/share/lib/zoneinfo/.
 549  */
 550 static char *
 551 getSolarisDefaultZoneID() {
 552     char *tz = NULL;
 553     struct stat statbuf;
 554     size_t size;
 555     char *buf;
 556     int fd;
 557     /* scf specific variables */
 558     scf_handle_t *h = NULL;
 559     scf_snapshot_t *snap = NULL;
 560     scf_instance_t *inst = NULL;
 561     scf_propertygroup_t *pg = NULL;
 562     scf_property_t *prop = NULL;
 563     scf_value_t *val = NULL;
 564 
 565     if ((h = scf_handle_create(SCF_VERSION)) != NULL
 566         && scf_handle_bind(h) == 0
 567         && (inst = scf_instance_create(h)) != NULL
 568         && (snap = scf_snapshot_create(h)) != NULL
 569         && (pg = scf_pg_create(h)) != NULL
 570         && (prop = scf_property_create(h)) != NULL
 571         && (val = scf_value_create(h)) != NULL
 572         && scf_handle_decode_fmri(h, TIMEZONE_FMRI, NULL, NULL, inst,
 573                                   NULL, NULL, SCF_DECODE_FMRI_REQUIRE_INSTANCE) == 0
 574         && scf_instance_get_snapshot(inst, "running", snap) == 0
 575         && scf_instance_get_pg_composed(inst, snap, TIMEZONE_PG, pg) == 0
 576         && scf_pg_get_property(pg, LOCALTIME_PROP, prop) == 0
 577         && scf_property_get_value(prop, val) == 0) {
 578         ssize_t len;
 579 
 580         /* Gets the length of the zone ID string */
 581         len = scf_value_get_astring(val, NULL, 0);
 582         if (len != -1) {
 583             tz = malloc(++len); /* +1 for a null byte */
 584             if (tz != NULL && scf_value_get_astring(val, tz, len) != -1) {
 585                 cleanupScf(h, snap, inst, pg, prop, val, NULL);
 586                 return tz;
 587             }
 588         }
 589     }
 590     cleanupScf(h, snap, inst, pg, prop, val, tz);
 591 
 592     if (stat(DEFAULT_ZONEINFO_FILE, &statbuf) == -1) {
 593         return NULL;
 594     }
 595     size = (size_t) statbuf.st_size;
 596     buf = malloc(size);
 597     if (buf == NULL) {
 598         return NULL;
 599     }
 600     if ((fd = open(DEFAULT_ZONEINFO_FILE, O_RDONLY)) == -1) {
 601         free((void *) buf);
 602         return NULL;
 603     }
 604 
 605     if (read(fd, buf, size) != (ssize_t) size) {
 606         (void) close(fd);
 607         free((void *) buf);
 608         return NULL;
 609     }
 610     (void) close(fd);
 611     tz = findZoneinfoFile(buf, size, ZONEINFO_DIR);
 612     free((void *) buf);
 613     return tz;
 614 }
 615 #endif /*__solaris__*/
 616 #endif /*__linux__*/
 617 
 618 #ifdef _AIX
 619 static char *
 620 getPlatformTimeZoneID()
 621 {
 622     return NULL;
 623 }
 624 #endif
 625 
 626 /*
 627  * findJavaTZ_md() maps platform time zone ID to Java time zone ID
 628  * using <java_home>/lib/tzmappings. If the TZ value is not found, it
 629  * trys some libc implementation dependent mappings. If it still
 630  * can't map to a Java time zone ID, it falls back to the GMT+/-hh:mm
 631  * form. `country', which can be null, is not used for UNIX platforms.
 632  */
 633 /*ARGSUSED1*/
 634 char *
 635 findJavaTZ_md(const char *java_home_dir, const char *country)
 636 {
 637     char *tz;
 638     char *javatz = NULL;
 639     char *freetz = NULL;
 640 
 641     tz = getenv("TZ");
 642 
 643 #if defined(__linux__) || defined(_ALLBSD_SOURCE)
 644     if (tz == NULL) {
 645 #else
 646 #if defined (__solaris__) || defined(_AIX)
 647     if (tz == NULL || *tz == '\0') {
 648 #endif
 649 #endif
 650         tz = getPlatformTimeZoneID();
 651         freetz = tz;
 652     }
 653 
 654     /*
 655      * Remove any preceding ':'
 656      */
 657     if (tz != NULL && *tz == ':') {
 658         tz++;
 659     }
 660 
 661 #ifdef __solaris__
 662     if (tz != NULL && strcmp(tz, "localtime") == 0) {
 663         tz = getSolarisDefaultZoneID();
 664         freetz = tz;
 665     }
 666 #endif
 667 
 668     if (tz != NULL) {
 669 #ifdef __linux__
 670         /*
 671          * Ignore "posix/" prefix.
 672          */
 673         if (strncmp(tz, "posix/", 6) == 0) {
 674             tz += 6;
 675         }
 676 #endif
 677         javatz = strdup(tz);
 678         if (freetz != NULL) {
 679             free((void *) freetz);
 680         }
 681     }
 682     return javatz;
 683 }
 684 /**
 685  * Returns a GMT-offset-based zone ID. (e.g., "GMT-08:00")
 686  */
 687 
 688 #ifdef MACOSX
 689 
 690 char *
 691 getGMTOffsetID()
 692 {
 693     time_t offset;
 694     char sign, buf[32];
 695     struct tm *local_tm;
 696     time_t clock;
 697     time_t currenttime;
 698 
 699     clock = time(NULL);
 700     tzset();
 701     local_tm = localtime(&clock);
 702     if (local_tm->tm_gmtoff >= 0) {
 703         offset = (time_t) local_tm->tm_gmtoff;
 704         sign = "+";
 705     } else {
 706         offset = (time_t) -local_tm->tm_gmtoff;
 707         sign = "-";
 708     }
 709     sprintf(buf, (const char *)"GMT%c%02d:%02d",
 710             sign, (int)(offset/3600), (int)((offset%3600)/60));
 711     return strdup(buf);
 712 }
 713 #else
 714 
 715 char *
 716 getGMTOffsetID()
 717 {
 718     time_t offset;
 719     char sign, buf[32];
 720 #ifdef __solaris__
 721     struct tm localtm;
 722     time_t currenttime;
 723 
 724     currenttime = time(NULL);
 725     if (localtime_r(&currenttime, &localtm) == NULL) {
 726         return NULL;
 727     }
 728 
 729     offset = localtm.tm_isdst ? altzone : timezone;
 730 #else
 731     offset = timezone;
 732 #endif /*__linux__*/
 733 
 734     if (offset == 0) {
 735         return strdup("GMT");
 736     }
 737 
 738     /* Note that the time offset direction is opposite. */
 739     if (offset > 0) {
 740         sign = '-';
 741     } else {
 742         offset = -offset;
 743         sign = '+';
 744     }
 745     sprintf(buf, (const char *)"GMT%c%02d:%02d",
 746             sign, (int)(offset/3600), (int)((offset%3600)/60));
 747     return strdup(buf);
 748 }
 749 #endif /* MACOSX */