Repository: powsybl/powsybl-core
Pull request #3309: CGMES loading from zipped profiles inside a folder
Author: login: quinarygio
Labels: name: PR: do-not-merge
name: Release 03/2025
Created: 2025-02-05T16:58:50Z | Merged: 2025-03-13T12:05:48Z

**Please check if the PR fulfills these requirements**
<!-- please use `'[x]'` to check the checkboxes, or submit the PR and then click the checkboxes -->
- [x] The commit message follows our guidelines
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)


**Does this PR already have an issue describing the problem?**
<!-- If so, link to this issue using `'Fixes #XXX'` and skip the rest -->
#3259

**Does this PR introduce a breaking change or deprecate an API?**
- [ ] Yes
- [x] No


**Other information**:
<!-- if any of the questions/checkboxes don't apply, please delete them entirely -->



Diff:
diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
index 04b55011048..7a2c5b4e3a4 100644
--- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
+++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
@@ -14,7 +14,9 @@
 import com.powsybl.cgmes.conversion.naming.NamingStrategyFactory;
 import com.powsybl.cgmes.model.*;
 import com.powsybl.commons.PowsyblException;
+import com.powsybl.commons.compress.SafeZipInputStream;
 import com.powsybl.commons.config.PlatformConfig;
+import com.powsybl.commons.datasource.CompressionFormat;
 import com.powsybl.commons.datasource.DataSource;
 import com.powsybl.commons.datasource.DataSourceUtil;
 import com.powsybl.commons.datasource.GenericReadOnlyDataSource;
@@ -45,6 +47,7 @@
 import java.util.*;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
+import java.util.zip.ZipInputStream;
 
 import static java.util.function.Predicate.not;
 
@@ -303,8 +306,27 @@ private Set<ReadOnlyDataSource> separateByModelingAuthority() {
 
         private Optional<String> readModelingAuthority(String name) {
             String modellingAuthority = null;
-            try (InputStream is = dataSource.newInputStream(name)) {
-                XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(is);
+            try (InputStream in = dataSource.newInputStream(name)) {
+                String fileExtension = name.substring(name.lastIndexOf('.') + 1);
+                if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 1, 1024000L)) {
+                        zis.getNextEntry();
+                        modellingAuthority = readModelingAuthority(zis);
+                    }
+                } else {
+                    modellingAuthority = readModelingAuthority(in);
+                }
+            } catch (IOException | XMLStreamException e) {
+                throw new PowsyblException(e);
+            }
+            return Optional.ofNullable(modellingAuthority);
+        }
+
+        private String readModelingAuthority(InputStream is) throws XMLStreamException {
+            String modellingAuthority = null;
+            XMLStreamReader reader = null;
+            try {
+                reader = xmlInputFactory.createXMLStreamReader(is);
                 boolean stopReading = false;
                 while (reader.hasNext() && !stopReading) {
                     int token = reader.next();
@@ -317,11 +339,12 @@ private Optional<String> readModelingAuthority(String name) {
                         stopReading = true;
                     }
                 }
-                reader.close();
-            } catch (IOException | XMLStreamException e) {
-                throw new PowsyblException(e);
+            } finally {
+                if (reader != null) {
+                    reader.close();
+                }
             }
-            return Optional.ofNullable(modellingAuthority);
+            return modellingAuthority;
         }
 
         private Set<ReadOnlyDataSource> separateByIgmName() {
diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/LoadZippedProfilesTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/LoadZippedProfilesTest.java
new file mode 100644
index 00000000000..588d5a0b9ff
--- /dev/null
+++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/LoadZippedProfilesTest.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.cgmes.conversion.test;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.commons.datasource.DataSource;
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+import com.powsybl.iidm.network.Network;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+class LoadZippedProfilesTest {
+
+    @Test
+    void oneFolderAndOneZipPerProfileTest() throws Exception {
+        var testDataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
+        Set<String> profiles = testDataSource.listNames(".*");
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            // copy and compress each of the profile to the file system
+            Path workDir = fileSystem.getPath("/work");
+            for (String profile : profiles) {
+                try (var is = testDataSource.newInputStream(profile);
+                        var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
+                    os.putNextEntry(new ZipEntry(profile));
+                    ByteStreams.copy(is, os);
+                    os.closeEntry();
+                }
+            }
+
+            Network network = Network.read(DataSource.fromPath(workDir));
+            assertNotNull(network);
+        }
+    }
+
+    @Test
+    void emptyZipErrorTest() throws Exception {
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            Path workDir = fileSystem.getPath("/work");
+            try (var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve("empty.zip")))) {
+                os.closeEntry();
+            }
+
+            ReadOnlyDataSource datasource = DataSource.fromPath(workDir);
+            PowsyblException ex = assertThrows(PowsyblException.class, () -> Network.read(datasource));
+            assertEquals("Unsupported file format or invalid file.", ex.getMessage());
+        }
+    }
+}
diff --git a/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java b/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java
index b1eaf3d9044..7720287ce9e 100644
--- a/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java
+++ b/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2018, RTE (http://www.rte-france.com)
+ * Copyright (c) 2017-2025, RTE (http://www.rte-france.com)
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
@@ -8,15 +8,17 @@
 
 package com.powsybl.cgmes.model;
 
+import com.powsybl.commons.compress.SafeZipInputStream;
+import com.powsybl.commons.datasource.CompressionFormat;
 import com.powsybl.commons.datasource.ReadOnlyDataSource;
 
-import javax.xml.stream.XMLStreamException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.zip.ZipInputStream;
 
 import static com.powsybl.cgmes.model.CgmesNamespace.*;
 
@@ -24,6 +26,7 @@
  * @author Luma Zamarreño {@literal <zamarrenolm at aia.es>}
  */
 public class CgmesOnDataSource {
+    private static final String LISTING_CGMES_NAMES_IN_DATA_SOURCE = "Listing CGMES names in data source %s";
     private static final String EXTENSION = "xml";
 
     public CgmesOnDataSource(ReadOnlyDataSource ds) {
@@ -87,13 +90,7 @@ private boolean existsNamespacesCim14(Set<String> namespaces) {
     public String baseName() {
         // Get the base URI if present, else build an absolute URI from the data source base name
         return names().stream()
-                .map(n -> {
-                    try (InputStream is = dataSource.newInputStream(n)) {
-                        return NamespaceReader.base(is);
-                    } catch (IOException x) {
-                        throw new UncheckedIOException(x);
-                    }
-                })
+                .map(n -> loadInputStreamAndGetNamespace(n, NamespaceReader::base))
                 .filter(Objects::nonNull)
                 .findFirst()
                 .orElseGet(() -> {
@@ -109,34 +106,47 @@ public Set<String> names() {
         try {
             // the set of names may be empty if the data source does not contain CGMES data
             Set<String> allNames = dataSource.listNames(REGEX_VALID_NAME);
-            allNames.removeIf(n -> !containsValidNamespace(n));
+            allNames.removeIf(n -> !existsInDatasource(n) || !containsValidNamespace(n));
             return allNames;
         } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
+            throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
         }
     }
 
-    private boolean containsValidNamespace(String name) {
-        try (InputStream is = dataSource.newInputStream(name)) {
-            Set<String> ns = NamespaceReader.namespaces1(is);
-            return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
-        } catch (XMLStreamException e) {
+    private boolean existsInDatasource(String fileName) {
+        try {
+            return dataSource.exists(fileName);
+        } catch (IOException e) {
             return false;
-        } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
         }
     }
 
-    public Set<String> namespaces() {
-        Set<String> ns = new HashSet<>();
-        names().forEach(n -> {
-            try (InputStream is = dataSource.newInputStream(n)) {
-                ns.addAll(NamespaceReader.namespaces(is));
-            } catch (IOException x) {
-                throw new UncheckedIOException(x);
+    private <T> T loadInputStreamAndGetNamespace(String n, Function<InputStream, T> namespaceGetter) {
+        try (InputStream in = dataSource.newInputStream(n)) {
+            String fileExtension = n.substring(n.lastIndexOf('.') + 1);
+            if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 1, 1024000L)) {
+                    zis.getNextEntry();
+                    return namespaceGetter.apply(zis);
+                }
+            } else {
+                return namespaceGetter.apply(in);
             }
-        });
-        return ns;
+        } catch (IOException e) {
+            throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), e);
+        }
+    }
+
+    private boolean containsValidNamespace(String name) {
+        Set<String> ns = loadInputStreamAndGetNamespace(name, NamespaceReader::namespacesOrEmpty);
+        return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
+    }
+
+    public Set<String> namespaces() {
+        return names().stream()
+                .map(name -> loadInputStreamAndGetNamespace(name, NamespaceReader::namespaces))
+                .flatMap(Set::stream)
+                .collect(Collectors.toSet());
     }
 
     public String cimNamespace() {
@@ -154,5 +164,5 @@ public String cimNamespace() {
         // Any number of characters from the start
         + "^.*"
         // Ending with extension .xml
-        + "\\.XML$";
+        + "\\.(XML|ZIP)$";
 }
diff --git a/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/NamespaceReader.java b/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/NamespaceReader.java
index 373eccc68a6..453b2e421a1 100644
--- a/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/NamespaceReader.java
+++ b/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/NamespaceReader.java
@@ -36,7 +36,15 @@ public static Set<String> namespaces(InputStream is) {
         }
     }
 
-    public static Set<String> namespaces1(InputStream is) throws XMLStreamException {
+    public static Set<String> namespacesOrEmpty(InputStream is) {
+        try {
+            return namespaces1(is);
+        } catch (XMLStreamException x) {
+            return Set.of();
+        }
+    }
+
+    private static Set<String> namespaces1(InputStream is) throws XMLStreamException {
         Set<String> found = new HashSet<>();
         XMLStreamReader xmlsr = XML_INPUT_FACTORY_SUPPLIER.get().createXMLStreamReader(is);
         try {
diff --git a/commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java b/commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
new file mode 100644
index 00000000000..d3524b9bc4b
--- /dev/null
+++ b/commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.io.ForwardingInputStream;
+
+public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
+
+    private long bytesRead;
+    private int entriesRead;
+    private final long maxBytesToRead;
+    private final int maxEntriesToRead;
+
+    public SafeZipInputStream(ZipInputStream in, int maxEntriesToRead, long maxBytesToRead) {
+        super(in);
+        this.maxEntriesToRead = maxEntriesToRead;
+        this.maxBytesToRead = maxBytesToRead;
+    }
+
+    public ZipEntry getNextEntry() throws IOException {
+        if (++this.entriesRead > this.maxEntriesToRead) {
+            throw new IOException("Max entries to read exceeded");
+        }
+        return this.getDelegate().getNextEntry();
+    }
+
+    @Override
+    public int read() throws IOException {
+        int byteRead = super.read();
+        if (byteRead != -1 && ++this.bytesRead > this.maxBytesToRead) {
+            throw new IOException("Max bytes to read exceeded");
+        }
+        return byteRead;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        int byteRead = super.read(b, off, len);
+        if (byteRead != -1) {
+            this.bytesRead += byteRead;
+            if (this.bytesRead > this.maxBytesToRead) {
+                throw new IOException("Max bytes to read exceeded");
+            }
+        }
+        return byteRead;
+    }
+}
diff --git a/commons/src/main/java/com/powsybl/commons/io/ForwardingInputStream.java b/commons/src/main/java/com/powsybl/commons/io/ForwardingInputStream.java
index 3f85f09de73..5a8f00c776b 100644
--- a/commons/src/main/java/com/powsybl/commons/io/ForwardingInputStream.java
+++ b/commons/src/main/java/com/powsybl/commons/io/ForwardingInputStream.java
@@ -22,6 +22,10 @@ public ForwardingInputStream(I delegate) {
         this.delegate = Objects.requireNonNull(delegate);
     }
 
+    protected I getDelegate() {
+        return delegate;
+    }
+
     @Override
     public int available() throws IOException {
         return delegate.available();
diff --git a/commons/src/test/java/com/powsybl/commons/compress/SafeZipInputStreamTest.java b/commons/src/test/java/com/powsybl/commons/compress/SafeZipInputStreamTest.java
new file mode 100644
index 00000000000..913df5c2f74
--- /dev/null
+++ b/commons/src/test/java/com/powsybl/commons/compress/SafeZipInputStreamTest.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class SafeZipInputStreamTest {
+
+    private ZipInputStream zipInputStream;
+    private SafeZipInputStream safeZipInputStream;
+
+    @BeforeEach
+    void setUp() {
+        zipInputStream = mock(ZipInputStream.class);
+    }
+
+    @Test
+    void testConstructorWithValidEntryNumber() throws IOException {
+        when(zipInputStream.getNextEntry()).thenReturn(new ZipEntry("entry1"), new ZipEntry("entry2"));
+        safeZipInputStream = new SafeZipInputStream(zipInputStream, 1, 100);
+        assertNotNull(safeZipInputStream);
+    }
+
+    @Test
+    void testGetNextEntry() throws IOException {
+        when(zipInputStream.getNextEntry()).thenReturn(new ZipEntry("entry1"), new ZipEntry("entry2"), null);
+        safeZipInputStream = new SafeZipInputStream(zipInputStream, 3, 100);
+        ZipEntry entry1 = safeZipInputStream.getNextEntry();
+        assertNotNull(entry1);
+        assertEquals("entry1", entry1.getName());
+        ZipEntry entry2 = safeZipInputStream.getNextEntry();
+        assertNotNull(entry2);
+        assertEquals("entry2", entry2.getName());
+        ZipEntry entry3 = safeZipInputStream.getNextEntry();
+        assertNull(entry3);
+    }
+
+    @Test
+    void testExceedMaxEntries() throws IOException {
+        when(zipInputStream.getNextEntry()).thenReturn(new ZipEntry("entry1"), new ZipEntry("entry2"), null);
+        safeZipInputStream = new SafeZipInputStream(zipInputStream, 1, 100);
+        ZipEntry entry1 = safeZipInputStream.getNextEntry();
+        assertNotNull(entry1);
+        assertEquals("entry1", entry1.getName());
+        IOException exception = assertThrows(IOException.class, () -> {
+            safeZipInputStream.getNextEntry();
+        });
+        assertEquals("Max entries to read exceeded", exception.getMessage());
+    }
+
+    @Test
+    void testReadExceedsMaxBytes() throws IOException {
+        when(zipInputStream.read()).thenReturn(1, 1, 1, -1);
+
+        safeZipInputStream = new SafeZipInputStream(zipInputStream, 1, 2);
+        safeZipInputStream.getNextEntry();
+        assertEquals(1, safeZipInputStream.read());
+        assertEquals(1, safeZipInputStream.read());
+        IOException exception = assertThrows(IOException.class, () -> {
+            safeZipInputStream.read();
+        });
+        assertEquals("Max bytes to read exceeded", exception.getMessage());
+    }
+
+    @Test
+    void testReadByteArrayExceedsMaxBytes() throws IOException {
+        byte[] buffer = new byte[10];
+        when(zipInputStream.read(buffer, 0, 10)).thenReturn(10);
+
+        safeZipInputStream = new SafeZipInputStream(zipInputStream, 1, 5);
+        safeZipInputStream.getNextEntry();
+
+        IOException exception = assertThrows(IOException.class, () -> {
+            safeZipInputStream.read(buffer, 0, 10);
+        });
+        assertEquals("Max bytes to read exceeded", exception.getMessage());
+    }
+}
diff --git a/docs/grid_exchange_formats/cgmes/import.md b/docs/grid_exchange_formats/cgmes/import.md
index 3763d4633fd..02593121494 100644
--- a/docs/grid_exchange_formats/cgmes/import.md
+++ b/docs/grid_exchange_formats/cgmes/import.md
@@ -5,6 +5,12 @@ The CIM-CGMES importer reads and converts a CIM-CGMES model to the PowSyBl grid
 - Convert CIM-CGMES data retrieved by SPARQL requests from the created triplestore to PowSyBl grid model
 
 The data in input CIM/XML files uses RDF (Resource Description Framework) syntax. In RDF, data is described making statements about resources using triplet expressions: (subject, predicate, object).
+
+The CIM-CGMES importer supports reading CIM/XML profile files from one of the following data sources:
+- a folder containing all uncompressed profile files
+- a folder containing the zipped profile files, each one being in a separate zip file
+- a zipped file containing all profile files
+
 To describe the conversion from CGMES to PowSyBl, we first introduce some generic considerations about the level of detail of the model (node/breaker or bus/branch), the identity of the equipments and equipment containment in substations and voltage levels. After that, the conversion for every CGMES relevant class is explained. Consistency checks and validations performed during the conversion are mentioned in the corresponding sections.
 
 (cgmes-import-level-of-detail)=


Reviews:
user: login: alicecaron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T08:48:07Z
user: login: quinarygio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T09:34:33Z
user: login: flo-dup
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-02-19T14:24:06Z
user: login: alicecaron
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T14:47:01Z
user: login: olperr1
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-19T15:00:57Z
user: login: alicecaron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-21T09:16:20Z
user: login: alicecaron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:50:48Z
user: login: olperr1
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-27T12:34:01Z
user: login: flo-dup
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T09:56:22Z
user: login: quinarygio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T15:16:59Z
user: login: quinarygio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T18:24:21Z
user: login: flo-dup
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T08:52:41Z
user: login: flo-dup
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T09:21:05Z
user: login: flo-dup
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T09:41:49Z
user: login: olperr1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T15:02:02Z
user: login: flo-dup
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T16:53:10Z
user: login: flo-dup
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T10:40:55Z
user: login: flo-dup
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-10T13:57:13Z
user: login: flo-dup
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T16:40:28Z
user: login: flo-dup
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T17:10:39Z
user: login: quinarygio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T18:17:11Z
user: login: flo-dup
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T20:06:31Z
user: login: quinarygio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T08:29:17Z
user: login: alicecaron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:11:40Z
user: login: flo-dup
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T10:15:09Z
user: login: flo-dup
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T10:19:33Z
user: login: flo-dup
body: Approving again! :sweat_smile: 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-12T10:21:18Z
user: login: olperr1
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-12T15:38:49Z
user: login: olperr1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T11:48:11Z
user: login: olperr1
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T11:48:36Z

Review comments:
diff_hunk: @@ -109,27 +123,59 @@ public Set<String> names() {
         try {
             // the set of names may be empty if the data source does not contain CGMES data
             Set<String> allNames = dataSource.listNames(REGEX_VALID_NAME);
-            allNames.removeIf(n -> !containsValidNamespace(n));
+            allNames.removeIf(n -> !existsInDatasource(n) || !containsValidNamespace(n));
             return allNames;
         } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
+            throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
+        }
+    }
+
+    private boolean existsInDatasource(String fileName) {
+        try {
+            return dataSource.exists(fileName);
+        } catch (IOException e) {
+            return false;
         }
     }
 
     private boolean containsValidNamespace(String name) {
+        String fileExtension = name.substring(name.lastIndexOf('.') + 1);
+        if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+            ZipSecurityHelper.checkIfZipExtractionIsSafe(dataSource, name);
+            try (ZipInputStream is = new ZipInputStream(dataSource.newInputStream(name))) {
+                is.getNextEntry();
+                Set<String> ns = NamespaceReader.namespaces1(is);
+                return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
+            } catch (XMLStreamException e) {
+                return false;
+            } catch (IOException x) {
+                throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
+            }
+        }
         try (InputStream is = dataSource.newInputStream(name)) {
             Set<String> ns = NamespaceReader.namespaces1(is);
             return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
path: cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java
user: login: alicecaron
body: You can mutualize some code by loading the `InputStream` whether it is from an xml or zip file by creating a private method doing all the work and not closing the input stream.  
Keeping the try/catch outside the private method allows to then put the logic as it is. the private method can be called for the 3 times you need the `InputStream` (thank you for the tip @olperr1 !)  

```suggestion
try (InputStream is = loadInputStream(n)) {
    return NamespaceReader.base(is);
} catch (IOException x) {
    throw new UncheckedIOException(x);
}
```
Example for the private method to load the InputStream:
```
private InputStream loadInputStream(String n) throws IOException {
        InputStream is;
        String fileExtension = n.substring(n.lastIndexOf('.') + 1);
        if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
            ZipSecurityHelper.checkIfZipExtractionIsSafe(dataSource, n);
            is = new ZipInputStream(dataSource.newInputStream(n));
            ((ZipInputStream) is).getNextEntry();
        } else {
            is = dataSource.newInputStream(n);
        }
        return is;
    }
```
created_at: 2025-02-14T08:48:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,27 +123,59 @@ public Set<String> names() {
         try {
             // the set of names may be empty if the data source does not contain CGMES data
             Set<String> allNames = dataSource.listNames(REGEX_VALID_NAME);
-            allNames.removeIf(n -> !containsValidNamespace(n));
+            allNames.removeIf(n -> !existsInDatasource(n) || !containsValidNamespace(n));
             return allNames;
         } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
+            throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
+        }
+    }
+
+    private boolean existsInDatasource(String fileName) {
+        try {
+            return dataSource.exists(fileName);
+        } catch (IOException e) {
+            return false;
         }
     }
 
     private boolean containsValidNamespace(String name) {
+        String fileExtension = name.substring(name.lastIndexOf('.') + 1);
+        if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+            ZipSecurityHelper.checkIfZipExtractionIsSafe(dataSource, name);
+            try (ZipInputStream is = new ZipInputStream(dataSource.newInputStream(name))) {
+                is.getNextEntry();
+                Set<String> ns = NamespaceReader.namespaces1(is);
+                return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
+            } catch (XMLStreamException e) {
+                return false;
+            } catch (IOException x) {
+                throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
+            }
+        }
         try (InputStream is = dataSource.newInputStream(name)) {
             Set<String> ns = NamespaceReader.namespaces1(is);
             return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
path: cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java
user: login: quinarygio
body: In fact this was my first implementation (see my first commit), but then Sonar complained because the InputStream was not closed, so I had to duplicate some code...
created_at: 2025-02-14T09:34:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+public final class ZipSecurityHelper {
+
+    public static final int THRESHOLD_ENTRIES = 10000;
+    public static final int THRESHOLD_SIZE = 1000000000; // 1 GB
+    public static final double THRESHOLD_RATIO = 10;
+
+    private ZipSecurityHelper() {
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name) {
+        checkIfZipExtractionIsSafe(dataSource, name, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) {
+        try (ZipInputStream is = new ZipInputStream(dataSource.newInputStream(name))) {
+            if (!ZipSecurityHelper.isZipFileSafe(is, thresholdEntries, thresholdSize, thresholdCompressionRatio)) {
+                throw new UncheckedIOException("Zip file extraction is not safe", new IOException());
+            }
+        } catch (IOException x) {
+            throw new UncheckedIOException(x);
+        }
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream) throws IOException {
+        return isZipFileSafe(zipInputStream, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) throws IOException {
+        int totalSizeArchive = 0;
+        int totalEntryArchive = 0;
+        ZipEntry ze = zipInputStream.getNextEntry();
+        while (ze != null) {
+            totalEntryArchive++;
+
+            int nBytes = -1;
+            byte[] buffer = new byte[2048];
+            int totalSizeEntry = 0;
+            while ((nBytes = zipInputStream.read(buffer)) > 0) { // Compliant
+                totalSizeEntry += nBytes;
+                totalSizeArchive += nBytes;
+
+            }
path: commons/src/main/java/com/powsybl/commons/compress/ZipSecurityHelper.java
user: login: olperr1
body: ```suggestion
            while ((nBytes = zipInputStream.read(buffer)) > 0) {
                totalSizeEntry += nBytes;
                totalSizeArchive += nBytes;
            }
```
created_at: 2025-02-19T13:29:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import static org.junit.jupiter.api.Assertions.*;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.powsybl.commons.datasource.DirectoryDataSource;
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+public class ZipSecurityHelperTest {
path: commons/src/test/java/com/powsybl/commons/compress/ZipSecurityHelperTest.java
user: login: flo-dup
body: Could you remove the public access modifiers in this file to avoid the 7 sonar code smells?
created_at: 2025-02-19T14:21:48Z
author_association: COLLABORATOR
diff_hunk: @@ -109,27 +123,59 @@ public Set<String> names() {
         try {
             // the set of names may be empty if the data source does not contain CGMES data
             Set<String> allNames = dataSource.listNames(REGEX_VALID_NAME);
-            allNames.removeIf(n -> !containsValidNamespace(n));
+            allNames.removeIf(n -> !existsInDatasource(n) || !containsValidNamespace(n));
             return allNames;
         } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
+            throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
+        }
+    }
+
+    private boolean existsInDatasource(String fileName) {
+        try {
+            return dataSource.exists(fileName);
+        } catch (IOException e) {
+            return false;
         }
     }
 
     private boolean containsValidNamespace(String name) {
+        String fileExtension = name.substring(name.lastIndexOf('.') + 1);
+        if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+            ZipSecurityHelper.checkIfZipExtractionIsSafe(dataSource, name);
+            try (ZipInputStream is = new ZipInputStream(dataSource.newInputStream(name))) {
+                is.getNextEntry();
+                Set<String> ns = NamespaceReader.namespaces1(is);
+                return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
+            } catch (XMLStreamException e) {
+                return false;
+            } catch (IOException x) {
+                throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
+            }
+        }
         try (InputStream is = dataSource.newInputStream(name)) {
             Set<String> ns = NamespaceReader.namespaces1(is);
             return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
         } catch (XMLStreamException e) {
             return false;
         } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
+            throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
         }
     }
 
     public Set<String> namespaces() {
         Set<String> ns = new HashSet<>();
         names().forEach(n -> {
+            String fileExtension = n.substring(n.lastIndexOf('.') + 1);
+            if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                ZipSecurityHelper.checkIfZipExtractionIsSafe(dataSource, n);
+                try (ZipInputStream is = new ZipInputStream(dataSource.newInputStream(n))) {
+                    is.getNextEntry();
path: cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java
user: login: flo-dup
body: we take the first entry only, shouldn't we check there's only one entry in the zip file? and throw a clear exception otherwise.
created_at: 2025-02-19T14:24:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+public final class ZipSecurityHelper {
+
+    public static final int THRESHOLD_ENTRIES = 10000;
+    public static final int THRESHOLD_SIZE = 1000000000; // 1 GB
+    public static final double THRESHOLD_RATIO = 10;
+
+    private ZipSecurityHelper() {
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name) {
+        checkIfZipExtractionIsSafe(dataSource, name, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) {
+        try (ZipInputStream is = new ZipInputStream(dataSource.newInputStream(name))) {
+            if (!ZipSecurityHelper.isZipFileSafe(is, thresholdEntries, thresholdSize, thresholdCompressionRatio)) {
+                throw new UncheckedIOException("Zip file extraction is not safe", new IOException());
+            }
+        } catch (IOException x) {
+            throw new UncheckedIOException(x);
+        }
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream) throws IOException {
+        return isZipFileSafe(zipInputStream, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) throws IOException {
+        int totalSizeArchive = 0;
+        int totalEntryArchive = 0;
+        ZipEntry ze = zipInputStream.getNextEntry();
+        while (ze != null) {
+            totalEntryArchive++;
+
+            int nBytes = -1;
+            byte[] buffer = new byte[2048];
+            int totalSizeEntry = 0;
+            while ((nBytes = zipInputStream.read(buffer)) > 0) {
+                totalSizeEntry += nBytes;
+                totalSizeArchive += nBytes;
+
+            }
+
+            if (totalSizeArchive > thresholdSize) {
+                // the uncompressed data size is too much for the application resource capacity
+                return false;
+            }
+
+            if (totalEntryArchive > thresholdEntries) {
+                // too many entries in this archive, can lead to inodes exhaustion of the system
+                return false;
+            }
+            ZipEntry currentEntry = ze;
+            ze = zipInputStream.getNextEntry();
+            long entrySize = currentEntry.getCompressedSize();
+            double compressionRatio = (double) totalSizeEntry / (double) entrySize;
+            if (compressionRatio > thresholdCompressionRatio) {
path: commons/src/main/java/com/powsybl/commons/compress/ZipSecurityHelper.java
user: login: alicecaron
body: I tried with real files, and for a EQ xml zipped the compressed ratio is not sufficient for the current proposed `THRESHOLD_RATIO=10`. I had a `compressionRatio` of around `11`. In consequence a `java.io.UncheckedIOException` error was thrown: `Zip file extraction is not safe`  
Maybe it is necessar to increase this threshold because of the size of CGMES EQ files and poor ratio? @flo-dup @olperr1 
created_at: 2025-02-21T09:14:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+public final class ZipSecurityHelper {
+
+    public static final int THRESHOLD_ENTRIES = 10000;
+    public static final int THRESHOLD_SIZE = 1000000000; // 1 GB
+    public static final double THRESHOLD_RATIO = 10;
+
+    private ZipSecurityHelper() {
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name) {
+        checkIfZipExtractionIsSafe(dataSource, name, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) {
+        try (ZipInputStream is = new ZipInputStream(dataSource.newInputStream(name))) {
+            if (!ZipSecurityHelper.isZipFileSafe(is, thresholdEntries, thresholdSize, thresholdCompressionRatio)) {
+                throw new UncheckedIOException("Zip file extraction is not safe", new IOException());
+            }
+        } catch (IOException x) {
+            throw new UncheckedIOException(x);
+        }
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream) throws IOException {
+        return isZipFileSafe(zipInputStream, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) throws IOException {
+        int totalSizeArchive = 0;
+        int totalEntryArchive = 0;
+        ZipEntry ze = zipInputStream.getNextEntry();
+        while (ze != null) {
+            totalEntryArchive++;
+
+            int nBytes = -1;
+            byte[] buffer = new byte[2048];
+            int totalSizeEntry = 0;
+            while ((nBytes = zipInputStream.read(buffer)) > 0) {
+                totalSizeEntry += nBytes;
+                totalSizeArchive += nBytes;
+
+            }
+
+            if (totalSizeArchive > thresholdSize) {
+                // the uncompressed data size is too much for the application resource capacity
+                return false;
+            }
+
+            if (totalEntryArchive > thresholdEntries) {
+                // too many entries in this archive, can lead to inodes exhaustion of the system
+                return false;
+            }
+            ZipEntry currentEntry = ze;
+            ze = zipInputStream.getNextEntry();
+            long entrySize = currentEntry.getCompressedSize();
+            double compressionRatio = (double) totalSizeEntry / (double) entrySize;
+            if (compressionRatio > thresholdCompressionRatio) {
path: commons/src/main/java/com/powsybl/commons/compress/ZipSecurityHelper.java
user: login: alicecaron
body: It is ok with `THRESHOLD_RATIO = 12` in my case.
created_at: 2025-02-21T09:16:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.cgmes.conversion.test;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
+import com.powsybl.cgmes.conversion.CgmesImport;
+import com.powsybl.cgmes.model.CgmesModelException;
+import com.powsybl.commons.datasource.DataSource;
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+import com.powsybl.iidm.network.Network;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+class LoadZippedProfilesTest {
+
+    @Test
+    void oneFolderAndOneZipPerProfileTest() throws Exception {
+        var testDataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
+        Set<String> profiles = testDataSource.listNames(".*");
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            // copy and compress each of the profile to the file system
+            Path workDir = fileSystem.getPath("/work");
+            for (String profile : profiles) {
+                try (var is = testDataSource.newInputStream(profile);
+                        var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
+                    os.putNextEntry(new ZipEntry(profile));
path: cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/LoadZippedProfilesTest.java
user: login: flo-dup
body: In my previous comment I didn't mean a zip file with no entry. I meant, what's happening if you add here another entry? That could happen if there's a side file next to it. I don't think we should support his, but we could throw a clear exception if there are more than one entry. Rather than the obscure exception currently throwing in that case.

```suggestion
                    os.putNextEntry(new ZipEntry(""));
                    os.putNextEntry(new ZipEntry(profile));
```
created_at: 2025-02-21T15:49:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.cgmes.conversion.test;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
+import com.powsybl.cgmes.conversion.CgmesImport;
+import com.powsybl.cgmes.model.CgmesModelException;
+import com.powsybl.commons.datasource.DataSource;
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+import com.powsybl.iidm.network.Network;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+class LoadZippedProfilesTest {
+
+    @Test
+    void oneFolderAndOneZipPerProfileTest() throws Exception {
+        var testDataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
+        Set<String> profiles = testDataSource.listNames(".*");
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            // copy and compress each of the profile to the file system
+            Path workDir = fileSystem.getPath("/work");
+            for (String profile : profiles) {
+                try (var is = testDataSource.newInputStream(profile);
+                        var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
+                    os.putNextEntry(new ZipEntry(profile));
+                    ByteStreams.copy(is, os);
+                    os.closeEntry();
+                }
+            }
+
+            Properties importParams = new Properties();
+            importParams.put(CgmesImport.IMPORT_CGM_WITH_SUBNETWORKS, "false");
path: cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/LoadZippedProfilesTest.java
user: login: alicecaron
body: @quinarygio  We should be able to import these files without the property here.  
But it is currently not working as you need to also fix the `CgmesImport` class when reading the zip entry in the `readModelingAuthority` method.  
The same problem as before appears about the input stream and mutualize code
created_at: 2025-02-24T12:50:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.cgmes.conversion.test;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
+import com.powsybl.cgmes.model.CgmesModelException;
+import com.powsybl.commons.datasource.DataSource;
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+import com.powsybl.iidm.network.Network;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+class LoadZippedProfilesTest {
+
+    @Test
+    void oneFolderAndOneZipPerProfileTest() throws Exception {
+        var testDataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
+        Set<String> profiles = testDataSource.listNames(".*");
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            // copy and compress each of the profile to the file system
+            Path workDir = fileSystem.getPath("/work");
+            for (String profile : profiles) {
+                try (var is = testDataSource.newInputStream(profile);
+                        var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
+                    os.putNextEntry(new ZipEntry(profile));
+                    ByteStreams.copy(is, os);
+                    os.closeEntry();
+                }
+            }
+
+            Network network = Network.read(DataSource.fromPath(workDir));
+            assertNotNull(network);
+        }
+    }
+
+    @Test
+    void emptyZipErrorTest() throws Exception {
+        var testDataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
+        Set<String> profiles = testDataSource.listNames(".*");
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            // copy and compress each of the profile to the file system
+            Path workDir = fileSystem.getPath("/work");
+            for (String profile : profiles) {
+                try (var is = testDataSource.newInputStream(profile);
+                        var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
+                    os.closeEntry();
+                }
+            }
path: cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/LoadZippedProfilesTest.java
user: login: olperr1
body: ```suggestion
            Path workDir = fileSystem.getPath("/work");
            for (String profile : profiles) {
                try (var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
                    os.closeEntry();
                }
            }
```
created_at: 2025-02-27T08:46:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+public final class ZipSecurityHelper {
+
+    public static final int THRESHOLD_ENTRIES = 10000;
+    public static final int THRESHOLD_SIZE = 1000000000; // 1 GB
path: commons/src/main/java/com/powsybl/commons/compress/ZipSecurityHelper.java
user: login: olperr1
body: With some CGMES files, 1 GB may not be enough:

```suggestion
    public static final int THRESHOLD_SIZE = 1_610_612_736; // 1.5 GB
```

created_at: 2025-02-27T11:36:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+public final class ZipSecurityHelper {
+
+    public static final int THRESHOLD_ENTRIES = 10000;
+    public static final int THRESHOLD_SIZE = 1000000000; // 1 GB
+    public static final double THRESHOLD_RATIO = 10;
+
+    private ZipSecurityHelper() {
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name) {
+        checkIfZipExtractionIsSafe(dataSource, name, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) {
+        try (ZipInputStream is = new ZipInputStream(dataSource.newInputStream(name))) {
+            if (!ZipSecurityHelper.isZipFileSafe(is, thresholdEntries, thresholdSize, thresholdCompressionRatio)) {
+                throw new UncheckedIOException("Zip file extraction is not safe", new IOException());
+            }
+        } catch (IOException x) {
+            throw new UncheckedIOException(x);
+        }
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream) throws IOException {
+        return isZipFileSafe(zipInputStream, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) throws IOException {
+        int totalSizeArchive = 0;
+        int totalEntryArchive = 0;
+        ZipEntry ze = zipInputStream.getNextEntry();
+        while (ze != null) {
+            totalEntryArchive++;
+
+            int nBytes = -1;
+            byte[] buffer = new byte[2048];
+            int totalSizeEntry = 0;
+            while ((nBytes = zipInputStream.read(buffer)) > 0) {
+                totalSizeEntry += nBytes;
+                totalSizeArchive += nBytes;
+
path: commons/src/main/java/com/powsybl/commons/compress/ZipSecurityHelper.java
user: login: olperr1
body: Could you remove this unnecessary new line?
```suggestion
```
created_at: 2025-02-27T11:43:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+public final class ZipSecurityHelper {
+
+    public static final int THRESHOLD_ENTRIES = 10000;
+    public static final int THRESHOLD_SIZE = 1000000000; // 1 GB
+    public static final double THRESHOLD_RATIO = 10;
+
+    private ZipSecurityHelper() {
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name) {
+        checkIfZipExtractionIsSafe(dataSource, name, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static void checkIfZipExtractionIsSafe(ReadOnlyDataSource dataSource, String name, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) {
+        try (ZipInputStream is = new ZipInputStream(dataSource.newInputStream(name))) {
+            if (!ZipSecurityHelper.isZipFileSafe(is, thresholdEntries, thresholdSize, thresholdCompressionRatio)) {
+                throw new UncheckedIOException("Zip file extraction is not safe", new IOException());
+            }
+        } catch (IOException x) {
+            throw new UncheckedIOException(x);
+        }
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream) throws IOException {
+        return isZipFileSafe(zipInputStream, THRESHOLD_ENTRIES, THRESHOLD_SIZE, THRESHOLD_RATIO);
+    }
+
+    public static boolean isZipFileSafe(ZipInputStream zipInputStream, int thresholdEntries, int thresholdSize, double thresholdCompressionRatio) throws IOException {
+        int totalSizeArchive = 0;
+        int totalEntryArchive = 0;
+        ZipEntry ze = zipInputStream.getNextEntry();
+        while (ze != null) {
+            totalEntryArchive++;
+
+            int nBytes = -1;
+            byte[] buffer = new byte[2048];
+            int totalSizeEntry = 0;
+            while ((nBytes = zipInputStream.read(buffer)) > 0) {
+                totalSizeEntry += nBytes;
+                totalSizeArchive += nBytes;
+
+            }
+
+            if (totalSizeArchive > thresholdSize) {
+                // the uncompressed data size is too much for the application resource capacity
+                return false;
+            }
+
+            if (totalEntryArchive > thresholdEntries) {
+                // too many entries in this archive, can lead to inodes exhaustion of the system
+                return false;
+            }
+            ZipEntry currentEntry = ze;
+            ze = zipInputStream.getNextEntry();
+            long entrySize = currentEntry.getCompressedSize();
+            double compressionRatio = (double) totalSizeEntry / (double) entrySize;
+            if (compressionRatio > thresholdCompressionRatio) {
path: commons/src/main/java/com/powsybl/commons/compress/ZipSecurityHelper.java
user: login: olperr1
body: 12 won't be sufficient.

I checked some of our test files and found some with a bigger ratio when zipped with the maximum compression level:

```shell
path='cgmes/cgmes-conformity/src/main/resources/cgmes3-test-models/Svedala'
cgmes_file="$path/20201202T1843Z_1D_Svedala Area_DL_001.xml"
cgmes_size="$(ls -l "$cgmes_file"  | awk '{print $5}')"
rm /tmp/test.zip
zip -9 /tmp/test.zip "$cgmes_file"
zip_size="$(ls -l /tmp/test.zip  | awk '{print $5}')"
bc -l <<< "$cgmes_size / $zip_size"
```
=> 12.11357674909463524857

We can maybe use 20 to have a security margin.
created_at: 2025-02-27T12:29:13Z
author_association: MEMBER
diff_hunk: @@ -109,34 +107,51 @@ public Set<String> names() {
         try {
             // the set of names may be empty if the data source does not contain CGMES data
             Set<String> allNames = dataSource.listNames(REGEX_VALID_NAME);
-            allNames.removeIf(n -> !containsValidNamespace(n));
+            allNames.removeIf(n -> !existsInDatasource(n) || !containsValidNamespace(n));
             return allNames;
         } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
+            throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
         }
     }
 
-    private boolean containsValidNamespace(String name) {
-        try (InputStream is = dataSource.newInputStream(name)) {
-            Set<String> ns = NamespaceReader.namespaces1(is);
-            return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
-        } catch (XMLStreamException e) {
+    private boolean existsInDatasource(String fileName) {
+        try {
+            return dataSource.exists(fileName);
+        } catch (IOException e) {
             return false;
-        } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
         }
     }
 
-    public Set<String> namespaces() {
-        Set<String> ns = new HashSet<>();
-        names().forEach(n -> {
-            try (InputStream is = dataSource.newInputStream(n)) {
-                ns.addAll(NamespaceReader.namespaces(is));
-            } catch (IOException x) {
-                throw new UncheckedIOException(x);
+    private <T> T loadInputStreamAndGetNamespace(String n, Function<InputStream, T> namespaceGetter) {
+        try (InputStream in = dataSource.newInputStream(n)) {
+            String fileExtension = n.substring(n.lastIndexOf('.') + 1);
+            if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                ZipSecurityHelper.checkIfZipExtractionIsSafe(dataSource, n);
path: cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java
user: login: flo-dup
body: There's a performance issue here. We're reading the whole unzipped file to detect if the zip extraction is safe but we only need the first tag (for namespaces definition or the base attribute). I think we need to do something smarter to get safely the first characters only, not unzipping the complete file.
created_at: 2025-02-28T09:56:17Z
author_association: COLLABORATOR
diff_hunk: @@ -109,34 +107,51 @@ public Set<String> names() {
         try {
             // the set of names may be empty if the data source does not contain CGMES data
             Set<String> allNames = dataSource.listNames(REGEX_VALID_NAME);
-            allNames.removeIf(n -> !containsValidNamespace(n));
+            allNames.removeIf(n -> !existsInDatasource(n) || !containsValidNamespace(n));
             return allNames;
         } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
+            throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
         }
     }
 
-    private boolean containsValidNamespace(String name) {
-        try (InputStream is = dataSource.newInputStream(name)) {
-            Set<String> ns = NamespaceReader.namespaces1(is);
-            return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
-        } catch (XMLStreamException e) {
+    private boolean existsInDatasource(String fileName) {
+        try {
+            return dataSource.exists(fileName);
+        } catch (IOException e) {
             return false;
-        } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
         }
     }
 
-    public Set<String> namespaces() {
-        Set<String> ns = new HashSet<>();
-        names().forEach(n -> {
-            try (InputStream is = dataSource.newInputStream(n)) {
-                ns.addAll(NamespaceReader.namespaces(is));
-            } catch (IOException x) {
-                throw new UncheckedIOException(x);
+    private <T> T loadInputStreamAndGetNamespace(String n, Function<InputStream, T> namespaceGetter) {
+        try (InputStream in = dataSource.newInputStream(n)) {
+            String fileExtension = n.substring(n.lastIndexOf('.') + 1);
+            if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                ZipSecurityHelper.checkIfZipExtractionIsSafe(dataSource, n);
path: cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java
user: login: quinarygio
body: I propose to check just first zip entry safety, checking only the compression ratio
created_at: 2025-03-03T15:16:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.cgmes.conversion.test;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
+import com.powsybl.cgmes.conversion.CgmesImport;
+import com.powsybl.cgmes.model.CgmesModelException;
+import com.powsybl.commons.datasource.DataSource;
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+import com.powsybl.iidm.network.Network;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+class LoadZippedProfilesTest {
+
+    @Test
+    void oneFolderAndOneZipPerProfileTest() throws Exception {
+        var testDataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
+        Set<String> profiles = testDataSource.listNames(".*");
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            // copy and compress each of the profile to the file system
+            Path workDir = fileSystem.getPath("/work");
+            for (String profile : profiles) {
+                try (var is = testDataSource.newInputStream(profile);
+                        var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
+                    os.putNextEntry(new ZipEntry(profile));
path: cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/LoadZippedProfilesTest.java
user: login: quinarygio
body: The issue is that it is not possible to check the number of entries in a zip file without reading them from the input stream. Additionally, if we attempt to read more than one entry, we cannot go back and read from the first one without reopening the ZipInputStream.
Currently, the exception thrown is a PowsyblException with the message 'Unsupported file format or invalid file', similar to when attempting to read an incompatible file.

created_at: 2025-03-03T18:24:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.cgmes.conversion.test;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
+import com.powsybl.cgmes.conversion.CgmesImport;
+import com.powsybl.cgmes.model.CgmesModelException;
+import com.powsybl.commons.datasource.DataSource;
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+import com.powsybl.iidm.network.Network;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+class LoadZippedProfilesTest {
+
+    @Test
+    void oneFolderAndOneZipPerProfileTest() throws Exception {
+        var testDataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
+        Set<String> profiles = testDataSource.listNames(".*");
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            // copy and compress each of the profile to the file system
+            Path workDir = fileSystem.getPath("/work");
+            for (String profile : profiles) {
+                try (var is = testDataSource.newInputStream(profile);
+                        var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
+                    os.putNextEntry(new ZipEntry(profile));
path: cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/LoadZippedProfilesTest.java
user: login: flo-dup
body: You're right, the error you mentioned is correct. We couldn't throw an exception as I said, the method is rather meant to return true if file is supported or false if not.
created_at: 2025-03-04T08:52:41Z
author_association: COLLABORATOR
diff_hunk: @@ -109,34 +107,51 @@ public Set<String> names() {
         try {
             // the set of names may be empty if the data source does not contain CGMES data
             Set<String> allNames = dataSource.listNames(REGEX_VALID_NAME);
-            allNames.removeIf(n -> !containsValidNamespace(n));
+            allNames.removeIf(n -> !existsInDatasource(n) || !containsValidNamespace(n));
             return allNames;
         } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
+            throw new CgmesModelException(String.format(LISTING_CGMES_NAMES_IN_DATA_SOURCE, dataSource), x);
         }
     }
 
-    private boolean containsValidNamespace(String name) {
-        try (InputStream is = dataSource.newInputStream(name)) {
-            Set<String> ns = NamespaceReader.namespaces1(is);
-            return ns.contains(RDF_NAMESPACE) && ns.stream().anyMatch(CgmesNamespace::isValid);
-        } catch (XMLStreamException e) {
+    private boolean existsInDatasource(String fileName) {
+        try {
+            return dataSource.exists(fileName);
+        } catch (IOException e) {
             return false;
-        } catch (IOException x) {
-            throw new CgmesModelException(String.format("Listing CGMES names in data source %s", dataSource), x);
         }
     }
 
-    public Set<String> namespaces() {
-        Set<String> ns = new HashSet<>();
-        names().forEach(n -> {
-            try (InputStream is = dataSource.newInputStream(n)) {
-                ns.addAll(NamespaceReader.namespaces(is));
-            } catch (IOException x) {
-                throw new UncheckedIOException(x);
+    private <T> T loadInputStreamAndGetNamespace(String n, Function<InputStream, T> namespaceGetter) {
+        try (InputStream in = dataSource.newInputStream(n)) {
+            String fileExtension = n.substring(n.lastIndexOf('.') + 1);
+            if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                ZipSecurityHelper.checkIfZipExtractionIsSafe(dataSource, n);
path: cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/CgmesOnDataSource.java
user: login: flo-dup
body: I think a simpler way is to use a `SafeZipInputStream` class wrapping the `ZipInputStream` instead of the `ZipInputStream` itself:
```java
public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
    @Override
    public int read() throws IOException {
        int byteRead = super.read();
        if (byteRead != -1 && this.bytesRead++ > this.maxBytesToRead) {
          throw new IOException();
        }
        return byteRead;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        ... // similar way
    }
}
```
That way you'll only read the first lines, not unzipping the full file, which might be more than 1GB uncompressed. Besides we could restrict maxBytesRead a lot in this PR (a few kB, or 1MB?), as we only read the first tag anyway (we stop at first `START_ELEMENT` in the 3 use cases).
created_at: 2025-03-04T09:21:05Z
author_association: COLLABORATOR
diff_hunk: @@ -5,6 +5,8 @@ The CIM-CGMES importer reads and converts a CIM-CGMES model to the PowSyBl grid
 - Convert CIM-CGMES data retrieved by SPARQL requests from the created triplestore to PowSyBl grid model
 
 The data in input CIM/XML files uses RDF (Resource Description Framework) syntax. In RDF, data is described making statements about resources using triplet expressions: (subject, predicate, object).
+The CIM-CGMES importer supports also ZIP compressed input files. The importer will decompress the ZIP files to read CIM/XML data.
path: docs/grid_exchange_formats/cgmes/import.md
user: login: flo-dup
body: It's not so clear here, I think we should enumerate the 3 ways (or more?) to import a CGMES file:
- a folder containing all uncompressed profile files
- a folder containing the zipped profile files, each one being in a separate zip file
- a zipped file containing all profile files
created_at: 2025-03-04T09:41:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.cgmes.conversion.test;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
+import com.powsybl.cgmes.model.CgmesModelException;
+import com.powsybl.commons.datasource.DataSource;
+import com.powsybl.commons.datasource.ReadOnlyDataSource;
+import com.powsybl.iidm.network.Network;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+/**
+ * @author Giovanni Ferrari {@literal <giovanni.ferrari at soft.it>}
+ */
+class LoadZippedProfilesTest {
+
+    @Test
+    void oneFolderAndOneZipPerProfileTest() throws Exception {
+        var testDataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
+        Set<String> profiles = testDataSource.listNames(".*");
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            // copy and compress each of the profile to the file system
+            Path workDir = fileSystem.getPath("/work");
+            for (String profile : profiles) {
+                try (var is = testDataSource.newInputStream(profile);
+                        var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
+                    os.putNextEntry(new ZipEntry(profile));
+                    ByteStreams.copy(is, os);
+                    os.closeEntry();
+                }
+            }
+
+            Network network = Network.read(DataSource.fromPath(workDir));
+            assertNotNull(network);
+        }
+    }
+
+    @Test
+    void emptyZipErrorTest() throws Exception {
+        var testDataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
+        Set<String> profiles = testDataSource.listNames(".*");
+        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
+            // copy and compress each of the profile to the file system
+            Path workDir = fileSystem.getPath("/work");
+            for (String profile : profiles) {
+                try (var is = testDataSource.newInputStream(profile);
+                        var os = new ZipOutputStream(Files.newOutputStream(workDir.resolve(profile + ".zip")))) {
+                    os.closeEntry();
+                }
+            }
path: cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/LoadZippedProfilesTest.java
user: login: olperr1
body: In `emptyZipErrorTest()`, there's no need to create an input stream for each profile since they are not read.
created_at: 2025-03-04T15:02:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.io.ForwardingInputStream;
+
+public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
+
+    private int bytesRead;
+    private int maxBytesToRead;
path: commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
user: login: flo-dup
body: We might want to go further than 2GB, so an int is not enough.
```suggestion
    private long bytesRead;
    private long maxBytesToRead;
```
created_at: 2025-03-06T16:17:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.io.ForwardingInputStream;
+
+public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
+
+    private int bytesRead;
+    private int maxBytesToRead;
+
+    public SafeZipInputStream(ZipInputStream in, int entryNumber, int maxBytesToRead) throws IOException {
+        super(in);
+        this.maxBytesToRead = maxBytesToRead;
+        for (int i = 0; i < entryNumber; i++) {
+            ZipEntry zipEntry = in.getNextEntry();
path: commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
user: login: flo-dup
body: We might want to reuse this for other usecases where several ZipInputStream entries have to be read. This is not really easy to use that way, I think you'd need to create a `new SafeZipInputStream(zin, 1, max)` for each entry?

Adding a `SafeZipInputStream::getNextEntry` method would do the trick, at the cost of adding a `protected T getDelegate()` in `ForwardingInputStream`.
created_at: 2025-03-06T16:29:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.io.ForwardingInputStream;
+
+public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
+
+    private int bytesRead;
+    private int maxBytesToRead;
+
+    public SafeZipInputStream(ZipInputStream in, int entryNumber, int maxBytesToRead) throws IOException {
+        super(in);
+        this.maxBytesToRead = maxBytesToRead;
+        for (int i = 0; i < entryNumber; i++) {
+            ZipEntry zipEntry = in.getNextEntry();
+            if (zipEntry == null) {
+                throw new IOException(String.format("Zip entry index out of bounds: %s", entryNumber));
+            }
+        }
+    }
+
+    @Override
+    public int read() throws IOException {
+        int byteRead = super.read();
+        if (byteRead != -1 && ++this.bytesRead > this.maxBytesToRead) {
+            throw new IOException("Max bytes to read exceeded");
+        }
+        return byteRead;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        int byteRead = super.read(b, off, len);
+        if (byteRead != -1 && (this.bytesRead + byteRead) > this.maxBytesToRead) {
+            throw new IOException("Max bytes to read exceeded");
+        }
+        this.bytesRead += byteRead;
path: commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
user: login: flo-dup
body: to avoid doing twice the addition include this in an `if (byteRead + -1)` before comparing to the max
created_at: 2025-03-06T16:31:44Z
author_association: COLLABORATOR
path: commons/src/main/java/com/powsybl/commons/compress/ZipSecurityHelper.java
user: login: flo-dup
body: Indeed as it's not needed anymore (in this PR at least!) we should not keep it
created_at: 2025-03-06T16:53:05Z
author_association: COLLABORATOR
diff_hunk: @@ -15,8 +15,8 @@
 
 public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
 
-    private int bytesRead;
-    private int maxBytesToRead;
+    private long bytesRead;
+    private long maxBytesToRead;
 
     public SafeZipInputStream(ZipInputStream in, int entryNumber, int maxBytesToRead) throws IOException {
path: commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
user: login: flo-dup
body: You forgot to change maxBytesToRead to a long here. 

Besides it's not clear what entryNumber is here. It's not exactly the number of entries to skip, as when you just created the zipInputStream and give entryNumber = 1 you're not skipping anything. And it's not really something to do with the "safe" responsability of the SafeZipInputStream class. Therefore, could you remove this argument and let the caller do the call(s) to getNextEntry?
created_at: 2025-03-10T10:40:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.io.ForwardingInputStream;
+
+public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
+
+    private long bytesRead;
+    private long maxBytesToRead;
+
+    public SafeZipInputStream(ZipInputStream in, long maxBytesToRead) throws IOException {
path: commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
user: login: flo-dup
body: woops didn't see the code smell on sonar! IOException cannot be thrown anymore
created_at: 2025-03-10T16:39:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.io.ForwardingInputStream;
+
+public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
+
+    private long bytesRead;
+    private long maxBytesToRead;
path: commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
user: login: flo-dup
body: and this field can be marked final
created_at: 2025-03-10T16:40:09Z
author_association: COLLABORATOR
diff_hunk: @@ -303,8 +306,27 @@ private Set<ReadOnlyDataSource> separateByModelingAuthority() {
 
         private Optional<String> readModelingAuthority(String name) {
             String modellingAuthority = null;
-            try (InputStream is = dataSource.newInputStream(name)) {
-                XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(is);
+            try (InputStream in = dataSource.newInputStream(name)) {
+                String fileExtension = name.substring(name.lastIndexOf('.') + 1);
+                if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 2048)) {
path: cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
user: login: flo-dup
body: Just saw an xml file with a copyright header! It's probably too drastic to put 2048 as a limit here, and to 1024 in the other call, as you might have some comments in the header (like a copyright). Shouldn't we raise it a bit?
created_at: 2025-03-10T17:10:39Z
author_association: COLLABORATOR
diff_hunk: @@ -303,8 +306,27 @@ private Set<ReadOnlyDataSource> separateByModelingAuthority() {
 
         private Optional<String> readModelingAuthority(String name) {
             String modellingAuthority = null;
-            try (InputStream is = dataSource.newInputStream(name)) {
-                XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(is);
+            try (InputStream in = dataSource.newInputStream(name)) {
+                String fileExtension = name.substring(name.lastIndexOf('.') + 1);
+                if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 2048)) {
path: cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
user: login: quinarygio
body: Which value could be reasonable ? A standard copyright should not take more than 1000 bytes, but there could be additional comments...
created_at: 2025-03-10T18:17:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -303,8 +306,27 @@ private Set<ReadOnlyDataSource> separateByModelingAuthority() {
 
         private Optional<String> readModelingAuthority(String name) {
             String modellingAuthority = null;
-            try (InputStream is = dataSource.newInputStream(name)) {
-                XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(is);
+            try (InputStream in = dataSource.newInputStream(name)) {
+                String fileExtension = name.substring(name.lastIndexOf('.') + 1);
+                if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 2048)) {
path: cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
user: login: flo-dup
body: I agree it should be ok with current parameters, but we should still take some more margin just in case. Plus the values are hardcoded. As the maximum value is only to avoid zip bombing, you could take something like 10kB instead, we're still pretty safe.
created_at: 2025-03-10T20:06:31Z
author_association: COLLABORATOR
diff_hunk: @@ -303,8 +306,27 @@ private Set<ReadOnlyDataSource> separateByModelingAuthority() {
 
         private Optional<String> readModelingAuthority(String name) {
             String modellingAuthority = null;
-            try (InputStream is = dataSource.newInputStream(name)) {
-                XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(is);
+            try (InputStream in = dataSource.newInputStream(name)) {
+                String fileExtension = name.substring(name.lastIndexOf('.') + 1);
+                if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 2048)) {
path: cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
user: login: quinarygio
body: I wonder if we should reset the bytesRead counter when getNextEntry() is called. Doing so the 10kB limit would be for each entry, otherwise it is an overall limit.
created_at: 2025-03-11T08:29:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -309,7 +309,7 @@ private Optional<String> readModelingAuthority(String name) {
             try (InputStream in = dataSource.newInputStream(name)) {
                 String fileExtension = name.substring(name.lastIndexOf('.') + 1);
                 if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
-                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 2048)) {
+                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 1024000L)) {
path: cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
user: login: alicecaron
body: is it ok to pass from `2048` to `1024000L` and not `2048000L`?
created_at: 2025-03-12T09:11:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -309,7 +309,7 @@ private Optional<String> readModelingAuthority(String name) {
             try (InputStream in = dataSource.newInputStream(name)) {
                 String fileExtension = name.substring(name.lastIndexOf('.') + 1);
                 if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
-                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 2048)) {
+                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 1024000L)) {
path: cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
user: login: flo-dup
body: yest I think it is :slightly_smiling_face: There's no reason to have a different margin for both use cases.
created_at: 2025-03-12T10:15:09Z
author_association: COLLABORATOR
diff_hunk: @@ -303,8 +306,27 @@ private Set<ReadOnlyDataSource> separateByModelingAuthority() {
 
         private Optional<String> readModelingAuthority(String name) {
             String modellingAuthority = null;
-            try (InputStream is = dataSource.newInputStream(name)) {
-                XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(is);
+            try (InputStream in = dataSource.newInputStream(name)) {
+                String fileExtension = name.substring(name.lastIndexOf('.') + 1);
+                if (fileExtension.equals(CompressionFormat.ZIP.getExtension())) {
+                    try (SafeZipInputStream zis = new SafeZipInputStream(new ZipInputStream(in), 2048)) {
path: cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesImport.java
user: login: flo-dup
body: The overall limit is fine, and in fact that's safer.

Besides right now it's only used for a single entry. If we read several entries in another usecase, the overall  limit would be raise in that use case. And at that time we would also need to improve the SafeZipInputStream to check the number of entries.
created_at: 2025-03-12T10:19:33Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.io.ForwardingInputStream;
+
+public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
+
+    private long bytesRead;
+    private final long maxBytesToRead;
+
+    public SafeZipInputStream(ZipInputStream in, long maxBytesToRead) {
+        super(in);
+        this.maxBytesToRead = maxBytesToRead;
+    }
+
+    public ZipEntry getNextEntry() throws IOException {
+        return this.getDelegate().getNextEntry();
path: commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
user: login: olperr1
body: I think we could also limit the number of calls to `getNextEntry()` on the delegate, just in case this class is used later for more complex zips:
- new constructor parameter `int maxEntriesToRead`
- control in `getNextEntry()`: if counter > maxEntriesToRead then throw an IOException.

created_at: 2025-03-12T15:38:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.compress;
+
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import com.powsybl.commons.io.ForwardingInputStream;
+
+public class SafeZipInputStream extends ForwardingInputStream<ZipInputStream> {
+
+    private long bytesRead;
+    private final long maxBytesToRead;
+
+    public SafeZipInputStream(ZipInputStream in, long maxBytesToRead) {
+        super(in);
+        this.maxBytesToRead = maxBytesToRead;
+    }
+
+    public ZipEntry getNextEntry() throws IOException {
+        return this.getDelegate().getNextEntry();
path: commons/src/main/java/com/powsybl/commons/compress/SafeZipInputStream.java
user: login: olperr1
body: Thanks!
created_at: 2025-03-13T11:48:11Z
author_association: MEMBER

PR comments:
user: login: quinarygio
created_at: 2025-03-04T16:22:50Z
body: Using the new SafeZipInputStream class to read from the ZipInputStream, the ZipSecurityHelper class is not used anymore. Should we keep it or not ?
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-03-13T12:00:07Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-core&pullRequest=3309) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-core&pullRequest=3309&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-core&pullRequest=3309&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=com.powsybl%3Apowsybl-core&pullRequest=3309&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [90.4% Coverage on New Code](https://sonarcloud.io/component_measures?id=com.powsybl%3Apowsybl-core&pullRequest=3309&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=com.powsybl%3Apowsybl-core&pullRequest=3309&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-core&pullRequest=3309)


author_association: NONE