803 lines
25 KiB
C++
803 lines
25 KiB
C++
/*++
|
|
|
|
Copyright (c) 1995 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
rprintf.cxx
|
|
|
|
Abstract:
|
|
|
|
Contains my own version of printf(), sprintf() and vprintf(). Useful since
|
|
adding new printf escape sequences becomes easy
|
|
|
|
Contents:
|
|
rprintf limited re-entrant version of printf
|
|
rsprintf limited re-entrant version of sprintf
|
|
_sprintf routine which does the work
|
|
|
|
Author:
|
|
|
|
Richard L Firth (rfirth) 20-Jun-1995
|
|
|
|
Revision History:
|
|
|
|
29-Aug-1989 rfirth
|
|
Created
|
|
|
|
--*/
|
|
|
|
#include <wininetp.h>
|
|
#include "rprintf.h"
|
|
|
|
//
|
|
// defines for flags word
|
|
//
|
|
|
|
#define F_SPACES 0x00000001 // prefix field with spaces
|
|
#define F_ZEROES 0x00000002 // prefix field with zeroes
|
|
#define F_MINUS 0x00000004 // field is left justified
|
|
#define F_HASH 0x00000008 // hex field is prefixed with 0x/0X
|
|
#define F_XUPPER 0x00000010 // hex field has upper case letters
|
|
#define F_LONG 0x00000020 // long int/hex/oct prefix
|
|
#define F_PLUS 0x00000040 // prefix +'ve signed number with +
|
|
#define F_DOT 0x00000080 // separator for field and precision
|
|
#define F_NEAR 0x00000100 // far pointer has near prefix
|
|
#define F_FAR 0x00000200 // near pointer has far prefix
|
|
#define F_SREPLICATE 0x00000400 // this field replicated
|
|
#define F_EREPLICATE 0x00000800 // end of replications
|
|
#define F_UNICODE 0x00001000 // string is wide character (%ws/%wq)
|
|
#define F_QUOTING 0x00002000 // strings enclosed in double quotes
|
|
#define F_ELLIPSE 0x00004000 // a sized, quoted string ends in "..."
|
|
|
|
#define BUFFER_SIZE 1024
|
|
|
|
//
|
|
// minimum field widths for various ASCII representations of numbers
|
|
//
|
|
|
|
#define MIN_BIN_WIDTH 16 // minimum field width in btoa
|
|
#define MIN_HEX_WIDTH 8 // minimum field width in xtoa
|
|
#define MIN_INT_WIDTH 10 // minimum field width in itoa
|
|
#define MIN_LHEX_WIDTH 8 // minimum field width in long xtoa
|
|
#define MIN_LINT_WIDTH 10 // minimum field width in long itoa
|
|
#define MIN_LOCT_WIDTH 11 // minimum field width in long otoa
|
|
#define MIN_OCT_WIDTH 11 // minimum field width in otoa
|
|
#define MIN_UINT_WIDTH 10 // minimum field width in utoa
|
|
|
|
//
|
|
// character defines
|
|
//
|
|
|
|
#define EOSTR '\0'
|
|
#define CR '\x0d'
|
|
#define LF '\x0a'
|
|
|
|
#if !defined(min)
|
|
|
|
#define min(a, b) ((a)<(b)) ? (a) : (b)
|
|
|
|
#endif
|
|
|
|
PRIVATE int _atoi(char**);
|
|
PRIVATE void convert(char**, ULONG_PTR, int, int, unsigned, char(*)(ULONG_PTR*));
|
|
PRIVATE char btoa(ULONG_PTR *);
|
|
PRIVATE char otoa(ULONG_PTR *);
|
|
PRIVATE char utoa(ULONG_PTR *);
|
|
PRIVATE char xtoa(ULONG_PTR *);
|
|
PRIVATE char Xasc(ULONG_PTR *);
|
|
|
|
/*** rprintf - a re-entrant cut-down version of printf. Understands usual
|
|
* printf format characters introduced by '%' plus one or
|
|
* two additions
|
|
*
|
|
* ENTRY format - pointer to buffer containing format string defining
|
|
* the output. As per usual printf the arguments to
|
|
* fill in the blanks in the format string are on the
|
|
* the stack after the format string
|
|
*
|
|
* <args> - arguments on stack, size and type determined from
|
|
* the format string
|
|
*
|
|
* EXIT format string used to convert arguments (if any) and print
|
|
* the results to stdout.
|
|
* The number of character copied is the value returned
|
|
*/
|
|
|
|
#ifdef UNUSED
|
|
int cdecl rprintf(char* format, ...) {
|
|
|
|
int charsPrinted = 0;
|
|
char buffer[BUFFER_SIZE];
|
|
DWORD nwritten;
|
|
va_list args;
|
|
|
|
/* print the output into buffer then print the formatted buffer to the
|
|
* screen
|
|
*/
|
|
|
|
va_start(args, format);
|
|
charsPrinted = _sprintf(buffer, format, args);
|
|
va_end(args);
|
|
|
|
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),
|
|
buffer,
|
|
charsPrinted,
|
|
&nwritten,
|
|
0
|
|
);
|
|
return nwritten;
|
|
}
|
|
#endif
|
|
|
|
/*** rsprintf - a re-entrant cut-down version of sprintf. See rprintf for
|
|
* details
|
|
*
|
|
* ENTRY buffer - pointer to the buffer which will receive the
|
|
* formatted output
|
|
*
|
|
* format - pointer to buffer which defines the formatted
|
|
* output. Consists of normal printing characters
|
|
* and printf-style format characters (see rprintf)
|
|
*
|
|
* EXIT characters from format string and arguments converted to
|
|
* character format based on format string are copied into the
|
|
* buffer
|
|
* The number of character copied is the value returned
|
|
*/
|
|
|
|
int cdecl rsprintf(char* buffer, char* format, ...) {
|
|
|
|
va_list args;
|
|
int n;
|
|
|
|
va_start(args, format);
|
|
n = _sprintf(buffer, format, args);
|
|
va_end(args);
|
|
return n;
|
|
}
|
|
|
|
/*** _sprintf - performs the sprintf function. Receives an extra parameter
|
|
* on the stack which is the pointer to the variable argument
|
|
* list of rprintf and rsprintf
|
|
*
|
|
* ENTRY buffer - pointer to buffer which will receive the output
|
|
*
|
|
* format - pointer to the format string
|
|
*
|
|
* args - variable argument list which will be used to fill in
|
|
* the escape sequences in the format string
|
|
*
|
|
* EXIT The characters in the format string are used to convert the
|
|
* arguments and copy them to the buffer.
|
|
* The number of character copied is the value returned
|
|
*/
|
|
|
|
int cdecl _sprintf(char* buffer, char* format, va_list args) {
|
|
|
|
char* original = buffer;
|
|
int FieldWidth;
|
|
int FieldPrecision;
|
|
int FieldLen;
|
|
BOOL SubDone;
|
|
int StrLen;
|
|
int i;
|
|
DWORD flags;
|
|
int replications;
|
|
|
|
while (*format) {
|
|
switch ((unsigned)*format) {
|
|
case '\n':
|
|
|
|
//
|
|
// convert line-feed to carriage-return, line-feed. But only if the
|
|
// format string doesn't already contain a carriage-return directly
|
|
// before the line-feed! This way we can make multiple calls into
|
|
// this function, with the same buffer, and only once expand the
|
|
// line-feed
|
|
//
|
|
|
|
if (*(buffer - 1) != CR) {
|
|
*buffer++ = CR;
|
|
}
|
|
*buffer++ = LF;
|
|
break;
|
|
|
|
case '%':
|
|
SubDone = FALSE;
|
|
flags = 0;
|
|
FieldWidth = 0;
|
|
FieldPrecision = 0;
|
|
replications = 1; /* default replication is 1 */
|
|
while (!SubDone) {
|
|
switch ((unsigned)*++format) {
|
|
case '%':
|
|
*buffer++ = '%';
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case ' ':
|
|
flags |= F_SPACES;
|
|
break;
|
|
|
|
case '#':
|
|
flags |= F_HASH;
|
|
break;
|
|
|
|
case '-':
|
|
flags |= F_MINUS;
|
|
break;
|
|
|
|
case '+':
|
|
flags |= F_PLUS;
|
|
break;
|
|
|
|
case '.':
|
|
flags |= F_DOT;
|
|
break;
|
|
|
|
case '*':
|
|
if (flags & F_DOT) {
|
|
FieldPrecision = va_arg(args, int);
|
|
} else {
|
|
FieldWidth = va_arg(args, int);
|
|
}
|
|
break;
|
|
|
|
case '@':
|
|
replications = _atoi(&format);
|
|
break;
|
|
|
|
case '[':
|
|
flags |= F_SREPLICATE;
|
|
break;
|
|
|
|
case ']':
|
|
flags |= F_EREPLICATE;
|
|
break;
|
|
|
|
case '0':
|
|
/* if this is leading zero then caller wants
|
|
* zero prefixed number of given width (%04x)
|
|
*/
|
|
if (!(flags & F_ZEROES)) {
|
|
flags |= F_ZEROES;
|
|
break;
|
|
}
|
|
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
if (flags & F_DOT) {
|
|
FieldPrecision = _atoi(&format);
|
|
} else {
|
|
FieldWidth = _atoi(&format);
|
|
}
|
|
break;
|
|
|
|
case 'b':
|
|
|
|
//
|
|
// Binary representation
|
|
//
|
|
|
|
while (replications--) {
|
|
convert(&buffer,
|
|
va_arg(args, unsigned int),
|
|
(FieldWidth) ? FieldWidth : MIN_BIN_WIDTH,
|
|
MIN_BIN_WIDTH,
|
|
flags,
|
|
btoa
|
|
);
|
|
}
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case 'B':
|
|
|
|
//
|
|
// Boolean representation
|
|
//
|
|
|
|
if (va_arg(args, BOOL)) {
|
|
*buffer++ = 'T';
|
|
*buffer++ = 'R';
|
|
*buffer++ = 'U';
|
|
*buffer++ = 'E';
|
|
} else {
|
|
*buffer++ = 'F';
|
|
*buffer++ = 'A';
|
|
*buffer++ = 'L';
|
|
*buffer++ = 'S';
|
|
*buffer++ = 'E';
|
|
}
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case 'c':
|
|
|
|
//
|
|
// assume that a character is the size of the
|
|
// width of the stack which in turn has the same
|
|
// width as an integer
|
|
//
|
|
|
|
--FieldWidth;
|
|
while (replications--) {
|
|
for (i = 0; i < FieldWidth; i++) {
|
|
*buffer++ = ' ';
|
|
}
|
|
*buffer++ = (char) va_arg(args, int);
|
|
}
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case 'd':
|
|
case 'i':
|
|
while (replications--) {
|
|
|
|
long l;
|
|
|
|
l = (flags & F_LONG) ? va_arg(args, long) : (long)va_arg(args, int);
|
|
if (l < 0) {
|
|
*buffer++ = '-';
|
|
if (flags & F_LONG) {
|
|
l = -(long)l;
|
|
} else {
|
|
l = -(int)l;
|
|
}
|
|
} else if (flags & F_PLUS) {
|
|
*buffer++ = '+';
|
|
}
|
|
convert(&buffer,
|
|
l,
|
|
FieldWidth,
|
|
(flags & F_LONG) ? MIN_LINT_WIDTH : MIN_INT_WIDTH,
|
|
flags,
|
|
utoa
|
|
);
|
|
}
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case 'e':
|
|
/* not currently handled */
|
|
break;
|
|
|
|
case 'f':
|
|
/* not currently handled */
|
|
break;
|
|
|
|
case 'F':
|
|
flags |= F_FAR;
|
|
break;
|
|
|
|
case 'g':
|
|
case 'G':
|
|
/* not currently handled */
|
|
break;
|
|
|
|
case 'h':
|
|
/* not currently handled */
|
|
break;
|
|
|
|
case 'l':
|
|
flags |= F_LONG;
|
|
break;
|
|
|
|
case 'L':
|
|
/* not currently handled */
|
|
break;
|
|
|
|
case 'n':
|
|
*(va_arg(args, int*)) = (int)(buffer - original);
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case 'N':
|
|
flags |= F_NEAR;
|
|
break;
|
|
|
|
case 'o':
|
|
while (replications--) {
|
|
convert(&buffer,
|
|
(flags & F_LONG) ? va_arg(args, unsigned long) : (unsigned long)va_arg(args, unsigned),
|
|
FieldWidth,
|
|
(flags & F_LONG) ? MIN_LOCT_WIDTH : MIN_OCT_WIDTH,
|
|
flags,
|
|
otoa
|
|
);
|
|
}
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case 'p':
|
|
while (replications--) {
|
|
|
|
void* p;
|
|
|
|
if (!(flags & F_NEAR)) {
|
|
convert(&buffer,
|
|
(ULONG_PTR) va_arg(args, char near *),
|
|
MIN_HEX_WIDTH,
|
|
MIN_HEX_WIDTH,
|
|
flags | F_XUPPER,
|
|
Xasc
|
|
);
|
|
*buffer++ = ':';
|
|
}
|
|
convert(&buffer,
|
|
(ULONG_PTR)va_arg(args, unsigned),
|
|
MIN_HEX_WIDTH,
|
|
MIN_HEX_WIDTH,
|
|
flags | F_XUPPER,
|
|
Xasc
|
|
);
|
|
}
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case 'Q': // quoted unicode string
|
|
flags |= F_UNICODE;
|
|
// *** FALL THROUGH ***
|
|
|
|
case 'q':
|
|
*buffer++ = '"';
|
|
flags |= F_QUOTING;
|
|
|
|
//
|
|
// *** FALL THROUGH ***
|
|
//
|
|
|
|
case 's':
|
|
while (replications--) {
|
|
|
|
char* s;
|
|
|
|
s = va_arg(args, char*);
|
|
if (s != NULL) {
|
|
|
|
// darrenmi 2/24/00 Note that if the string has a field precision,
|
|
// it's not always null terminated!! Don't depend on it being psz
|
|
// and stop when we hit our max length.
|
|
StrLen = 0;
|
|
|
|
if (flags & F_UNICODE) {
|
|
WCHAR *pWork = (LPWSTR)s;
|
|
while((!FieldPrecision || StrLen < FieldPrecision) && *pWork)
|
|
{
|
|
pWork++;
|
|
StrLen++;
|
|
}
|
|
} else {
|
|
CHAR *pWork = s;
|
|
while((!FieldPrecision || StrLen < FieldPrecision) && *pWork)
|
|
{
|
|
pWork++;
|
|
StrLen++;
|
|
}
|
|
}
|
|
|
|
FieldLen = (FieldPrecision)
|
|
? min(StrLen, FieldPrecision)
|
|
: StrLen
|
|
;
|
|
if ((flags & F_QUOTING) && (FieldPrecision > 3) && (FieldLen == FieldPrecision)) {
|
|
FieldLen -= 3;
|
|
flags |= F_ELLIPSE;
|
|
}
|
|
|
|
for (i = 0; i < (FieldWidth - FieldLen); i++) {
|
|
*buffer++ = ' ';
|
|
}
|
|
|
|
if (flags & F_UNICODE) {
|
|
|
|
char wbuf[4096];
|
|
int wi;
|
|
|
|
WideCharToMultiByte(CP_ACP, 0,
|
|
(LPWSTR)s, -1,
|
|
wbuf, 4096,
|
|
NULL, NULL);
|
|
|
|
for (wi = 0; wbuf[wi] && FieldLen; ++wi) {
|
|
*buffer = wbuf[wi];
|
|
|
|
//
|
|
// if this is a quoted string, and it contains
|
|
// \r and/or \n, then we reverse-convert these
|
|
// characters, since we don't want then to
|
|
// break the string. Do the same for \t
|
|
//
|
|
|
|
if (flags & F_QUOTING) {
|
|
|
|
char ch;
|
|
|
|
ch = *buffer;
|
|
if ((ch == '\r') || (ch == '\n') || (ch == '\t')) {
|
|
*buffer++ = '\\';
|
|
*buffer = (ch == '\r')
|
|
? 'r'
|
|
: (ch == '\n')
|
|
? 'n'
|
|
: 't'
|
|
;
|
|
}
|
|
}
|
|
++buffer;
|
|
--FieldLen;
|
|
}
|
|
} else {
|
|
while (*s && FieldLen) {
|
|
*buffer = *s++;
|
|
|
|
//
|
|
// if this is a quoted string, and it contains
|
|
// \r and/or \n, then we reverse-convert these
|
|
// characters, since we don't want then to
|
|
// break the string. Do the same for \t
|
|
//
|
|
|
|
if (flags & F_QUOTING) {
|
|
|
|
char ch;
|
|
|
|
ch = *buffer;
|
|
if ((ch == '\r') || (ch == '\n') || (ch == '\t')) {
|
|
*buffer++ = '\\';
|
|
*buffer = (ch == '\r')
|
|
? 'r'
|
|
: (ch == '\n')
|
|
? 'n'
|
|
: 't'
|
|
;
|
|
}
|
|
}
|
|
++buffer;
|
|
--FieldLen;
|
|
}
|
|
}
|
|
if (flags & F_ELLIPSE) {
|
|
*buffer++ = '.';
|
|
*buffer++ = '.';
|
|
*buffer++ = '.';
|
|
}
|
|
} else if (!(flags & F_QUOTING)) {
|
|
*buffer++ = '(';
|
|
*buffer++ = 'n';
|
|
*buffer++ = 'u';
|
|
*buffer++ = 'l';
|
|
*buffer++ = 'l';
|
|
*buffer++ = ')';
|
|
}
|
|
}
|
|
if (flags & F_QUOTING) {
|
|
*buffer++ = '"';
|
|
}
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case 'S':
|
|
break;
|
|
|
|
case 'u':
|
|
while (replications--) {
|
|
convert(&buffer,
|
|
va_arg(args, unsigned),
|
|
FieldWidth,
|
|
MIN_UINT_WIDTH,
|
|
flags,
|
|
utoa
|
|
);
|
|
}
|
|
SubDone = TRUE;
|
|
break;
|
|
|
|
case 'w':
|
|
flags |= F_UNICODE;
|
|
break;
|
|
|
|
case 'X':
|
|
flags |= F_XUPPER;
|
|
|
|
//
|
|
// *** FALL THROUGH ***
|
|
//
|
|
|
|
case 'x':
|
|
while (replications--) {
|
|
if (flags & F_HASH) {
|
|
*buffer++ = '0';
|
|
*buffer++ = (flags & F_XUPPER) ? (char)'X' : (char)'x';
|
|
}
|
|
convert(&buffer,
|
|
(flags & F_LONG) ? va_arg(args, unsigned long) : va_arg(args, unsigned),
|
|
FieldWidth,
|
|
(flags & F_LONG) ? MIN_LHEX_WIDTH : MIN_HEX_WIDTH,
|
|
flags,
|
|
(flags & F_XUPPER) ? Xasc : xtoa
|
|
);
|
|
}
|
|
SubDone = TRUE;
|
|
break;
|
|
} /* switch <%-specifier> */
|
|
}
|
|
break;
|
|
|
|
default:
|
|
*buffer++ = *format;
|
|
} /* switch <character> */
|
|
++format;
|
|
} /* while */
|
|
*buffer = EOSTR;
|
|
return (int)(buffer - original);
|
|
}
|
|
|
|
/*** _atoi - ascii to integer conversion used to get the field width out
|
|
* of the format string
|
|
*
|
|
* ENTRY p - pointer to pointer to format string
|
|
*
|
|
* EXIT returns the number found in the prefix string as a (16-bit)
|
|
* int format string pointer is updated past the field width
|
|
*/
|
|
|
|
PRIVATE
|
|
int _atoi(char** p) {
|
|
|
|
int n = 0;
|
|
int i = 5;
|
|
|
|
while ((**p >= '0' && **p <= '9') && i--) {
|
|
n = n*10+((int)(*(*p)++)-(int)'0');
|
|
}
|
|
|
|
/* put the format pointer back one since the major loop tests *++format */
|
|
|
|
--*p;
|
|
return n;
|
|
}
|
|
|
|
/*** convert - convert number to representation defined by procedure
|
|
*
|
|
* ENTRY buffer - pointer to buffer to receive conversion
|
|
* n - number to convert
|
|
* width - user defined field width
|
|
* mwidth - minimum width for representation
|
|
* flags - flags controlling conversion
|
|
* proc - pointer to conversion routine
|
|
*
|
|
* EXIT buffer is updated to point past the number representation
|
|
* just put into it
|
|
*/
|
|
|
|
PRIVATE
|
|
void
|
|
convert(
|
|
char** buffer,
|
|
ULONG_PTR n,
|
|
int width,
|
|
int mwidth,
|
|
unsigned flags,
|
|
char (*proc)(ULONG_PTR*)
|
|
)
|
|
{
|
|
char numarray[33];
|
|
int MinWidth;
|
|
int i;
|
|
|
|
MinWidth = (width < mwidth) ? mwidth : width;
|
|
i = MinWidth;
|
|
do {
|
|
numarray[--i] = (*proc)(&n);
|
|
} while (n);
|
|
while (width > MinWidth-i) {
|
|
numarray[--i] = (char)((flags & F_SPACES) ? ' ' : '0');
|
|
}
|
|
while (i < MinWidth) {
|
|
*(*buffer)++ = numarray[i++];
|
|
}
|
|
}
|
|
|
|
/*** btoa - return next (least significant) char in a binary to ASCII
|
|
* conversion
|
|
*
|
|
* ENTRY pn - pointer to number to convert
|
|
*
|
|
* EXIT returns next (LS) character, updates original number
|
|
*/
|
|
|
|
PRIVATE
|
|
char btoa(ULONG_PTR *pn) {
|
|
|
|
char rch;
|
|
|
|
rch = (char)(*pn&1)+'0';
|
|
*pn >>= 1;
|
|
return rch;
|
|
}
|
|
|
|
/*** otoa - return next (least significant) char in an octal to ASCII
|
|
* conversion
|
|
*
|
|
* ENTRY pn - pointer to number to convert
|
|
*
|
|
* EXIT returns next (LS) character, updates original number
|
|
*/
|
|
|
|
PRIVATE
|
|
char otoa(ULONG_PTR *pn) {
|
|
|
|
char rch;
|
|
|
|
rch = (char)'0'+(char)(*pn&7);
|
|
*pn >>= 3;
|
|
return rch;
|
|
}
|
|
|
|
/*** utoa - return next (least significant) char in an unsigned int to
|
|
* ASCII conversion
|
|
*
|
|
* ENTRY pn - pointer to number to convert
|
|
*
|
|
* EXIT returns next (LS) character, updates original number
|
|
*/
|
|
|
|
PRIVATE
|
|
char utoa(ULONG_PTR *pn) {
|
|
|
|
char rch;
|
|
|
|
rch = (char)'0'+(char)(*pn%10);
|
|
*pn /= 10;
|
|
return rch;
|
|
}
|
|
|
|
/*** xtoa - return next (least significant) char in a hexadecimal to
|
|
* ASCII conversion. Returns lower case hex characters
|
|
*
|
|
* ENTRY pn - pointer to number to convert
|
|
*
|
|
* EXIT returns next (LS) character, updates original number
|
|
*/
|
|
|
|
PRIVATE
|
|
char xtoa(ULONG_PTR *pn) {
|
|
|
|
ULONG_PTR n = *pn & 0x000f;
|
|
char rch = (n <= 9)
|
|
? (char)n+'0'
|
|
: (char)n+'0'+('a'-'9'-1);
|
|
|
|
*pn >>= 4;
|
|
return rch;
|
|
}
|
|
|
|
/*** Xasc - return next (least significant) char in a hexadecimal to
|
|
* ASCII conversion. Returns upper case hex characters
|
|
*
|
|
* ENTRY pn - pointer to number to convert
|
|
*
|
|
* EXIT returns next (LS) character, updates original number
|
|
*/
|
|
|
|
PRIVATE
|
|
char Xasc(ULONG_PTR *pn) {
|
|
|
|
ULONG_PTR n = *pn & 0x000f;
|
|
char rch = (n <= 9)
|
|
? (char)n+'0'
|
|
: (char)n+'0'+('A'-'9'-1);
|
|
|
|
*pn >>= 4;
|
|
return rch;
|
|
}
|