Skip to main content

rustpython_vm/
cformat.rs

1//cspell:ignore bytesobject
2
3//! Implementation of Printf-Style string formatting
4//! as per the [Python Docs](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting).
5
6use itertools::Itertools;
7use num_traits::cast::ToPrimitive;
8
9use crate::{
10    AsObject, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, TryFromObject,
11    VirtualMachine,
12    builtins::{
13        PyBaseExceptionRef, PyByteArray, PyBytes, PyFloat, PyInt, PyStr,
14        int::check_int_to_str_digits, try_f64_to_bigint, tuple,
15    },
16    common::{
17        cformat::{
18            CCharacterType, CConversionFlags, CFormatBytes, CFormatConversion, CFormatPart,
19            CFormatPrecision, CFormatQuantity, CFormatSpec, CFormatSpecKeyed, CFormatType,
20            CFormatWtf8, CNumberType,
21        },
22        wtf8::{CodePoint, Wtf8, Wtf8Buf},
23    },
24    function::ArgIntoFloat,
25    protocol::{BufferFlags, PyBuffer},
26    stdlib::builtins,
27};
28
29fn format_decimal_object(
30    vm: &VirtualMachine,
31    spec: &CFormatSpec,
32    obj: &PyObject,
33) -> PyResult<String> {
34    let type_error = || {
35        vm.new_type_error(format!(
36            "%{} format: a real number is required, not {}",
37            spec.format_type.to_char(),
38            obj.class().slot_name()
39        ))
40    };
41    // Decimal conversions prefer __int__; __index__ is only a fallback when absent.
42    let i = obj
43        .number()
44        .int(vm)
45        .or_else(|| obj.try_index_opt(vm))
46        .ok_or_else(type_error)?
47        .map_err(|error| {
48            if error.fast_isinstance(vm.ctx.exceptions.type_error) {
49                type_error()
50            } else {
51                error
52            }
53        })?;
54    check_int_to_str_digits(i.as_bigint(), vm)?;
55    Ok(spec.format_number(i.as_bigint()))
56}
57
58fn spec_format_bytes(
59    vm: &VirtualMachine,
60    spec: &CFormatSpec,
61    obj: PyObjectRef,
62) -> PyResult<Vec<u8>> {
63    match &spec.format_type {
64        CFormatType::Unsupported { ch, index } => Err(vm.new_value_error(format!(
65            "unsupported format character '{}' ({:#x}) at index {index}",
66            ch.to_char_lossy(),
67            ch.to_u32(),
68        ))),
69        // Unlike strings, %r and %a are identical for bytes: the behaviour corresponds to
70        // %a for strings (not %r)
71        CFormatType::String(CFormatConversion::Repr | CFormatConversion::Ascii) => {
72            Ok(spec.format_bytes(builtins::ascii(obj, vm)?.as_bytes()))
73        }
74        // %b and %s are equivalent for bytes formatting.
75        // Mirrors CPython's format_obj() in bytesobject.c
76        CFormatType::Bytes | CFormatType::String(CFormatConversion::Str) => {
77            if let Some(bytes) = obj.downcast_ref::<PyBytes>() {
78                return Ok(spec.format_bytes(bytes.as_bytes()));
79            }
80            if let Some(bytearray) = obj.downcast_ref::<PyByteArray>() {
81                return Ok(spec.format_bytes(&bytearray.borrow_buf()));
82            }
83            if let Some(method) = vm.get_special_method(&obj, identifier!(vm, __bytes__))? {
84                let bytes = method.invoke((), vm)?;
85                let bytes = PyBytes::try_from_borrowed_object(vm, &bytes)?;
86                return Ok(spec.format_bytes(bytes.as_bytes()));
87            }
88            if obj.check_buffer() {
89                let buffer = PyBuffer::from_object(vm, &obj, BufferFlags::FULL_RO)?;
90                return Ok(buffer.contiguous_or_collect(|bytes| spec.format_bytes(bytes)));
91            }
92            let msg = format!(
93                "%b requires a bytes-like object, or an object that \
94                    implements __bytes__, not '{}'",
95                obj.class().name()
96            );
97            Err(vm.new_type_error(msg))
98        }
99        CFormatType::Number(number_type) => match number_type {
100            CNumberType::DecimalD | CNumberType::DecimalI | CNumberType::DecimalU => {
101                if let Some(i) = obj.downcast_ref::<PyInt>() {
102                    check_int_to_str_digits(i.as_bigint(), vm)?;
103                    Ok(spec.format_number(i.as_bigint()).into_bytes())
104                } else if let Some(f) = obj.downcast_ref_if_exact::<PyFloat>(vm) {
105                    let bigint = try_f64_to_bigint(f.to_f64(), vm)?;
106                    check_int_to_str_digits(&bigint, vm)?;
107                    Ok(spec.format_number(&bigint).into_bytes())
108                } else {
109                    format_decimal_object(vm, spec, &obj).map(String::into_bytes)
110                }
111            }
112            _ => {
113                // CPython parity: `%x` / `%o` / `%X` accept any object with
114                // `__index__`, not just PyInt. Mirrors PyNumber_Index dispatch.
115                if let Some(i) = obj.downcast_ref::<PyInt>() {
116                    Ok(spec.format_number(i.as_bigint()).into_bytes())
117                } else if let Some(int_result) = obj.try_index_opt(vm) {
118                    let i = int_result?;
119                    Ok(spec.format_number(i.as_bigint()).into_bytes())
120                } else {
121                    Err(vm.new_type_error(format!(
122                        "%{} format: an integer is required, not {}",
123                        spec.format_type.to_char(),
124                        obj.class().name()
125                    )))
126                }
127            }
128        },
129        CFormatType::Float(_) => {
130            let class = obj.class().to_owned();
131            let value = ArgIntoFloat::try_from_object(vm, obj).map_err(|e| {
132                if e.fast_isinstance(vm.ctx.exceptions.type_error) {
133                    // formatfloat in bytesobject.c generates its own specific exception
134                    // text in this case, mirror it here.
135                    vm.new_type_error(format!("float argument required, not {}", class.name()))
136                } else {
137                    e
138                }
139            })?;
140            Ok(spec.format_float(value.into()).into_bytes())
141        }
142        CFormatType::Character(CCharacterType::Character) => {
143            // CPython parity: bytes `%c` accepts a single byte or any object
144            // with `__index__` in range(256).
145            if let Some(b) = obj.downcast_ref::<PyBytes>() {
146                if b.as_bytes().len() == 1 {
147                    return Ok(spec.format_char(b.as_bytes()[0]));
148                }
149            } else if let Some(ba) = obj.downcast_ref::<PyByteArray>() {
150                let buf = ba.borrow_buf();
151                if buf.len() == 1 {
152                    return Ok(spec.format_char(buf[0]));
153                }
154            }
155            let int = if let Some(i) = obj.downcast_ref::<PyInt>() {
156                i.to_owned()
157            } else if let Some(int_result) = obj.try_index_opt(vm) {
158                int_result?
159            } else {
160                // A bytes-like argument that is not one byte long is named by
161                // its length rather than by its type.
162                let what = if let Some(b) = obj.downcast_ref::<PyBytes>() {
163                    format!("a bytes object of length {}", b.as_bytes().len())
164                } else if let Some(ba) = obj.downcast_ref::<PyByteArray>() {
165                    format!("a bytearray object of length {}", ba.borrow_buf().len())
166                } else {
167                    obj.class().name().to_string()
168                };
169                return Err(vm.new_type_error(format!(
170                    "%c requires an integer in range(256) or a single byte, not {what}"
171                )));
172            };
173            let ch = int
174                .try_to_primitive::<u8>(vm)
175                .map_err(|_| vm.new_overflow_error("%c arg not in range(256)"))?;
176            Ok(spec.format_char(ch))
177        }
178    }
179}
180
181fn spec_format_string(
182    vm: &VirtualMachine,
183    spec: &CFormatSpec,
184    obj: PyObjectRef,
185) -> PyResult<Wtf8Buf> {
186    match &spec.format_type {
187        CFormatType::Unsupported { ch, index } => Err(vm.new_value_error(format!(
188            "unsupported format character '{}' ({:#x}) at index {index}",
189            ch.to_char_lossy(),
190            ch.to_u32(),
191        ))),
192        CFormatType::String(conversion) => {
193            let result = match conversion {
194                CFormatConversion::Ascii => builtins::ascii(obj, vm)?.as_wtf8().to_owned(),
195                CFormatConversion::Str => obj.str(vm)?.as_wtf8().to_owned(),
196                CFormatConversion::Repr => obj.repr(vm)?.as_wtf8().to_owned(),
197            };
198            Ok(spec.format_string(result))
199        }
200        CFormatType::Bytes => {
201            // 'b' is rejected at parse time in Str context, see `CFormatContext`.
202            unreachable!("%b cannot be parsed in a str format string")
203        }
204        CFormatType::Number(number_type) => match number_type {
205            CNumberType::DecimalD | CNumberType::DecimalI | CNumberType::DecimalU => {
206                if let Some(i) = obj.downcast_ref::<PyInt>() {
207                    check_int_to_str_digits(i.as_bigint(), vm)?;
208                    Ok(spec.format_number(i.as_bigint()).into())
209                } else if let Some(f) = obj.downcast_ref_if_exact::<PyFloat>(vm) {
210                    let bigint = try_f64_to_bigint(f.to_f64(), vm)?;
211                    check_int_to_str_digits(&bigint, vm)?;
212                    Ok(spec.format_number(&bigint).into())
213                } else {
214                    format_decimal_object(vm, spec, &obj).map(Into::into)
215                }
216            }
217            _ => {
218                // CPython parity: `%x` / `%o` / `%X` accept any object with
219                // `__index__`, not just PyInt. Mirrors PyNumber_Index dispatch.
220                if let Some(i) = obj.downcast_ref::<PyInt>() {
221                    Ok(spec.format_number(i.as_bigint()).into())
222                } else if let Some(int_result) = obj.try_index_opt(vm) {
223                    let i = int_result?;
224                    Ok(spec.format_number(i.as_bigint()).into())
225                } else {
226                    Err(vm.new_type_error(format!(
227                        "%{} format: an integer is required, not {}",
228                        spec.format_type.to_char(),
229                        obj.class().name()
230                    )))
231                }
232            }
233        },
234        CFormatType::Float(_) => {
235            let value = ArgIntoFloat::try_from_object(vm, obj)?;
236            Ok(spec.format_float(value.into()).into())
237        }
238        CFormatType::Character(CCharacterType::Character) => {
239            // CPython parity: `%c` accepts a single-char str or any object with
240            // `__index__` (the latter via PyNumber_Index dispatch).
241            if let Some(s) = obj.downcast_ref::<PyStr>()
242                && let Ok(ch) = s.as_wtf8().code_points().exactly_one()
243            {
244                return Ok(spec.format_char(ch));
245            }
246            let int = if let Some(i) = obj.downcast_ref::<PyInt>() {
247                i.to_owned()
248            } else if let Some(int_result) = obj.try_index_opt(vm) {
249                int_result?
250            } else {
251                // A string argument that is not one character long is named by
252                // its length rather than by its type.
253                let what = match obj.downcast_ref::<PyStr>() {
254                    Some(s) => format!("a string of length {}", s.char_len()),
255                    None => obj.class().name().to_string(),
256                };
257                return Err(vm.new_type_error(format!(
258                    "%c requires an int or a unicode character, not {what}"
259                )));
260            };
261            let ch = int
262                .as_bigint()
263                .to_u32()
264                .and_then(CodePoint::from_u32)
265                .ok_or_else(|| vm.new_overflow_error("%c arg not in range(0x110000)"))?;
266            Ok(spec.format_char(ch))
267        }
268    }
269}
270
271fn get_star_arg(vm: &VirtualMachine, element: Option<&PyObject>) -> PyResult<i32> {
272    match element {
273        Some(obj) => {
274            if let Some(i) = obj.downcast_ref::<PyInt>() {
275                i.try_to_primitive::<i32>(vm)
276            } else {
277                Err(vm.new_type_error("* wants int"))
278            }
279        }
280        None => Err(vm.new_type_error("not enough arguments for format string")),
281    }
282}
283
284fn try_update_quantity_from_tuple<'a, I: Iterator<Item = &'a PyObject>>(
285    vm: &VirtualMachine,
286    elements: &mut I,
287    q: &mut Option<CFormatQuantity>,
288    f: &mut CConversionFlags,
289) -> PyResult<()> {
290    let Some(CFormatQuantity::FromValuesTuple) = q else {
291        return Ok(());
292    };
293
294    let width = get_star_arg(vm, elements.next())?;
295    if width < 0 {
296        f.insert(CConversionFlags::LEFT_ADJUST);
297    }
298    *q = Some(CFormatQuantity::Amount(width.unsigned_abs() as usize));
299    Ok(())
300}
301
302fn try_update_precision_from_tuple<'a, I: Iterator<Item = &'a PyObject>>(
303    vm: &VirtualMachine,
304    elements: &mut I,
305    p: &mut Option<CFormatPrecision>,
306) -> PyResult<()> {
307    let Some(CFormatPrecision::Quantity(CFormatQuantity::FromValuesTuple)) = p else {
308        return Ok(());
309    };
310
311    let precision = get_star_arg(vm, elements.next())?.max(0) as usize;
312    *p = Some(CFormatPrecision::Quantity(CFormatQuantity::Amount(
313        precision,
314    )));
315    Ok(())
316}
317
318fn specifier_error(vm: &VirtualMachine) -> PyBaseExceptionRef {
319    vm.new_type_error("format requires a mapping")
320}
321
322pub(crate) fn cformat_bytes(
323    vm: &VirtualMachine,
324    format_string: &[u8],
325    values_obj: &PyObject,
326) -> PyResult<Vec<u8>> {
327    let mut format = CFormatBytes::parse_from_bytes(format_string)
328        .map_err(|err| vm.new_value_error(err.to_string()))?;
329    let (num_specifiers, mapping_required) = format
330        .check_specifiers()
331        .ok_or_else(|| specifier_error(vm))?;
332
333    let mut result = vec![];
334
335    let is_mapping = values_obj.class().has_attr(identifier!(vm, __getitem__))
336        && !values_obj.fast_isinstance(vm.ctx.types.tuple_type)
337        && !values_obj.fast_isinstance(vm.ctx.types.bytes_type)
338        && !values_obj.fast_isinstance(vm.ctx.types.bytearray_type);
339
340    if num_specifiers == 0 {
341        if !is_mapping
342            && values_obj
343                .downcast_ref::<tuple::PyTuple>()
344                .is_none_or(|e| !e.as_slice().is_empty())
345        {
346            return Err(vm.new_type_error("not all arguments converted during bytes formatting"));
347        }
348
349        // literal only
350        for (_, part) in format.iter_mut() {
351            if let CFormatPart::Literal(literal) = part {
352                result.append(literal)
353            } else {
354                unreachable!()
355            }
356        }
357
358        return Ok(result);
359    }
360
361    if mapping_required {
362        if !is_mapping {
363            return Err(vm.new_type_error("format requires a mapping"));
364        }
365
366        // dict
367        for (_, part) in format {
368            match part {
369                CFormatPart::Literal(literal) => result.extend(literal),
370                CFormatPart::Spec(CFormatSpecKeyed { mapping_key, spec }) => {
371                    let key = mapping_key.unwrap();
372                    let value = values_obj.get_item(&key, vm)?;
373                    let part_result = spec_format_bytes(vm, &spec, value)?;
374                    result.extend(part_result);
375                }
376            }
377        }
378
379        return Ok(result);
380    }
381
382    // tuple
383    let mut slice_iter;
384    let mut once_iter;
385    let mut value_iter: &mut dyn Iterator<Item = &PyObject> =
386        if let Some(tup) = values_obj.downcast_ref::<tuple::PyTuple>() {
387            slice_iter = tup.as_slice().iter().map(|v| &**v);
388            &mut slice_iter
389        } else {
390            once_iter = core::iter::once(values_obj);
391            &mut once_iter
392        };
393
394    for (_, part) in format {
395        match part {
396            CFormatPart::Literal(literal) => result.extend(literal),
397            CFormatPart::Spec(CFormatSpecKeyed { mut spec, .. }) => {
398                try_update_quantity_from_tuple(
399                    vm,
400                    &mut value_iter,
401                    &mut spec.min_field_width,
402                    &mut spec.flags,
403                )?;
404                try_update_precision_from_tuple(vm, &mut value_iter, &mut spec.precision)?;
405
406                let Some(value) = value_iter.next() else {
407                    return Err(vm.new_type_error("not enough arguments for format string"));
408                };
409
410                let part_result = spec_format_bytes(vm, &spec, value.to_owned())?;
411                result.extend(part_result);
412            }
413        }
414    }
415
416    // check that all arguments were converted
417    if !is_mapping && value_iter.next().is_some() {
418        Err(vm.new_type_error("not all arguments converted during bytes formatting"))
419    } else {
420        Ok(result)
421    }
422}
423
424pub(crate) fn cformat_string(
425    vm: &VirtualMachine,
426    format_string: &Wtf8,
427    values_obj: &PyObject,
428) -> PyResult<Wtf8Buf> {
429    let format = CFormatWtf8::parse_from_wtf8(format_string)
430        .map_err(|err| vm.new_value_error(err.to_string()))?;
431    let (num_specifiers, mapping_required) = format
432        .check_specifiers()
433        .ok_or_else(|| specifier_error(vm))?;
434
435    let mut result = Wtf8Buf::new();
436
437    let is_mapping = values_obj.class().has_attr(identifier!(vm, __getitem__))
438        && !values_obj.fast_isinstance(vm.ctx.types.tuple_type)
439        && !values_obj.fast_isinstance(vm.ctx.types.str_type);
440
441    if num_specifiers == 0 {
442        if !is_mapping
443            && values_obj
444                .downcast_ref::<tuple::PyTuple>()
445                .is_none_or(|e| !e.as_slice().is_empty())
446        {
447            return Err(vm.new_type_error("not all arguments converted during string formatting"));
448        }
449
450        // literal only
451        for (_, part) in format.iter() {
452            if let CFormatPart::Literal(literal) = part {
453                result.push_wtf8(literal)
454            } else {
455                unreachable!()
456            }
457        }
458
459        return Ok(result);
460    }
461
462    if mapping_required {
463        if !is_mapping {
464            return Err(vm.new_type_error("format requires a mapping"));
465        }
466
467        // dict
468        for (_, part) in format {
469            match part {
470                CFormatPart::Literal(literal) => result.push_wtf8(&literal),
471                CFormatPart::Spec(CFormatSpecKeyed { mapping_key, spec }) => {
472                    let value = values_obj.get_item(&mapping_key.unwrap(), vm)?;
473                    let part_result = spec_format_string(vm, &spec, value)?;
474                    result.push_wtf8(&part_result);
475                }
476            }
477        }
478
479        return Ok(result);
480    }
481
482    // tuple
483    let mut slice_iter;
484    let mut once_iter;
485    let mut value_iter: &mut dyn Iterator<Item = &PyObject> =
486        if let Some(tup) = values_obj.downcast_ref::<tuple::PyTuple>() {
487            slice_iter = tup.as_slice().iter().map(|v| &**v);
488            &mut slice_iter
489        } else {
490            once_iter = core::iter::once(values_obj);
491            &mut once_iter
492        };
493
494    for (_, part) in format {
495        match part {
496            CFormatPart::Literal(literal) => result.push_wtf8(&literal),
497            CFormatPart::Spec(CFormatSpecKeyed { mut spec, .. }) => {
498                try_update_quantity_from_tuple(
499                    vm,
500                    &mut value_iter,
501                    &mut spec.min_field_width,
502                    &mut spec.flags,
503                )?;
504                try_update_precision_from_tuple(vm, &mut value_iter, &mut spec.precision)?;
505
506                let Some(value) = value_iter.next() else {
507                    return Err(vm.new_type_error("not enough arguments for format string"));
508                };
509
510                let part_result = spec_format_string(vm, &spec, value.to_owned())?;
511                result.push_wtf8(&part_result);
512            }
513        }
514    }
515
516    // check that all arguments were converted
517    if !is_mapping && value_iter.next().is_some() {
518        Err(vm.new_type_error("not all arguments converted during string formatting"))
519    } else {
520        Ok(result)
521    }
522}