private FSRL getFullyQualifiedFSRL(GFileSystem fs, FSRL fsrl, TaskMonitor monitor)
			throws CancelledException, IOException {
		if (fsrl.getMD5() != null) {
			return fsrl;
		}
		GFile file = fs.lookup(fsrl.getPath());
		if (file == null) {
			throw new IOException("File not found: " + fsrl);
		}
		if (file.getFSRL().getMD5() != null || file.isDirectory()) {
			return file.getFSRL();
		}

		FSRL containerFSRL = fsrl.getFS().getContainer();
		if (containerFSRL != null && containerFSRL.getMD5() == null) {
			// re-home the fsrl to the parent container's fsrl since
			// filesystems will always have fully qualified fsrl
			containerFSRL = fs.getFSRL().getContainer();
			fsrl = FSRLRoot.nestedFS(containerFSRL, fsrl.getFS()).withPath(fsrl);
		}

		if (fs instanceof GFileHashProvider) {
			GFileHashProvider hashProvider = (GFileHashProvider) fs;
			return fsrl.withMD5(hashProvider.getMD5Hash(file, true, monitor));
		}

		String md5 = (containerFSRL != null)
				? fileCacheNameIndex.get(containerFSRL.getMD5(), fsrl.getPath())
				: null;
		if (md5 == null) {
			try (ByteProvider bp = fs.getByteProvider(file, monitor)) {
				if (bp == null) {
					throw new IOException("Unable to get bytes for " + fsrl);
				}
				md5 = (bp.getFSRL().getMD5() != null)
						? bp.getFSRL().getMD5()
						: FSUtilities.getMD5(bp, monitor);
			}
		}
		if (containerFSRL != null && fs.isStatic()) {
			fileCacheNameIndex.add(containerFSRL.getMD5(), fsrl.getPath(), md5);
		}
		return fsrl.withMD5(md5);
	}
Returns a cloned copy of the {@code FSRL} that should have MD5 values specified.
(excluding GFile objects that don't have data streams)

@param fsrl {@link FSRL} of the file that should be forced to have a MD5
@param monitor {@link TaskMonitor} to watch and update with progress.
@return possibly new {@link FSRL} instance with a MD5 value.
@throws CancelledException if user cancels.
@throws IOException if IO problem.
Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemService.java