@Override
    public AttributedList<Path> read(final Path directory, final List<String> replies) throws FTPInvalidListException {
        final AttributedList<Path> children = new AttributedList<>();
        if(replies.isEmpty()) {
            return children;
        }
        // At least one entry successfully parsed
        boolean success = false;
        for(String line : replies) {
            final Map<String, Map<String, String>> file = this.parseFacts(line);
            if(null == file) {
                log.error(String.format("Error parsing line %s", line));
                continue;
            }
            for(Map.Entry<String, Map<String, String>> f : file.entrySet()) {
                final String name = f.getKey();
                // size       -- Size in octets
                // modify     -- Last modification time
                // create     -- Creation time
                // type       -- Entry type
                // unique     -- Unique id of file/directory
                // perm       -- File permissions, whether read, write, execute is allowed for the login id.
                // lang       -- Language of the file name per IANA [11] registry.
                // media-type -- MIME media-type of file contents per IANA registry.
                // charset    -- Character set per IANA registry (if not UTF-8)
                final Map<String, String> facts = f.getValue();
                if(!facts.containsKey("type")) {
                    log.error(String.format("No type fact in line %s", line));
                    continue;
                }
                final Path parsed;
                if("dir".equals(facts.get("type").toLowerCase(Locale.ROOT))) {
                    parsed = new Path(directory, PathNormalizer.name(f.getKey()), EnumSet.of(Path.Type.directory));
                }
                else if("file".equals(facts.get("type").toLowerCase(Locale.ROOT))) {
                    parsed = new Path(directory, PathNormalizer.name(f.getKey()), EnumSet.of(Path.Type.file));
                }
                else if(facts.get("type").toLowerCase(Locale.ROOT).matches("os\\.unix=slink:.*")) {
                    parsed = new Path(directory, PathNormalizer.name(f.getKey()), EnumSet.of(Path.Type.file, Path.Type.symboliclink));
                    // Parse symbolic link target in Type=OS.unix=slink:/foobar;Perm=;Unique=keVO1+4G4; foobar
                    final String[] type = facts.get("type").split(":");
                    if(type.length == 2) {
                        final String target = type[1];
                        if(target.startsWith(String.valueOf(Path.DELIMITER))) {
                            parsed.setSymlinkTarget(new Path(PathNormalizer.normalize(target), EnumSet.of(Path.Type.file)));
                        }
                        else {
                            parsed.setSymlinkTarget(new Path(PathNormalizer.normalize(String.format("%s/%s", directory.getAbsolute(), target)), EnumSet.of(Path.Type.file)));
                        }
                    }
                    else {
                        log.warn(String.format("Missing symbolic link target for type %s in line %s", facts.get("type"), line));
                        continue;
                    }
                }
                else {
                    log.warn(String.format("Ignored type %s in line %s", facts.get("type"), line));
                    continue;
                }
                if(!success) {
                    if(parsed.isDirectory() && directory.getName().equals(name)) {
                        log.warn(String.format("Possibly bogus response line %s", line));
                    }
                    else {
                        success = true;
                    }
                }
                if(name.equals(".") || name.equals("..")) {
                    if(log.isDebugEnabled()) {
                        log.debug(String.format("Skip %s", name));
                    }
                    continue;
                }
                if(facts.containsKey("size")) {
                    parsed.attributes().setSize(Long.parseLong(facts.get("size")));
                }
                if(facts.containsKey("unix.uid")) {
                    parsed.attributes().setOwner(facts.get("unix.uid"));
                }
                if(facts.containsKey("unix.owner")) {
                    parsed.attributes().setOwner(facts.get("unix.owner"));
                }
                if(facts.containsKey("unix.gid")) {
                    parsed.attributes().setGroup(facts.get("unix.gid"));
                }
                if(facts.containsKey("unix.group")) {
                    parsed.attributes().setGroup(facts.get("unix.group"));
                }
                if(facts.containsKey("unix.mode")) {
                    parsed.attributes().setPermission(new Permission(facts.get("unix.mode")));
                }
                else if(facts.containsKey("perm")) {
                    if(PreferencesFactory.get().getBoolean("ftp.parser.mlsd.perm.enable")) {
                        Permission.Action user = Permission.Action.none;
                        final String flags = facts.get("perm");
                        if(StringUtils.contains(flags, 'r') || StringUtils.contains(flags, 'l')) {
                            // RETR command may be applied to that object
                            // Listing commands, LIST, NLST, and MLSD may be applied
                            user = user.or(Permission.Action.read);
                        }
                        if(StringUtils.contains(flags, 'w') || StringUtils.contains(flags, 'm') || StringUtils.contains(flags, 'c')) {
                            user = user.or(Permission.Action.write);
                        }
                        if(StringUtils.contains(flags, 'e')) {
                            // CWD command naming the object should succeed
                            user = user.or(Permission.Action.execute);
                            if(parsed.isDirectory()) {
                                user = user.or(Permission.Action.read);
                            }
                        }
                        final Permission permission = new Permission(user, Permission.Action.none, Permission.Action.none);
                        parsed.attributes().setPermission(permission);
                    }
                }
                if(facts.containsKey("modify")) {
                    // Time values are always represented in UTC
                    parsed.attributes().setModificationDate(this.parseTimestamp(facts.get("modify")));
                }
                if(facts.containsKey("create")) {
                    // Time values are always represented in UTC
                    parsed.attributes().setCreationDate(this.parseTimestamp(facts.get("create")));
                }
                children.add(parsed);
            }
        }
        if(!success) {
            throw new FTPInvalidListException(children);
        }
        return children;
    }
@Test(expected = FTPInvalidListException.class)
    public void testBrokenMlsd() throws Exception {
        Path path = new Path(
                "/Dummies_Infoblaetter", EnumSet.of(Path.Type.directory));
        String[] replies = new String[]{
                "Type=dir;Modify=20101209140859;Win32.ea=0x00000010; Dummies_Infoblaetter",
        };
        new FTPMlsdListResponseReader().read(path, Arrays.asList(replies));
    }