Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 47 additions & 18 deletions .github/workflows/reusable-san.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ permissions:

env:
FORCE_COLOR: 1
OPENSSL_VER: 3.5.7

jobs:
build-san-reusable:
Expand All @@ -38,28 +39,55 @@ jobs:
run: |
sudo ./.github/workflows/posix-deps-apt.sh
# On ubuntu-26.04 image, clang is clang-21 by default

if [ "${SANITIZER}" = "TSan" ]; then
# Reduce ASLR to avoid TSan crashing
sudo sysctl -w vm.mmap_rnd_bits=28
fi

- name: Sanitizer option setup
run: |
if [ "${SANITIZER}" = "TSan" ]; then
echo "TSAN_OPTIONS=${SAN_LOG_OPTION} suppressions=${GITHUB_WORKSPACE}/Tools/tsan/suppressions${{
fromJSON(inputs.free-threading)
&& '_free_threading'
|| ''
}}.txt handle_segv=0" >> "$GITHUB_ENV"
else
echo "UBSAN_OPTIONS=${SAN_LOG_OPTION} halt_on_error=1 suppressions=${GITHUB_WORKSPACE}/Tools/ubsan/suppressions.txt" >> "$GITHUB_ENV"
fi
echo "CC=clang" >> "$GITHUB_ENV"
echo "CXX=clang++" >> "$GITHUB_ENV"
- name: TSan option setup
if: inputs.sanitizer == 'TSan'
run: |
sudo sysctl -w vm.mmap_rnd_bits=28 # Reduce ASLR to avoid TSan crashing

echo "MULTISSL_DIR=${GITHUB_WORKSPACE}/multissl" >> "$GITHUB_ENV"
echo "OPENSSL_DIR=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}" >> "$GITHUB_ENV"
echo "TSAN_OPTIONS=${SAN_LOG_OPTION} suppressions=${GITHUB_WORKSPACE}/Tools/tsan/suppressions${SUPPRESSIONS_SUFFIX}.txt handle_segv=0" >> "$GITHUB_ENV"
env:
SANITIZER: ${{ inputs.sanitizer }}
SAN_LOG_OPTION: log_path=${{ github.workspace }}/san_log
SUPPRESSIONS_SUFFIX: >-
${{
fromJSON(inputs.free-threading)
&& '_free_threading'
|| ''
}}
- name: UBSan option setup
if: inputs.sanitizer != 'TSan'
run: >-
echo
"UBSAN_OPTIONS=${SAN_LOG_OPTION}
halt_on_error=1
suppressions=${GITHUB_WORKSPACE}/Tools/ubsan/suppressions.txt"
>> "$GITHUB_ENV"
env:
SAN_LOG_OPTION: log_path=${{ github.workspace }}/san_log
- name: Add ccache to PATH
run: |
echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- name: 'Restore OpenSSL build (TSan)'
id: cache-openssl
if: inputs.sanitizer == 'TSan'
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ./multissl/openssl/${{ env.OPENSSL_VER }}
key: ${{ env.IMAGE_OS_VERSION }}-multissl-openssl-tsan-${{ env.OPENSSL_VER }}
- name: Install OpenSSL (TSan)
if: >-
inputs.sanitizer == 'TSan'
&& steps.cache-openssl.outputs.cache-hit != 'true'
run: >-
python3 Tools/ssl/multissltests.py
--steps=library
--base-directory="${MULTISSL_DIR}"
--openssl="${OPENSSL_VER}"
--system=Linux
--tsan
- name: Configure CPython
run: >-
./configure
Expand All @@ -70,6 +98,7 @@ jobs:
|| '--with-undefined-behavior-sanitizer --with-strict-overflow'
}}
--with-pydebug
${{ inputs.sanitizer == 'TSan' && '--with-openssl="$OPENSSL_DIR" --with-openssl-rpath=auto' || '' }}
${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }}
- name: Build CPython
run: make -j4
Expand Down
8 changes: 8 additions & 0 deletions Doc/library/codecs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,14 @@ encodings.
| | | code actually uses UTF-8 |
| | | by default. |
+--------------------+---------+---------------------------+
| utf-7-imap | mUTF-7 | Modified UTF-7 encoding |
| | | of :rfc:`3501` for IMAP4 |
| | | mailbox names. Only |
| | | ``errors='strict'`` is |
| | | supported. |
| | | |
| | | .. versionadded:: next |
+--------------------+---------+---------------------------+

.. versionchanged:: 3.8
"unicode_internal" codec is removed.
Expand Down
30 changes: 30 additions & 0 deletions Doc/library/ipaddress.rst
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,35 @@ dictionaries.
.. deprecated:: 3.7
It uses the same ordering and comparison algorithm as "<", "==", and ">"

.. method:: next_network(next_prefix=None)

Finds the next closest network of prefix size *next_prefix*. If
*next_prefix=None*, then the current network prefix will be used.
Returns a single network object.

Raises :exc:`ValueError` if *next_prefix* is out of range or no further
network of the requested size exists.

>>> IPv4Network('192.0.2.0/24').next_network()
IPv4Network('192.0.3.0/24')
>>> IPv4Network('192.0.2.0/24').next_network(next_prefix=25)
IPv4Network('192.0.3.0/25')
>>> IPv4Network('192.0.2.0/24').next_network(next_prefix=23)
IPv4Network('192.0.4.0/23')
>>> IPv4Network('192.0.80.0/22').next_network(next_prefix=18)
IPv4Network('192.0.128.0/18')
>>> IPv4Network('192.0.80.0/22').next_network(next_prefix=50)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise ValueError(
ValueError: next prefix must be between 1 and 32
>>> IPv4Network('255.255.255.0/24').next_network()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise ValueError(
ValueError: out of address space, cannot make another /24 network

.. versionadded:: 3.16

.. class:: IPv6Network(address, strict=True)

Expand Down Expand Up @@ -790,6 +819,7 @@ dictionaries.
.. method:: supernet(prefixlen_diff=1, new_prefix=None)
.. method:: subnet_of(other)
.. method:: supernet_of(other)
.. method:: next_network(next_prefix=None)
.. method:: compare_networks(other)

Refer to the corresponding attribute documentation in
Expand Down
8 changes: 7 additions & 1 deletion Doc/library/tkinter.font.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ The different font weights and slants are:
requested ones because of platform limitations.
With no *option*, return a dictionary of all the attributes; if *option*
is given, return the value of that single attribute.
The attributes are resolved on the display of the *displayof* widget,
or the main application window if it is not specified.

.. method:: cget(option)

Expand All @@ -97,7 +99,11 @@ The different font weights and slants are:

.. method:: copy()

Return new instance of the current font.
Return a distinct copy of the current font:
a new named font with the same attributes but a different name,
which can be reconfigured independently of the original.
If the current font wraps a font description,
the copy is instead a named font with its resolved attributes.

.. method:: measure(text, displayof=None)

Expand Down
16 changes: 16 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,14 @@ curses
wide-character support.
(Contributed by Serhiy Storchaka in :gh:`133031`.)

encodings
---------

* Add the ``utf-7-imap`` codec, implementing the modified UTF-7 encoding
used for international IMAP4 mailbox names (:rfc:`3501`).
(Contributed by Serhiy Storchaka in :gh:`66788`.)


gzip
----

Expand Down Expand Up @@ -245,6 +253,14 @@ imaplib
(Contributed by Przemysław Buczkowski and Serhiy Storchaka in :gh:`89869`.)


ipaddress
---------

Add :meth:`~ipaddress.IPv4Network.next_network` and
:meth:`~ipaddress.IPv6Network.next_network` methods to find the next nearest
network with a specific prefix size.


logging
-------

Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,8 @@ static inline int _PyType_SUPPORTS_WEAKREFS(PyTypeObject *type) {
return (type->tp_weaklistoffset != 0);
}

extern PyObject* _PyType_AllocNoTrack(PyTypeObject *type, Py_ssize_t nitems);
// Export for 'array' shared extension.
PyAPI_FUNC(PyObject*) _PyType_AllocNoTrack(PyTypeObject *type, Py_ssize_t nitems);
PyAPI_FUNC(PyObject *) _PyType_NewManagedObject(PyTypeObject *type);

extern PyTypeObject* _PyType_CalculateMetaclass(PyTypeObject *, PyObject *);
Expand Down
4 changes: 4 additions & 0 deletions Lib/encodings/aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,10 @@
'utf7' : 'utf_7',
'unicode_1_1_utf_7' : 'utf_7',

# utf_7_imap codec
'csutf7imap' : 'utf_7_imap',
'mutf_7' : 'utf_7_imap',

# utf_8 codec
'csutf8' : 'utf_8',
'u8' : 'utf_8',
Expand Down
130 changes: 130 additions & 0 deletions Lib/encodings/utf_7_imap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
""" Codec for the modified UTF-7 encoding used for IMAP4 mailbox names,
as specified in RFC 3501, section 5.1.3.

It differs from UTF-7 (RFC 2152) as follows:

* "&" (not "+") is the shift character introducing a Base64 sequence,
and "&-" encodes a literal "&";
* "," is used instead of "/" in the modified Base64 alphabet;
* only printable US-ASCII characters (except "&") may be represented
directly, so all other characters, including other controls, are
Base64-encoded.
"""

import binascii
import codecs

# The modified Base64 alphabet of RFC 3501: standard Base64 but with "," in
# place of "/".
_alphabet = binascii.BASE64_ALPHABET[:-1] + b','

### Codec APIs

def utf_7_imap_encode(input, errors='strict'):
if errors != 'strict':
raise UnicodeError(f"Unsupported error handling: {errors}")
res = bytearray()
start = 0 # start of the current run
b64run = False # is input[start:i] a Base64 run?
def flush(end):
if start < end:
if b64run:
b64 = binascii.b2a_base64(input[start:end].encode('utf-16-be'),
alphabet=_alphabet, padded=False,
newline=False)
res.extend(b'&' + b64 + b'-')
else:
res.extend(input[start:end].encode('ascii'))
for i, ch in enumerate(input):
if ch == '&':
flush(i)
res.extend(b'&-')
start = i + 1
b64run = False
elif ' ' <= ch <= '~': # printable ASCII, represented directly
if b64run:
flush(i)
start = i
b64run = False
else: # everything else is Base64-encoded
if not b64run:
flush(i)
start = i
b64run = True
flush(len(input))
return res.take_bytes(), len(input)

def utf_7_imap_decode(input, errors='strict'):
if errors != 'strict':
raise UnicodeError(f"Unsupported error handling: {errors}")
input = bytes(input)
res = []
start = 0 # start of the current direct ASCII run
i = 0
n = len(input)
def flush(end):
if start < end:
res.append(input[start:end].decode('ascii'))
while i < n:
c = input[i]
if c == b'&'[0]:
flush(i)
j = input.find(b'-', i + 1)
if j < 0:
raise UnicodeDecodeError('utf-7-imap', input, i, n,
'unterminated shift sequence')
if j == i + 1: # '&-'
res.append('&')
else:
b64 = input[i + 1:j]
try:
data = binascii.a2b_base64(b64, alphabet=_alphabet,
strict_mode=True, padded=False)
except binascii.Error:
data = b''
if not data or len(data) % 2:
raise UnicodeDecodeError('utf-7-imap', input, i, j + 1,
'invalid shift sequence')
res.append(data.decode('utf-16-be'))
i = j + 1
start = i
elif b' '[0] <= c <= b'~'[0]:
i += 1
else:
raise UnicodeDecodeError('utf-7-imap', input, i, i + 1,
'unexpected byte')
flush(n)
return ''.join(res), len(input)

class Codec(codecs.Codec):
def encode(self, input, errors='strict'):
return utf_7_imap_encode(input, errors)
def decode(self, input, errors='strict'):
return utf_7_imap_decode(input, errors)

class IncrementalEncoder(codecs.IncrementalEncoder):
def encode(self, input, final=False):
return utf_7_imap_encode(input, self.errors)[0]

class IncrementalDecoder(codecs.IncrementalDecoder):
def decode(self, input, final=False):
return utf_7_imap_decode(input, self.errors)[0]

class StreamWriter(Codec, codecs.StreamWriter):
pass

class StreamReader(Codec, codecs.StreamReader):
pass

### encodings module API

def getregentry():
return codecs.CodecInfo(
name='utf-7-imap',
encode=Codec().encode,
decode=Codec().decode,
incrementalencoder=IncrementalEncoder,
incrementaldecoder=IncrementalDecoder,
streamwriter=StreamWriter,
streamreader=StreamReader,
)
Loading
Loading