diff --git a/Doc/library/fcntl.rst b/Doc/library/fcntl.rst index 5c078df44ffa8b..3981758bee9db1 100644 --- a/Doc/library/fcntl.rst +++ b/Doc/library/fcntl.rst @@ -83,6 +83,13 @@ descriptor. On Linux >= 6.1, the :mod:`!fcntl` module exposes the ``F_DUPFD_QUERY`` to query a file descriptor pointing to the same file. +.. versionchanged:: next + On macOS, the :mod:`!fcntl` module exposes the ``F_PREALLOCATE`` constant + and related constants (``F_ALLOCATECONTIG``, ``F_ALLOCATEALL``, + ``F_ALLOCATEPERSIST``, ``F_PEOFPOSMODE``, ``F_VOLPOSMODE``) for file + preallocation operations. The module also provides the :class:`fstore` type + for use with the ``F_PREALLOCATE`` command. + The module defines the following functions: @@ -248,6 +255,53 @@ The module defines the following functions: .. audit-event:: fcntl.lockf fd,cmd,len,start,whence fcntl.lockf + +.. class:: fstore(flags=0, posmode=0, offset=0, length=0) + + A Python type that wraps the ``struct fstore`` C structure used with the + ``F_PREALLOCATE`` command on macOS. This type implements the buffer protocol, + allowing it to be used directly with :func:`fcntl`. + + .. attribute:: flags + + Allocation flags. Can be one of: + + * :const:`!F_ALLOCATECONTIG` - Allocate contiguous space + * :const:`!F_ALLOCATEALL` - Allocate all requested space + * :const:`!F_ALLOCATEPERSIST` - Make the allocation persistent + + .. attribute:: posmode + + Position mode. Can be one of: + + * :const:`!F_PEOFPOSMODE` - Allocate relative to the physical end of file + * :const:`!F_VOLPOSMODE` - Allocate relative to volume position + + .. attribute:: offset + + File offset for the allocation. + + .. attribute:: length + + Length of space to allocate. + + .. attribute:: bytesalloc + + Number of bytes actually allocated (read-only). This field is populated + by the system after the F_PREALLOCATE operation. + + .. staticmethod:: from_buffer(data) + + Create an :class:`fstore` instance from bytes data. + + This is useful for creating an fstore instance from the result of + :func:`fcntl.fcntl` when using the F_PREALLOCATE command. + + .. availability:: macOS + + .. versionadded:: next + + Examples (all on a SVR4 compliant system):: import struct, fcntl, os diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 1f6b27b14d074b..4e7aa64b03af0d 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1962,6 +1962,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pos1)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pos2)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(posix)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(posmode)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(prec)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(preserve_exc)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(print_file_and_line)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 6959343947c1f4..b179e0c479c9ed 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -685,6 +685,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(pos1) STRUCT_FOR_ID(pos2) STRUCT_FOR_ID(posix) + STRUCT_FOR_ID(posmode) STRUCT_FOR_ID(prec) STRUCT_FOR_ID(preserve_exc) STRUCT_FOR_ID(print_file_and_line) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index be4eae42b5de1b..34ebaaf995ef38 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1960,6 +1960,7 @@ extern "C" { INIT_ID(pos1), \ INIT_ID(pos2), \ INIT_ID(posix), \ + INIT_ID(posmode), \ INIT_ID(prec), \ INIT_ID(preserve_exc), \ INIT_ID(print_file_and_line), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 45b00a20a07dda..eaf73dcd2414f1 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -2528,6 +2528,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(posmode); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(prec); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/test/test_fcntl.py b/Lib/test/test_fcntl.py index 222b69a6d250cd..4fac31584498e2 100644 --- a/Lib/test/test_fcntl.py +++ b/Lib/test/test_fcntl.py @@ -288,6 +288,22 @@ def test_bad_fd(self): with self.assertRaises(OSError): fcntl.fcntl(fd, fcntl.F_DUPFD, b'\0' * 2048) + @unittest.skipUnless(hasattr(fcntl, 'F_PREALLOCATE'), 'need F_PREALLOCATE') + def test_fcntl_preallocate(self): + self.f = open(TESTFN, 'wb+') + fd = self.f.fileno() + + fs = fcntl.fstore( + flags=fcntl.F_ALLOCATECONTIG | fcntl.F_ALLOCATEALL | fcntl.F_ALLOCATEPERSIST, + posmode=fcntl.F_PEOFPOSMODE, + offset=0, + length=1024 + ) + + bs = fcntl.fcntl(fd, fcntl.F_PREALLOCATE, fs) + result = fcntl.fstore.from_buffer(bs) + self.assertEqual(result.bytesalloc, 1024) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2025-10-11-19-40-33.gh-issue-113092.belDmD.rst b/Misc/NEWS.d/next/Library/2025-10-11-19-40-33.gh-issue-113092.belDmD.rst new file mode 100644 index 00000000000000..69feeba6d5c59d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-11-19-40-33.gh-issue-113092.belDmD.rst @@ -0,0 +1 @@ +Add ``fstore`` type, ``F_PREALLOCATE`` and related constants support to the :mod:`fcntl` module diff --git a/Modules/clinic/fcntlmodule.c.h b/Modules/clinic/fcntlmodule.c.h index 2b61d9f87083f0..9a77939ed35770 100644 --- a/Modules/clinic/fcntlmodule.c.h +++ b/Modules/clinic/fcntlmodule.c.h @@ -2,6 +2,10 @@ preserve [clinic start generated code]*/ +#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) +# include "pycore_gc.h" // PyGC_Head +# include "pycore_runtime.h" // _Py_ID() +#endif #include "pycore_modsupport.h" // _PyArg_CheckPositional() PyDoc_STRVAR(fcntl_fcntl__doc__, @@ -265,4 +269,152 @@ fcntl_lockf(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=9773e44da302dc7c input=a9049054013a1b77]*/ + +#if defined(F_PREALLOCATE) + +PyDoc_STRVAR(fcntl_fstore___init____doc__, +"fstore(flags=0, posmode=0, offset=0, length=0)\n" +"--\n" +"\n" +"Initialize an fstore structure.\n" +"\n" +"This structure is used with the F_PREALLOCATE command to preallocate\n" +"file space on macOS systems."); + +static int +fcntl_fstore___init___impl(FStoreObject *self, int flags, int posmode, + long offset, long length); + +static int +fcntl_fstore___init__(PyObject *self, PyObject *args, PyObject *kwargs) +{ + int return_value = -1; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 4 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(flags), &_Py_ID(posmode), &_Py_ID(offset), &_Py_ID(length), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"flags", "posmode", "offset", "length", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "fstore", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[4]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; + int flags = 0; + int posmode = 0; + long offset = 0; + long length = 0; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 0, /*maxpos*/ 4, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos; + } + if (fastargs[0]) { + flags = PyLong_AsInt(fastargs[0]); + if (flags == -1 && PyErr_Occurred()) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } + } + if (fastargs[1]) { + posmode = PyLong_AsInt(fastargs[1]); + if (posmode == -1 && PyErr_Occurred()) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } + } + if (fastargs[2]) { + offset = PyLong_AsLong(fastargs[2]); + if (offset == -1 && PyErr_Occurred()) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } + } + length = PyLong_AsLong(fastargs[3]); + if (length == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional_pos: + return_value = fcntl_fstore___init___impl((FStoreObject *)self, flags, posmode, offset, length); + +exit: + return return_value; +} + +#endif /* defined(F_PREALLOCATE) */ + +#if defined(F_PREALLOCATE) + +PyDoc_STRVAR(fcntl_fstore_from_buffer__doc__, +"from_buffer($type, data, /)\n" +"--\n" +"\n" +"Create an fstore instance from bytes data.\n" +"\n" +"This is useful for creating an fstore instance from the result of\n" +"fcntl.fcntl when using the F_PREALLOCATE command.\n" +"\n" +"Raises ValueError if the data is not exactly the size of struct fstore."); + +#define FCNTL_FSTORE_FROM_BUFFER_METHODDEF \ + {"from_buffer", (PyCFunction)fcntl_fstore_from_buffer, METH_O|METH_CLASS, fcntl_fstore_from_buffer__doc__}, + +static PyObject * +fcntl_fstore_from_buffer_impl(PyTypeObject *type, Py_buffer *data); + +static PyObject * +fcntl_fstore_from_buffer(PyObject *type, PyObject *arg) +{ + PyObject *return_value = NULL; + Py_buffer data = {NULL, NULL}; + + if (PyObject_GetBuffer(arg, &data, PyBUF_SIMPLE) != 0) { + goto exit; + } + return_value = fcntl_fstore_from_buffer_impl((PyTypeObject *)type, &data); + +exit: + /* Cleanup for data */ + if (data.obj) { + PyBuffer_Release(&data); + } + + return return_value; +} + +#endif /* defined(F_PREALLOCATE) */ + +#ifndef FCNTL_FSTORE_FROM_BUFFER_METHODDEF + #define FCNTL_FSTORE_FROM_BUFFER_METHODDEF +#endif /* !defined(FCNTL_FSTORE_FROM_BUFFER_METHODDEF) */ +/*[clinic end generated code: output=e6b4d62e7dcc1068 input=a9049054013a1b77]*/ diff --git a/Modules/fcntlmodule.c b/Modules/fcntlmodule.c index df2c9994127997..8bb047c87803c3 100644 --- a/Modules/fcntlmodule.c +++ b/Modules/fcntlmodule.c @@ -11,6 +11,10 @@ #include // fcntl() #include // memcpy() #include // ioctl() + +#ifdef F_PREALLOCATE +# include // offsetof() +#endif #ifdef HAVE_SYS_FILE_H # include // flock() #endif @@ -25,10 +29,18 @@ // NUL followed by random bytes. static const char guard[GUARDSZ] _Py_NONSTRING = "\x00\xfa\x69\xc4\x67\xa3\x6c\x58"; +#ifdef F_PREALLOCATE +typedef struct { + PyObject_HEAD + struct fstore fstore; +} FStoreObject; +#endif /* F_PREALLOCATE */ + /*[clinic input] module fcntl +class fcntl.fstore "FStoreObject *" "&PyType_Type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=124b58387c158179]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=54136216ba908cf8]*/ #include "clinic/fcntlmodule.c.h" @@ -502,6 +514,118 @@ fcntl_lockf_impl(PyObject *module, int fd, int code, PyObject *lenobj, Py_RETURN_NONE; } +#ifdef F_PREALLOCATE + +/*[clinic input] +fcntl.fstore.__init__ + + flags: int = 0 + posmode: int = 0 + offset: long = 0 + length: long = 0 + +Initialize an fstore structure. + +This structure is used with the F_PREALLOCATE command to preallocate +file space on macOS systems. +[clinic start generated code]*/ + +static int +fcntl_fstore___init___impl(FStoreObject *self, int flags, int posmode, + long offset, long length) +/*[clinic end generated code: output=4fcf4413aa57e2dd input=f4caa1fbbe7a0e32]*/ +{ + memset(&self->fstore, 0, sizeof(struct fstore)); + self->fstore.fst_flags = flags; + self->fstore.fst_posmode = posmode; + self->fstore.fst_offset = (off_t)offset; + self->fstore.fst_length = (off_t)length; + + return 0; +} + +static int +fstore_getbuffer(FStoreObject *self, Py_buffer *view, int flags) +{ + return PyBuffer_FillInfo(view, (PyObject *)self, (void *)&self->fstore, + sizeof(struct fstore), 1, flags); +} + +/*[clinic input] +@classmethod +fcntl.fstore.from_buffer + + data: Py_buffer + / + +Create an fstore instance from bytes data. + +This is useful for creating an fstore instance from the result of +fcntl.fcntl when using the F_PREALLOCATE command. + +Raises ValueError if the data is not exactly the size of struct fstore. +[clinic start generated code]*/ + +static PyObject * +fcntl_fstore_from_buffer_impl(PyTypeObject *type, Py_buffer *data) +/*[clinic end generated code: output=e401a2f775342265 input=3148957e92e9570f]*/ +{ + FStoreObject *self; + + if (data->len != sizeof(struct fstore)) { + PyErr_Format(PyExc_ValueError, + "data must be exactly %zu bytes, got %zd bytes", + sizeof(struct fstore), data->len); + return NULL; + } + + self = (FStoreObject *)PyType_GenericNew(type, NULL, NULL); + if (self == NULL) { + return NULL; + } + + memcpy(&self->fstore, data->buf, sizeof(struct fstore)); + + return (PyObject *)self; +} + +static PyMethodDef fstore_methods[] = { + FCNTL_FSTORE_FROM_BUFFER_METHODDEF + {NULL, NULL} +}; + +static PyMemberDef fstore_members[] = { + {"flags", Py_T_INT, offsetof(FStoreObject, fstore.fst_flags), 0, + "Allocation flags"}, + {"posmode", Py_T_INT, offsetof(FStoreObject, fstore.fst_posmode), 0, + "Position mode"}, + {"offset", Py_T_LONGLONG, offsetof(FStoreObject, fstore.fst_offset), 0, + "File offset"}, + {"length", Py_T_LONGLONG, offsetof(FStoreObject, fstore.fst_length), 0, + "Length to allocate"}, + {"bytesalloc", Py_T_LONGLONG, offsetof(FStoreObject, fstore.fst_bytesalloc), Py_READONLY, + "Number of bytes actually allocated"}, + {NULL}, +}; + +static PyType_Slot fstore_slots[] = { + {Py_tp_init, (initproc)fcntl_fstore___init__}, + {Py_tp_members, fstore_members}, + {Py_tp_methods, fstore_methods}, + {Py_tp_doc, "fstore structure for F_PREALLOCATE"}, + {Py_bf_getbuffer, (getbufferproc)fstore_getbuffer}, + {0, NULL}, +}; + +static PyType_Spec fstore_spec = { + .name = "fcntl.fstore", + .basicsize = sizeof(FStoreObject), + .flags = Py_TPFLAGS_DEFAULT, + .slots = fstore_slots, +}; + +#endif /* F_PREALLOCATE */ + /* List of functions */ static PyMethodDef fcntl_methods[] = { @@ -687,6 +811,24 @@ all_ins(PyObject* m) #ifdef F_NOCACHE if (PyModule_AddIntMacro(m, F_NOCACHE)) return -1; #endif +#ifdef F_PREALLOCATE + if (PyModule_AddIntMacro(m, F_PREALLOCATE)) return -1; +#endif +#ifdef F_ALLOCATECONTIG + if (PyModule_AddIntMacro(m, F_ALLOCATECONTIG)) return -1; +#endif +#ifdef F_ALLOCATEALL + if (PyModule_AddIntMacro(m, F_ALLOCATEALL)) return -1; +#endif +#ifdef F_ALLOCATEPERSIST + if (PyModule_AddIntMacro(m, F_ALLOCATEPERSIST)) return -1; +#endif +#ifdef F_PEOFPOSMODE + if (PyModule_AddIntMacro(m, F_PEOFPOSMODE)) return -1; +#endif +#ifdef F_VOLPOSMODE + if (PyModule_AddIntMacro(m, F_VOLPOSMODE)) return -1; +#endif /* FreeBSD specifics */ #ifdef F_DUP2FD @@ -808,6 +950,18 @@ fcntl_exec(PyObject *module) if (all_ins(module) < 0) { return -1; } + +#ifdef F_PREALLOCATE + PyObject *fstore_type = PyType_FromSpec(&fstore_spec); + if (fstore_type == NULL) { + return -1; + } + if (PyModule_AddObject(module, "fstore", fstore_type) < 0) { + Py_DECREF(fstore_type); + return -1; + } +#endif + return 0; }