From b4832a0e443a0915a8d54fb5b148896058e69d7c Mon Sep 17 00:00:00 2001 From: mhucka <1450019+mhucka@users.noreply.github.com> Date: Mon, 30 Mar 2026 04:21:12 +0000 Subject: [PATCH 01/10] [security fix] Replace insecure temporary file creation in MolecularData.save Replaced the use of `uuid.uuid4()` for temporary file names with `tempfile.NamedTemporaryFile`. This ensures that temporary files are created with restricted permissions (0600) and in a secure manner. The temporary file is now created in the same directory as the target file to ensure an atomic `shutil.move`. Removed the now-unused `uuid` import. --- src/openfermion/chem/molecular_data.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index d95bf43a9..57e8cc281 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -12,7 +12,7 @@ """Class and functions to store quantum chemistry data.""" import os -import uuid +import tempfile import shutil import numpy import h5py @@ -703,8 +703,14 @@ def save(self): """Method to save the class under a systematic name.""" # Create a temporary file and swap it to the original name in case # data needs to be loaded while saving - tmp_name = uuid.uuid4() - with h5py.File("{}.hdf5".format(tmp_name), "w") as f: + with tempfile.NamedTemporaryFile( + delete=False, + suffix='.hdf5', + dir=os.path.dirname(os.path.abspath(self.filename)), + ) as tmp_file: + tmp_name = tmp_file.name + + with h5py.File(tmp_name, "w") as f: # Save geometry (atoms and positions need to be separate): d_geom = f.create_group("geometry") if not isinstance(self.geometry, basestring): @@ -844,7 +850,7 @@ def save(self): # Nothing to do but carry on. pass - shutil.move("{}.hdf5".format(tmp_name), "{}.hdf5".format(self.filename)) + shutil.move(tmp_name, "{}.hdf5".format(self.filename)) def load(self): geometry = [] From 81a0c7ba32b633432c309ab2dd28ddb446f63bd8 Mon Sep 17 00:00:00 2001 From: mhucka Date: Mon, 30 Mar 2026 04:43:13 +0000 Subject: [PATCH 02/10] Run check/format-incremental --apply --- src/openfermion/chem/molecular_data.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index 57e8cc281..28112682a 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -704,9 +704,7 @@ def save(self): # Create a temporary file and swap it to the original name in case # data needs to be loaded while saving with tempfile.NamedTemporaryFile( - delete=False, - suffix='.hdf5', - dir=os.path.dirname(os.path.abspath(self.filename)), + delete=False, suffix='.hdf5', dir=os.path.dirname(os.path.abspath(self.filename)) ) as tmp_file: tmp_name = tmp_file.name From 379cc75e7fb6d24989e55c2e528b4ff1272d1790 Mon Sep 17 00:00:00 2001 From: mhucka Date: Mon, 30 Mar 2026 05:24:03 +0000 Subject: [PATCH 03/10] Refactor to fix incorrect code nesting This also pulls out the code to write the data into a separate method, but better readability and modularity. --- src/openfermion/chem/molecular_data.py | 292 +++++++++++++------------ 1 file changed, 148 insertions(+), 144 deletions(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index 28112682a..825d71773 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -699,156 +699,160 @@ def ccsd_double_amps(self): def ccsd_double_amps(self, value): self._ccsd_double_amps = value + def _write_hdf5_data(self, hdf5_file): + """Method to write the class data to an HDF5 file.""" + # Save geometry (atoms and positions need to be separate): + d_geom = hdf5_file.create_group("geometry") + if not isinstance(self.geometry, basestring): + atoms = [numpy.bytes_(item[0]) for item in self.geometry] + positions = numpy.array([list(item[1]) for item in self.geometry]) + else: + atoms = numpy.bytes_(self.geometry) + positions = None + d_geom.create_dataset("atoms", data=(atoms if atoms is not None else False)) + d_geom.create_dataset("positions", data=(positions if positions is not None else False)) + # Save basis: + hdf5_file.create_dataset("basis", data=numpy.bytes_(self.basis)) + # Save multiplicity: + hdf5_file.create_dataset("multiplicity", data=self.multiplicity) + # Save charge: + hdf5_file.create_dataset("charge", data=self.charge) + # Save description: + hdf5_file.create_dataset("description", data=numpy.bytes_(self.description)) + # Save name: + hdf5_file.create_dataset("name", data=numpy.bytes_(self.name)) + # Save n_atoms: + hdf5_file.create_dataset("n_atoms", data=self.n_atoms) + # Save atoms: + hdf5_file.create_dataset("atoms", data=numpy.bytes_(self.atoms)) + # Save protons: + hdf5_file.create_dataset("protons", data=self.protons) + # Save n_electrons: + hdf5_file.create_dataset("n_electrons", data=self.n_electrons) + # Save generic attributes from calculations: + hdf5_file.create_dataset( + "n_orbitals", data=(self.n_orbitals if self.n_orbitals is not None else False) + ) + hdf5_file.create_dataset( + "n_qubits", data=(self.n_qubits if self.n_qubits is not None else False) + ) + hdf5_file.create_dataset( + "nuclear_repulsion", + data=(self.nuclear_repulsion if self.nuclear_repulsion is not None else False), + ) + # Save attributes generated from SCF calculation. + hdf5_file.create_dataset( + "hf_energy", data=(self.hf_energy if self.hf_energy is not None else False) + ) + hdf5_file.create_dataset( + "canonical_orbitals", + data=(self.canonical_orbitals if self.canonical_orbitals is not None else False), + compression=("gzip" if self.canonical_orbitals is not None else None), + ) + hdf5_file.create_dataset( + "overlap_integrals", + data=(self.overlap_integrals if self.overlap_integrals is not None else False), + compression=("gzip" if self.overlap_integrals is not None else None), + ) + hdf5_file.create_dataset( + "orbital_energies", + data=(self.orbital_energies if self.orbital_energies is not None else False), + ) + # Save attributes generated from integrals. + hdf5_file.create_dataset( + "one_body_integrals", + data=(self.one_body_integrals if self.one_body_integrals is not None else False), + compression=("gzip" if self.one_body_integrals is not None else None), + ) + hdf5_file.create_dataset( + "two_body_integrals", + data=(self.two_body_integrals if self.two_body_integrals is not None else False), + compression=("gzip" if self.two_body_integrals is not None else None), + ) + # Save attributes generated from MP2 calculation. + hdf5_file.create_dataset( + "mp2_energy", data=(self.mp2_energy if self.mp2_energy is not None else False) + ) + # Save attributes generated from CISD calculation. + hdf5_file.create_dataset( + "cisd_energy", data=(self.cisd_energy if self.cisd_energy is not None else False) + ) + hdf5_file.create_dataset( + "cisd_one_rdm", + data=(self.cisd_one_rdm if self.cisd_one_rdm is not None else False), + compression=("gzip" if self.cisd_one_rdm is not None else None), + ) + hdf5_file.create_dataset( + "cisd_two_rdm", + data=(self.cisd_two_rdm if self.cisd_two_rdm is not None else False), + compression=("gzip" if self.cisd_two_rdm is not None else None), + ) + # Save attributes generated from exact diagonalization. + hdf5_file.create_dataset( + "fci_energy", data=(self.fci_energy if self.fci_energy is not None else False) + ) + hdf5_file.create_dataset( + "fci_one_rdm", + data=(self.fci_one_rdm if self.fci_one_rdm is not None else False), + compression=("gzip" if self.fci_one_rdm is not None else None), + ) + hdf5_file.create_dataset( + "fci_two_rdm", + data=(self.fci_two_rdm if self.fci_two_rdm is not None else False), + compression=("gzip" if self.fci_two_rdm is not None else None), + ) + # Save attributes generated from CCSD calculation. + hdf5_file.create_dataset( + "ccsd_energy", data=(self.ccsd_energy if self.ccsd_energy is not None else False) + ) + hdf5_file.create_dataset( + "ccsd_single_amps", + data=(self.ccsd_single_amps if self.ccsd_single_amps is not None else False), + compression=("gzip" if self.ccsd_single_amps is not None else None), + ) + hdf5_file.create_dataset( + "ccsd_double_amps", + data=(self.ccsd_double_amps if self.ccsd_double_amps is not None else False), + compression=("gzip" if self.ccsd_double_amps is not None else None), + ) + + # Save general calculation data + key_list = list(self.general_calculations.keys()) + hdf5_file.create_dataset( + "general_calculations_keys", + data=([numpy.bytes_(key) for key in key_list] if len(key_list) > 0 else False), + ) + hdf5_file.create_dataset( + "general_calculations_values", + data=( + [self.general_calculations[key] for key in key_list] if len(key_list) > 0 else False + ), + ) + def save(self): """Method to save the class under a systematic name.""" - # Create a temporary file and swap it to the original name in case - # data needs to be loaded while saving + # Create a temporary file and swap it to the original name in case data needs to be loaded + # while saving. Use delete=False because we will delete it manually. with tempfile.NamedTemporaryFile( delete=False, suffix='.hdf5', dir=os.path.dirname(os.path.abspath(self.filename)) ) as tmp_file: tmp_name = tmp_file.name - - with h5py.File(tmp_name, "w") as f: - # Save geometry (atoms and positions need to be separate): - d_geom = f.create_group("geometry") - if not isinstance(self.geometry, basestring): - atoms = [numpy.bytes_(item[0]) for item in self.geometry] - positions = numpy.array([list(item[1]) for item in self.geometry]) - else: - atoms = numpy.bytes_(self.geometry) - positions = None - d_geom.create_dataset("atoms", data=(atoms if atoms is not None else False)) - d_geom.create_dataset("positions", data=(positions if positions is not None else False)) - # Save basis: - f.create_dataset("basis", data=numpy.bytes_(self.basis)) - # Save multiplicity: - f.create_dataset("multiplicity", data=self.multiplicity) - # Save charge: - f.create_dataset("charge", data=self.charge) - # Save description: - f.create_dataset("description", data=numpy.bytes_(self.description)) - # Save name: - f.create_dataset("name", data=numpy.bytes_(self.name)) - # Save n_atoms: - f.create_dataset("n_atoms", data=self.n_atoms) - # Save atoms: - f.create_dataset("atoms", data=numpy.bytes_(self.atoms)) - # Save protons: - f.create_dataset("protons", data=self.protons) - # Save n_electrons: - f.create_dataset("n_electrons", data=self.n_electrons) - # Save generic attributes from calculations: - f.create_dataset( - "n_orbitals", data=(self.n_orbitals if self.n_orbitals is not None else False) - ) - f.create_dataset( - "n_qubits", data=(self.n_qubits if self.n_qubits is not None else False) - ) - f.create_dataset( - "nuclear_repulsion", - data=(self.nuclear_repulsion if self.nuclear_repulsion is not None else False), - ) - # Save attributes generated from SCF calculation. - f.create_dataset( - "hf_energy", data=(self.hf_energy if self.hf_energy is not None else False) - ) - f.create_dataset( - "canonical_orbitals", - data=(self.canonical_orbitals if self.canonical_orbitals is not None else False), - compression=("gzip" if self.canonical_orbitals is not None else None), - ) - f.create_dataset( - "overlap_integrals", - data=(self.overlap_integrals if self.overlap_integrals is not None else False), - compression=("gzip" if self.overlap_integrals is not None else None), - ) - f.create_dataset( - "orbital_energies", - data=(self.orbital_energies if self.orbital_energies is not None else False), - ) - # Save attributes generated from integrals. - f.create_dataset( - "one_body_integrals", - data=(self.one_body_integrals if self.one_body_integrals is not None else False), - compression=("gzip" if self.one_body_integrals is not None else None), - ) - f.create_dataset( - "two_body_integrals", - data=(self.two_body_integrals if self.two_body_integrals is not None else False), - compression=("gzip" if self.two_body_integrals is not None else None), - ) - # Save attributes generated from MP2 calculation. - f.create_dataset( - "mp2_energy", data=(self.mp2_energy if self.mp2_energy is not None else False) - ) - # Save attributes generated from CISD calculation. - f.create_dataset( - "cisd_energy", data=(self.cisd_energy if self.cisd_energy is not None else False) - ) - f.create_dataset( - "cisd_one_rdm", - data=(self.cisd_one_rdm if self.cisd_one_rdm is not None else False), - compression=("gzip" if self.cisd_one_rdm is not None else None), - ) - f.create_dataset( - "cisd_two_rdm", - data=(self.cisd_two_rdm if self.cisd_two_rdm is not None else False), - compression=("gzip" if self.cisd_two_rdm is not None else None), - ) - # Save attributes generated from exact diagonalization. - f.create_dataset( - "fci_energy", data=(self.fci_energy if self.fci_energy is not None else False) - ) - f.create_dataset( - "fci_one_rdm", - data=(self.fci_one_rdm if self.fci_one_rdm is not None else False), - compression=("gzip" if self.fci_one_rdm is not None else None), - ) - f.create_dataset( - "fci_two_rdm", - data=(self.fci_two_rdm if self.fci_two_rdm is not None else False), - compression=("gzip" if self.fci_two_rdm is not None else None), - ) - # Save attributes generated from CCSD calculation. - f.create_dataset( - "ccsd_energy", data=(self.ccsd_energy if self.ccsd_energy is not None else False) - ) - f.create_dataset( - "ccsd_single_amps", - data=(self.ccsd_single_amps if self.ccsd_single_amps is not None else False), - compression=("gzip" if self.ccsd_single_amps is not None else None), - ) - f.create_dataset( - "ccsd_double_amps", - data=(self.ccsd_double_amps if self.ccsd_double_amps is not None else False), - compression=("gzip" if self.ccsd_double_amps is not None else None), - ) - - # Save general calculation data - key_list = list(self.general_calculations.keys()) - f.create_dataset( - "general_calculations_keys", - data=([numpy.bytes_(key) for key in key_list] if len(key_list) > 0 else False), - ) - f.create_dataset( - "general_calculations_values", - data=( - [self.general_calculations[key] for key in key_list] - if len(key_list) > 0 - else False - ), - ) - - # Remove old file first for compatibility with systems that don't allow - # rename replacement. Catching OSError for when file does not exist - # yet - try: - os.remove("{}.hdf5".format(self.filename)) - except OSError: - # Nothing to do but carry on. - pass - - shutil.move(tmp_name, "{}.hdf5".format(self.filename)) + tmp_file.close() + try: + with h5py.File(tmp_name, "w") as hdf5_file: + self._write_hdf5_data(hdf5_file) + + # Remove old file first for compatibility with systems that don't allow rename + # replacement. Catch OSError for when file does not exist yet. + try: + os.remove("{}.hdf5".format(self.filename)) + except OSError: + pass + + shutil.move(tmp_name, "{}.hdf5".format(self.filename)) + finally: + if os.path.exists(tmp_name): + os.remove(tmp_name) def load(self): geometry = [] From 26632bf5acd034ef530ecdd54116db55feaf1acc Mon Sep 17 00:00:00 2001 From: mhucka Date: Tue, 31 Mar 2026 01:51:50 +0000 Subject: [PATCH 04/10] Rewrite save() again Second round. Better code, hopefully. --- src/openfermion/chem/molecular_data.py | 41 ++++++++++++-------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index 825d71773..2c63dee05 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -700,7 +700,10 @@ def ccsd_double_amps(self, value): self._ccsd_double_amps = value def _write_hdf5_data(self, hdf5_file): - """Method to write the class data to an HDF5 file.""" + """Method to write the class data to an HDF5 file. + + The file parameter is expected to be an already-open h5py.File handle. + """ # Save geometry (atoms and positions need to be separate): d_geom = hdf5_file.create_group("geometry") if not isinstance(self.geometry, basestring): @@ -831,33 +834,27 @@ def _write_hdf5_data(self, hdf5_file): def save(self): """Method to save the class under a systematic name.""" - # Create a temporary file and swap it to the original name in case data needs to be loaded - # while saving. Use delete=False because we will delete it manually. - with tempfile.NamedTemporaryFile( - delete=False, suffix='.hdf5', dir=os.path.dirname(os.path.abspath(self.filename)) - ) as tmp_file: - tmp_name = tmp_file.name - tmp_file.close() - try: + # Create a temporary file in the same directory as self.filename, write data to it, then + # finally replace self.filename with the temp file. + output_dir = os.path.dirname(os.path.abspath(self.filename)) + final_filename = f"{self.filename}.hdf5" + tmp_name = '' + try: + with tempfile.NamedTemporaryFile(delete=True, suffix='.hdf5', dir=output_dir) as tmp_file: + tmp_name = tmp_file.name with h5py.File(tmp_name, "w") as hdf5_file: self._write_hdf5_data(hdf5_file) - - # Remove old file first for compatibility with systems that don't allow rename - # replacement. Catch OSError for when file does not exist yet. - try: - os.remove("{}.hdf5".format(self.filename)) - except OSError: - pass - - shutil.move(tmp_name, "{}.hdf5".format(self.filename)) - finally: - if os.path.exists(tmp_name): - os.remove(tmp_name) + # The copy will overwrite the destination if it exists. + shutil.copy(tmp_name, final_filename) + finally: + # This 'finally' block handles cases where an error might occur during the process. + if os.path.exists(tmp_name): + os.remove(tmp_name) def load(self): geometry = [] - with h5py.File("{}.hdf5".format(self.filename), "r") as f: + with h5py.File(f"{self.filename}.hdf5", "r") as f: # Load geometry: data = f["geometry/atoms"] if data.shape != (()): From c65c54f36be4fdc0d7b7f468cc5019cd1d2179af Mon Sep 17 00:00:00 2001 From: mhucka Date: Tue, 31 Mar 2026 01:59:00 +0000 Subject: [PATCH 05/10] Format --- src/openfermion/chem/molecular_data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index 2c63dee05..4c14d7e75 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -840,7 +840,9 @@ def save(self): final_filename = f"{self.filename}.hdf5" tmp_name = '' try: - with tempfile.NamedTemporaryFile(delete=True, suffix='.hdf5', dir=output_dir) as tmp_file: + with tempfile.NamedTemporaryFile( + delete=True, suffix='.hdf5', dir=output_dir + ) as tmp_file: tmp_name = tmp_file.name with h5py.File(tmp_name, "w") as hdf5_file: self._write_hdf5_data(hdf5_file) From 4d50153ce3967355d6fa3901049f3718d2b0f71e Mon Sep 17 00:00:00 2001 From: mhucka Date: Tue, 31 Mar 2026 02:21:28 +0000 Subject: [PATCH 06/10] Can't cover that line so let's annotate it for now --- src/openfermion/chem/molecular_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index 4c14d7e75..45537e233 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -851,7 +851,7 @@ def save(self): finally: # This 'finally' block handles cases where an error might occur during the process. if os.path.exists(tmp_name): - os.remove(tmp_name) + os.remove(tmp_name) # pragma: no cover def load(self): geometry = [] From c842259e9c920d4f7af534e38da43a66333672f1 Mon Sep 17 00:00:00 2001 From: Michael Hucka Date: Tue, 31 Mar 2026 10:09:51 -0700 Subject: [PATCH 07/10] Update src/openfermion/chem/molecular_data.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/openfermion/chem/molecular_data.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index 45537e233..0fe2b4231 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -838,20 +838,23 @@ def save(self): # finally replace self.filename with the temp file. output_dir = os.path.dirname(os.path.abspath(self.filename)) final_filename = f"{self.filename}.hdf5" - tmp_name = '' + tmp_name = None try: + # Use delete=False for Windows compatibility (allows h5py to open the path). with tempfile.NamedTemporaryFile( - delete=True, suffix='.hdf5', dir=output_dir + delete=False, suffix='.hdf5', dir=output_dir ) as tmp_file: tmp_name = tmp_file.name - with h5py.File(tmp_name, "w") as hdf5_file: - self._write_hdf5_data(hdf5_file) - # The copy will overwrite the destination if it exists. - shutil.copy(tmp_name, final_filename) + + with h5py.File(tmp_name, "w") as hdf5_file: + self._write_hdf5_data(hdf5_file) + + # os.replace is atomic and works across platforms. + os.replace(tmp_name, final_filename) + tmp_name = None finally: - # This 'finally' block handles cases where an error might occur during the process. - if os.path.exists(tmp_name): - os.remove(tmp_name) # pragma: no cover + if tmp_name and os.path.exists(tmp_name): + os.remove(tmp_name) def load(self): geometry = [] From 3adf6bcd28a6c8598afb25a30e76e25401d4d6aa Mon Sep 17 00:00:00 2001 From: mhucka Date: Tue, 31 Mar 2026 18:08:38 +0000 Subject: [PATCH 08/10] Add pragma no cover for os.remove() call --- src/openfermion/chem/molecular_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index 0fe2b4231..dbf85a183 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -854,7 +854,7 @@ def save(self): tmp_name = None finally: if tmp_name and os.path.exists(tmp_name): - os.remove(tmp_name) + os.remove(tmp_name) # pragma: no cover def load(self): geometry = [] From 8dcf2ba672fe32364aaa4554e97dd7c98cd99c7c Mon Sep 17 00:00:00 2001 From: Michael Hucka Date: Tue, 31 Mar 2026 11:47:40 -0700 Subject: [PATCH 09/10] Update src/openfermion/chem/molecular_data.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/openfermion/chem/molecular_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index dbf85a183..1c3c17b73 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -702,7 +702,7 @@ def ccsd_double_amps(self, value): def _write_hdf5_data(self, hdf5_file): """Method to write the class data to an HDF5 file. - The file parameter is expected to be an already-open h5py.File handle. + The hdf5_file parameter is expected to be an already-open h5py.File handle. """ # Save geometry (atoms and positions need to be separate): d_geom = hdf5_file.create_group("geometry") From 1aa8ba7f398b42573d6cf2071c49064a93701c86 Mon Sep 17 00:00:00 2001 From: mhucka Date: Mon, 6 Apr 2026 03:54:41 +0000 Subject: [PATCH 10/10] Fix handling of file and umask in save() method This moves the h5py.File context handler inside the scope of NamedTemporaryFile, which is likely to be more correct. In addition, this reads the user's umask and sets the permissions of the file appropriately. --- src/openfermion/chem/molecular_data.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/openfermion/chem/molecular_data.py b/src/openfermion/chem/molecular_data.py index 1c3c17b73..e5ea479c9 100644 --- a/src/openfermion/chem/molecular_data.py +++ b/src/openfermion/chem/molecular_data.py @@ -845,14 +845,21 @@ def save(self): delete=False, suffix='.hdf5', dir=output_dir ) as tmp_file: tmp_name = tmp_file.name + with h5py.File(tmp_file, "w") as hdf5_file: + self._write_hdf5_data(hdf5_file) - with h5py.File(tmp_name, "w") as hdf5_file: - self._write_hdf5_data(hdf5_file) + # Determine the umask by setting it to an arbitrary value & immediately restoring it. + # Note: trying to set umask(0) can be problematic, so we use something else. + current_umask = os.umask(0o600) + os.umask(current_umask) + # Now apply the umask to the file and move it. + os.chmod(tmp_name, 0o666 & ~current_umask) # os.replace is atomic and works across platforms. os.replace(tmp_name, final_filename) tmp_name = None finally: + # Clean up if something went wrong. if tmp_name and os.path.exists(tmp_name): os.remove(tmp_name) # pragma: no cover