diff --git a/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/ResourceManager.java b/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/ResourceManager.java deleted file mode 100644 index e74529a..0000000 --- a/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/ResourceManager.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2023 Samsung Electronics Co., Ltd All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.samsung.watchface; - -import com.samsung.watchface.utils.UnzipUtility; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.nio.file.Files; -import java.util.Objects; - -class ResourceManager { - private static final String RESOURCE_ZIP_XSD_DOCS = "/docs.zip"; - private static final int BUFFER_SIZE = 4096; - - private final File xsdTempDirectory; - - ResourceManager() { - // load resources - String zipFilePath = getResourceAsFile(RESOURCE_ZIP_XSD_DOCS).toString(); - xsdTempDirectory = createTempDirectory(); - UnzipUtility.unzip(zipFilePath, xsdTempDirectory.toString()); - } - - File getXsdFile(String version) { - return new File(xsdTempDirectory + File.separator + - version + File.separator + "watchface.xsd"); - } - - private File getResourceAsFile(String resource) { - File file; - URL res = getClass().getResource(resource); - if (res == null) { - throw new RuntimeException("No resource File : " + resource); - } - if (res.getProtocol().equals("jar")) { - try { - file = File.createTempFile("dwf_temp", "tmp"); - } catch (IOException e) { - throw new RuntimeException(e); - } - file.deleteOnExit(); - - try (InputStream input = getClass().getResourceAsStream(resource); - OutputStream output = Files.newOutputStream(file.toPath())) { - byte[] bytes = new byte[BUFFER_SIZE]; - int read; - while ((read = Objects.requireNonNull(input).read(bytes)) != -1) { - output.write(bytes, 0, read); - } - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); - } - } else { // for IDE - file = new File(res.getFile()); - } - - if (!file.exists()) { - throw new RuntimeException( - "Error: Cannot read Resource File " + file + "(" + resource + ")!"); - } - return file; - } - - private File createTempDirectory() { - try { - File file = new File(Files.createTempDirectory("validator").toString()); - file.deleteOnExit(); - return file; - } catch (IOException e) { - throw new RuntimeException(e.getMessage()); - } - } -} diff --git a/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/WatchFaceXmlValidator.java b/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/WatchFaceXmlValidator.java index a07bb34..13f2db8 100644 --- a/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/WatchFaceXmlValidator.java +++ b/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/WatchFaceXmlValidator.java @@ -18,6 +18,15 @@ import com.samsung.watchface.utils.Log; +import com.samsung.watchface.utils.UnzipUtility; +import java.io.InputStream; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; import javax.xml.transform.Source; import javax.xml.transform.dom.DOMSource; import org.w3c.dom.Document; @@ -36,11 +45,10 @@ * Validator of the watchface.xml */ public class WatchFaceXmlValidator { - private final ResourceManager resourceManager; + private final Map validatorPerVersion; public WatchFaceXmlValidator() { - // load resources for validation via xsd documents - resourceManager = new ResourceManager(); + validatorPerVersion = parseSchemas(); } /** Exception thrown when the watch face format validation has failed to execute. */ @@ -58,8 +66,7 @@ public WatchFaceFormatValidationException(String message, Throwable cause) { * @return true if supported, else false */ public boolean isSupportedVersion(String version) { - File xsdFile = resourceManager.getXsdFile(version); - return xsdFile != null && xsdFile.exists(); + return validatorPerVersion.containsKey(version); } /** @@ -78,8 +85,7 @@ public boolean validate(String xmlPath, String version) { if (!xmlFile.exists()) { throw new RuntimeException("xml path is invalid : " + xmlPath); } - validateXMLSchema( - resourceManager.getXsdFile(version).getCanonicalPath(), new StreamSource(xmlFile)); + validatorPerVersion.get(version).validate(new StreamSource(xmlFile)); return true; } catch (SAXParseException e) { String errorMessage = String.format( @@ -109,8 +115,7 @@ public boolean validate(Document xmlDocument, String version) { throw new RuntimeException("Validator does not support the version #" + version); } - validateXMLSchema( - resourceManager.getXsdFile(version).getCanonicalPath(), new DOMSource(xmlDocument)); + validatorPerVersion.get(version).validate(new DOMSource(xmlDocument)); return true; } catch (Exception e) { Log.e(e.getMessage()); @@ -135,8 +140,7 @@ public boolean validateOrThrow(Document xmlDocument, String version) } try { - validateXMLSchema( - resourceManager.getXsdFile(version).getCanonicalPath(), new DOMSource(xmlDocument)); + validatorPerVersion.get(version).validate(new DOMSource(xmlDocument)); return true; } catch (SAXException | IOException | NullPointerException e) { Log.e("Could not validate xml: " + e.getMessage()); @@ -146,13 +150,54 @@ public boolean validateOrThrow(Document xmlDocument, String version) } } - - private static void validateXMLSchema(String xsdPath, Source xmlSource) throws - IllegalArgumentException, SAXException, IOException, NullPointerException { - // https://stackoverflow.com/questions/20807066/how-to-validate-xml-against-xsd-1-1-in-java + private static Validator createXmlSchema(File xsdFile) { SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/XML/XMLSchema/v1.1"); - Schema schema = factory.newSchema(new File(xsdPath)); - Validator validator = schema.newValidator(); - validator.validate(xmlSource); + try { + return factory.newSchema(xsdFile).newValidator(); + } catch (SAXException e) { + throw new RuntimeException(e); + } + } + + private static Map parseSchemas() { + Map validators = new HashMap<>(); + + try (InputStream docsStream = WatchFaceXmlValidator.class.getResourceAsStream("/docs.zip")) { + Path unzipDocsPath = Files.createTempDirectory("watch_face_validator"); + try { + UnzipUtility.unzip(docsStream, unzipDocsPath); + + Files.list(unzipDocsPath).forEach(docChild -> { + File docChildFile = docChild.toFile(); + File watchFaceXsdFile = docChild.resolve("watchface.xsd").toFile(); + if (docChildFile.isDirectory() && watchFaceXsdFile.exists()) { + validators.put( + docChild.getName(docChild.getNameCount() - 1).toString(), + createXmlSchema(watchFaceXsdFile) + ); + } + }); + } finally { + deleteDirectory(unzipDocsPath); + } + } catch (IOException e) { + throw new RuntimeException("Failed to parse XSD schemas", e); + } + return validators; + } + + private static void deleteDirectory(Path dirPath) throws IOException { + if (Files.exists(dirPath)) { + try (Stream walk = Files.walk(dirPath)) { + walk.sorted(Comparator.reverseOrder()) // Reverse order to delete contents first + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + // Do nothing + } + }); + } + } } } \ No newline at end of file diff --git a/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/utils/UnzipUtility.java b/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/utils/UnzipUtility.java index 7058607..02f918e 100644 --- a/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/utils/UnzipUtility.java +++ b/third_party/wff/specification/validator/src/main/java/com/samsung/watchface/utils/UnzipUtility.java @@ -16,62 +16,68 @@ package com.samsung.watchface.utils; +import static java.nio.file.Files.*; + import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class UnzipUtility { private static final int BUFFER_SIZE = 4096; - public static void unzip(String zipFilePath, String destDirectory) { - tryCreateDirectory(destDirectory); + /** + * Unzips a ZIP file from an InputStream to a destination directory. + * + * @param inputStream The stream of the ZIP file to unzip. + * @param destinationDir The directory where files will be extracted. + * @throws IOException If an I/O error occurs. + */ + public static void unzip(InputStream inputStream, Path destinationDir) throws IOException { + // Create a buffer for reading/writing data + byte[] buffer = new byte[BUFFER_SIZE]; - try (FileInputStream fileIn = new FileInputStream(zipFilePath); - ZipInputStream zipIn = new ZipInputStream(fileIn)) { - ZipEntry entry = zipIn.getNextEntry(); - while (entry != null) { - final String filePath = destDirectory + File.separator + entry.getName(); - if (entry.isDirectory()) { - tryCreateDirectory(filePath); - } else { - tryExtractFile(zipIn, filePath); - } - zipIn.closeEntry(); - entry = zipIn.getNextEntry(); - } - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); - } - } + // Use try-with-resources to ensure the stream is closed automatically + try (ZipInputStream zis = new ZipInputStream(inputStream)) { + ZipEntry zipEntry = zis.getNextEntry(); - private static void tryCreateDirectory(String directory) { - File destDir = new File(directory); - if (!destDir.exists()) { - if (!destDir.mkdirs()) { - throw new RuntimeException("Couldn't create directory : " + directory); - } - } - } + while (zipEntry != null) { + Path newFilePath = destinationDir.resolve(zipEntry.getName()); - private static void tryExtractFile(ZipInputStream zipIn, String filePath) { - File file = new File(filePath); - File parent = file.getParentFile(); - if (parent != null && !parent.exists()) { - tryCreateDirectory(parent.getAbsolutePath()); - } + // SECURITY CHECK: Prevent Zip Slip vulnerability + if (!newFilePath.toAbsolutePath().normalize().startsWith(destinationDir.toAbsolutePath().normalize())) { + throw new IOException("Bad zip entry: " + zipEntry.getName()); + } - try (FileOutputStream fileOutputStream = new FileOutputStream(filePath); - BufferedOutputStream outStream = new BufferedOutputStream(fileOutputStream)) { - byte[] bytesIn = new byte[BUFFER_SIZE]; - int read; - while ((read = zipIn.read(bytesIn)) != -1) { - outStream.write(bytesIn, 0, read); + if (zipEntry.isDirectory()) { + // If the entry is a directory, create it + if (!isDirectory(newFilePath)) { + createDirectories(newFilePath); + } + } else { + // If the entry is a file, write it out + // Ensure parent directories exist + Path parentDir = newFilePath.getParent(); + if (parentDir != null && !isDirectory(parentDir)) { + createDirectories(parentDir); + } + + // Write the file content + try (FileOutputStream fos = new FileOutputStream(newFilePath.toFile())) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + } + zis.closeEntry(); + zipEntry = zis.getNextEntry(); } - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); } } } \ No newline at end of file