int mark;
ByteList name = null;
Encoding encoding = null;
// used for RubyString functions to manage encoding, etc
RubyString wrapper = RubyString.newString(runtime, buf);
if (charFormat instanceof ByteList) {
ByteList list = (ByteList)charFormat;
format = list.getUnsafeBytes();
int begin = list.begin();
offset = begin;
length = begin + list.length();
start = begin;
mark = begin;
encoding = list.getEncoding();
} else {
format = stringToBytes(charFormat, false);
offset = 0;
length = charFormat.length();
start = 0;
mark = 0;
encoding = UTF8Encoding.INSTANCE;
}
while (offset < length) {
start = offset;
for ( ; offset < length && format[offset] != '%'; offset++) {}
if (offset > start) {
buf.append(format,start,offset-start);
start = offset;
}
if (offset++ >= length) break;
IRubyObject arg = null;
int flags = 0;
int width = 0;
int precision = 0;
int number = 0;
byte fchar = 0;
boolean incomplete = true;
for ( ; incomplete && offset < length ; ) {
switch (fchar = format[offset]) {
default:
if (fchar == '\0' && flags == FLAG_NONE) {
// MRI 1.8.6 behavior: null byte after '%'
// leads to "%" string. Null byte in
// other places, like "%5\0", leads to error.
buf.append('%');
buf.append(fchar);
incomplete = false;
offset++;
break;
} else if (isPrintable(fchar)) {
raiseArgumentError(args,"malformed format string - %" + (char)fchar);
} else {
raiseArgumentError(args,ERR_MALFORMED_FORMAT);
}
break;
case '<': {
// Ruby 1.9 named args
int nameStart = ++offset;
int nameEnd = nameStart;
for ( ; offset < length ; offset++) {
if (format[offset] == '>') {
nameEnd = offset;
offset++;
break;
}
}
if (nameEnd == nameStart) raiseArgumentError(args, ERR_MALFORMED_NAME);
ByteList oldName = name;
name = new ByteList(format, nameStart, nameEnd - nameStart, encoding, false);
if (oldName != null) raiseArgumentError(args, "name<" + name + "> after <" + oldName + ">");
break;
}
case '{': {
// Ruby 1.9 named replacement
int nameStart = ++offset;
int nameEnd = nameStart;
for ( ; offset < length ; offset++) {
if (format[offset] == '}') {
nameEnd = offset;
offset++;
break;
}
}
if (nameEnd == nameStart) raiseArgumentError(args, ERR_MALFORMED_NAME);
ByteList localName = new ByteList(format, nameStart, nameEnd - nameStart, encoding, false);
buf.append(args.next(localName).asString().getByteList());
incomplete = false;
break;
}
case ' ':
flags |= FLAG_SPACE;
offset++;
break;
case '0':
flags |= FLAG_ZERO;
offset++;
break;
case '+':
flags |= FLAG_PLUS;
offset++;
break;
case '-':
flags |= FLAG_MINUS;
offset++;
break;
case '#':
flags |= FLAG_SHARP;
offset++;
break;
case '1':case '2':case '3':case '4':case '5':
case '6':case '7':case '8':case '9':
// MRI doesn't flag it as an error if width is given multiple
// times as a number (but it does for *)
number = 0;
for ( ; offset < length && isDigit(fchar = format[offset]); offset++) {
number = extendWidth(args, number, fchar);
}
checkOffset(args,offset,length,ERR_MALFORMED_NUM);
if (fchar == '$') {
if (arg != null) {
raiseArgumentError(args,"value given twice - " + number + "$");
}
arg = args.getNth(number);
offset++;
} else {
width = number;
flags |= FLAG_WIDTH;
}
break;
case '*':
if ((flags & FLAG_WIDTH) != 0) {
raiseArgumentError(args,"width given twice");
}
flags |= FLAG_WIDTH;
// TODO: factor this chunk as in MRI/YARV GETASTER
checkOffset(args,++offset,length,ERR_MALFORMED_STAR_NUM);
mark = offset;
number = 0;
for ( ; offset < length && isDigit(fchar = format[offset]); offset++) {
number = extendWidth(args,number,fchar);
}
checkOffset(args,offset,length,ERR_MALFORMED_STAR_NUM);
if (fchar == '$') {
width = args.getNthInt(number);
if (width < 0) {
flags |= FLAG_MINUS;
width = -width;
}
offset++;
} else {
width = args.nextInt();
if (width < 0) {
flags |= FLAG_MINUS;
width = -width;
}
// let the width (if any), get processed in the next loop,
// so any leading 0 gets treated correctly
offset = mark;
}
break;
case '.':
if ((flags & FLAG_PRECISION) != 0) {
raiseArgumentError(args,"precision given twice");
}
flags |= FLAG_PRECISION;
checkOffset(args,++offset,length,ERR_MALFORMED_DOT_NUM);
fchar = format[offset];
if (fchar == '*') {
// TODO: factor this chunk as in MRI/YARV GETASTER
checkOffset(args,++offset,length,ERR_MALFORMED_STAR_NUM);
mark = offset;
number = 0;
for ( ; offset < length && isDigit(fchar = format[offset]); offset++) {
number = extendWidth(args,number,fchar);
}
checkOffset(args,offset,length,ERR_MALFORMED_STAR_NUM);
if (fchar == '$') {
precision = args.getNthInt(number);
if (precision < 0) {
flags &= ~FLAG_PRECISION;
}
offset++;
} else {
precision = args.nextInt();
if (precision < 0) {
flags &= ~FLAG_PRECISION;
}
// let the width (if any), get processed in the next loop,
// so any leading 0 gets treated correctly
offset = mark;
}
} else {
number = 0;
for ( ; offset < length && isDigit(fchar = format[offset]); offset++) {
number = extendWidth(args,number,fchar);
}
checkOffset(args,offset,length,ERR_MALFORMED_DOT_NUM);
precision = number;
}
break;
case '\n':
offset--;
case '%':
if (flags != FLAG_NONE) {
raiseArgumentError(args,ERR_ILLEGAL_FORMAT_CHAR);
}
buf.append('%');
offset++;
incomplete = false;
break;
case 'c': {
if (arg == null || name != null) {
arg = args.next(name);
name = null;
}
int c = 0;
// MRI 1.8.5-p12 doesn't support 1-char strings, but
// YARV 0.4.1 does. I don't think it hurts to include
// this; sprintf('%c','a') is nicer than sprintf('%c','a'[0])
if (arg instanceof RubyString) {
ByteList bytes = ((RubyString)arg).getByteList();
if (bytes.length() == 1) {
c = bytes.getUnsafeBytes()[bytes.begin()];
} else {
raiseArgumentError(args,"%c requires a character");
}
} else {
c = args.intValue(arg);
}
if ((flags & FLAG_WIDTH) != 0 && width > 1) {
if ((flags & FLAG_MINUS) != 0) {
buf.append(c);
buf.fill(' ', width-1);
} else {
buf.fill(' ',width-1);
buf.append(c);
}
} else {
buf.append(c);
}
offset++;
incomplete = false;
break;
}
case 'p':
case 's': {
if (arg == null || name != null) {
arg = args.next(name);
name = null;
}
if (fchar == 'p') {
arg = arg.callMethod(arg.getRuntime().getCurrentContext(),"inspect");
}
RubyString strArg = arg.asString();
ByteList bytes = strArg.getByteList();
Encoding enc = wrapper.checkEncoding(strArg);
int len = bytes.length();
int strLen = strArg.strLength();
if (arg.isTaint()) tainted = true;
if ((flags & FLAG_PRECISION) != 0 && precision < len) {
// TODO: precision is not considering actual character length