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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,11 +45,10 @@
* Validator of the watchface.xml
*/
public class WatchFaceXmlValidator {
private final ResourceManager resourceManager;
private final Map<String, Validator> 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. */
Expand All @@ -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);
}

/**
Expand All @@ -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(
Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand All @@ -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);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can throw other exception types, why are we catching this particular one? Could we leave as it was, as we were relying on that to identify whether the schema couldn't be created?


private static Map<String, Validator> parseSchemas() {
Map<String, Validator> 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<Path> 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
}
});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
Loading